mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* fix(pricing): unify chat-model pricing into one canonical source; add Opus 4.8 (#1819) Single canonical CANONICAL_PRICING table (src/core/model-pricing.ts) with canonicalLookup; ANTHROPIC_PRICING and takes-quality MODEL_PRICING become derived views. cost-tracker, cross-modal runner, skillopt preflight, brainstorm orchestrator, and brain-score all source from it. Adds Opus 4.8 ($5/$25) so --max-cost-usd and the dream-cycle budget meter enforce on 4.8 runs; fixes a stale Opus 4.7 $15/$75 in the takes-quality gate and reconciles Gemini 2.0 Flash to $0.10/$0.40. Because every table derives from canonical, cross-table price drift is structurally impossible. * chore: bump version and changelog (v0.42.25.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: document canonical chat-pricing table for v0.42.25.0 Add KEY_FILES.md entries for src/core/model-pricing.ts (canonical CANONICAL_PRICING + canonicalLookup) and refresh the now-derived anthropic-pricing.ts + takes-quality-eval/pricing.ts entries to current-state. Add the "one canonical chat-pricing table" cross-cutting invariant to CLAUDE.md. Fix the stale model-price snapshot pointer in SEARCH_MODE_METHODOLOGY.md (anthropic-pricing.ts -> model-pricing.ts). Regenerate llms-full.txt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
76 lines
3.5 KiB
TypeScript
76 lines
3.5 KiB
TypeScript
/**
|
|
* v0.41.20.0 — pin estimateMaxCostUsd across bare/colon/slash/unknown ids.
|
|
*
|
|
* No prior coverage existed for this helper. The slash-form bug class
|
|
* (#1540) refired here for OpenRouter and CLI `--judge-model` users
|
|
* before this fix; this file pins the centralized parse path so any
|
|
* future refactor of parseModelId or estimateMaxCostUsd can't silently
|
|
* drop slash-form support.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { ANTHROPIC_PRICING, estimateMaxCostUsd } from '../src/core/anthropic-pricing.ts';
|
|
|
|
describe('estimateMaxCostUsd', () => {
|
|
// Sonnet 4.6 = $3 input / $15 output per MTok.
|
|
// 1M input + 0 output → $3.00
|
|
// 0 input + 1M output → $15.00
|
|
|
|
test('bare key claude-sonnet-4-6 → hits pricing', () => {
|
|
const cost = estimateMaxCostUsd('claude-sonnet-4-6', 1_000_000, 0);
|
|
expect(cost).toBeCloseTo(3.0, 5);
|
|
});
|
|
|
|
test('colon-prefixed anthropic:claude-sonnet-4-6 → hits pricing via tail', () => {
|
|
const cost = estimateMaxCostUsd('anthropic:claude-sonnet-4-6', 1_000_000, 0);
|
|
expect(cost).toBeCloseTo(3.0, 5);
|
|
});
|
|
|
|
test('slash-prefixed anthropic/claude-sonnet-4-6 → hits pricing via tail (THE FIX)', () => {
|
|
// Pre-v0.41.20.0: this returned null because the inline split only
|
|
// handled `:`. CLI `--judge-model anthropic/...` + `--max-cost N` then
|
|
// hit BudgetTracker no_pricing fail-closed.
|
|
const cost = estimateMaxCostUsd('anthropic/claude-sonnet-4-6', 1_000_000, 0);
|
|
expect(cost).toBeCloseTo(3.0, 5);
|
|
});
|
|
|
|
test('mixed input + output cost math', () => {
|
|
// 100K input + 50K output for opus 4.7 ($5/$25)
|
|
// = 0.1 * 5 + 0.05 * 25 = 0.5 + 1.25 = 1.75
|
|
const cost = estimateMaxCostUsd('anthropic/claude-opus-4-7', 100_000, 50_000);
|
|
expect(cost).toBeCloseTo(1.75, 5);
|
|
});
|
|
|
|
test('opus 4.8 priced same as 4.7 ($5/$25) — closes #1819', () => {
|
|
// 100K in + 50K out = 0.1*5 + 0.05*25 = 0.5 + 1.25 = 1.75. Pre-fix this
|
|
// model was absent from the pricing table → estimateMaxCostUsd returned
|
|
// null and the dream-cycle budget gate silently no-op'd on 4.8 runs.
|
|
const cost = estimateMaxCostUsd('anthropic:claude-opus-4-8', 100_000, 50_000);
|
|
expect(cost).toBeCloseTo(1.75, 5);
|
|
});
|
|
|
|
test('unknown model → returns null (caller warn-once + bypass)', () => {
|
|
expect(estimateMaxCostUsd('mistral:medium', 1_000, 1_000)).toBeNull();
|
|
expect(estimateMaxCostUsd('gpt-5', 1_000, 1_000)).toBeNull();
|
|
});
|
|
|
|
test('OpenRouter nested form returns null — tail is `anthropic/claude-...` which is not a pricing key', () => {
|
|
// Per D2 architecture: parseModelId returns {provider:'openrouter',
|
|
// model:'anthropic/claude-sonnet-4-6'}; lookup on the tail
|
|
// 'anthropic/claude-sonnet-4-6' misses (table has bare 'claude-sonnet-4-6').
|
|
// OpenRouter pricing is intentionally out of scope (TODO #2).
|
|
expect(estimateMaxCostUsd('openrouter:anthropic/claude-sonnet-4-6', 1_000, 1_000)).toBeNull();
|
|
});
|
|
|
|
test('every key in ANTHROPIC_PRICING is reachable via bare/colon/slash form', () => {
|
|
// Regression guard: if someone adds a new entry to ANTHROPIC_PRICING,
|
|
// it should be reachable via all three forms automatically (the route
|
|
// is structural, not per-key).
|
|
for (const key of Object.keys(ANTHROPIC_PRICING)) {
|
|
expect(estimateMaxCostUsd(key, 1_000_000, 0)).not.toBeNull();
|
|
expect(estimateMaxCostUsd(`anthropic:${key}`, 1_000_000, 0)).not.toBeNull();
|
|
expect(estimateMaxCostUsd(`anthropic/${key}`, 1_000_000, 0)).not.toBeNull();
|
|
}
|
|
});
|
|
});
|