#!/usr/bin/env node /** * Inject per-model pricing into ~/.agents/models.json for the `omni` provider. * * WHY THIS EXISTS * --------------- * `pi --list-models` / the omni extension's /omni sync regenerates models.json * from OmniRoute and writes entries WITHOUT a `cost` field. pi's calculateCost() * then reports $0.00 for every run, so Paperclip's cost events and budgets all * read zero even though real money is being spent. * * The fix is to add { cost: { input, output, cacheRead, cacheWrite } } (USD per * million tokens) to the model entries we actually use. * * Because /omni sync rewrites the whole file, this script must be re-run after * every sync. That is the whole reason it is a script and not a one-off edit. * * PRICE SOURCES (both authoritative, no guessing) * - deepseek/* -> OmniRoute /api/pricing, group "deepseek" * - openrouter/* -> OpenRouter /api/v1/models (strip the "openrouter/" prefix) * * Model ids that use routing prefixes (aug/, ds/, oc/, tllm/, opencode-go/) do * NOT map cleanly to pricing groups and are deliberately skipped — add them to * MANUAL below if you need them. * * USAGE * node omni-pricing.mjs --dry-run # show what would change * node omni-pricing.mjs # write models.json */ import fs from "node:fs"; import path from "node:path"; const MODELS_JSON = path.join(process.env.HOME, ".agents", "models.json"); const OMNIROUTE_PRICING = "http://192.168.20.13:20128/api/pricing"; const OPENROUTER_MODELS = "https://openrouter.ai/api/v1/models"; // Models we actually assign to workers. Keep this list short and deliberate. const TARGETS = [ "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-pro", "openrouter/google/gemini-3.1-flash-lite", "openrouter/google/gemini-2.5-flash", ]; // Hand-priced entries: id -> {input, output, cacheRead, cacheWrite} const MANUAL = {}; const dryRun = process.argv.includes("--dry-run"); function omniKey() { const cfg = JSON.parse(fs.readFileSync(MODELS_JSON, "utf8")); return cfg.providers?.omni?.apiKey ?? ""; } /** Fetch OmniRoute's own price table: { group: { model: {input,output,cached,cache_creation} } } */ async function fetchOmniRoutePricing() { const res = await fetch(OMNIROUTE_PRICING, { headers: { Authorization: `Bearer ${omniKey()}` }, }); if (!res.ok) throw new Error(`OmniRoute pricing HTTP ${res.status}`); return res.json(); } /** Fetch OpenRouter prices keyed by bare model id (no "openrouter/" prefix). */ async function fetchOpenRouterPricing() { const res = await fetch(OPENROUTER_MODELS); if (!res.ok) throw new Error(`OpenRouter models HTTP ${res.status}`); const { data } = await res.json(); const out = {}; for (const m of data) { const p = m.pricing ?? {}; out[m.id] = { input: Number(p.prompt ?? 0) * 1e6, output: Number(p.completion ?? 0) * 1e6, cacheRead: Number(p.input_cache_read ?? 0) * 1e6, cacheWrite: Number(p.input_cache_write ?? 0) * 1e6, }; } return out; } const r6 = (n) => Math.round(Number(n) * 1e6) / 1e6; /** Resolve a pi model id to a {input,output,cacheRead,cacheWrite} price, or null. */ function resolvePrice(id, omniPricing, orPricing) { if (MANUAL[id]) return MANUAL[id]; // deepseek/ -> OmniRoute group "deepseek" if (id.startsWith("deepseek/")) { const name = id.slice("deepseek/".length); const row = omniPricing?.deepseek?.[name]; if (row) { return { input: r6(row.input), output: r6(row.output), cacheRead: r6(row.cached ?? 0), cacheWrite: r6(row.cache_creation ?? 0), }; } } // openrouter/ -> OpenRouter catalogue if (id.startsWith("openrouter/")) { const bare = id.slice("openrouter/".length); if (orPricing[bare]) { const p = orPricing[bare]; return { input: r6(p.input), output: r6(p.output), cacheRead: r6(p.cacheRead), cacheWrite: r6(p.cacheWrite) }; } } return null; } const cfg = JSON.parse(fs.readFileSync(MODELS_JSON, "utf8")); const models = cfg.providers?.omni?.models; if (!Array.isArray(models)) { console.error("models.json: providers.omni.models is not an array — aborting"); process.exit(1); } const [omniPricing, orPricing] = await Promise.all([ fetchOmniRoutePricing(), fetchOpenRouterPricing(), ]); const byId = new Map(models.filter((m) => m && m.id).map((m) => [m.id, m])); let changed = 0; const missing = []; for (const id of TARGETS) { const entry = byId.get(id); if (!entry) { missing.push(id); continue; } const price = resolvePrice(id, omniPricing, orPricing); if (!price) { missing.push(id); continue; } const current = JSON.stringify(entry.cost ?? null); const next = JSON.stringify(price); if (current !== next) { entry.cost = price; changed++; console.log( `${dryRun ? "[dry-run] " : ""}${id} -> in $${price.input} out $${price.output} cacheR $${price.cacheRead} cacheW $${price.cacheWrite}`, ); } else { console.log(`${id} -> already priced, unchanged`); } } if (missing.length) console.log(`\nnot found / no price (skipped): ${missing.join(", ")}`); if (dryRun) { console.log(`\ndry-run: ${changed} entr(y/ies) would change`); } else if (changed) { fs.writeFileSync(MODELS_JSON, JSON.stringify(cfg, null, 2) + "\n"); console.log(`\nwrote ${MODELS_JSON} (${changed} changed)`); } else { console.log("\nnothing to do"); }