Skip to content

Commit c966743

Browse files
committed
Simplify provider routing and retire QMD
1 parent 7e1db7b commit c966743

46 files changed

Lines changed: 428 additions & 1520 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.env.example‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ OPENCODE_QUERY_PORT=8282
1818
OPENCODE_QUERY_TIMEOUT_MS=120000
1919
OPENCODE_PROMPT_HEARTBEAT_MS=15000
2020
OPENCODE_WORKER_SHUTDOWN_GRACE_MS=5000
21-
OPENCODE_MODEL=openai/gpt-5.5
2221

2322
SYNC_WORKER_PORT=4000
2423
SYNC_INTERVAL_SECONDS=300
@@ -34,7 +33,5 @@ AWS_ACCESS_KEY_ID=
3433
AWS_SECRET_ACCESS_KEY=
3534
AWS_SESSION_TOKEN=
3635
OPENROUTER_API_KEY=
37-
# Optional: used only with docker-compose.subscription.yml.
38-
OPENCODE_OPENAI_AUTH_HOST_FILE=../../opencode-auth.json
3936

4037
CLOUDFLARE_TUNNEL_TOKEN=

‎Makefile‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ SHELL := /bin/bash
22

33
COMPOSE_FILE := infra/docker/docker-compose.yml
44
COMPOSE_FLAGS := --env-file .env -f $(COMPOSE_FILE)
5-
ifeq ($(SUBSCRIPTION),1)
6-
COMPOSE_FLAGS += -f infra/docker/docker-compose.subscription.yml
7-
endif
85

96
-include .env
107
export

‎README.md‎

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ The scheduler triggers S3 sync at a fixed interval. OpenCode reads the mirror di
1919
## Repository layout
2020

2121
- `apps/web/`: Next.js UI, authenticated API routes, SQLite chat persistence
22-
- `services/opencode-query/`: grounded answers, model catalog API, folder listing
22+
- `services/opencode-query/`: grounded answers, folder listing
2323
- `services/sync-worker/`: read-only S3 sync, run status and logs
2424
- `services/scheduler/`: periodic sync trigger
25-
- `packages/model-catalog/`: shared model definitions and route defaults
25+
- `packages/model-catalog/`: fixed model and provider constants
2626
- `showcase-website/`: separate marketing website
2727
- `infra/`: Docker Compose, AWS IAM, Cloudflare and systemd configuration
2828

@@ -54,27 +54,13 @@ sfw pnpm --filter lattice-web dev
5454

5555
Backend containers expose ports only on the Docker network. Running only the web process on the host does not make those services reachable automatically.
5656

57-
## Models and authentication
57+
## Model and authentication
5858

59-
New chats default to GPT-5.5 through **OpenRouter API billing**. The existing catalog also includes Claude Sonnet 4.6, Claude Opus 4.6, and Gemini 2.5 Pro. `OPENCODE_MODEL` controls the backend default model; model definitions live in `packages/model-catalog/catalog.json`.
59+
All questions use **GPT-6 Luna (`openai/gpt-6-luna`) through OpenRouter**. Set `OPENROUTER_API_KEY`; there are no model or billing-route pickers. ChatGPT subscription authentication and `OPENCODE_MODEL` overrides are no longer supported.
6060

61-
The OpenAI route toggle supports OpenRouter and ChatGPT subscription OAuth. There is no direct OpenAI API-key integration. Existing explicit subscription choices and legacy OpenAI chat choices remain preserved.
61+
Existing threads migrate to Luna/OpenRouter for future questions. Historical answers retain their original model and route metadata. Old OAuth files are no longer mounted or used by Lattice; existing files on disk are left untouched.
6262

