|
| 1 | +/** |
| 2 | + * Refresh floe-guard's vendored LiteLLM cost map — BOTH copies — from the same |
| 3 | + * public source the Floe proxy uses. This keeps the open-source package's pricing |
| 4 | + * current without coupling it to the private monorepo. |
| 5 | + * |
| 6 | + * Writes (identically, so the cost-map-sync CI guard stays green): |
| 7 | + * - src/floe_guard/cost_map.json (Python package) |
| 8 | + * - js/src/cost_map.json (JS package) |
| 9 | + * |
| 10 | + * The transform mirrors the proxy's scripts/update-llm-cost-map.ts exactly, and |
| 11 | + * serialises with the same JSON.stringify(…, 2) so refreshes show up as clean |
| 12 | + * price diffs rather than reformatting noise. (A Python re-serialiser would emit |
| 13 | + * floats differently, e.g. 1e-06 vs 1e-7, and churn the whole file.) |
| 14 | + * |
| 15 | + * Run: node scripts/update-cost-map.mjs |
| 16 | + */ |
| 17 | +import { writeFileSync } from "node:fs"; |
| 18 | +import { fileURLToPath } from "node:url"; |
| 19 | +import { dirname, join } from "node:path"; |
| 20 | + |
| 21 | +const SOURCE_URL = |
| 22 | + process.env.LITELLM_COST_MAP_URL ?? |
| 23 | + "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"; |
| 24 | + |
| 25 | +// Providers floe-guard prices (matches the proxy's ROUTABLE_PROVIDERS). |
| 26 | +const ROUTABLE_PROVIDERS = new Set(["openai", "anthropic"]); |
| 27 | + |
| 28 | +/** |
| 29 | + * A model is vendored only if we can fully price it: a numeric input rate, a |
| 30 | + * routable provider, and either embedding mode (input-only) or chat mode WITH a |
| 31 | + * numeric output rate. Coercing a missing output rate to 0 would ship a chat |
| 32 | + * model that bills output free, which fail-closed pricing can't catch (0 is |
| 33 | + * finite). An excluded model is simply absent. |
| 34 | + */ |
| 35 | +function isUsable(v) { |
| 36 | + // Number.isFinite (not typeof === "number") so a NaN, or a huge upstream value |
| 37 | + // that JSON.parse turns into Infinity, is treated as unpriceable and dropped — |
| 38 | + // matching the fail-closed pricing paths. |
| 39 | + return ( |
| 40 | + !!v && |
| 41 | + Number.isFinite(v.input_cost_per_token) && |
| 42 | + v.litellm_provider !== undefined && |
| 43 | + ROUTABLE_PROVIDERS.has(v.litellm_provider) && |
| 44 | + (v.mode === "embedding" || |
| 45 | + (v.mode === "chat" && Number.isFinite(v.output_cost_per_token))) |
| 46 | + ); |
| 47 | +} |
| 48 | + |
| 49 | +const res = await fetch(SOURCE_URL); |
| 50 | +if (!res.ok) { |
| 51 | + throw new Error(`Failed to fetch LiteLLM cost map: HTTP ${res.status}`); |
| 52 | +} |
| 53 | +const raw = await res.json(); |
| 54 | + |
| 55 | +const entries = Object.entries(raw) |
| 56 | + .filter(([, v]) => isUsable(v)) |
| 57 | + .sort(([a], [b]) => a.localeCompare(b)); |
| 58 | + |
| 59 | +// null-prototype: model keys come from remote JSON, so a "__proto__" (or similar) |
| 60 | +// key is stored as plain data instead of mutating the object's prototype. |
| 61 | +const out = Object.create(null); |
| 62 | +for (const [k, v] of entries) { |
| 63 | + out[k] = { |
| 64 | + input_cost_per_token: v.input_cost_per_token, |
| 65 | + output_cost_per_token: v.mode === "embedding" ? 0 : v.output_cost_per_token, |
| 66 | + litellm_provider: v.litellm_provider, |
| 67 | + mode: v.mode, |
| 68 | + }; |
| 69 | +} |
| 70 | + |
| 71 | +const json = `${JSON.stringify(out, null, 2)}\n`; |
| 72 | +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); |
| 73 | +const targets = [ |
| 74 | + join(root, "src", "floe_guard", "cost_map.json"), |
| 75 | + join(root, "js", "src", "cost_map.json"), |
| 76 | +]; |
| 77 | +for (const dest of targets) { |
| 78 | + writeFileSync(dest, json); |
| 79 | +} |
| 80 | + |
| 81 | +console.log( |
| 82 | + `Wrote ${entries.length} models to ${targets.length} files (source: ${SOURCE_URL}).`, |
| 83 | +); |
0 commit comments