UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

201 lines (190 loc) • 8.47 kB
import { createHash } from "node:crypto"; import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { parse, stringify } from "yaml"; export * from "./price-calc.ts"; import type { PriceCatalog, ModelPriceRow, PriceTier } from "./price-calc.ts"; import { PRICES_SCHEMA } from "./price-calc.ts"; export function hashPriceCatalog(catalog: Omit<PriceCatalog, "sha256">): string { const canonical = { schema: catalog.schema, date: catalog.date, currency: catalog.currency ?? "USD", models: catalog.models.map((m) => ({ id: m.id, provider: m.provider, model: m.model, aliases: m.aliases ? [...m.aliases].sort() : [], tiers: m.tiers.map((t) => ({ upToContextTokens: t.upToContextTokens ?? null, inputPerMillion: t.inputPerMillion, outputPerMillion: t.outputPerMillion, cacheReadPerMillion: t.cacheReadPerMillion ?? null, cacheWritePerMillion: t.cacheWritePerMillion ?? null, })), })).sort((a, b) => a.id.localeCompare(b.id)), }; return createHash("sha256").update(JSON.stringify(canonical), "utf8").digest("hex"); } export function parsePriceCatalog(text: string): PriceCatalog { const parsed = parse(text) as Record<string, unknown>; if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error("price catalog must be an object"); } if (parsed.schema !== PRICES_SCHEMA) { throw new Error(`price catalog schema must be ${PRICES_SCHEMA}`); } if (typeof parsed.date !== "string" || !/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(parsed.date)) { throw new Error("price catalog date must be YYYY-MM-DD"); } if (!Number.isFinite(Date.parse(parsed.date)) || new Date(parsed.date).toISOString().slice(0, 10) !== parsed.date) { throw new Error("price catalog date must be a valid calendar date"); } if (parsed.currency !== undefined && parsed.currency !== "USD") throw new Error("price catalog currency must be USD"); if (typeof parsed.sha256 !== "string" || !/^(?:sha256:)?[a-f0-9]{64}$/.test(parsed.sha256)) { throw new Error("price catalog sha256 must be a 64-character hex digest"); } if (!Array.isArray(parsed.models) || parsed.models.length === 0) { throw new Error("price catalog models must be a non-empty array"); } const models: ModelPriceRow[] = parsed.models.map((item, index) => { if (!item || typeof item !== "object" || Array.isArray(item)) { throw new Error(`price catalog model at index ${index} must be an object`); } const m = item as Record<string, unknown>; if (typeof m.id !== "string" || !m.id) { throw new Error(`price catalog model at index ${index} must have a non-empty id`); } if (typeof m.provider !== "string" || !m.provider) { throw new Error(`price catalog model at index ${index} must have a non-empty provider`); } if (typeof m.model !== "string" || !m.model) { throw new Error(`price catalog model at index ${index} must have a non-empty model`); } const aliases = Array.isArray(m.aliases) ? m.aliases.filter((a): a is string => typeof a === "string" && Boolean(a)) : undefined; if (!Array.isArray(m.tiers) || m.tiers.length === 0) { throw new Error(`price catalog model ${m.id} must have at least one tier`); } const rawTiers = m.tiers; let previousBound = 0; const tiers: PriceTier[] = rawTiers.map((tItem, tIndex) => { if (!tItem || typeof tItem !== "object" || Array.isArray(tItem)) { throw new Error(`tier at index ${tIndex} for model ${m.id} must be an object`); } const t = tItem as Record<string, unknown>; const rate = (field: string, short: string, required = false): number | null => { const value = t[field] !== undefined ? t[field] : t[short]; if (value == null && !required) return null; if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { throw new Error(`tier ${tIndex} for model ${m.id} requires finite non-negative ${field}`); } return value; }; const bound = t.upToContextTokens ?? null; if (bound === null) { if (tIndex !== rawTiers.length - 1) throw new Error("unbounded price tier must be last"); } else { if (typeof bound !== "number" || !Number.isSafeInteger(bound) || bound <= previousBound) throw new Error("price tier bounds must be positive and strictly increasing"); previousBound = bound; } return { upToContextTokens: bound, inputPerMillion: rate("inputPerMillion", "input", true)!, outputPerMillion: rate("outputPerMillion", "output", true)!, cacheReadPerMillion: rate("cacheReadPerMillion", "cacheRead"), cacheWritePerMillion: rate("cacheWritePerMillion", "cacheWrite"), }; }); return { id: m.id, provider: m.provider, model: m.model, aliases, tiers, }; }); const catalog: PriceCatalog = { schema: PRICES_SCHEMA, date: parsed.date, sha256: parsed.sha256.replace(/^sha256:/, ""), currency: typeof parsed.currency === "string" ? parsed.currency : "USD", models, }; if (new Set(models.map((row) => row.id)).size !== models.length) throw new Error("duplicate price catalog model id"); if (hashPriceCatalog(catalog) !== catalog.sha256) throw new Error("price catalog hash mismatch"); return catalog; } export function loadPriceCatalog(rootOrPath: string): PriceCatalog | undefined { const candidatePath = existsSync(join(rootOrPath, ".kxm", "prices.yaml")) ? join(rootOrPath, ".kxm", "prices.yaml") : (existsSync(join(rootOrPath, "prices.yaml")) ? join(rootOrPath, "prices.yaml") : (existsSync(rootOrPath) && statSync(rootOrPath).isFile() ? rootOrPath : undefined)); if (!candidatePath || !existsSync(candidatePath)) { return undefined; } const content = readFileSync(candidatePath, "utf8"); return parsePriceCatalog(content); } /** Result of a hash-verified, freshness-gated catalog load used for list estimates. */ export interface PriceCatalogEstimateSource { readonly catalog: PriceCatalog | undefined; readonly unavailable: boolean; readonly stale: boolean; } /** * Load a price catalog the way one-shot settlement does: re-parse injected * catalogs so the digest is checked, treat missing files as unknown, and drop * any snapshot whose date is not today. Callers must not throw after spend. */ export function loadPriceCatalogForEstimate(options?: { priceCatalog?: PriceCatalog | undefined; projectRoot?: string | undefined; }): PriceCatalogEstimateSource { let catalog: PriceCatalog | undefined; try { catalog = options?.priceCatalog ? parsePriceCatalog(JSON.stringify(options.priceCatalog)) : loadPriceCatalog(options?.projectRoot ?? process.cwd()); } catch { return { catalog: undefined, unavailable: true, stale: false }; } // Without a freshness-aware vendor feed, don't silently quote an older // local snapshot. Even today's hash-verified snapshot is only an estimate. if (catalog && catalog.date !== new Date().toISOString().slice(0, 10)) { return { catalog: undefined, unavailable: false, stale: true }; } return { catalog, unavailable: false, stale: false }; } /** * Stamp the existing project list-price file as today's estimate. * This does not fetch vendor rates. Until a catalog's date is today, * `loadPriceCatalogForEstimate` keeps the estimate unknown. */ export function acknowledgePriceCatalog(projectRoot: string, now = new Date()): PriceCatalog { const path = join(projectRoot, ".kxm", "prices.yaml"); const loaded = loadPriceCatalog(projectRoot); if (!loaded) throw new Error("price catalog missing"); const date = now.toISOString().slice(0, 10); const body = { schema: loaded.schema, date, currency: loaded.currency ?? "USD", models: loaded.models, }; const stamped: PriceCatalog = { ...body, sha256: hashPriceCatalog(body) }; writeFileSync(path, stringify({ schema: stamped.schema, date: stamped.date, sha256: stamped.sha256, currency: stamped.currency ?? "USD", models: stamped.models, }), "utf8"); const again = loadPriceCatalog(projectRoot); if (!again || again.date !== date || again.sha256 !== stamped.sha256) { throw new Error("price catalog acknowledge failed verification"); } return again; }