63-
### Optional subscription access
64-
65-
API-only setup does not require an OAuth file. To also use subscription authentication:
66-
67-
1. Copy the OpenCode `auth.json` from a machine where you are already logged in to repository-root `opencode-auth.json`.
68-
2. Set `OPENCODE_OPENAI_AUTH_HOST_FILE=../../opencode-auth.json` in `.env`.
69-
3. Start the optional mount overlay:
70-
71-
```bash
72-
make up SUBSCRIPTION=1
73-
# Equivalent:
74-
docker compose --env-file .env -f infra/docker/docker-compose.yml -f infra/docker/docker-compose.subscription.yml up --build -d
75-
```
76-
77-
Use `SUBSCRIPTION=1` for subsequent `make up` calls while retaining subscription access. The overlay bind-mounts the file at `/app/opencode-data/opencode/auth.json` and disables automatic host-path creation. OpenCode token refreshes persist to that file. Never commit or log it; Git and Docker build contexts exclude it.
63+
Luna uses Chat Completions with reasoning disabled so it can call OpenCode's file tools. Both the main and helper model are pinned to Luna in the worker configuration.
7864

7965
## Environment
8066

@@ -85,9 +71,7 @@ Use `SUBSCRIPTION=1` for subsequent `make up` calls while retaining subscription
8571
| `CHAT_DB_PATH` | Chat SQLite database path inside the web container |
8672
| `WEB_AUTH_MODE` | `dev`, `cloudflare`, or `auto` |
8773
| `WEB_DEV_USER_EMAIL` | Development identity; optional fallback with `auto` |
88-
| `OPENROUTER_API_KEY` | API key for all OpenRouter-backed models |
89-
| `OPENCODE_MODEL` | Default model ID; invalid/unset values fall back to GPT-5.5 |
90-
| `OPENCODE_OPENAI_AUTH_HOST_FILE` | OAuth file used only by the subscription Compose overlay |
74+
| `OPENROUTER_API_KEY` | Required API key for GPT-6 Luna through OpenRouter |
9175
| `OPENCODE_QUERY_TIMEOUT_MS` | Inactivity timeout; default 120000, `0` disables it |
9276
| `OPENCODE_PROMPT_HEARTBEAT_MS` | Worker heartbeat interval; default 15000 |
9377
| `OPENCODE_WORKER_SHUTDOWN_GRACE_MS` | Grace period before process-group force-kill; default 5000 |
@@ -125,7 +109,7 @@ After upgrading from a version containing QMD, remove the orphaned service conta
125109
docker compose --env-file .env -f infra/docker/docker-compose.yml up --build -d --remove-orphans
126110
```
127111

128-
Include the subscription overlay when needed. This does not delete old bind-mounted index/cache directories.
112+
This does not delete old bind-mounted index/cache directories. Start with only the main Compose file to remove the old subscription authentication mount.
129113

130114
## Checks and troubleshooting
131115

@@ -139,4 +123,4 @@ docker compose --env-file .env.example -f infra/docker/docker-compose.yml config
139123

140124
CI runs type checks, unit/integration tests, production builds, and container builds. SQLite tests use disposable databases; OpenCode integration tests use isolated configuration and make no paid model calls. Actual provider answers still require a configured API key and a manual query checking streaming, citations, file tools, and cancellation.
141125

142-
Use `make logs`, `make ps`, and `scripts/healthcheck.sh` for diagnostics. For provider failures, verify `OPENROUTER_API_KEY`, selected model availability, or the optional subscription mount. For sync failures, inspect AWS permissions and sync-worker logs.
126+
Use `make logs`, `make ps`, and `scripts/healthcheck.sh` for diagnostics. For provider failures, verify `OPENROUTER_API_KEY`, OpenRouter credits, and GPT-6 Luna availability. For sync failures, inspect AWS permissions and sync-worker logs.

‎apps/web/app/api/ask/route.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ export async function POST(request: Request) {
1010
question: parsed.question,
1111
folder: parsed.folder,
1212
engine: parsed.engine ?? "opencode",
13-
model: parsed.model,
14-
openAiRoute: parsed.openAiRoute,
1513
limit: parsed.limit
1614
});
1715

‎apps/web/app/api/chat/ask/route.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,6 @@ export async function POST(request: Request) {
6060
question: parsed.question,
6161
engine: parsed.engine,
6262
folder: parsed.folder,
63-
model: parsed.model,
64-
openAiRoute: parsed.openAiRoute,
6563
successResponse: result.ok ? result.response : undefined,
6664
errorResponse: result.ok ? undefined : result.error,
6765
assistantStream: finalizeAssistantStreamState(assistantStream)

‎apps/web/app/api/chat/threads/[threadId]/route.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@ export async function PATCH(request: Request, context: RouteContext) {
5252
threadId,
5353
userEmail: identity.userEmail,
5454
...parsed,
55-
model: parsed.model,
56-
openAiRoute: parsed.openAiRoute
5755
});
5856

5957
if (!thread) {

‎apps/web/app/api/models/route.ts‎

Lines changed: 0 additions & 29 deletions
This file was deleted.

‎apps/web/components/chat-composer.tsx‎

Lines changed: 1 addition & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,17 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
88
import { Textarea } from "@/components/ui/textarea";
99
import { InputGroup, InputGroupBody, InputGroupFooter } from "@/components/ui/input-group";
1010
import { LoadingDots } from "@/components/ui/loading-dots";
11-
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
12-
import { shouldShowOpenAiRouteToggle } from "@/lib/chat-local-state";
13-
import type { OpencodeModelId, OpencodeModelOption, OpencodeOpenAiRoute, SourceFolder } from "@/lib/schemas";
11+
import type { SourceFolder } from "@/lib/schemas";
1412
import { cn } from "@/lib/utils";
1513

1614
type ChatComposerProps = {
1715
docked?: boolean;
1816
draftQuestion: string;
1917
onDraftQuestionChange: (value: string) => void;
20-
selectedModel: OpencodeModelId;
21-
onModelChange: (value: OpencodeModelId) => void;
22-
selectedOpenAiRoute: OpencodeOpenAiRoute;
23-
onOpenAiRouteChange: (value: OpencodeOpenAiRoute) => void;
2418
mobileSettingsOpen: boolean;
2519
onMobileSettingsOpenChange: (open: boolean) => void;
2620
selectedFolder: string;
2721
onFolderChange: (value: string) => void;
28-
opencodeModels: OpencodeModelOption[];
2922
sourceFolders: SourceFolder[];
3023
loadingFolders: boolean;
3124
loadingAnswer: boolean;
@@ -37,24 +30,17 @@ export function ChatComposer({
3730
docked = false,
3831
draftQuestion,
3932
onDraftQuestionChange,
40-
selectedModel,
41-
onModelChange,
42-
selectedOpenAiRoute,
43-
onOpenAiRouteChange,
4433
mobileSettingsOpen,
4534
onMobileSettingsOpenChange,
4635
selectedFolder,
4736
onFolderChange,
48-
opencodeModels,
4937
sourceFolders,
5038
loadingFolders,
5139
loadingAnswer,
5240
sourcesError,
5341
onSubmit
5442
}: ChatComposerProps) {
5543
const mobileTextareaRef = useRef<HTMLTextAreaElement | null>(null);
56-
const selectedModelOption = opencodeModels.find((model) => model.id === selectedModel) ?? opencodeModels[0] ?? null;
57-
const showOpenAiRouteToggle = shouldShowOpenAiRouteToggle(selectedModel);
5844

5945
useEffect(() => {
6046
const textarea = mobileTextareaRef.current;
@@ -150,57 +136,6 @@ export function ChatComposer({
150136

151137
<InputGroupFooter className={cn("hidden lg:flex", docked ? "chat-composer-footer" : undefined)}>
152138
<div className="flex flex-wrap items-center gap-3 text-[var(--text-tertiary)]">
153-
{opencodeModels.length > 0 ? (
154-
<Select
155-
value={selectedModel}
156-
onValueChange={(value) => onModelChange(value as OpencodeModelId)}
157-
disabled={loadingAnswer}
158-
>
159-
<SelectTrigger
160-
className={cn(
161-
"linear-pill w-fit max-w-[320px] gap-3 border-[var(--border-strong)] px-4 py-2 font-[400] shadow-none transition-[width,padding,background-color,border-color,color] duration-300 ease-out",
162-
docked ? "chat-composer-pill h-9" : "bg-[var(--bg-page)]"
163-
)}
164-
>
165-
<span className="min-w-0 truncate text-left">
166-
{selectedModelOption?.label ?? "OpenCode model"}
167-
</span>
168-
</SelectTrigger>
169-
<SelectContent className="min-w-[280px]">
170-
{opencodeModels.map((model) => (
171-
<SelectItem key={model.id} value={model.id}>
172-
<div className="flex min-w-0 flex-col">
173-
<span>{model.label}</span>
174-
<span className="text-[12px] leading-[1.4] text-[var(--text-tertiary)]">{model.description}</span>
175-
</div>
176-
</SelectItem>
177-
))}
178-
</SelectContent>
179-
</Select>
180-
) : null}
181-
182-
{showOpenAiRouteToggle ? (
183-
<ToggleGroup
184-
type="single"
185-
value={selectedOpenAiRoute}
186-
onValueChange={(value) => {
187-
if (value === "subscription" || value === "openrouter") {
188-
onOpenAiRouteChange(value);
189-
}
190-
}}
191-
disabled={loadingAnswer}
192-
className="h-9 rounded-full bg-[var(--bg-button-subtle)] p-1"
193-
aria-label="OpenAI route"
194-
>
195-
<ToggleGroupItem value="subscription" className="h-7">
196-
Subscription
197-
</ToggleGroupItem>
198-
<ToggleGroupItem value="openrouter" className="h-7">
199-
OpenRouter
200-
</ToggleGroupItem>
201-
</ToggleGroup>
202-
) : null}
203-
204139
<Select
205140
value={selectedFolder || "__all__"}
206141
onValueChange={(value) => onFolderChange(value === "__all__" ? "" : value)}
@@ -245,13 +180,8 @@ export function ChatComposer({
245180
<MobileChatSettings
246181
open={mobileSettingsOpen}
247182
onOpenChange={onMobileSettingsOpenChange}
248-
selectedModel={selectedModel}
249-
onModelChange={onModelChange}
250-
selectedOpenAiRoute={selectedOpenAiRoute}
251-
onOpenAiRouteChange={onOpenAiRouteChange}
252183
selectedFolder={selectedFolder}
253184
onFolderChange={onFolderChange}
254-
opencodeModels={opencodeModels}
255185
sourceFolders={sourceFolders}
256186
loadingFolders={loadingFolders}
257187
loadingAnswer={loadingAnswer}

‎apps/web/components/mobile-chat-settings.tsx‎

Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,13 @@
33
import { Globe } from "lucide-react";
44
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
55
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
6-
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
7-
import { shouldShowOpenAiRouteToggle } from "@/lib/chat-local-state";
8-
import type { OpencodeModelId, OpencodeModelOption, OpencodeOpenAiRoute, SourceFolder } from "@/lib/schemas";
6+
import type { SourceFolder } from "@/lib/schemas";
97

108
type MobileChatSettingsProps = {
119
open: boolean;
1210
onOpenChange: (open: boolean) => void;
13-
selectedModel: OpencodeModelId;
14-
onModelChange: (value: OpencodeModelId) => void;
15-
selectedOpenAiRoute: OpencodeOpenAiRoute;
16-
onOpenAiRouteChange: (value: OpencodeOpenAiRoute) => void;
1711
selectedFolder: string;
1812
onFolderChange: (value: string) => void;
19-
opencodeModels: OpencodeModelOption[];
2013
sourceFolders: SourceFolder[];
2114
loadingFolders: boolean;
2215
loadingAnswer: boolean;
@@ -25,77 +18,21 @@ type MobileChatSettingsProps = {
2518
export function MobileChatSettings({
2619
open,
2720
onOpenChange,
28-
selectedModel,
29-
onModelChange,
30-
selectedOpenAiRoute,
31-
onOpenAiRouteChange,
3221
selectedFolder,
3322
onFolderChange,
34-
opencodeModels,
3523
sourceFolders,
3624
loadingFolders,
3725
loadingAnswer
3826
}: MobileChatSettingsProps) {
39-
const selectedModelOption = opencodeModels.find((model) => model.id === selectedModel) ?? opencodeModels[0] ?? null;
40-
const showOpenAiRouteToggle = shouldShowOpenAiRouteToggle(selectedModel);
41-
4227
return (
4328
<Sheet open={open} onOpenChange={onOpenChange}>
4429
<SheetContent side="bottom" className="lg:hidden">
4530
<SheetHeader>
4631
<SheetTitle>Chat settings</SheetTitle>
47-
<SheetDescription>Choose the model and source scope for this conversation.</SheetDescription>
32+
<SheetDescription>Choose the source scope for this conversation.</SheetDescription>
4833
</SheetHeader>
4934

5035
<div className="mt-6 space-y-5">
51-
{opencodeModels.length > 0 ? (
52-
<div className="space-y-2">
53-
<p className="text-[12px] font-[600] uppercase tracking-[0.12em] text-[var(--text-quaternary)]">Model</p>
54-
<Select value={selectedModel} onValueChange={(value) => onModelChange(value as OpencodeModelId)} disabled={loadingAnswer}>
55-
<SelectTrigger className="linear-subsurface h-11 w-full min-w-0 justify-start rounded-2xl px-4 text-left shadow-none">
56-
<span className="block min-w-0 flex-1 truncate text-left">
57-
{selectedModelOption?.label ?? "OpenCode model"}
58-
</span>
59-
</SelectTrigger>
60-
<SelectContent>
61-
{opencodeModels.map((model) => (
62-
<SelectItem key={model.id} value={model.id}>
63-
<div className="flex min-w-0 flex-col">
64-
<span className="truncate">{model.label}</span>
65-
<span className="break-words text-[12px] leading-[1.4] text-[var(--text-tertiary)]">{model.description}</span>
66-
</div>
67-
</SelectItem>
68-
))}
69-
</SelectContent>
70-
</Select>
71-
</div>
72-
) : null}
73-
74-
{showOpenAiRouteToggle ? (
75-
<div className="space-y-2">
76-
<p className="text-[12px] font-[600] uppercase tracking-[0.12em] text-[var(--text-quaternary)]">OpenAI route</p>
77-
<ToggleGroup
78-
type="single"
79-
value={selectedOpenAiRoute}
80-
onValueChange={(value) => {
81-
if (value === "subscription" || value === "openrouter") {
82-
onOpenAiRouteChange(value);
83-
}
84-
}}
85-
disabled={loadingAnswer}
86-
className="grid grid-cols-2 rounded-2xl bg-[var(--bg-button-subtle)] p-1"
87-
aria-label="OpenAI route"
88-
>
89-
<ToggleGroupItem value="subscription" className="h-10 rounded-xl px-2 text-[13px]">
90-
Subscription
91-
</ToggleGroupItem>
92-
<ToggleGroupItem value="openrouter" className="h-10 rounded-xl px-2 text-[13px]">
93-
OpenRouter
94-
</ToggleGroupItem>
95-
</ToggleGroup>
96-
</div>
97-
) : null}
98-
9936
<div className="space-y-2">
10037
<p className="text-[12px] font-[600] uppercase tracking-[0.12em] text-[var(--text-quaternary)]">Sources</p>
10138
<Select

0 commit comments

Comments
 (0)