fix(budget): price non-Anthropic chat models via the canonical table (takeover of #2127)

lookupPricing consulted only ANTHROPIC_PRICING for chat/rerank, so any
openai:/google:/deepseek: model hit BudgetExhausted(no_pricing) BEFORE the
provider call whenever a cost cap was set. Fall through to canonicalLookup,
per the one-canonical-pricing-table invariant. The original PR's $0
CANONICAL_PRICING entries for openrouter: ids are dropped — openrouter-
prefixed ids intentionally MISS (markup != native pricing), and a $0
canonical entry would zero cost accounting for everyone routing those
models; free-route pricing belongs in per-install config.

Co-authored-by: troyhoffman-oss <troyhoffman-oss@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-21 14:50:54 -07:00
co-authored by troyhoffman-oss Claude Fable 5
parent 2de65136c5
commit 2307a827d8
2 changed files with 41 additions and 0 deletions
+10
View File
@@ -32,6 +32,7 @@ import { mkdirSync, appendFileSync } from 'node:fs';
import { dirname } from 'node:path';
import { gbrainPath } from '../config.ts';
import { ANTHROPIC_PRICING, type ModelPricing } from '../anthropic-pricing.ts';
import { canonicalLookup } from '../model-pricing.ts';
import { EMBEDDING_PRICING, lookupEmbeddingPrice } from '../embedding-pricing.ts';
import { splitProviderModelId } from '../model-id.ts';
import { isoWeekFilename, resolveAuditDir } from '../audit-week-file.ts';
@@ -194,6 +195,15 @@ function lookupPricing(modelId: string, kind: BudgetKind): ModelPricing | null {
const tailHit = ANTHROPIC_PRICING[modelTail];
if (tailHit) return tailHit;
}
// Non-Anthropic chat models (openai:, google:, deepseek:, ...) live in the
// canonical pricing table, not the Anthropic-only ANTHROPIC_PRICING view.
// Without this fall-through every non-Anthropic chat model returns null,
// and under a cost cap reserve() throws BudgetExhausted(no_pricing) BEFORE
// the provider call — silently zeroing extraction for those providers.
// OpenRouter-prefixed ids still MISS by design (canonicalLookup's contract:
// OpenRouter markup ≠ native pricing).
const canonical = canonicalLookup(modelId);
if (canonical) return canonical;
// v0.40.6.1: zero-price local-inference rerank providers so the budget
// tracker's TX2 hard-fail doesn't trip on `llama-server-reranker:<model>`
// under `--max-cost`. Only the rerank kind — chat/embed already have
+31
View File
@@ -170,6 +170,37 @@ describe('BudgetTracker.reserve', () => {
).not.toThrow();
});
test('non-Anthropic chat model under --max-cost prices via the canonical table (no no_pricing throw)', () => {
// Pre-fix: chat lookup consulted only ANTHROPIC_PRICING, so any
// openai:/google:/deepseek: chat model hit TX2 no_pricing BEFORE the
// provider call whenever a cost cap was set (takeover of #2127).
const t = new BudgetTracker({ maxCostUsd: 10.0, label: 'test', auditPath });
for (const modelId of ['openai:gpt-5', 'google:gemini-2.0-flash', 'deepseek:deepseek-chat']) {
expect(() =>
t.reserve({
modelId,
estimatedInputTokens: 100,
maxOutputTokens: 100,
kind: 'chat',
}),
).not.toThrow();
}
});
test('openrouter-prefixed chat model still TX2 no_pricing-fails under cap (canonical MISS contract)', () => {
// canonicalLookup intentionally misses openrouter:* ids (markup != native
// pricing); the budget gate must stay fail-closed for them.
const t = new BudgetTracker({ maxCostUsd: 10.0, label: 'test', auditPath });
expect(() =>
t.reserve({
modelId: 'openrouter:anthropic/claude-sonnet-4-6',
estimatedInputTokens: 100,
maxOutputTokens: 100,
kind: 'chat',
}),
).toThrow(BudgetExhausted);
});
test('no cap + unknown pricing: warns once per process, no throw', () => {
const t = new BudgetTracker({ label: 'test', auditPath });
expect(() =>