SPB Git

Repository Public

catalogue.ts 8.0 KB 132 lines typescript

1/**2 * Static model catalogue and versioned price table (USD per million tokens).3 *4 * Sources, read on 2026-11-10:5 *  - Anthropic: bundled Claude API reference (`shared/models.md`, `shared/model-migration.md`, `shared/prompt-caching.md`).6 *  - OpenAI: https://developers.openai.com/api/docs/models and …/pricing (standard tier).7 * `available` is always false here: only `listModels()` (live Models API) may set it to true.8 */9import type { Effort, ModelInfo, Provider, Usage } from '@aos/shared';10import { ProviderError } from './errors.js';11import type { PriceDb } from './types.js';1213const ALL_EFFORTS: Effort[] = ['low', 'medium', 'high', 'xhigh', 'max'];1415export const CATALOGUE: ModelInfo[] = [16  // Anthropic — 1M context, 128K max output, effort low…max on all four (models.md).17  { provider: 'anthropic', id: 'claude-fable-5-2', label: 'Claude Fable 5.2', tier: 'frontier', contextTokens: 1_000_000, maxOutputTokens: 128_000, inputUsd: 8, outputUsd: 40, efforts: ALL_EFFORTS, available: false },18  { provider: 'anthropic', id: 'claude-opus-5-5', label: 'Claude Opus 5.5', tier: 'main', contextTokens: 1_000_000, maxOutputTokens: 128_000, inputUsd: 4, outputUsd: 20, efforts: ALL_EFFORTS, available: false },19  { provider: 'anthropic', id: 'claude-sonnet-5-5', label: 'Claude Sonnet 5.5', tier: 'worker', contextTokens: 1_000_000, maxOutputTokens: 128_000, inputUsd: 2, outputUsd: 10, efforts: ALL_EFFORTS, available: false },20  { provider: 'anthropic', id: 'claude-haiku-5-5', label: 'Claude Haiku 5.5', tier: 'volume', contextTokens: 1_000_000, maxOutputTokens: 128_000, inputUsd: 0.1, outputUsd: 0.5, efforts: ALL_EFFORTS, available: false },21  // OpenAI — 1.05M context, 128K max output; the models page lists low…max for all four (`none` on some is outside our Effort enum).22  { provider: 'openai', id: 'gpt-6-astra', label: 'GPT-6 Astra', tier: 'frontier', contextTokens: 1_050_000, maxOutputTokens: 128_000, inputUsd: 10, outputUsd: 50, efforts: ALL_EFFORTS, available: false },23  { provider: 'openai', id: 'gpt-6.1-sol', label: 'GPT-6.1 Sol', tier: 'main', contextTokens: 1_050_000, maxOutputTokens: 128_000, inputUsd: 2, outputUsd: 10, efforts: ALL_EFFORTS, available: false },24  { provider: 'openai', id: 'gpt-6.1-terra', label: 'GPT-6.1 Terra', tier: 'worker', contextTokens: 1_050_000, maxOutputTokens: 128_000, inputUsd: 1, outputUsd: 5, efforts: ALL_EFFORTS, available: false },25  { provider: 'openai', id: 'gpt-6-luna', label: 'GPT-6 Luna', tier: 'volume', contextTokens: 1_050_000, maxOutputTokens: 128_000, inputUsd: 0.1, outputUsd: 0.5, efforts: ALL_EFFORTS, available: false },26];2728export function catalogueEntry(provider: Provider, model: string): ModelInfo | undefined {29  return CATALOGUE.find((m) => m.provider === provider && m.id === model);30}3132export interface PriceTier {33  inputUsd: number;34  outputUsd: number;35  cacheReadUsd: number;36  cacheWriteUsd: number;37}38export interface PriceRow extends PriceTier {39  provider: Provider;40  model: string;41  /** ISO date; the row with the latest `effectiveFrom` not after the pricing date applies. */42  effectiveFrom: string;43  /** Applies to the WHOLE call when its prompt (uncached + cache read + cache write) exceeds `aboveTokens`. */44  longPrompt?: PriceTier & { aboveTokens: number };45}4647export const PRICE_EFFECTIVE_FROM = '2026-11-10';4849const row = (provider: Provider, model: string, t: [number, number, number, number], long?: [number, number, number, number, number]): PriceRow => ({50  provider, model, effectiveFrom: PRICE_EFFECTIVE_FROM,51  inputUsd: t[0], outputUsd: t[1], cacheReadUsd: t[2], cacheWriteUsd: t[3],52  ...(long ? { longPrompt: { aboveTokens: long[0], inputUsd: long[1], outputUsd: long[2], cacheReadUsd: long[3], cacheWriteUsd: long[4] } } : {}),53});5455/**56 * [input, output, cache read, cache write]. Cache write = 5-minute TTL on Anthropic (1.25× input) and the57 * default 30-minute TTL on OpenAI (1.25× input) — the only TTLs the adapters request.58 */59export const PRICE_TABLE: PriceRow[] = [60  row('anthropic', 'claude-fable-5-2', [8, 40, 0.2, 10]),61  row('anthropic', 'claude-opus-5-5', [4, 20, 0.2, 5]),62  row('anthropic', 'claude-sonnet-5-5', [2, 10, 0.1, 2.5]),63  // Haiku 5.5: $0.50 / $2.50 above a 100K-token prompt (models.md). The reference gives no cache rates for64  // that tier; 0.1× / 1.25× of its input rate are DERIVED from the base-tier ratios, not verified.65  row('anthropic', 'claude-haiku-5-5', [0.1, 0.5, 0.01, 0.125], [100_000, 0.5, 2.5, 0.05, 0.625]),66  // Server-side fallback targets (never requested directly; they appear in `usage.iterations` after a refusal).67  // Input/output from the reference; cache read = 0.1× input (prompt-caching.md "Economics"), write = 1.25×.68  row('anthropic', 'claude-opus-5', [5, 25, 0.5, 6.25]),69  row('anthropic', 'claude-opus-4-8', [5, 25, 0.5, 6.25]),70  row('anthropic', 'claude-sonnet-5', [2, 10, 0.2, 2.5]),71  // OpenAI standard tier; long context = input > 272K tokens (pricing page, all five columns verbatim).72  row('openai', 'gpt-6-astra', [10, 50, 1, 12.5], [272_000, 20, 75, 2, 25]),73  row('openai', 'gpt-6.1-sol', [2, 10, 0.1, 2.5], [272_000, 4, 15, 0.2, 5]),74  row('openai', 'gpt-6.1-terra', [1, 5, 0.05, 1.25], [272_000, 2, 7.5, 0.1, 2.5]),75  row('openai', 'gpt-6-luna', [0.1, 0.5, 0.01, 0.125], [272_000, 0.2, 0.75, 0.02, 0.25]),76];7778/**79 * Latest row for a model at a date. A dated snapshot ID (`gpt-6-luna-2026-10-01`, `claude-haiku-4-5-20251001`)80 * is priced as its base ID when it has no row of its own.81 */82export function priceRow(provider: Provider, model: string, at: string = '9999-12-31'): PriceRow | undefined {83  const find = (id: string): PriceRow | undefined => {84    let best: PriceRow | undefined;85    for (const r of PRICE_TABLE) {86      if (r.provider !== provider || r.model !== id || r.effectiveFrom > at) continue;87      if (!best || r.effectiveFrom > best.effectiveFrom) best = r;88    }89    return best;90  };91  return find(model) ?? find(model.replace(/-(\d{4}-\d{2}-\d{2}|\d{8})$/, ''));92}9394export type TokenUsage = Pick<Usage, 'inputTokens' | 'outputTokens' | 'cacheReadTokens' | 'cacheWriteTokens'>;9596/** Tokens that entered the model's context for one call. `Usage.inputTokens` alone is the UNCACHED part only. */97export function promptTokens(usage: Pick<Usage, 'inputTokens' | 'cacheReadTokens' | 'cacheWriteTokens'>): number {98  return usage.inputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;99}100101/**102 * USD for ONE model call. `inputTokens` is the uncached remainder; cache reads and writes are priced at their103 * own rates. Throws `ProviderError{code:'unknown_model_price'}` rather than pricing an unknown model at zero.104 */105export function priceUsage(provider: Provider, model: string, usage: TokenUsage): number {106  const r = priceRow(provider, model);107  if (!r) {108    throw new ProviderError(`No price for ${provider}/${model} in the price table`, { provider, retryable: false, code: 'unknown_model_price' });109  }110  const tier: PriceTier = r.longPrompt && promptTokens(usage) > r.longPrompt.aboveTokens ? r.longPrompt : r;111  const usd = (usage.inputTokens * tier.inputUsd + usage.outputTokens * tier.outputUsd112    + usage.cacheReadTokens * tier.cacheReadUsd + usage.cacheWriteTokens * tier.cacheWriteUsd) / 1_000_000;113  return Math.round(usd * 1e9) / 1e9;114}115116/**117 * Idempotent upsert of the base tier of every price row into `model_prices` (010_kernel.sql). The long-prompt118 * tiers have no column there; they live in `PRICE_TABLE` and are applied by `price()`.119 */120export async function seedPrices(db: PriceDb): Promise<void> {121  for (const r of PRICE_TABLE) {122    await db.query(123      `INSERT INTO model_prices (provider, model, effective_from, input_usd, output_usd, cache_read_usd, cache_write_usd)124       VALUES ($1, $2, $3, $4, $5, $6, $7)125       ON CONFLICT (provider, model, effective_from) DO UPDATE SET126         input_usd = EXCLUDED.input_usd, output_usd = EXCLUDED.output_usd,127         cache_read_usd = EXCLUDED.cache_read_usd, cache_write_usd = EXCLUDED.cache_write_usd`,128      [r.provider, r.model, r.effectiveFrom, r.inputUsd, r.outputUsd, r.cacheReadUsd, r.cacheWriteUsd],129    );130  }131}132
Files main
Menu

Theme