forked from getagentseal/codeburn
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbundle-litellm.mjs
More file actions
168 lines (155 loc) · 7.9 KB
/
Copy pathbundle-litellm.mjs
File metadata and controls
168 lines (155 loc) · 7.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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`)