From eb260a36e3e1eb82afb0b375b150d266c71c334d Mon Sep 17 00:00:00 2001 From: Kem-Ada <283358351+Kem-Ada@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:58:58 +0100 Subject: [PATCH] feat(pricing): port getPricing, setPricingTable and listKnownModels Brings src/pricing.ts to parity with the Python SDK's identark/pricing.py. - Add the `ModelPricing` interface, replacing `Record` values. - Add the five bundled models the TS table was missing (`o1`, `o1-mini`, `claude-sonnet-4-20250514`, `claude-opus-4-20250514`) plus a new `GEMINI_PRICING` table, and compose them into `BUNDLED_PRICING`. - Add `getPricing`, `setPricingTable` and `listKnownModels` over an active, override-aware pricing table. - Support the same overrides as Python, at the same precedence: `IDENTARK_PRICING_URL` > `~/.identark/pricing.json` > bundled defaults. Node has no synchronous HTTP primitive, so the remote fetch is exposed as `await initPricing()`; the local-file and `setPricingTable` paths stay sync. - Rewire `estimateCost` onto the active table so overrides reach the cost figures DirectGateway reports. Two parity bugs in `estimateCost` fall out of the rewire. A model priced at `{input: 0, output: 0}` was billed at the unknown-model fallback rate because of a falsy check on the rates, and a model named after an `Object.prototype` member resolved to the inherited value and produced a `NaN` cost. Python's `dict.get` does neither. The existing `OPENAI_PRICING` / `ANTHROPIC_PRICING` / `MISTRAL_PRICING` exports are kept as-is for backwards compatibility. Tests: 35 new cases in tests/pricing.test.ts covering the eight Python unit tests in tests/unit/test_pricing.py plus negative cases for unknown models, malformed and missing override files, failing and non-OK remote fetches, override precedence, and zero-priced models. Co-Authored-By: Claude --- src/index.ts | 16 +- src/pricing.ts | 174 +++++++++++++++++-- tests/pricing.test.ts | 392 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 570 insertions(+), 12 deletions(-) create mode 100644 tests/pricing.test.ts diff --git a/src/index.ts b/src/index.ts index 7c32a79..21b8181 100644 --- a/src/index.ts +++ b/src/index.ts @@ -95,4 +95,18 @@ export const version = "1.0.3"; export const author = "Gold Okpa"; export const license = "MIT"; -export { estimateCost, OPENAI_PRICING, ANTHROPIC_PRICING, MISTRAL_PRICING } from "./pricing.js"; +// Pricing +export { + estimateCost, + getPricing, + setPricingTable, + listKnownModels, + initPricing, + type ModelPricing, + BUNDLED_PRICING, + OPENAI_PRICING, + ANTHROPIC_PRICING, + MISTRAL_PRICING, + GEMINI_PRICING, + UNKNOWN_MODEL_RATE, +} from "./pricing.js"; diff --git a/src/pricing.ts b/src/pricing.ts index a9f89d5..96d5c52 100644 --- a/src/pricing.ts +++ b/src/pricing.ts @@ -1,39 +1,195 @@ /** * identark/pricing * ~~~~~~~~~~~~~~~~ - * Model pricing tables and cost estimation. + * LLM token pricing tables with support for external overrides. * * Mirrors the Python SDK's dedicated `identark.pricing` module so both SDKs * share one source of truth per language. Cost per 1M tokens (USD) — * approximate; update as providers change pricing. + * + * Pricing can be customized via: + * 1. Environment variable: `IDENTARK_PRICING_URL` (fetched by `initPricing()`) + * 2. Local file: `~/.identark/pricing.json` + * 3. Programmatic override: `setPricingTable()` + * + * Falls back to bundled defaults if no override is found. */ -export const OPENAI_PRICING: Record> = { +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +/** Cost of a model in USD per 1M tokens. */ +export interface ModelPricing { + /** USD per 1M input (prompt) tokens. */ + input: number; + /** USD per 1M output (completion) tokens. */ + output: number; +} + +export const OPENAI_PRICING: Record = { "gpt-4o": { input: 2.5, output: 10.0 }, "gpt-4o-mini": { input: 0.15, output: 0.6 }, "gpt-4-turbo": { input: 10.0, output: 30.0 }, "gpt-3.5-turbo": { input: 0.5, output: 1.5 }, + o1: { input: 15.0, output: 60.0 }, + "o1-mini": { input: 3.0, output: 12.0 }, }; -export const ANTHROPIC_PRICING: Record> = { +export const ANTHROPIC_PRICING: Record = { "claude-3-5-sonnet-20241022": { input: 3.0, output: 15.0 }, "claude-3-5-haiku-20241022": { input: 0.8, output: 4.0 }, "claude-3-opus-20240229": { input: 15.0, output: 75.0 }, + "claude-sonnet-4-20250514": { input: 3.0, output: 15.0 }, + "claude-opus-4-20250514": { input: 15.0, output: 75.0 }, }; -export const MISTRAL_PRICING: Record> = { +export const MISTRAL_PRICING: Record = { "mistral-large-latest": { input: 2.0, output: 6.0 }, "mistral-small-latest": { input: 0.2, output: 0.6 }, "open-mistral-nemo": { input: 0.15, output: 0.15 }, "codestral-latest": { input: 0.2, output: 0.6 }, }; +export const GEMINI_PRICING: Record = { + "gemini-1.5-pro": { input: 1.25, output: 5.0 }, + "gemini-1.5-flash": { input: 0.075, output: 0.3 }, + "gemini-2.0-flash-exp": { input: 0.1, output: 0.4 }, +}; + +/** Bundled defaults — updated periodically with SDK releases. */ +export const BUNDLED_PRICING: Record = { + ...OPENAI_PRICING, + ...ANTHROPIC_PRICING, + ...MISTRAL_PRICING, + ...GEMINI_PRICING, +}; + /** Fallback rate (USD per token) for unknown models. */ export const UNKNOWN_MODEL_RATE = 0.00001; +/** Timeout for a remote pricing fetch, in milliseconds. */ +const REMOTE_FETCH_TIMEOUT_MS = 5000; + +// Active pricing table — starts as bundled, can be overridden. +let pricingTable: Record = { ...BUNDLED_PRICING }; +let initialized = false; + +/** Load pricing from `~/.identark/pricing.json` if it exists. */ +function loadLocalOverrides(): Record | undefined { + const configPath = join(homedir(), ".identark", "pricing.json"); + if (!existsSync(configPath)) { + return undefined; + } + try { + return JSON.parse(readFileSync(configPath, "utf8")) as Record; + } catch (error) { + console.warn(`identark.pricing: failed to load ${configPath}: ${String(error)}`); + return undefined; + } +} + +/** Fetch pricing from a remote URL (best-effort — never throws). */ +async function fetchRemotePricing(url: string): Promise | undefined> { + try { + const response = await fetch(url, { + signal: AbortSignal.timeout(REMOTE_FETCH_TIMEOUT_MS), + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return (await response.json()) as Record; + } catch (error) { + console.warn(`identark.pricing: failed to fetch pricing from ${url}: ${String(error)}`); + return undefined; + } +} + +/** + * Apply the overrides reachable without async I/O. + * + * Priority is `IDENTARK_PRICING_URL` > `~/.identark/pricing.json` > bundled. + * Node has no synchronous HTTP primitive, so when a pricing URL is configured + * this leaves the table on bundled defaults — and deliberately does *not* mark + * initialization as done — so that a later `initPricing()` can still apply the + * remote table. The lower-priority local file is never consulted in that case, + * matching the Python SDK's `elif` branch. + */ +function initializeSync(): void { + if (initialized || process.env.IDENTARK_PRICING_URL) { + return; + } + const local = loadLocalOverrides(); + if (local) { + pricingTable = { ...BUNDLED_PRICING, ...local }; + } + initialized = true; +} + +/** + * Initialize the pricing table, including the remote `IDENTARK_PRICING_URL`. + * + * Call this once at startup if you use `IDENTARK_PRICING_URL`; the local-file + * and `setPricingTable()` overrides need no such call. A no-op once the table + * has been initialized. Never throws — a failed fetch leaves bundled defaults. + */ +export async function initPricing(): Promise { + if (initialized) { + return; + } + const url = process.env.IDENTARK_PRICING_URL; + if (url) { + const remote = await fetchRemotePricing(url); + if (remote) { + pricingTable = { ...BUNDLED_PRICING, ...remote }; + } + } else { + const local = loadLocalOverrides(); + if (local) { + pricingTable = { ...BUNDLED_PRICING, ...local }; + } + } + initialized = true; +} + +/** + * Get pricing for a model, or `undefined` if unknown. + * + * @param model - Model identifier (e.g. "gpt-4o") + */ +export function getPricing(model: string): ModelPricing | undefined { + initializeSync(); + // `hasOwn` keeps inherited members ("toString", "constructor", ...) from + // resolving as models, matching Python's `dict.get`. + return Object.hasOwn(pricingTable, model) ? pricingTable[model] : undefined; +} + +/** + * Programmatically override the pricing table. + * + * Entries are merged over the bundled defaults, so unlisted models stay + * resolvable. This also completes initialization, so any `IDENTARK_PRICING_URL` + * or `~/.identark/pricing.json` override is skipped from here on. + * + * @param table - Model identifier to pricing, merged over the bundled defaults + */ +export function setPricingTable(table: Record): void { + pricingTable = { ...BUNDLED_PRICING, ...table }; + initialized = true; +} + +/** Return the list of models with known pricing. */ +export function listKnownModels(): string[] { + initializeSync(); + return Object.keys(pricingTable); +} + /** * Estimate the USD cost of a completion. * + * Returns 0 for local providers (Ollama, self-hosted models) and a + * conservative estimate for unknown models. + * * @param model - Model identifier (e.g. "gpt-4o") * @param inputTokens - Prompt tokens consumed * @param outputTokens - Completion tokens produced @@ -48,13 +204,9 @@ export function estimateCost( if (provider === "local") { return 0.0; } - const pricing = { ...OPENAI_PRICING, ...ANTHROPIC_PRICING, ...MISTRAL_PRICING }; - if (!(model in pricing)) { - return (inputTokens + outputTokens) * UNKNOWN_MODEL_RATE; - } - const rates = pricing[model]; - if (!rates || !rates.input || !rates.output) { + const pricing = getPricing(model); + if (pricing === undefined) { return (inputTokens + outputTokens) * UNKNOWN_MODEL_RATE; } - return (inputTokens * rates.input + outputTokens * rates.output) / 1_000_000; + return (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000; } diff --git a/tests/pricing.test.ts b/tests/pricing.test.ts new file mode 100644 index 0000000..6b41cdb --- /dev/null +++ b/tests/pricing.test.ts @@ -0,0 +1,392 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +// `loadLocalOverrides` reads `~/.identark/pricing.json`. Point HOME at a throwaway +// directory so the suite never depends on (or is polluted by) the real one. +const mocks = vi.hoisted(() => ({ home: "" })); + +vi.mock("node:os", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, homedir: () => mocks.home }; +}); + +type PricingModule = typeof import("../src/pricing.js"); + +/** Pricing caches its table in module scope, so each test needs a fresh copy. */ +async function loadPricing(): Promise { + vi.resetModules(); + return import("../src/pricing.js"); +} + +function writeLocalOverrides(contents: string): void { + mkdirSync(join(mocks.home, ".identark"), { recursive: true }); + writeFileSync(join(mocks.home, ".identark", "pricing.json"), contents); +} + +let originalPricingUrl: string | undefined; + +beforeEach(() => { + mocks.home = mkdtempSync(join(tmpdir(), "identark-pricing-")); + originalPricingUrl = process.env.IDENTARK_PRICING_URL; + delete process.env.IDENTARK_PRICING_URL; +}); + +afterEach(() => { + rmSync(mocks.home, { recursive: true, force: true }); + if (originalPricingUrl === undefined) { + delete process.env.IDENTARK_PRICING_URL; + } else { + process.env.IDENTARK_PRICING_URL = originalPricingUrl; + } + vi.unstubAllGlobals(); + vi.restoreAllMocks(); +}); + +describe("Pricing", () => { + describe("getPricing", () => { + it("should return pricing for a known model", async () => { + const { getPricing } = await loadPricing(); + const pricing = getPricing("gpt-4o"); + expect(pricing).toBeDefined(); + expect(pricing?.input).toBe(2.5); + expect(pricing?.output).toBe(10.0); + }); + + it("should return undefined for an unknown model", async () => { + const { getPricing } = await loadPricing(); + expect(getPricing("unknown-model-xyz")).toBeUndefined(); + }); + + it("should return correct Anthropic pricing", async () => { + const { getPricing } = await loadPricing(); + const pricing = getPricing("claude-3-5-sonnet-20241022"); + expect(pricing?.input).toBe(3.0); + expect(pricing?.output).toBe(15.0); + }); + + it("should return correct Mistral pricing", async () => { + const { getPricing } = await loadPricing(); + const pricing = getPricing("mistral-large-latest"); + expect(pricing?.input).toBe(2.0); + expect(pricing?.output).toBe(6.0); + }); + + it("should return correct Gemini pricing", async () => { + const { getPricing } = await loadPricing(); + const pricing = getPricing("gemini-1.5-flash"); + expect(pricing?.input).toBe(0.075); + expect(pricing?.output).toBe(0.3); + }); + + it("should return an empty string model as undefined", async () => { + const { getPricing } = await loadPricing(); + expect(getPricing("")).toBeUndefined(); + }); + + it("should not resolve inherited Object properties as models", async () => { + const { getPricing } = await loadPricing(); + expect(getPricing("toString")).toBeUndefined(); + expect(getPricing("constructor")).toBeUndefined(); + }); + }); + + describe("listKnownModels", () => { + it("should include models from every bundled provider", async () => { + const { listKnownModels } = await loadPricing(); + const models = listKnownModels(); + expect(models).toContain("gpt-4o"); + expect(models).toContain("gpt-4o-mini"); + expect(models).toContain("o1"); + expect(models).toContain("claude-3-5-sonnet-20241022"); + expect(models).toContain("claude-opus-4-20250514"); + expect(models).toContain("mistral-large-latest"); + expect(models).toContain("gemini-1.5-pro"); + }); + + it("should not include unknown models", async () => { + const { listKnownModels } = await loadPricing(); + expect(listKnownModels()).not.toContain("unknown-model-xyz"); + }); + + it("should include models added by setPricingTable", async () => { + const { listKnownModels, setPricingTable } = await loadPricing(); + setPricingTable({ "custom-model": { input: 1.0, output: 2.0 } }); + const models = listKnownModels(); + expect(models).toContain("custom-model"); + expect(models).toContain("gpt-4o"); + }); + }); + + describe("setPricingTable", () => { + it("should override pricing and keep bundled models reachable", async () => { + const { getPricing, setPricingTable } = await loadPricing(); + setPricingTable({ "custom-model": { input: 1.0, output: 2.0 } }); + + const pricing = getPricing("custom-model"); + expect(pricing?.input).toBe(1.0); + expect(pricing?.output).toBe(2.0); + expect(getPricing("gpt-4o")).toBeDefined(); + }); + + it("should replace the price of a bundled model", async () => { + const { getPricing, setPricingTable } = await loadPricing(); + setPricingTable({ "gpt-4o": { input: 99.0, output: 100.0 } }); + expect(getPricing("gpt-4o")?.input).toBe(99.0); + }); + + it("should flow through to estimateCost", async () => { + const { estimateCost, setPricingTable } = await loadPricing(); + setPricingTable({ "custom-model": { input: 1.0, output: 2.0 } }); + expect(estimateCost("custom-model", 1_000_000, 1_000_000)).toBeCloseTo(3.0, 10); + }); + + it("should suppress the local override file", async () => { + writeLocalOverrides(JSON.stringify({ "gpt-4o": { input: 1.0, output: 1.0 } })); + const { getPricing, setPricingTable } = await loadPricing(); + + setPricingTable({ "custom-model": { input: 1.0, output: 2.0 } }); + + expect(getPricing("gpt-4o")?.input).toBe(2.5); + }); + }); + + describe("estimateCost", () => { + it("should estimate cost for a known model", async () => { + const { estimateCost } = await loadPricing(); + const expected = (1000 * 2.5 + 500 * 10.0) / 1_000_000; + expect(estimateCost("gpt-4o", 1000, 500)).toBeCloseTo(expected, 10); + }); + + it("should return zero for the local provider", async () => { + const { estimateCost } = await loadPricing(); + expect(estimateCost("any-model", 1_000_000, 1_000_000, "local")).toBe(0); + }); + + it("should use the fallback rate for an unknown model", async () => { + const { estimateCost, UNKNOWN_MODEL_RATE } = await loadPricing(); + const expected = (1000 + 500) * UNKNOWN_MODEL_RATE; + expect(estimateCost("unknown-model", 1000, 500)).toBeCloseTo(expected, 10); + }); + + it("should return zero for a zero-priced model rather than the fallback", async () => { + const { estimateCost, setPricingTable } = await loadPricing(); + setPricingTable({ "free-model": { input: 0, output: 0 } }); + expect(estimateCost("free-model", 1000, 500)).toBe(0); + }); + + it("should charge only for output when input is free", async () => { + const { estimateCost, setPricingTable } = await loadPricing(); + setPricingTable({ "half-free": { input: 0, output: 10.0 } }); + expect(estimateCost("half-free", 1_000_000, 1_000_000)).toBeCloseTo(10.0, 10); + }); + + it("should return zero for zero tokens", async () => { + const { estimateCost } = await loadPricing(); + expect(estimateCost("gpt-4o", 0, 0)).toBe(0); + }); + }); + + describe("local override file", () => { + it("should merge a valid override file over the bundled defaults", async () => { + writeLocalOverrides( + JSON.stringify({ + "gpt-4o": { input: 1.0, output: 2.0 }, + "house-model": { input: 0.5, output: 0.5 }, + }), + ); + const { getPricing } = await loadPricing(); + + expect(getPricing("gpt-4o")?.input).toBe(1.0); + expect(getPricing("house-model")?.output).toBe(0.5); + // Unlisted bundled models survive the merge. + expect(getPricing("claude-3-opus-20240229")?.input).toBe(15.0); + }); + + it("should fall back to bundled defaults when no override file exists", async () => { + const { getPricing } = await loadPricing(); + expect(getPricing("gpt-4o")?.input).toBe(2.5); + }); + + it("should fall back to bundled defaults when the file is malformed", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + writeLocalOverrides("{ not valid json"); + const { getPricing } = await loadPricing(); + + expect(getPricing("gpt-4o")?.input).toBe(2.5); + expect(warn).toHaveBeenCalledOnce(); + }); + + it("should read the override file only once", async () => { + writeLocalOverrides(JSON.stringify({ "gpt-4o": { input: 1.0, output: 2.0 } })); + const { getPricing } = await loadPricing(); + expect(getPricing("gpt-4o")?.input).toBe(1.0); + + // A later rewrite is ignored — the table is cached after first use. + writeLocalOverrides(JSON.stringify({ "gpt-4o": { input: 7.0, output: 8.0 } })); + expect(getPricing("gpt-4o")?.input).toBe(1.0); + }); + }); + + describe("initPricing", () => { + it("should apply a remote pricing table", async () => { + process.env.IDENTARK_PRICING_URL = "https://pricing.example/table.json"; + vi.stubGlobal( + "fetch", + vi.fn(async () => + Promise.resolve( + new Response(JSON.stringify({ "gpt-4o": { input: 4.0, output: 20.0 } }), { + status: 200, + }), + ), + ), + ); + const { getPricing, initPricing } = await loadPricing(); + + await initPricing(); + + expect(getPricing("gpt-4o")?.input).toBe(4.0); + // Bundled models not named remotely are still reachable. + expect(getPricing("mistral-large-latest")?.input).toBe(2.0); + }); + + it("should keep bundled defaults when the fetch rejects", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + process.env.IDENTARK_PRICING_URL = "https://pricing.example/table.json"; + vi.stubGlobal("fetch", vi.fn(async () => Promise.reject(new Error("network down")))); + const { getPricing, initPricing } = await loadPricing(); + + await expect(initPricing()).resolves.toBeUndefined(); + + expect(getPricing("gpt-4o")?.input).toBe(2.5); + expect(warn).toHaveBeenCalledOnce(); + }); + + it("should keep bundled defaults on a non-OK HTTP status", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + process.env.IDENTARK_PRICING_URL = "https://pricing.example/table.json"; + vi.stubGlobal( + "fetch", + vi.fn(async () => Promise.resolve(new Response("nope", { status: 500 }))), + ); + const { getPricing, initPricing } = await loadPricing(); + + await initPricing(); + + expect(getPricing("gpt-4o")?.input).toBe(2.5); + expect(warn).toHaveBeenCalledOnce(); + }); + + it("should ignore the local override file when a pricing URL is set", async () => { + writeLocalOverrides(JSON.stringify({ "gpt-4o": { input: 1.0, output: 2.0 } })); + process.env.IDENTARK_PRICING_URL = "https://pricing.example/table.json"; + vi.stubGlobal( + "fetch", + vi.fn(async () => + Promise.resolve( + new Response(JSON.stringify({ "gpt-4o": { input: 4.0, output: 20.0 } }), { + status: 200, + }), + ), + ), + ); + const { getPricing, initPricing } = await loadPricing(); + + await initPricing(); + + expect(getPricing("gpt-4o")?.input).toBe(4.0); + }); + + it("should not apply the local override file when a pricing URL is set but never fetched", async () => { + writeLocalOverrides(JSON.stringify({ "gpt-4o": { input: 1.0, output: 2.0 } })); + process.env.IDENTARK_PRICING_URL = "https://pricing.example/table.json"; + const { getPricing } = await loadPricing(); + + // No initPricing() call — the higher-priority URL wins, so the lower-priority + // file stays unread and the table is left on bundled defaults. + expect(getPricing("gpt-4o")?.input).toBe(2.5); + }); + + it("should fetch only once across repeated calls", async () => { + process.env.IDENTARK_PRICING_URL = "https://pricing.example/table.json"; + const fetchMock = vi.fn(async () => + Promise.resolve( + new Response(JSON.stringify({ "gpt-4o": { input: 4.0, output: 20.0 } }), { + status: 200, + }), + ), + ); + vi.stubGlobal("fetch", fetchMock); + const { initPricing } = await loadPricing(); + + await initPricing(); + await initPricing(); + + expect(fetchMock).toHaveBeenCalledOnce(); + }); + + it("should not overwrite an explicit setPricingTable call", async () => { + process.env.IDENTARK_PRICING_URL = "https://pricing.example/table.json"; + const fetchMock = vi.fn(async () => + Promise.resolve( + new Response(JSON.stringify({ "gpt-4o": { input: 4.0, output: 20.0 } }), { + status: 200, + }), + ), + ); + vi.stubGlobal("fetch", fetchMock); + const { getPricing, initPricing, setPricingTable } = await loadPricing(); + + setPricingTable({ "gpt-4o": { input: 9.0, output: 9.0 } }); + await initPricing(); + + expect(getPricing("gpt-4o")?.input).toBe(9.0); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("should apply the local override file when no pricing URL is set", async () => { + writeLocalOverrides(JSON.stringify({ "gpt-4o": { input: 1.0, output: 2.0 } })); + const { getPricing, initPricing } = await loadPricing(); + + await initPricing(); + + expect(getPricing("gpt-4o")?.input).toBe(1.0); + }); + }); + + describe("bundled tables", () => { + it("should expose every provider table through BUNDLED_PRICING", async () => { + const { + BUNDLED_PRICING, + OPENAI_PRICING, + ANTHROPIC_PRICING, + MISTRAL_PRICING, + GEMINI_PRICING, + } = await loadPricing(); + + const expected = + Object.keys(OPENAI_PRICING).length + + Object.keys(ANTHROPIC_PRICING).length + + Object.keys(MISTRAL_PRICING).length + + Object.keys(GEMINI_PRICING).length; + expect(Object.keys(BUNDLED_PRICING)).toHaveLength(expected); + }); + + it("should price every bundled model with positive, finite rates", async () => { + const { BUNDLED_PRICING } = await loadPricing(); + for (const [model, pricing] of Object.entries(BUNDLED_PRICING)) { + expect(pricing.input, model).toBeGreaterThan(0); + expect(pricing.output, model).toBeGreaterThan(0); + expect(Number.isFinite(pricing.input), model).toBe(true); + expect(Number.isFinite(pricing.output), model).toBe(true); + } + }); + + it("should not be mutated by setPricingTable", async () => { + const { BUNDLED_PRICING, setPricingTable } = await loadPricing(); + setPricingTable({ "custom-model": { input: 1.0, output: 2.0 } }); + expect(BUNDLED_PRICING["custom-model"]).toBeUndefined(); + }); + }); +});