mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
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:
co-authored by
troyhoffman-oss
Claude Fable 5
parent
2de65136c5
commit
2307a827d8
@@ -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
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
Reference in New Issue
Block a user