import { readFileSync } from "node:fs"; import { registerProviderSafetyStopModel } from "./adapter-internals/provider-safety-stop"; import { getOpenAIModelCost } from "./model-pricing"; import { isRetiredModelKey } from "./model-retirements"; import { applyGeneratedModelPolicies, enrichModelThinking } from "./model-thinking"; // `with { type: "file" }` is embedded by `bun build --compile` and resolves to // the bunfs path inside standalone binaries (and to the on-disk path in dev). // A plain `createRequire` of a `.json` listed as an extra compile entrypoint is // NOT emitted into the bunfs, and its cwd-fallback masks the failure whenever // the process runs inside a repo checkout — see PR body for the minimal repro. import modelsJsonPath from "./models.json" with { type: "file" }; import type { Api, KnownProvider, Model, Usage } from "./types"; import { isClaudeForcedToolChoiceIncapableModelId } from "./utils/tool-choice-capability"; /** * Static bundled model registry loaded lazily from `models.json`. * * This module intentionally exposes compile-time defaults only. * It does not include runtime discovery, models.dev overlays, or on-disk cache state. * * For runtime-aware resolution, use `createModelManager()` / `resolveProviderModels()`. */ type BundledCatalog = typeof import("./models.json"); let bundledCatalog: BundledCatalog | undefined; let providerNames: KnownProvider[] | undefined; const providerModelRegistry: Map>> = new Map(); function getBundledCatalog(): BundledCatalog { // TS types a .json import as its contents; at runtime `with { type: "file" }` // yields the file path (bunfs path in compiled binaries, disk path in dev). bundledCatalog ??= JSON.parse(readFileSync(modelsJsonPath as unknown as string, "utf8")) as BundledCatalog; return bundledCatalog; } function getProviderModels(provider: GeneratedProvider): Map> | undefined { const cached = providerModelRegistry.get(provider); if (cached) return cached; const models = getBundledCatalog()[provider]; if (!models) return undefined; const providerModels = new Map>(); for (const [id, model] of Object.entries(models)) { if (isRetiredModelKey(provider, id)) { continue; } const bundledModel = applyBundledCompatDefaults(enrichModelThinking(model as Model)); registerProviderSafetyStopModel(bundledModel); providerModels.set(id, bundledModel); } providerModelRegistry.set(provider, providerModels); return providerModels; } /** * Bundled-catalog compat defaults applied at load time so stale committed * models.json snapshots still receive policy-critical fields (e.g. Claude * Mythos rejecting forced tool use) without a full regeneration. */ function applyBundledCompatDefaults(model: Model): Model { let normalized = model; if (normalized.id === "minimax-m3" && normalized.name === "MiniMax M3") { normalized = { ...normalized, name: "MiniMax-M3" }; } if ( (normalized.api === "anthropic-messages" || normalized.api === "bedrock-converse-stream") && isClaudeForcedToolChoiceIncapableModelId(normalized.id) && (normalized.compat as { toolChoiceSupport?: string } | undefined)?.toolChoiceSupport === undefined ) { return { ...normalized, compat: { ...(normalized.compat ?? {}), toolChoiceSupport: "auto" } as Model["compat"], }; } const policyModels = [normalized]; applyGeneratedModelPolicies(policyModels); return policyModels[0] ?? normalized; } export type GeneratedProvider = keyof BundledCatalog; export function getBundledModel(provider: GeneratedProvider, modelId: string): Model { const providerModels = getProviderModels(provider); return providerModels?.get(modelId) as Model; } export function getBundledProviders(): KnownProvider[] { // Defensive copy: the old eager path returned a fresh Array.from(...), so // callers may freely mutate their result without corrupting enumeration. providerNames ??= Object.keys(getBundledCatalog()) as KnownProvider[]; return providerNames.slice(); } export function getBundledModels(provider: GeneratedProvider): Model[] { const models = getProviderModels(provider); return models ? (Array.from(models.values()) as Model[]) : []; } export function calculateCost(model: Model, usage: Usage): Usage["cost"] { const inputTokens = usage.input + usage.cacheRead + usage.cacheWrite; const pricing = getOpenAIModelCost(model, inputTokens) ?? model.cost; usage.cost.input = (pricing.input / 1000000) * usage.input; usage.cost.output = (pricing.output / 1000000) * usage.output; usage.cost.cacheRead = (pricing.cacheRead / 1000000) * usage.cacheRead; usage.cost.cacheWrite = (pricing.cacheWrite / 1000000) * usage.cacheWrite; usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite; return usage.cost; } /** * Check if two models are equal by comparing both their id and provider. * Returns false if either model is null or undefined. */ export function modelsAreEqual( a: Model | null | undefined, b: Model | null | undefined, ): boolean { if (!a || !b) return false; return a.id === b.id && a.provider === b.provider; }