diff --git a/lib/pricing.ts b/lib/pricing.ts index 3bf6a0bb2..c3d547399 100644 --- a/lib/pricing.ts +++ b/lib/pricing.ts @@ -20,7 +20,8 @@ * start a priced run on such a model (bin/gstack-eval-plan dry-run). * - Anthropic: https://www.anthropic.com/pricing#api * - OpenAI: https://openai.com/api/pricing/ - * - Google AI: https://ai.google.dev/pricing + * - Google AI: https://ai.google.dev/gemini-api/docs/pricing + * - Voyage AI (embeddings): https://docs.voyageai.com/docs/pricing */ export interface ModelPricing { @@ -45,7 +46,10 @@ export const PRICING: Record = { 'claude-fable-5-1': { input_per_mtok: 10.00, output_per_mtok: 50.00, cached_input_per_mtok: 0.25, as_of: '2026-09' }, // https://platform.claude.com/docs/en/models/opus-5-5/overview#pricing (list prices effective 2026-10-07) 'claude-opus-5-5': { input_per_mtok: 4.00, output_per_mtok: 20.00, cached_input_per_mtok: 0.20, as_of: '2026-10' }, - 'claude-opus-4-7': { input_per_mtok: 15.00, output_per_mtok: 75.00, as_of: '2026-04' }, + // https://platform.claude.com/docs/en/about-claude/pricing (fetched 2026-10-12) + 'claude-opus-4-7': { input_per_mtok: 5.00, output_per_mtok: 25.00, cached_input_per_mtok: 0.50, as_of: '2026-10' }, + // https://platform.claude.com/docs/en/about-claude/pricing (fetched 2026-10-12) + 'claude-sonnet-5-5': { input_per_mtok: 2.00, output_per_mtok: 10.00, cached_input_per_mtok: 0.10, as_of: '2026-10' }, 'claude-sonnet-4-6': { input_per_mtok: 3.00, output_per_mtok: 15.00, as_of: '2026-04' }, 'claude-haiku-4-5': { input_per_mtok: 1.00, output_per_mtok: 5.00, as_of: '2026-04' }, @@ -60,14 +64,46 @@ export const PRICING: Record = { 'o4-mini': { input_per_mtok: 1.10, output_per_mtok: 4.40, as_of: '2026-04' }, // Google + // https://ai.google.dev/gemini-api/docs/pricing (fetched 2026-10-12; standard tier through 2026-12-31, + // the page lists $1.50 / $7.50 / $0.15 cached starting 2027-01-01) + 'gemini-3.8-flash': { input_per_mtok: 0.75, output_per_mtok: 3.75, cached_input_per_mtok: 0.075, as_of: '2026-10' }, + // https://ai.google.dev/gemini-api/docs/pricing (fetched 2026-10-12; same schedule as gemini-3.8-flash) + 'gemini-3.6-flash': { input_per_mtok: 0.75, output_per_mtok: 3.75, cached_input_per_mtok: 0.075, as_of: '2026-10' }, + // https://ai.google.dev/gemini-api/docs/pricing (fetched 2026-10-12; prompts <= 200k tokens, the long-context rate is excluded) + 'gemini-3.1-pro-preview': { input_per_mtok: 2.00, output_per_mtok: 12.00, cached_input_per_mtok: 0.20, as_of: '2026-10' }, 'gemini-2.5-pro': { input_per_mtok: 1.25, output_per_mtok: 5.00, as_of: '2026-04' }, 'gemini-2.5-flash': { input_per_mtok: 0.30, output_per_mtok: 1.20, as_of: '2026-04' }, }; -/** A provider-prefixed id (`openai/gpt-6-astra`, `anthropic/claude-fable-5-1`) resolves to its table row. */ +/** + * Embedding models bill input tokens only (output_per_mtok 0), so they live + * beside PRICING rather than in it: eval arms never resolve to an embedding + * model, but a call plan (docs/eval-calls.md) prices gbrain's embedding calls. + */ +export const EMBEDDING_PRICING: Record = { + // https://docs.voyageai.com/docs/pricing (fetched 2026-10-12; list price after the free-token allowance) + 'voyage-4': { input_per_mtok: 0.06, output_per_mtok: 0, as_of: '2026-10' }, + 'voyage-4-large': { input_per_mtok: 0.12, output_per_mtok: 0, as_of: '2026-10' }, + 'voyage-4-lite': { input_per_mtok: 0.02, output_per_mtok: 0, as_of: '2026-10' }, + 'voyage-code-4': { input_per_mtok: 0.12, output_per_mtok: 0, as_of: '2026-10' }, + 'voyage-code-3': { input_per_mtok: 0.18, output_per_mtok: 0, as_of: '2026-10' }, + 'voyage-3-large': { input_per_mtok: 0.18, output_per_mtok: 0, as_of: '2026-10' }, + 'voyage-multimodal-3': { input_per_mtok: 0.12, output_per_mtok: 0, as_of: '2026-10' }, // text tokens; pixels excluded + // https://developers.openai.com/api/docs/pricing (fetched 2026-10-12) + 'text-embedding-3-large': { input_per_mtok: 0.13, output_per_mtok: 0, as_of: '2026-10' }, + 'text-embedding-3-small': { input_per_mtok: 0.02, output_per_mtok: 0, as_of: '2026-10' }, + // https://ai.google.dev/gemini-api/docs/pricing (fetched 2026-10-12; text input, standard tier) + 'gemini-embedding-2': { input_per_mtok: 0.20, output_per_mtok: 0, as_of: '2026-10' }, +}; + +/** + * A provider-prefixed id (`openai/gpt-6-astra`, `anthropic/claude-fable-5-1`, + * or gbrain's `voyage:voyage-4`) resolves to its table row. + */ export function pricingRow(model: string | undefined): ModelPricing | undefined { if (!model) return undefined; - return PRICING[model] ?? PRICING[model.replace(/^[a-z-]+\//, '')]; + const bare = model.replace(/^[a-z-]+[/:]/, ''); + return PRICING[model] ?? PRICING[bare] ?? EMBEDDING_PRICING[model] ?? EMBEDDING_PRICING[bare]; } export function hasPricing(model: string | undefined): boolean { diff --git a/test/benchmark-runner.test.ts b/test/benchmark-runner.test.ts index 993de4759..f8e627960 100644 --- a/test/benchmark-runner.test.ts +++ b/test/benchmark-runner.test.ts @@ -23,10 +23,10 @@ test('estimateCostUsd returns 0 for unknown model (no crash)', () => { }); test('estimateCostUsd computes correctly for known Claude model', () => { - // claude-opus-4-7: $15/MTok input, $75/MTok output - // 1M input + 0.5M output = $15 + $37.50 = $52.50 + // claude-opus-4-7: $5/MTok input, $25/MTok output (platform.claude.com pricing, fetched 2026-10-12) + // 1M input + 0.5M output = $5 + $12.50 = $17.50 const cost = estimateCostUsd({ input: 1_000_000, output: 500_000 }, 'claude-opus-4-7'); - expect(cost).toBeCloseTo(52.50, 2); + expect(cost).toBeCloseTo(17.50, 2); }); for (const model of [CLAUDE_FRONTIER_EVAL_MODEL, CODEX_FRONTIER_MODEL]) { @@ -48,12 +48,12 @@ test('Astra cache reads use its standard cached-input rate', () => { test('estimateCostUsd applies cached input discount alongside uncached input', () => { // tokens.input is uncached-only; tokens.cached is disjoint cache-reads at 10%. - // 0 uncached input, 1M cached → 10% of 15 = $1.50 + // 0 uncached input, 1M cached → 10% of 5 = $0.50 const cost1 = estimateCostUsd({ input: 0, output: 0, cached: 1_000_000 }, 'claude-opus-4-7'); - expect(cost1).toBeCloseTo(1.50, 2); - // 500K uncached input + 500K cached → $7.50 + $0.75 = $8.25 + expect(cost1).toBeCloseTo(0.50, 2); + // 500K uncached input + 500K cached → $2.50 + $0.25 = $2.75 const cost2 = estimateCostUsd({ input: 500_000, output: 0, cached: 500_000 }, 'claude-opus-4-7'); - expect(cost2).toBeCloseTo(8.25, 2); + expect(cost2).toBeCloseTo(2.75, 2); }); test('PRICING table covers the key model families', () => { diff --git a/test/pricing-eval-plan.test.ts b/test/pricing-eval-plan.test.ts index d6e8e413a..399288e31 100644 --- a/test/pricing-eval-plan.test.ts +++ b/test/pricing-eval-plan.test.ts @@ -156,3 +156,15 @@ describe('priced pilot', () => { expect(r.projection.verdict).toBe('unknown'); }); }); + +describe('pricing rows with fetched sources (2026-10-12)', () => { + test('the newest Sonnet, the newest Gemini Flash and gbrain embedding models are priced; Opus 4.7 is at its current list price', () => { + expect(hasPricing('anthropic/claude-sonnet-5-5')).toBe(true); + expect(estimateCostUsd({ input: 1_000_000, output: 1_000_000 }, 'claude-sonnet-5-5')).toBe(12); + expect(estimateCostUsd({ input: 1_000_000, output: 1_000_000 }, 'claude-opus-4-7')).toBe(30); + expect(hasPricing('google/gemini-3.8-flash')).toBe(true); + for (const m of ['voyage:voyage-4', 'openai:text-embedding-3-large', 'text-embedding-3-small', 'gemini-embedding-2']) expect(hasPricing(m)).toBe(true); + expect(estimateCostUsd({ input: 1_000_000, output: 0 }, 'voyage:voyage-4')).toBe(0.06); + expect(hasPricing('voyage-4-nano')).toBe(false); + }); +});