136 lines
4.4 KiB
TypeScript
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();
|