Ctrl K
ComunidadGuías…

src/pricing.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Proxy-side pricing. Mirrors app/lib/model-pricing.ts — keep them in sync.
// TODO: extract to a shared JSON file consumed by both packages.
//
// Used by recordBudgetSpend after each forwarded request to INCRBY the
// :spent: counter. App-side `estimateCost` re-derives the same number on the
// cron path from traffic_logs rows (which include cache token columns).

export type ModelPrice = {
  input: number;
  output: number;
  cache_read_rate?: number;
  cache_write_rate?: number;
};

export const MODEL_PRICING: Record<string, ModelPrice> = {
  // OpenAI — cache_read = input × 0.50
  "gpt-5-mini":    { input: 0.25,  output: 2.00,  cache_read_rate: 0.125 },
  "gpt-4o":        { input: 2.50,  output: 10.00, cache_read_rate: 1.25  },
  "gpt-4o-mini":   { input: 0.15,  output: 0.60,  cache_read_rate: 0.075 },
  "gpt-4.1":       { input: 2.00,  output: 8.00,  cache_read_rate: 1.00  },
  "gpt-4.1-mini":  { input: 0.40,  output: 1.60,  cache_read_rate: 0.20  },
  "gpt-4.1-nano":  { input: 0.10,  output: 0.40,  cache_read_rate: 0.05  },
  "o3":            { input: 2.00,  output: 8.00,  cache_read_rate: 0.50  },
  "o3-mini":       { input: 1.10,  output: 4.40,  cache_read_rate: 0.55  },
  "o4-mini":       { input: 1.10,  output: 4.40,  cache_read_rate: 0.275 },
  // Anthropic — cache_read = input × 0.10, cache_write = input × 1.25
  "claude-opus-4-6":               { input: 15.00, output: 75.00, cache_read_rate: 1.50,  cache_write_rate: 18.75 },
  "claude-sonnet-4-6":             { input: 3.00,  output: 15.00, cache_read_rate: 0.30,  cache_write_rate: 3.75  },
  "claude-sonnet-4-5-20250514":    { input: 3.00,  output: 15.00, cache_read_rate: 0.30,  cache_write_rate: 3.75  },
  "claude-haiku-4-5-20251001":     { input: 1.00,  output: 5.00,  cache_read_rate: 0.10,  cache_write_rate: 1.25  },
  "claude-haiku-3-5-20241022":     { input: 0.80,  output: 4.00,  cache_read_rate: 0.08,  cache_write_rate: 1.00  },
  // Google — cache_read for Gemini context caching
  "gemini-2.5-pro":   { input: 1.25, output: 10.00, cache_read_rate: 0.3125 },
  "gemini-2.5-flash": { input: 0.15, output: 0.60,  cache_read_rate: 0.0375 },
};

function resolvePrice(model: string): ModelPrice | undefined {
  if (MODEL_PRICING[model]) return MODEL_PRICING[model];
  // OpenAI-style date suffix: gpt-4o-mini-2024-07-18 → gpt-4o-mini
  const stripOpenAi = model.replace(/-\d{4}-\d{2}-\d{2}$/, "");
  if (stripOpenAi !== model && MODEL_PRICING[stripOpenAi]) return MODEL_PRICING[stripOpenAi];
  // Anthropic-style date suffix: claude-haiku-3-5-20241022 → claude-haiku-3-5
  const stripAnthropic = model.replace(/-\d{8}$/, "");
  if (stripAnthropic !== model && MODEL_PRICING[stripAnthropic]) return MODEL_PRICING[stripAnthropic];
  return undefined;
}

function clamp(n: number | null | undefined): number {
  if (typeof n !== "number" || !Number.isFinite(n) || n < 0) return 0;
  return n;
}

function isCacheSubsetConvention(model: string): boolean {
  // OpenAI / Gemini report cached as a subset of prompt. Anthropic reports
  // cache_read / cache_creation as disjoint from input_tokens.
  return !model.toLowerCase().startsWith("claude");
}

/** Estimate cost in MICRO-cents (1 USD = 1,000,000 micro-cents = 100 cents). */
export function estimateCostMicroCents(
  model: string | null | undefined,
  promptTokens: number | null | undefined,
  completionTokens: number | null | undefined,
  cacheReadTokens?: number | null,
  cacheWriteTokens?: number | null,
): number {
  if (!model) return 0;
  const price = resolvePrice(model);
  if (!price) return 0;

  const prompt = clamp(promptTokens);
  const completion = clamp(completionTokens);
  const cacheRead = clamp(cacheReadTokens);
  const cacheWrite = clamp(cacheWriteTokens);

  const nonCachedInput = isCacheSubsetConvention(model)
    ? Math.max(prompt - cacheRead - cacheWrite, 0)
    : prompt;

  const cacheReadRate = price.cache_read_rate ?? price.input;
  const cacheWriteRate = price.cache_write_rate ?? price.input;

  const dollars =
    (nonCachedInput / 1_000_000) * price.input +
    (cacheRead      / 1_000_000) * cacheReadRate +
    (cacheWrite     / 1_000_000) * cacheWriteRate +
    (completion     / 1_000_000) * price.output;

  return Math.round(dollars * 100 * 10_000); // dollars → cents → micro-cents
}