agent-ecosystem/scripts/fetch-pricing-data.ts

136 lines
4.4 KiB
TypeScript

#!/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<Record<string, ModelPricing>> {
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<string, unknown>;
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<string, ModelPricing> = {};
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<void> {
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();