A guided explanation is only useful in a language the reader reads, so the panel header gets a language picker alongside the model one, defaulting to the interface language. Like the model, it is request state rather than a setting: the language travels with the read and the generation, and the one a walkthrough was written in is stored with it, so reopening a review describes what is there instead of what a fresh one would be. Only prose is translated. Hunk aliases resolve back to hunk ids and icon/importance are validated against fixed English values, so a translated one would be dropped by the normalizer — silently losing an anchor or a style. Identifiers and paths stay as they appear in the code. The language is part of the cache key, and a read now asks the cache for the exact request it was given before falling back to the pointer. Without that the panel answered a request to switch languages with the text it already had, leaving the other language unused in the cache. Alongside it: - The answer budget is derived from the resolved model instead of a flat 24k. That number was the same for a 64k-context model and for one that admits to 384k output tokens, and on the latter it was the only reason generation failed: the model spent the whole allowance reasoning and returned nothing. It is now min(96k, max(24k, a quarter of the context)) capped by the catalog's output limit, decided once so the input reserve and the request cannot drift apart. - A read no longer offers Cancel. It is a few hundred milliseconds of git with nothing to cancel, and the button flickered on every model or language change. When the panel is showing a fallback, a banner names what is on screen versus what was asked for — only once the read has settled. - The header keeps one 32px control height and drops its labels below 680px instead of squeezing them to two letters and an ellipsis. Docs and module documentation updated in every locale.
132 lines
8.0 KiB
Markdown
132 lines
8.0 KiB
Markdown
# Small Model
|
|
|
|
Server-side direct LLM calls that reuse the user's existing OpenCode provider
|
|
logins (`~/.local/share/opencode/auth.json`). OpenCode uses a "small model"
|
|
internally (titles, summaries) but does not expose it through the SDK or
|
|
plugins — this module replicates that mechanism as an OpenChamber runtime API.
|
|
|
|
## Security boundary
|
|
|
|
Credentials never leave the server process. The client sends only a prompt;
|
|
auth resolution, OAuth refresh, and provider dispatch all happen server-side.
|
|
Routes live under `/api/*` and are gated by the ui-auth middleware like every
|
|
other runtime API.
|
|
|
|
## Files
|
|
|
|
- `index.js` — orchestration: `generateSmallModelText()` / `describeSmallModel()`.
|
|
- `resolve.js` — model selection, mirroring OpenCode's `getSmallModel` chain:
|
|
0. OpenChamber's own settings override (Settings → Sessions → Small Model):
|
|
when `smallModelUseDefault` is `false`, `smallModelOverride`
|
|
(`provider/model`) outranks everything below. Sanitized in
|
|
`settings-helpers.js` (server), `persistence.ts` (client), and
|
|
`bridge-settings-runtime.ts` (VS Code).
|
|
1. `small_model` from the merged OpenCode config layers (`provider/model`).
|
|
2. Family-priority scan (`gemini-flash` → `gpt-nano` → `claude-haiku`)
|
|
**within the session's provider first** (`preferredProviderID`, like
|
|
OpenCode resolves within the current provider), then over the other
|
|
providers with a usable auth entry, newest `release_date` first.
|
|
3. GitHub Copilot hidden utility models (`gpt-*-nano/mini`) — these never
|
|
appear in the catalog, so they participate as the `gpt-nano` family entry
|
|
and as a final utility fallback.
|
|
4. Last resort: the session's own model (`preferredModelID`) when no small
|
|
model resolves anywhere — costlier, but always valid.
|
|
- Input clamp: the prompt is measured against the resolved model's catalog
|
|
`limit.context` (minus an output reserve, ~4 chars/token estimate;
|
|
conservative default when the model is not in the catalog). `onOverflow`
|
|
decides what an oversized prompt means:
|
|
- `truncate` (default) clips the tail and reports `inputTruncated: true`.
|
|
Correct for callers that degrade gracefully (summaries, commit messages).
|
|
- `error` throws a `413` with `code: 'context-too-small'` plus
|
|
`requiredChars`/`availableChars`. Correct for callers whose output would be
|
|
quietly wrong on a clipped input, so they can ask the user for a roomier
|
|
model instead of returning confident nonsense.
|
|
- Structured output: pass `responseSchema` (a JSON Schema) to get
|
|
schema-shaped JSON back as `text`. Wire support differs per format —
|
|
`response_format: {type: 'json_schema'}` for OpenAI-compatible chat,
|
|
`text.format` for the Responses API, a forced single tool call for the
|
|
Anthropic messages API, and `generationConfig.responseSchema` for Google
|
|
(whose OpenAPI-flavored dialect drops unknown JSON Schema keywords). The
|
|
ChatGPT-plan codex backend has no equivalent and rejects a schema request
|
|
with `code: 'structured-output-unsupported'` rather than silently returning
|
|
prose.
|
|
- Output budget: `maxOutputTokens` is capped at the catalog's `limit.output` for
|
|
the model, and the **same number** is reserved from the input allowance. The
|
|
two must not drift — a caller that asks for a large answer while the reserve
|
|
stays at the default overruns the context, and the failure looks like a
|
|
truncation bug rather than a budgeting one. `describeSmallModel` takes
|
|
`outputReserveTokens` so readiness checks agree with what generation will do.
|
|
It may be a **function** of `{ contextTokens, outputTokenLimit }` for callers
|
|
that want as much answer room as the resolved model allows — they cannot name
|
|
a number before knowing which model they got. The resolved value comes back as
|
|
`outputTokens`, which is what the caller should then request, so the reserve
|
|
and the request are the same number by construction.
|
|
- Reasoning models can spend the entire output budget thinking and return
|
|
nothing. That case (empty content with `finish_reason: 'length'`, or content
|
|
empty while `reasoning_content` is populated) throws with
|
|
`code: 'output-exhausted'` so callers can offer a different model instead of
|
|
showing a transport error.
|
|
- `timeoutMs` overrides the 60s default per call; `signal` lets a caller abort
|
|
a request that is no longer wanted. Both apply to every wire format.
|
|
- `describeSmallModel()` additionally reports `inputCharBudget`,
|
|
`contextTokens`, `contextKnown`, and `structuredOutput`. The last is
|
|
tri-state: `true`/`false` from the catalog, `null` when the catalog omits the
|
|
field — which it does for roughly half of all models, aggregators and proxies
|
|
especially. Callers must treat `null` as "try it", not "unsupported".
|
|
- `call.js` — wire formats and per-provider auth, replicating OpenCode's
|
|
plugin auth loaders:
|
|
- **GitHub Copilot**: fetches the requested model's authenticated `/models`
|
|
metadata from `https://api.githubcopilot.com` (or
|
|
`copilot-api.<enterprise>`) and honors its advertised endpoint, preferring
|
|
Anthropic-compatible `/v1/messages`, then OpenAI `/responses`, then
|
|
`/chat/completions`. Models without `supported_endpoints` retain the legacy
|
|
Chat Completions default; metadata, missing-model, and unsupported-endpoint
|
|
failures are surfaced instead of guessing. The stored device-OAuth token is
|
|
used as the bearer with no token exchange or expiry.
|
|
- **OpenAI OAuth (ChatGPT plan)**: streaming Responses API on
|
|
`https://chatgpt.com/backend-api/codex/responses` with
|
|
`ChatGPT-Account-Id`; expired tokens are refreshed against
|
|
`auth.openai.com` (single-flight) and written back to `auth.json`.
|
|
- **Anthropic** (`type: api`): `/v1/messages` with `x-api-key`.
|
|
- **Google** (`type: api`): `generateContent` with `x-goog-api-key`; Gemini 3
|
|
uses `thinkingLevel` while older Flash models use `thinkingBudget: 0`.
|
|
- Everything else: OpenAI-compatible `/chat/completions` against the
|
|
provider's base URL, resolved from (1) `provider.<id>.options.baseURL`
|
|
in the OpenCode config, (2) the hardcoded `https://api.openai.com/v1`
|
|
endpoint, or (3) the provider's `api` field from the models.dev catalog.
|
|
Configured API keys honor OpenCode's `{env:NAME}` and `{file:path}`
|
|
substitutions; file contents and resolved credentials remain server-side.
|
|
- `[small-model:diagnostic]` logs record provider/model, input character
|
|
counts, output budget, thinking toggle, HTTP/finish status, and
|
|
content/reasoning lengths without logging prompts, response text, or
|
|
credentials. Goal audit parsing similarly emits
|
|
`[session-goal:diagnostic]` structural verdict metadata.
|
|
- `catalog.js` — models.dev catalog via the shared in-process cache
|
|
(`../opencode/models-metadata.js`, also serving
|
|
`/api/openchamber/models-metadata`).
|
|
- `routes.js` — `GET /api/small-model` (resolution preview) and
|
|
`POST /api/small-model/generate` (`{ prompt, system?, maxOutputTokens?,
|
|
model?, directory? }` → `{ text, providerID, modelID, source }`).
|
|
|
|
## Registration
|
|
|
|
Mounted lazily from `feature-routes-runtime.js` (same pattern as quota): the
|
|
module is imported on first request, not at server startup.
|
|
|
|
## Known limitations
|
|
|
|
- OpenCode's free models (`opencode/big-pickle`, `*-free`) work without a
|
|
token only through OpenCode's own server — direct calls are rejected, and
|
|
piggybacking on their subsidized infra is out of bounds by design. Every
|
|
resolution step therefore requires a usable auth entry for the provider:
|
|
a session on an unauthenticated `opencode` provider falls through to the
|
|
global scan (or a clean 404 on a vanilla setup with no logins).
|
|
|
|
- Anthropic OAuth (Claude Pro/Max) entries are not supported — OpenCode itself
|
|
keeps those outside `auth.json` in this generation; only `type: api` keys
|
|
work for Anthropic.
|
|
- Amazon Bedrock, GitLab, Azure and other credential-chain providers are out
|
|
of scope; they need more than a key/token (regions, resource names).
|
|
- Responses from the codex backend are collected from the SSE stream; the
|
|
endpoint itself is non-streaming by design (small utility calls).
|