mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-10 01:29:41 +00:00
Mythos 5 ($10/$50) is not in LiteLLM or the models.dev/OpenRouter gap-fill yet (Fable is), so dropping the manual patch left it priced at $0. Add it to MANUAL_ENTRIES + the snapshot and map the "Mythos 5" display name.
168 lines
7.9 KiB
JavaScript
168 lines
7.9 KiB
JavaScript
import { writeFileSync, mkdirSync } from 'fs'
|
|
import { dirname, join } from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
// Pricing sources, in priority order:
|
|
// 1. LiteLLM - broad, maintained, tracks provider list prices.
|
|
// 2. MANUAL_ENTRIES - hand-curated overrides for the primary snapshot.
|
|
// 3. models.dev - only FIRST-PARTY maker providers (not the 100+
|
|
// gateways/resellers): official direct price for models
|
|
// LiteLLM hasn't added yet (e.g. MiniMax-M3).
|
|
// 4. OpenRouter - resale rates, one clean price per canonical model;
|
|
// a coverage backstop for makers not in models.dev.
|
|
//
|
|
// Output is TWO files:
|
|
// litellm-snapshot.json - primary (LiteLLM + MANUAL_ENTRIES). Used for the
|
|
// exact / canonical / prefix lookups.
|
|
// pricing-fallback.json - gap-fill (models.dev + OpenRouter). Consulted ONLY
|
|
// as a last resort, so a reseller variant name can
|
|
// never shadow an existing canonical/alias match.
|
|
const LITELLM_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json'
|
|
const MODELS_DEV_URL = 'https://models.dev/api.json'
|
|
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/models'
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
const dataDir = join(__dirname, '..', 'src', 'data')
|
|
const snapshotPath = join(dataDir, 'litellm-snapshot.json')
|
|
const fallbackPath = join(dataDir, 'pricing-fallback.json')
|
|
|
|
// models.dev provider ids that are the actual model MAKERS (publish official
|
|
// list prices), as opposed to gateways/resellers (openrouter, nano-gpt, vercel,
|
|
// poe, novita, etc.) that mark up or discount. An id missing here just means
|
|
// that maker's models fall through to OpenRouter; an unknown id is ignored.
|
|
const MODELS_DEV_FIRST_PARTY = new Set([
|
|
'openai', 'anthropic', 'google', 'google-vertex', 'mistral', 'deepseek',
|
|
'xai', 'minimax', 'minimax-cn', 'moonshotai', 'zhipuai', 'alibaba',
|
|
'alibaba-cn', 'cohere', 'perplexity', 'inception', 'morph',
|
|
])
|
|
|
|
const MANUAL_ENTRIES = {
|
|
'MiniMax-M2.7': [0.3e-6, 1.2e-6, 0.375e-6, 0.06e-6],
|
|
'MiniMax-M2.7-highspeed': [0.6e-6, 2.4e-6, 0.375e-6, 0.06e-6],
|
|
// LiteLLM PR #27056 is not merged yet. Source: https://api-docs.deepseek.com/quick_start/pricing
|
|
'deepseek-v4-flash': [1.4e-7, 2.8e-7, 0, 2.8e-9],
|
|
'deepseek-v4-pro': [4.35e-7, 8.7e-7, 0, 3.625e-9],
|
|
// Mythos 5 launch pricing; not yet in LiteLLM or the models.dev/OpenRouter gap-fill (Fable is).
|
|
'claude-mythos-5': [10e-6, 50e-6, 12.5e-6, 1e-6],
|
|
}
|
|
|
|
const snapshot = {}
|
|
|
|
// --- Pass 1+2: LiteLLM (primary) ---
|
|
const res = await fetch(LITELLM_URL)
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
const data = await res.json()
|
|
const entries = Object.entries(data).filter(([k]) => k !== 'sample_spec')
|
|
|
|
function toVal(entry) {
|
|
const inp = entry.input_cost_per_token
|
|
const out = entry.output_cost_per_token
|
|
if (inp == null || out == null) return null
|
|
return [inp, out, entry.cache_creation_input_token_cost ?? null, entry.cache_read_input_token_cost ?? null, entry.provider_specific_entry?.fast ?? null]
|
|
}
|
|
|
|
// Pass 1: direct entries (no prefix) get priority
|
|
for (const [name, entry] of entries) {
|
|
if (name.includes('/')) continue
|
|
const val = toVal(entry)
|
|
if (val) snapshot[name] = val
|
|
}
|
|
// Pass 2: prefixed entries - store full key + stripped (first-write-wins)
|
|
for (const [name, entry] of entries) {
|
|
if (!name.includes('/')) continue
|
|
const val = toVal(entry)
|
|
if (!val) continue
|
|
if (!snapshot[name]) snapshot[name] = val
|
|
const stripped = name.replace(/^[^/]+\//, '')
|
|
if (stripped !== name && !snapshot[stripped]) snapshot[stripped] = val
|
|
}
|
|
|
|
// A MANUAL_ENTRY that LiteLLM now ships is a candidate to delete (the override
|
|
// would otherwise shadow upstream forever with a possibly-stale hand value).
|
|
for (const k of Object.keys(MANUAL_ENTRIES)) {
|
|
if (snapshot[k]) console.log(`note: MANUAL_ENTRIES['${k}'] is now in LiteLLM - candidate to remove`)
|
|
}
|
|
Object.assign(snapshot, MANUAL_ENTRIES)
|
|
|
|
// --- Gap fill into a SEPARATE fallback map (last-resort only) ---
|
|
const fallback = {}
|
|
// Strip the vendor prefix to the last path segment, then the @pin and trailing
|
|
// -YYYYMMDD date that the runtime's getCanonicalName also strips, so a fallback
|
|
// key lines up with the canonical form actually queried (otherwise e.g.
|
|
// `vendor/claude-3-5-sonnet@20241022` becomes a key the lookup can never reach).
|
|
const bareKey = (name) => name.replace(/^.*\//, '').replace(/@.*$/, '').replace(/-\d{8}$/, '')
|
|
// `seen` holds every primary key AND its bareKey form (both lowercased) so we
|
|
// never re-add a model LiteLLM/MANUAL already covers under either shape; fallback
|
|
// keys are added too so the first source wins (models.dev before OpenRouter).
|
|
const seen = new Set()
|
|
for (const k of Object.keys(snapshot)) {
|
|
seen.add(k.toLowerCase())
|
|
seen.add(bareKey(k).toLowerCase())
|
|
}
|
|
const finite = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null }
|
|
// A rate pair is usable only if both sides are non-negative and not both zero.
|
|
// OpenRouter uses -1 as a "variable / BYOK price" sentinel; without this guard a
|
|
// negative per-token cost would ship and subtract from a user's spend totals.
|
|
const validRates = (inp, out) => inp != null && out != null && inp >= 0 && out >= 0 && !(inp === 0 && out === 0)
|
|
// Drop the same negative sentinel on optional cache fields.
|
|
const nonNeg = (v) => (v != null && v >= 0 ? v : null)
|
|
function addGap(key, val) {
|
|
if (!key || !val) return false
|
|
const lk = key.toLowerCase()
|
|
if (seen.has(lk)) return false
|
|
fallback[key] = val
|
|
seen.add(lk)
|
|
return true
|
|
}
|
|
|
|
// --- Pass 3: models.dev first-party makers (official list prices) ---
|
|
try {
|
|
const md = await (await fetch(MODELS_DEV_URL)).json()
|
|
// Surface drift in our hand-maintained maker allowlist: if an id we classify
|
|
// as first-party is gone from the API, it was renamed/removed and the set is
|
|
// stale (its models would silently fall through to OpenRouter resale rates).
|
|
for (const id of MODELS_DEV_FIRST_PARTY) {
|
|
if (!md[id]) console.warn(`note: models.dev no longer lists first-party id '${id}' - allowlist may be stale`)
|
|
}
|
|
let added = 0
|
|
for (const pid of Object.keys(md).sort()) {
|
|
if (!MODELS_DEV_FIRST_PARTY.has(pid)) continue
|
|
const models = md[pid].models ?? {}
|
|
for (const mid of Object.keys(models).sort()) {
|
|
const c = models[mid].cost
|
|
if (!c) continue
|
|
const inp = finite(c.input), out = finite(c.output)
|
|
if (!validRates(inp, out)) continue
|
|
// models.dev cost is per MILLION tokens; snapshot is per token.
|
|
const cw = nonNeg(c.cache_write != null ? finite(c.cache_write) : null)
|
|
const cr = nonNeg(c.cache_read != null ? finite(c.cache_read) : null)
|
|
if (addGap(bareKey(mid), [inp / 1e6, out / 1e6, cw != null ? cw / 1e6 : null, cr != null ? cr / 1e6 : null, null])) added++
|
|
}
|
|
}
|
|
console.log(`models.dev (first-party): +${added} models`)
|
|
} catch (e) {
|
|
console.warn(`models.dev skipped: ${e.message}`)
|
|
}
|
|
|
|
// --- Pass 4: OpenRouter (resale backstop) ---
|
|
try {
|
|
const or = (await (await fetch(OPENROUTER_URL)).json()).data ?? []
|
|
let added = 0
|
|
for (const m of or) {
|
|
const p = m.pricing ?? {}
|
|
const inp = finite(p.prompt), out = finite(p.completion)
|
|
if (!validRates(inp, out)) continue
|
|
// OpenRouter pricing fields are already per-token.
|
|
const cw = nonNeg(p.input_cache_write != null ? finite(p.input_cache_write) : null)
|
|
const cr = nonNeg(p.input_cache_read != null ? finite(p.input_cache_read) : null)
|
|
if (addGap(bareKey(m.id ?? ''), [inp, out, cw, cr, null])) added++
|
|
}
|
|
console.log(`openrouter (backstop): +${added} models`)
|
|
} catch (e) {
|
|
console.warn(`openrouter skipped: ${e.message}`)
|
|
}
|
|
|
|
mkdirSync(dataDir, { recursive: true })
|
|
writeFileSync(snapshotPath, JSON.stringify(snapshot))
|
|
writeFileSync(fallbackPath, JSON.stringify(fallback))
|
|
console.log(`Bundled ${Object.keys(snapshot).length} primary + ${Object.keys(fallback).length} fallback models`)
|