fix(opencode): price long-context steps at the catalog tier, including config-named models

Merged
GO
2mo ago
+106-1

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 tiers from 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, but Session.getUsage consults tiers before experimentalOver200K. 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 existing cost() converter.
  • models.dev spells a model's single long-context tier as context_over_200k even when its tiers put the threshold at 272K, and Session.getUsage falls 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 sets experimentalOver200K only when the model has no tiers, which keeps catalog-only and config-named models in agreement. Upstream tracks this as anomalyco/opencode#44224.

Testing Done

  • Three new it.instance cases in test/provider/provider.test.ts, each priced through Session.getUsage: a config entry that only names openrouter/openai/gpt-5.4 keeps 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 own context_over_200k replaces the catalog's 272K tier with a 200K tier and prices 300K at the configured rate; a config override on x-ai/grok-4 keeps 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 typecheck clean.

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.


Devin Review

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_200k is used only when the model has no explicit context tiers.
  • Provider tests cover pricing at 100K, 150K, 250K, and 300K context; bun run typecheck passes.