#!/usr/bin/env tsx /** * Fetch latest model pricing from LiteLLM and save to renderer assets. * Filters to the models this app currently exposes in the UI/runtime to reduce bundle size. * Runs automatically during prebuild. */ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const LITELLM_PRICING_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json'; const OUTPUT_PATH = path.join(__dirname, '..', 'resources', 'pricing.json'); const FETCH_TIMEOUT = 10000; // 10 seconds interface ModelPricing { input_cost_per_token: number; output_cost_per_token: number; cache_creation_input_token_cost?: number; cache_read_input_token_cost?: number; input_cost_per_token_above_200k_tokens?: number; output_cost_per_token_above_200k_tokens?: number; cache_creation_input_token_cost_above_200k_tokens?: number; cache_read_input_token_cost_above_200k_tokens?: number; [key: string]: unknown; } function isValidModelPricing(entry: unknown): entry is ModelPricing { return ( typeof entry === 'object' && entry !== null && 'input_cost_per_token' in entry && 'output_cost_per_token' in entry && typeof (entry as ModelPricing).input_cost_per_token === 'number' && typeof (entry as ModelPricing).output_cost_per_token === 'number' ); } const EXPLICIT_MODEL_ALLOWLIST = new Set([ 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.2-codex', 'gpt-5.2', 'gpt-5.1-codex-max', 'gpt-5.1-codex-mini', 'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', ]); function isClaudeModel(modelName: string): boolean { const lower = modelName.toLowerCase(); return lower.includes('claude'); } function isIncludedModel(modelName: string): boolean { return isClaudeModel(modelName) || EXPLICIT_MODEL_ALLOWLIST.has(modelName); } async function fetchPricingData(): Promise> { console.log('Fetching pricing data from LiteLLM...'); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT); try { const response = await fetch(LITELLM_PRICING_URL, { signal: controller.signal }); clearTimeout(timeout); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = (await response.json()) as Record; console.log(`Fetched pricing for ${Object.keys(data).length} models`); // Filter to the models currently exposed by this app and validate entries. const selectedModels: Record = {}; for (const [modelName, entry] of Object.entries(data)) { if (isIncludedModel(modelName) && isValidModelPricing(entry)) { selectedModels[modelName] = entry; } } // LiteLLM currently publishes no priced top-level entry for gpt-5.3-codex-spark. // Keep cost estimation non-zero by aliasing it to the closest published Codex tier. if (!selectedModels['gpt-5.3-codex-spark'] && selectedModels['gpt-5.3-codex']) { selectedModels['gpt-5.3-codex-spark'] = selectedModels['gpt-5.3-codex']; } console.log(`Filtered to ${Object.keys(selectedModels).length} supported models`); return selectedModels; } catch (error) { clearTimeout(timeout); if (error instanceof Error && error.name === 'AbortError') { throw new Error('Fetch timeout after 10 seconds'); } throw error; } } async function main(): Promise { try { console.log('Fetching pricing data for models...'); const pricing = await fetchPricingData(); // Ensure output directory exists const outputDir = path.dirname(OUTPUT_PATH); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Write formatted JSON fs.writeFileSync(OUTPUT_PATH, JSON.stringify(pricing, null, 2), 'utf-8'); // Calculate file size const stats = fs.statSync(OUTPUT_PATH); const sizeKB = (stats.size / 1024).toFixed(2); console.log(`✓ Wrote pricing data to ${OUTPUT_PATH}`); console.log(` Bundle size: ${sizeKB} KB`); } catch (error) { console.error('Failed to fetch pricing data:', error); console.error('Build will continue with existing pricing.json if available'); // Don't fail the build - allow using existing pricing.json process.exit(0); } } main();