Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -1468,3 +1468,16 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# QUOTA_SATURATION_THRESHOLD=0.5 # 0..1; >= threshold ativa modo strict (sem empréstimo)
# QUOTA_SOFT_DEPRIORITIZE_FACTOR=0.7 # 0..1; multiplicador do score quando soft policy ativa
# QUOTA_CONSUMPTION_RETENTION_DAYS=14 # GC de buckets quota_consumption.updated_at antigos

# ─── OpenCode config regeneration (scripts/ad-hoc/regen-opencode-config.ts) ───
# Base URL of the OmniRoute instance to query for /v1/models when regenerating
# an opencode.json with accurate limit.context values. Used by:
# scripts/ad-hoc/regen-opencode-config.ts. Default: http://localhost:20128
# OMNIROUTE_URL=
# API key to authenticate against the OmniRoute /v1/models endpoint. Falls back
# to OPENCODE_API_KEY when unset. Used by: scripts/ad-hoc/regen-opencode-config.ts.
# OMNIROUTE_KEY=
# OpenCode-style API key (sk-...) for the regenerated opencode.json. Used by:
# scripts/ad-hoc/regen-opencode-config.ts. Falls back to OMNIROUTE_KEY.
# OPENCODE_API_KEY=

108 changes: 108 additions & 0 deletions docs/fix-opencode-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# 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`:

1. `getSyncedCapability("opencode", "big-pickle")` → returns `null` because
the DB row is stored under `provider = "opencode-zen"`, not `"opencode"`.
2. `getRegistryModel("opencode", "big-pickle")` → returns `null` because
`PROVIDER_MODELS["opencode"]` (and `["oc"]`) don't have static entries
for these models.
3. `getModelSpec("big-pickle")` → returns `null` (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

```ts
// 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).

```ts
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.context` if 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)

1. Pull the latest from the branch:
`git fetch && git checkout fix/opencode-context-window`
2. Rebuild OmniRoute: `npm run build`
3. Restart the OmniRoute server (kill the running process and re-run).
4. Trigger a models.dev sync from the Settings → Models.dev panel
(or POST `/api/settings/models-dev` with `{"action": "sync"}`).
5. Re-run the opencode.json generator (the CLI command or the
`scripts/regen-opencode-config.ts` script).

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:

```bash
curl -s http://localhost:20128/v1/models \
-H "Authorization: Bearer $API_KEY" \
| jq '.data[] | select(.id == "Opencode FREE Omni") | .context_length'
# → 200000
```
13 changes: 13 additions & 0 deletions docs/reference/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -970,3 +970,16 @@ The following variables appeared in previous versions of `.env.example` but have
| ------------------------- | ------------------------ | ------------------- | ------------------------------------------------------ |
| `APP_LOG_RETENTION_DAYS` | `90` | `7` | ✅ Removed misleading value; documented `7` as default |
| `CALL_LOG_RETENTION_DAYS` | `90` | `7` | ✅ Removed misleading value; documented `7` as default |

### OpenCode config regeneration (ad-hoc tooling)

Used by `scripts/ad-hoc/regen-opencode-config.ts` to regenerate an `opencode.json`
with accurate `limit.context` and `limit.output` values pulled from the running
OmniRoute instance. None of these are required for normal operation — the script
is developer tooling only.

| Variable | Default | Source File | Description |
| ------------------- | ------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `OMNIROUTE_URL` | `http://localhost:20128` | `scripts/ad-hoc/regen-opencode-config.ts` | Base URL of the OmniRoute instance to query for `/v1/models`. |
| `OMNIROUTE_KEY` | _(unset)_ | `scripts/ad-hoc/regen-opencode-config.ts` | API key to authenticate against the OmniRoute `/v1/models` endpoint. Falls back to `OPENCODE_API_KEY` when unset. |
| `OPENCODE_API_KEY` | _(unset)_ | `scripts/ad-hoc/regen-opencode-config.ts` | OpenCode-style API key (`sk-...`) written into the regenerated `opencode.json`. Falls back to `OMNIROUTE_KEY` when unset. |
23 changes: 23 additions & 0 deletions scripts/ad-hoc/regen-opencode-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the Repository Style Guide (Section 1: File Placement & Organization), all maintenance, debugging, generation, or experimental scripts must be placed strictly inside one of the scripts/ subfolders (such as scripts/ad-hoc/ or scripts/dev/). Loose scripts must never be dumped in the top-level scripts/ folder. Please move this script to scripts/ad-hoc/regen-opencode-config.ts.

References
  1. ALL maintenance, debugging, generation, or experimental scripts MUST be placed strictly inside one of the scripts/ subfolders. NEVER dump loose scripts in the project root or the top-level scripts/ folder. (link)

* One-shot regen of ~/.config/opencode/opencode.json from the live
* OmniRoute /v1/models catalog. Run after a catalog change to refresh
* the opencode client.
*
* Usage: bun run scripts/regen-opencode-config.ts
* or npx tsx scripts/regen-opencode-config.ts
*/
import { generateOpencodeConfig } from "../src/lib/cli-helper/config-generator/opencode.ts";

const baseURL = process.env.OMNIROUTE_URL ?? "http://localhost:20128";
const apiKey = process.env.OMNIROUTE_KEY ?? process.env.OPENCODE_API_KEY ?? "";

if (!apiKey) {
console.error(
"OMNIROUTE_KEY (or OPENCODE_API_KEY) env var is required. " +
"Find it in OmniRoute dashboard → Settings → API Keys."
);
process.exit(1);
}

const out = await generateOpencodeConfig({ baseUrl: baseURL, apiKey });
console.log(out);
Loading