fix(opencode): price long-context steps at the catalog tier, including config-named models
Steps that run above a model's long-context threshold are now priced at the provider's tier instead of the base rate, so the cost recorded on each assistant message matches what OpenRouter actually bills. The threshold is per model: OpenRouter's pricing.overrides starts at 272K for openai/gpt-5.6-terra and openai/gpt-5.4, at 200K for google/gemini-3.1-pro-preview, and anthropic/claude-sonnet-5 has none.
Context
Replo's sandbox config declares every selectable model under provider.openrouter.models (most entries are just { options: {} }). When a config entry names a model that also exists in the models.dev catalog, the merge in provider.ts rebuilds cost from input, output and cache only — tiers and experimentalOver200K never make it onto the merged model, even though the catalog carried them and session.ts would have used them.
Found while comparing GPT-5.6 Terra to Sonnet on the agent-models dashboard: our recorded Terra cost fit flat base pricing to six decimals across 2,229 steps, while OpenRouter's analytics API billed the >200K tier. Recomputing our steps with the tier landed within 5% of the invoice; flat was 18% short. Pairs with replohq/andytown#26545, which pins a 200K compaction ceiling so Terra stops reaching the tier in the first place; this change keeps the cost honest for any model that still does.
Changes
- The config merge carries
tiersfrom the catalog model, so a config entry that only names a model prices its long-context steps at the catalog tier. - When a config entry supplies its own
context_over_200k, that price has to win everywhere above 200K, butSession.getUsageconsultstiersbeforeexperimentalOver200K. The merge keeps catalog tiers below 200K and carries the configured price as a 200K context tier above them, the same shape the v1 config migration uses. The override maps through the existingcost()converter. - models.dev spells a model's single long-context tier as
context_over_200keven when itstiersput the threshold at 272K, andSession.getUsagefalls back to that field below the tier size, so a 250K step on gpt-5.x was priced at the tier rate while OpenRouter bills it at base. The catalog converter now setsexperimentalOver200Konly when the model has notiers, which keeps catalog-only and config-named models in agreement. Upstream tracks this as anomalyco/opencode#44224.
Testing Done
- Three new
it.instancecases intest/provider/provider.test.ts, each priced throughSession.getUsage: a config entry that only namesopenrouter/openai/gpt-5.4keeps the fixture's 272K tier, drops the legacy field, and prices 250K tokens at base and 300K at the tier; a config entry with its owncontext_over_200kreplaces the catalog's 272K tier with a 200K tier and prices 300K at the configured rate; a config override onx-ai/grok-4keeps the fixture's 128K tier and prices 100K, 150K and 300K at base, 128K-tier and configured rates. Each fails without its source change. - Full provider test file: 98 pass.
bun run typecheckclean.
No release is cut here; this lands in the next v1.17.14-N tag per RELEASING.md, and the andytown Dockerfile and sandbox-upgrader pins move separately.
Summary by cubic
Model cost merging now follows each provider’s context-tier thresholds instead of pricing config-named models at the base rate or applying long-context pricing too early. A configured context_over_200k price becomes the 200K tier while preserving catalog tiers below it.
- Catalog
context_over_200kis used only when the model has no explicit context tiers. - Provider tests cover pricing at 100K, 150K, 250K, and 300K context;
bun run typecheckpasses.