4.0 KiB
Fix: OpenCode combo context window detection
Bug
OpenCode v1's opencode.json requires an explicit limit.context (and
limit.output) for every model. Without these fields, OpenCode's heuristic
kicks in and reports a wrong context window.
The OmniRoute /v1/models catalog is the single source of truth for context
windows. The bug was that some combos were published to the catalog WITHOUT
a computed context_length, so any OpenCode client pulling the catalog got
no answer for them.
Root cause
The "Opencode FREE Omni" combo references four opencode/<model> targets
(big-pickle, deepseek-v4-flash-free, minimax-m3-free,
nemotron-3-super-free). The catalog's buildComboCatalogMetadata computes
the combo's context_length as the minimum of its targets' known
contexts, but every target's lookup was returning null.
The lookup chain in getCanonicalModelMetadata:
getSyncedCapability("opencode", "big-pickle")→ returnsnullbecause the DB row is stored underprovider = "opencode-zen", not"opencode".getRegistryModel("opencode", "big-pickle")→ returnsnullbecausePROVIDER_MODELS["opencode"](and["oc"]) don't have static entries for these models.getModelSpec("big-pickle")→ returnsnull(no static spec).
All three lookups fail → metadata is null → combo has no context.
Why was the DB row under "opencode-zen" and not "opencode"?
MODELS_DEV_PROVIDER_MAP["opencode"] was ["opencode-zen"] only — a
historical one-way mapping. New syncs continued to write under the alias
side of the pair, while the catalog & combo targets use the canonical id
side.
Fix (3 layers, in this order)
1. src/lib/modelsDevSync.ts — symmetric mapProviderId mapping
// Before:
opencode: ["opencode-zen"],
"opencode-go": ["opencode-go"],
// After:
opencode: ["opencode", "opencode-zen"],
"opencode-go": ["opencode-go", "opencode-zen"],
Now models.dev data lands under BOTH the canonical id and the historical alias, so any future sync keeps the lookup paths in sync.
2. src/lib/modelsDevSync.ts — alias-aware fallback in getSyncedCapability
The runtime fix that takes effect immediately, without waiting for a
re-sync. Existing DB rows under "opencode-zen" are now found when
callers pass "opencode" (or vice-versa).
const SYNCED_CAPABILITY_FALLBACK_ALIASES: Record<string, string[]> = {
opencode: ["opencode-zen"],
"opencode-zen": ["opencode"],
"opencode-go": ["opencode-zen"],
};
3. src/lib/cli-helper/config-generator/opencode.ts — drop the hardcoded 128K fallback
The previous band-aid hardcoded FALLBACK_CONTEXT_LENGTH = 128_000 for
models whose context was unknown. That's wrong: combos like "Opencode
FREE Omni" should report 200K (the min of their 200K targets), not
the universal 128K default.
The generator now:
- Uses the catalog as the single source of truth for
limit.context. - Emits the model without
limit.contextif the catalog has no entry — OpenCode's own heuristic applies and the user can fix the upstream. - Throws if the catalog fetch fails outright — the CLI catches and surfaces the error. We never silently write a stale opencode.json.
Deployment steps (for the operator)
- Pull the latest from the branch:
git fetch && git checkout fix/opencode-context-window - Rebuild OmniRoute:
npm run build - Restart the OmniRoute server (kill the running process and re-run).
- Trigger a models.dev sync from the Settings → Models.dev panel
(or POST
/api/settings/models-devwith{"action": "sync"}). - Re-run the opencode.json generator (the CLI command or the
scripts/regen-opencode-config.tsscript).
After step 5, Opencode FREE Omni's limit.context will be 200000
and every other combo will reflect its targets' min context.
Verification
After the rebuild, hit GET /v1/models and inspect the response:
curl -s http://localhost:20128/v1/models \
-H "Authorization: Bearer $API_KEY" \
| jq '.data[] | select(.id == "Opencode FREE Omni") | .context_length'
# → 200000