Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
50 kB · 1610 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611#!/usr/bin/env tsx
import { writeFileSync } from "fs";import { dirname, join } from "path";import { fileURLToPath } from "url";import { getAnthropicCacheCosts } from "../src/cache-pricing.js";import { COPILOT_CLIENT_HEADERS } from "../src/copilot-client-version.js";import { getOpenRouterReasoningCapabilities } from "../src/openrouter-reasoning.js";import { isPrivatePrimeInferenceModelId, parsePrimeInferenceModelCatalog, type PrimeInferenceCatalogEntry,} from "../src/prime-inference-model-catalog.js";import { CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL, CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL, CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL, CLOUDFLARE_WORKERS_AI_BASE_URL,} from "../src/providers/cloudflare.js";import { Api, type AnthropicMessagesCompat, KnownProvider, Model, type OpenAICompletionsCompat,} from "../src/types.js";import { MODELS as EXISTING_MODELS } from "../src/models.generated.js";import { fetchModelsDevModels } from "../src/models-dev.js";import { renderModelsFile } from "./render-models.js";
const __filename = fileURLToPath(import.meta.url);const __dirname = dirname(__filename);const packageRoot = join(__dirname, "..");
interface ModelsDevModel { id: string; name: string; tool_call?: boolean; reasoning?: boolean; limit?: { context?: number; output?: number; }; cost?: { input?: number; output?: number; cache_read?: number; cache_write?: number; }; modalities?: { input?: string[]; output?: string[]; }; provider?: { npm?: string; };}
interface AiGatewayModel { id: string; name?: string; context_window?: number; max_tokens?: number; tags?: string[]; pricing?: { input?: string | number; output?: string | number; input_cache_read?: string | number; input_cache_write?: string | number; };}
const COPILOT_STATIC_HEADERS = COPILOT_CLIENT_HEADERS;
const KIMI_STATIC_HEADERS = { "User-Agent": "KimiCLI/1.5",} as const;
const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1";const AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";const ZAI_TOOL_STREAM_UNSUPPORTED_MODELS = new Set(["glm-4.5", "glm-4.5-air", "glm-4.5-flash", "glm-4.5v"]);const EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_ANTHROPIC_MODELS = new Set([ "github-copilot:claude-haiku-4.5", "github-copilot:claude-sonnet-4", "github-copilot:claude-sonnet-4.5",]);
const DEEPSEEK_V4_THINKING_LEVEL_MAP = { minimal: null, low: null, medium: null, high: "high", xhigh: "max", max: null,} as const;
const KIMI_K3_THINKING_LEVEL_MAP = { off: null, minimal: null, low: null, medium: null, high: null, xhigh: null, max: "max",} as const;
const DEEPSEEK_V4_COMPAT: OpenAICompletionsCompat = { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek",};
const ZAI_THINKING_COMPAT: OpenAICompletionsCompat = { supportsReasoningEffort: false, thinkingFormat: "zai",};
const PRIME_INFERENCE_BASE_URL = "https://api.pinference.ai/api/v1";const PRIME_INFERENCE_COMPAT: OpenAICompletionsCompat = { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: true, maxTokensField: "max_tokens", supportsStrictMode: false,};interface PrimeInferenceModelMetadata { contextWindow?: number; maxTokens?: number; vision?: boolean; name?: string;}
// Prime's /models endpoint is authoritative for route metadata. OpenRouter and// these overrides only fill gaps for older or incomplete endpoint entries;// requests always go to Prime's own baseUrl.const PRIME_INFERENCE_MODEL_METADATA: Record<string, PrimeInferenceModelMetadata> = { // These routes accept 200k, checked against the live API 2026-07-08. The // other Claude routes take the full window their spec lists. "anthropic/claude-sonnet-4": { contextWindow: 200000 }, "anthropic/claude-sonnet-4.5": { contextWindow: 200000 }, // Windows confirmed against the live API 2026-07-08 where they are SMALLER // than the published spec — over-declaring breaks context tracking. "meta-llama/llama-3.2-1b-instruct": { contextWindow: 60000 }, "meta-llama/llama-3.2-3b-instruct": { contextWindow: 80000 }, "minimax/minimax-m3": { contextWindow: 524288 }, "moonshotai/kimi-k2-0905": { contextWindow: 98304 }, "nvidia/nemotron-3-super-120b-a12b": { contextWindow: 262144, maxTokens: 4096 }, // Enforced window is LARGER than OpenRouter's listing. "qwen/qwen3-30b-a3b-instruct-2507": { contextWindow: 262144 }, // OpenRouter has no max_completion_tokens for the rest of these. "moonshotai/kimi-k2.5": { maxTokens: 65535 }, "minimax/minimax-m2.7": { maxTokens: 131072 }, // models.dev (moonshotai + openrouter) both list output = context for k2.6. "moonshotai/kimi-k2.6": { maxTokens: 262144 }, "moonshotai/kimi-k3": { maxTokens: 1048576 }, "openai/gpt-4.1": { maxTokens: 32768 }, "openai/gpt-5-nano": { maxTokens: 128000 }, "openai/gpt-oss-20b": { maxTokens: 131072 }, "qwen/qwen3.5-397b-a17b": { maxTokens: 65536 }, "x-ai/grok-4.20": { maxTokens: 30000 }, "x-ai/grok-4.20-multi-agent": { maxTokens: 30000 }, "xiaomi/mimo-v2.5": { maxTokens: 131072 }, "z-ai/glm-5": { maxTokens: 131072 },};
// Flagship models pinned above the long tail in the model picker, so the full// catalog doesn't flood /model. Everything else stays selectable via search.const PRIME_INFERENCE_FEATURED_MODELS = new Set([ "anthropic/claude-fable-5", "anthropic/claude-haiku-4.5", "anthropic/claude-opus-4.6", "anthropic/claude-opus-4.7", "anthropic/claude-opus-4.8", "anthropic/claude-sonnet-4.5", "anthropic/claude-sonnet-4.6", "anthropic/claude-sonnet-5", "deepseek/deepseek-v3.2", "deepseek/deepseek-flash", "deepseek/deepseek-v4-pro", "minimax/minimax-m3", "moonshotai/kimi-k2.7-code", "moonshotai/kimi-k3", "nvidia/nemotron-3-nano-30b-a3b", "openai/gpt-5.3-codex", "openai/gpt-5.4", "openai/gpt-5.4-mini", "openai/gpt-5.4-pro", "openai/gpt-5.5", "qwen/qwen3-30b-a3b-instruct-2507", "qwen/qwen3-coder-next", "qwen/qwen3-max", "qwen/qwen3-vl-235b-a22b-thinking", "qwen/qwen3.8-max", "x-ai/grok-4.20", "x-ai/grok-4.20-multi-agent", "z-ai/glm-5", "z-ai/glm-5.1", "z-ai/glm-5.2",]);
// Prime ids whose OpenRouter listing uses a different id (e.g. after an// OpenRouter route rename); metadata lookups resolve through this mapping.const PRIME_INFERENCE_OPENROUTER_ALIASES: Record<string, string> = { // OpenRouter renamed its route to the dated id; Prime still serves the undated one. "qwen/qwen3.8-max": "qwen/qwen3.8-max-0902",};
// Conservative fallbacks for catalog models with no OpenRouter match and no// override above: an under-declared window degrades gracefully, an// over-declared one breaks context tracking.const PRIME_INFERENCE_DEFAULT_CONTEXT_WINDOW = 128000;const PRIME_INFERENCE_DEFAULT_MAX_TOKENS = 8192;
const OPENAI_RESPONSES_NONE_REASONING_MODELS = new Set([ "gpt-5.1", "gpt-5.2", "gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5.5", "gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",]);
function mergeThinkingLevelMap(model: Model<any>, map: NonNullable<Model<any>["thinkingLevelMap"]>): void { model.thinkingLevelMap = { ...model.thinkingLevelMap, ...map };}
function supportsOpenAiXhigh(modelId: string): boolean { return ( modelId.includes("gpt-5.2") || modelId.includes("gpt-5.3") || modelId.includes("gpt-5.4") || modelId.includes("gpt-5.5") || modelId.includes("gpt-5.6") || modelId.includes("gpt-6") );}
function isGoogleThinkingApi(model: Model<any>): boolean { return model.api === "google-generative-ai" || model.api === "google-vertex";}
function isGemini3ProModel(modelId: string): boolean { return /gemini-3(?:\.\d+)?-pro/.test(modelId.toLowerCase());}
function isGemini3FlashModel(modelId: string): boolean { return /gemini-3(?:\.\d+)?-flash/.test(modelId.toLowerCase());}
function isGemma4Model(modelId: string): boolean { return /gemma-?4/.test(modelId.toLowerCase());}
function applyThinkingLevelMetadata(model: Model<any>): void { if ( (model.api === "openai-responses" || model.api === "azure-openai-responses") && (model.id.startsWith("gpt-5") || model.id.startsWith("gpt-6")) ) { mergeThinkingLevelMap(model, { off: null }); } if ( model.api === "openai-responses" && model.provider === "openai" && OPENAI_RESPONSES_NONE_REASONING_MODELS.has(model.id) ) { mergeThinkingLevelMap(model, { off: "none" }); } if (supportsOpenAiXhigh(model.id)) { mergeThinkingLevelMap(model, { xhigh: "xhigh" }); } if (model.id.includes("gpt-5.6") || model.id.includes("gpt-6")) { mergeThinkingLevelMap(model, { minimal: null, max: "max" }); } // gpt-6 reasoning is mandatory with no minimal effort; xhigh/max are supported (OpenRouter capability data). if (model.id.includes("gpt-6")) { mergeThinkingLevelMap(model, { minimal: null, xhigh: "xhigh", max: "max" }); } if ( (model.api === "openai-responses" || model.api === "azure-openai-responses" || model.api === "openai-codex-responses") && model.id.startsWith("gpt-6") ) { mergeThinkingLevelMap(model, { off: null }); } // Per-family effort support per the Anthropic effort docs. Opus 4.6 / Sonnet 4.6 // have no xhigh; Fable 5 / Mythos 5 / Mythos Preview think every turn (off: null). if ( model.id.includes("opus-4-6") || model.id.includes("opus-4.6") || model.id.includes("sonnet-4-6") || model.id.includes("sonnet-4.6") ) { mergeThinkingLevelMap(model, { max: "max" }); } if ( model.id.includes("opus-4-7") || model.id.includes("opus-4.7") || model.id.includes("opus-4-8") || model.id.includes("opus-4.8") || model.id.includes("opus-5") || model.id.includes("sonnet-5") ) { mergeThinkingLevelMap(model, { xhigh: "xhigh", max: "max" }); } if (model.id.includes("fable-5") || model.id.includes("mythos-5")) { mergeThinkingLevelMap(model, { off: null, xhigh: "xhigh", max: "max" }); } if (model.id.includes("mythos-preview")) { mergeThinkingLevelMap(model, { off: null, max: "max" }); } if (model.api === "openai-completions" && model.id.includes("deepseek-v4")) { mergeThinkingLevelMap(model, DEEPSEEK_V4_THINKING_LEVEL_MAP); } const kimiK3Id = model.id.toLowerCase(); if (!model.thinkingLevelMap && (/^k3(-|$)/.test(kimiK3Id) || /(^|\/)kimi-k3(-|$)/.test(kimiK3Id))) { mergeThinkingLevelMap(model, KIMI_K3_THINKING_LEVEL_MAP); } if (isGoogleThinkingApi(model) && isGemini3ProModel(model.id)) { mergeThinkingLevelMap(model, { off: null, minimal: null, low: "LOW", medium: null, high: "HIGH" }); } if (isGoogleThinkingApi(model) && isGemini3FlashModel(model.id)) { mergeThinkingLevelMap(model, { off: null }); } if (isGoogleThinkingApi(model) && isGemma4Model(model.id)) { mergeThinkingLevelMap(model, { off: null, minimal: "MINIMAL", low: null, medium: null, high: "HIGH" }); } if ( model.provider === "openai-codex" && supportsOpenAiXhigh(model.id) && !model.id.includes("gpt-5.6") && !model.id.includes("gpt-6") ) { mergeThinkingLevelMap(model, { minimal: "low" }); } if (model.provider === "openai-codex" && model.id === "gpt-5.1-codex-mini") { mergeThinkingLevelMap(model, { minimal: "medium", low: "medium", medium: "medium", high: "high" }); }}
function getAnthropicMessagesCompat(provider: string, modelId: string): AnthropicMessagesCompat | undefined { return EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_ANTHROPIC_MODELS.has(`${provider}:${modelId}`) ? { supportsEagerToolInputStreaming: false } : undefined;}
function getBedrockBaseUrl(modelId: string): string { if (modelId.startsWith("eu.")) return "https://bedrock-runtime.eu-central-1.amazonaws.com"; if (modelId.startsWith("au.")) return "https://bedrock-runtime.ap-southeast-2.amazonaws.com"; if (modelId.startsWith("jp.")) return "https://bedrock-runtime.ap-northeast-1.amazonaws.com"; return "https://bedrock-runtime.us-east-1.amazonaws.com";}
function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null;}
function getOptionalNumber(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) ? value : undefined;}
function getPrimeInferenceCacheCosts(modelId: string, inputCost: number): { cacheRead: number; cacheWrite: number } { return modelId.toLowerCase().startsWith("anthropic/") ? getAnthropicCacheCosts(inputCost, "5m") : { cacheRead: 0, cacheWrite: 0 };}
function getExistingPrimeInferenceModels(): Model<"openai-completions">[] { const models = EXISTING_MODELS["prime-inference"] as unknown as Record<string, Model<"openai-completions">>; return Object.values(models) .filter((model) => !isPrivatePrimeInferenceModelId(model.id)) .map((model) => ({ ...model, input: [...model.input], cost: { ...model.cost, ...getPrimeInferenceCacheCosts(model.id, model.cost.input), }, ...(model.compat ? { compat: { ...model.compat } } : {}), ...(model.thinkingLevelMap ? { thinkingLevelMap: { ...model.thinkingLevelMap } } : {}), ...(model.headers ? { headers: { ...model.headers } } : {}), }));}
function mergePrimeInferenceModels( snapshotModels: Model<"openai-completions">[], catalogModels: Model<"openai-completions">[],): Model<"openai-completions">[] { const models = new Map<string, Model<"openai-completions">>(); for (const model of snapshotModels) { models.set(model.id.toLowerCase(), model); } for (const model of catalogModels) { models.set(model.id.toLowerCase(), model); } return Array.from(models.values());}
function refreshPrimeInferenceAliasLimits( snapshotModels: Model<"openai-completions">[], catalogModels: Model<"openai-completions">[],): Model<"openai-completions">[] { const liveModels = new Map(catalogModels.map((model) => [model.id.toLowerCase(), model])); return snapshotModels.map((model) => { const canonicalId = PRIME_INFERENCE_OPENROUTER_ALIASES[model.id.toLowerCase()]; const canonical = canonicalId ? liveModels.get(canonicalId) : undefined; if (!canonical) { return model; } return { ...model, contextWindow: canonical.contextWindow, maxTokens: canonical.maxTokens, }; });}
function getPrimeInferenceDisplayName(modelId: string): string { const rawName = modelId.split("/").at(-1) ?? modelId; return rawName .split(/[-_]+/) .filter((part) => part.length > 0) .map((part) => { if (part === part.toUpperCase() || /\d/.test(part)) return part.toUpperCase(); if (part.length <= 3) return part.toUpperCase(); return part.charAt(0).toUpperCase() + part.slice(1); }) .join(" ");}
function isPrimeInferenceReasoningModel(modelId: string, catalogReasoning?: boolean): boolean { if (catalogReasoning !== undefined) { return catalogReasoning; }
const id = modelId.toLowerCase(); return ( id.includes("thinking") || id.includes("deepseek-v4") || id.startsWith("minimax/minimax-m") || id.startsWith("moonshotai/kimi") || id.startsWith("x-ai/grok-4") || id.startsWith("z-ai/glm-") || (id.startsWith("openai/gpt-5") && !id.includes("-chat")) || /^anthropic\/claude-(?:fable-5|opus-4|sonnet-(?:4|5))/.test(id) );}
function getPrimeInferenceCompat(modelId: string): OpenAICompletionsCompat { const id = modelId.toLowerCase(); if (id.includes("deepseek-v4")) { return { ...PRIME_INFERENCE_COMPAT, ...DEEPSEEK_V4_COMPAT, }; } if (id.startsWith("z-ai/glm-")) { return { ...PRIME_INFERENCE_COMPAT, ...ZAI_THINKING_COMPAT, }; }
return PRIME_INFERENCE_COMPAT;}
interface PrimeInferenceOpenRouterMetadata { contextWindow?: number; maxTokens?: number; vision: boolean; reasoning: boolean; thinkingLevelMap?: Model<"openai-completions">["thinkingLevelMap"]; supportsReasoningEffort?: boolean;}
function buildPrimeInferenceOpenRouterIndex(catalog: unknown[]): Map<string, PrimeInferenceOpenRouterMetadata> { const index = new Map<string, PrimeInferenceOpenRouterMetadata>(); for (const item of catalog) { if (!isRecord(item) || typeof item.id !== "string") { continue; } const topProvider = isRecord(item.top_provider) ? item.top_provider : {}; const architecture = isRecord(item.architecture) ? item.architecture : {}; const modalities = Array.isArray(architecture.input_modalities) ? architecture.input_modalities : []; const supportedParameters = Array.isArray(item.supported_parameters) ? item.supported_parameters : []; const reasoningCapabilities = getOpenRouterReasoningCapabilities(item); index.set(item.id.toLowerCase(), { contextWindow: getOptionalNumber(item.context_length) ?? getOptionalNumber(topProvider.context_length), maxTokens: getOptionalNumber(topProvider.max_completion_tokens), vision: modalities.includes("image"), // Same signal the OpenRouter provider path uses; the top-level // `reasoning` object over-reports (e.g. qwen3-max carries one despite // not accepting reasoning params). reasoning: supportedParameters.includes("reasoning"), ...(reasoningCapabilities?.thinkingLevelMap ? { thinkingLevelMap: reasoningCapabilities.thinkingLevelMap } : {}), ...(reasoningCapabilities?.supportsReasoningEffort === false ? { supportsReasoningEffort: false } : {}), }); } return index;}
function getPrimeInferenceOpenRouterMetadata( index: Map<string, PrimeInferenceOpenRouterMetadata>, modelId: string,): PrimeInferenceOpenRouterMetadata | undefined { const id = modelId.toLowerCase(); return index.get(PRIME_INFERENCE_OPENROUTER_ALIASES[id] ?? id);}
async function fetchPrimeInferenceModels(): Promise<Model<"openai-completions">[]> { let catalog: PrimeInferenceCatalogEntry[] = [];
try { console.log("Fetching public models from Prime Inference API..."); const response = await fetch(`${PRIME_INFERENCE_BASE_URL}/models`); catalog = parsePrimeInferenceModelCatalog(await response.json()); } catch (error) { console.error("Failed to fetch Prime Inference models:", error); }
let openRouterIndex = new Map<string, PrimeInferenceOpenRouterMetadata>(); try { openRouterIndex = buildPrimeInferenceOpenRouterIndex(await fetchOpenRouterCatalog()); } catch (error) { console.error("Failed to fetch OpenRouter catalog for Prime Inference metadata:", error); } if (openRouterIndex.size === 0) { // Without OpenRouter metadata every model would regress to the defaults; // keep the previous snapshot instead. console.error("OpenRouter catalog unavailable; keeping snapshot Prime Inference models"); return getExistingPrimeInferenceModels(); }
const catalogModels = catalog .filter((entry) => !isPrivatePrimeInferenceModelId(entry.id)) .map((entry) => createPrimeInferenceModel( entry, PRIME_INFERENCE_MODEL_METADATA[entry.id.toLowerCase()], getPrimeInferenceOpenRouterMetadata(openRouterIndex, entry.id), ), ); let snapshotModels = getExistingPrimeInferenceModels(); if (catalog.length > 0 && catalogModels.length < Math.ceil(snapshotModels.length * 0.5)) { console.error("Prime Inference catalog is severely truncated; keeping snapshot models"); return snapshotModels; } if (catalog.length > 0) { const liveIds = new Set(catalogModels.map((model) => model.id.toLowerCase())); snapshotModels = snapshotModels.filter((model) => liveIds.has(model.id.toLowerCase())); } snapshotModels = refreshPrimeInferenceAliasLimits(snapshotModels, catalogModels); const models = mergePrimeInferenceModels(snapshotModels, catalogModels); console.log(`Loaded ${models.length} Prime Inference models (${catalogModels.length} from the live catalog)`); return models;}
function createPrimeInferenceModel( entry: PrimeInferenceCatalogEntry, override: PrimeInferenceModelMetadata | undefined, openRouter: PrimeInferenceOpenRouterMetadata | undefined,): Model<"openai-completions"> { const vision = entry.vision ?? override?.vision ?? openRouter?.vision ?? false; const fallbackCacheCosts = getPrimeInferenceCacheCosts(entry.id, entry.input); const cacheCosts = { cacheRead: entry.cacheRead ?? fallbackCacheCosts.cacheRead, cacheWrite: entry.cacheWrite ?? fallbackCacheCosts.cacheWrite, }; const contextWindow = entry.contextWindow ?? override?.contextWindow ?? openRouter?.contextWindow ?? PRIME_INFERENCE_DEFAULT_CONTEXT_WINDOW; // Sources are independent, so an OpenRouter output cap can exceed a // gateway-measured window override; clamp to keep the pair coherent. const maxTokens = Math.min( entry.maxTokens ?? override?.maxTokens ?? openRouter?.maxTokens ?? PRIME_INFERENCE_DEFAULT_MAX_TOKENS, contextWindow, ); const compat = getPrimeInferenceCompat(entry.id); return { id: entry.id, ...(PRIME_INFERENCE_FEATURED_MODELS.has(entry.id.toLowerCase()) ? { featured: true } : {}), name: entry.name ?? override?.name ?? getPrimeInferenceDisplayName(entry.id), api: "openai-completions", provider: "prime-inference", baseUrl: PRIME_INFERENCE_BASE_URL, reasoning: isPrimeInferenceReasoningModel(entry.id, entry.reasoning ?? openRouter?.reasoning), ...(openRouter?.thinkingLevelMap ? { thinkingLevelMap: openRouter.thinkingLevelMap } : {}), input: vision ? ["text", "image"] : ["text"], cost: { input: entry.input, output: entry.output, ...cacheCosts, }, contextWindow, maxTokens, compat: { ...compat, ...(openRouter?.supportsReasoningEffort === false ? { supportsReasoningEffort: false, ...(!compat.thinkingFormat ? { thinkingFormat: "openrouter" as const } : {}), } : {}), }, };}
let openRouterCatalogPromise: Promise<any[]> | undefined;
function fetchOpenRouterCatalog(): Promise<any[]> { openRouterCatalogPromise ??= (async () => { console.log("Fetching models from OpenRouter API..."); const response = await fetch("https://openrouter.ai/api/v1/models"); const data = await response.json(); return Array.isArray(data?.data) ? data.data : []; })(); return openRouterCatalogPromise;}
async function fetchOpenRouterModels(): Promise<Model<any>[]> { try { const models: Model<any>[] = [];
for (const model of await fetchOpenRouterCatalog()) { // Only include models that support tools if (!model.supported_parameters?.includes("tools")) continue; // :batch routes are asynchronous batch variants, not streaming models if (model.id.endsWith(":batch")) continue;
// Parse provider from model ID let provider: KnownProvider = "openrouter"; let modelKey = model.id;
modelKey = model.id; // Keep full ID for OpenRouter
// Parse input modalities const input: ("text" | "image")[] = ["text"]; if (model.architecture?.modality?.includes("image")) { input.push("image"); }
// Convert pricing from $/token to $/million tokens. OpenRouter uses // negative values as a placeholder for unknown pricing (e.g. auto-beta). // Time-windowed tariff overrides (utc_start/utc_end) make the top-level // price clock-dependent (e.g. Tencent Hy3 peak/off-peak); commit the peak // rate so cost accounting never undercounts and regens stay hour-independent. const timeWindowedTariffs = (Array.isArray(model.pricing?.overrides) ? model.pricing.overrides : []).filter( (override: any) => typeof override?.utc_start === "number", ); const peakPrice = (field: string): number => Math.max( 0, parseFloat(model.pricing?.[field] || "0"), ...timeWindowedTariffs.map((override: any) => parseFloat(override?.[field] || "0")), ) * 1_000_000; const inputCost = peakPrice("prompt"); const outputCost = peakPrice("completion"); const cacheReadCost = peakPrice("input_cache_read"); const cacheWriteCost = peakPrice("input_cache_write"); const reasoningCapabilities = getOpenRouterReasoningCapabilities(model);
const normalizedModel: Model<any> = { id: modelKey, name: model.name, api: "openai-completions", baseUrl: "https://openrouter.ai/api/v1", provider, reasoning: model.supported_parameters?.includes("reasoning") || false, ...(reasoningCapabilities?.thinkingLevelMap ? { thinkingLevelMap: reasoningCapabilities.thinkingLevelMap } : {}), ...(reasoningCapabilities?.supportsReasoningEffort === false ? { compat: { supportsReasoningEffort: false } } : {}), input, cost: { input: inputCost, output: outputCost, cacheRead: cacheReadCost, cacheWrite: cacheWriteCost, }, contextWindow: model.context_length || 4096, maxTokens: model.top_provider?.max_completion_tokens || 4096, }; models.push(normalizedModel); }
console.log(`Fetched ${models.length} tool-capable models from OpenRouter`); return models; } catch (error) { console.error("Failed to fetch OpenRouter models:", error); return []; }}
async function fetchAiGatewayModels(): Promise<Model<any>[]> { try { console.log("Fetching models from Vercel AI Gateway API..."); const response = await fetch(`${AI_GATEWAY_MODELS_URL}/models`); const data = await response.json(); const models: Model<any>[] = [];
const toNumber = (value: string | number | undefined): number => { if (typeof value === "number") { return Number.isFinite(value) ? value : 0; } const parsed = parseFloat(value ?? "0"); return Number.isFinite(parsed) ? parsed : 0; };
const items = Array.isArray(data.data) ? (data.data as AiGatewayModel[]) : []; for (const model of items) { const tags = Array.isArray(model.tags) ? model.tags : []; // Only include models that support tools if (!tags.includes("tool-use")) continue;
const input: ("text" | "image")[] = ["text"]; if (tags.includes("vision")) { input.push("image"); }
const inputCost = toNumber(model.pricing?.input) * 1_000_000; const outputCost = toNumber(model.pricing?.output) * 1_000_000; const cacheReadCost = toNumber(model.pricing?.input_cache_read) * 1_000_000; const cacheWriteCost = toNumber(model.pricing?.input_cache_write) * 1_000_000;
models.push({ id: model.id, name: model.name || model.id, api: "anthropic-messages", baseUrl: AI_GATEWAY_BASE_URL, provider: "vercel-ai-gateway", // DeepSeek's *-thinking routes always think; the gateway omits the tag. reasoning: tags.includes("reasoning") || model.id.includes("-thinking"), input, cost: { input: inputCost, output: outputCost, cacheRead: cacheReadCost, cacheWrite: cacheWriteCost, }, contextWindow: model.context_window || 4096, maxTokens: model.max_tokens || 4096, }); }
console.log(`Fetched ${models.length} tool-capable models from Vercel AI Gateway`); return models; } catch (error) { console.error("Failed to fetch Vercel AI Gateway models:", error); return []; }}
async function loadModelsDevData(): Promise<Model<any>[]> { try { console.log("Fetching models from models.dev API..."); const models = await fetchModelsDevModels(); console.log(`Loaded ${models.length} tool-capable models from models.dev`); return models; } catch (error) { console.error("Failed to load models.dev data:", error); return []; }}
async function generateModels() { // Fetch models from both sources // models.dev: Anthropic, Google, OpenAI, Groq, Cerebras // OpenRouter: xAI and other providers (excluding Anthropic, Google, OpenAI) // AI Gateway: OpenAI-compatible catalog with tool-capable models const modelsDevModels = await loadModelsDevData(); const openRouterModels = await fetchOpenRouterModels(); const aiGatewayModels = await fetchAiGatewayModels();
// Combine models (models.dev has priority) const allModels = [...modelsDevModels, ...openRouterModels, ...aiGatewayModels].filter( (model) => !((model.provider === "opencode" || model.provider === "opencode-go") && model.id === "gpt-5.3-codex-spark"), );
// Fix incorrect cache pricing for Claude Opus 4.5 from models.dev // models.dev has 3x the correct pricing (1.5/18.75 instead of 0.5/6.25) const opus45 = allModels.find(m => m.provider === "anthropic" && m.id === "claude-opus-4-5"); if (opus45) { opus45.cost.cacheRead = 0.5; opus45.cost.cacheWrite = 6.25; }
// Temporary overrides until upstream model metadata is corrected. for (const candidate of allModels) { if (candidate.provider === "amazon-bedrock" && candidate.id.includes("anthropic.claude-opus-4-6-v1")) { candidate.cost.cacheRead = 0.5; candidate.cost.cacheWrite = 6.25; } if ( (candidate.provider === "anthropic" || candidate.provider === "opencode" || candidate.provider === "opencode-go" || candidate.provider === "github-copilot") && (candidate.id === "claude-opus-4-6" || candidate.id === "claude-sonnet-4-6" || candidate.id === "claude-opus-4.6" || candidate.id === "claude-sonnet-4.6") ) { candidate.contextWindow = 1000000; }
// OpenCode variants list Claude Sonnet 4/4.5 with 1M context, actual limit is 200K if ( (candidate.provider === "opencode" || candidate.provider === "opencode-go") && (candidate.id === "claude-sonnet-4-5" || candidate.id === "claude-sonnet-4") ) { candidate.contextWindow = 200000; } if ((candidate.provider === "opencode" || candidate.provider === "opencode-go") && candidate.id === "gpt-5.4") { candidate.contextWindow = 272000; candidate.maxTokens = 128000; } if (candidate.provider === "openai" && (candidate.id === "gpt-5.4" || candidate.id === "gpt-5.5")) { candidate.contextWindow = 272000; candidate.maxTokens = 128000; } // Keep selected OpenRouter model metadata stable until upstream settles. if (candidate.provider === "openrouter" && candidate.id === "moonshotai/kimi-k2.5") { candidate.cost.input = 0.41; candidate.cost.output = 2.06; candidate.cost.cacheRead = 0.07; candidate.maxTokens = 4096; } if (candidate.provider === "openrouter" && candidate.id === "moonshotai/kimi-k3") { candidate.maxTokens = 1048576; } if (candidate.provider === "openrouter" && candidate.id === "z-ai/glm-5") { candidate.cost.input = 0.6; candidate.cost.output = 1.9; candidate.cost.cacheRead = 0.119; }
}
// Add missing EU Opus 4.6 profile if (!allModels.some((m) => m.provider === "amazon-bedrock" && m.id === "eu.anthropic.claude-opus-4-6-v1")) { allModels.push({ id: "eu.anthropic.claude-opus-4-6-v1", name: "Claude Opus 4.6 (EU)", api: "bedrock-converse-stream", provider: "amazon-bedrock", baseUrl: getBedrockBaseUrl("eu.anthropic.claude-opus-4-6-v1"), reasoning: true, input: ["text", "image"], cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25, }, contextWindow: 200000, maxTokens: 128000, }); }
// Add missing Claude Opus 4.6 if (!allModels.some(m => m.provider === "anthropic" && m.id === "claude-opus-4-6")) { allModels.push({ id: "claude-opus-4-6", name: "Claude Opus 4.6", api: "anthropic-messages", baseUrl: "https://api.anthropic.com", provider: "anthropic", reasoning: true, input: ["text", "image"], cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25, }, contextWindow: 1000000, maxTokens: 128000, }); }
// Add missing Claude Opus 4.7 if (!allModels.some(m => m.provider === "anthropic" && m.id === "claude-opus-4-7")) { allModels.push({ id: "claude-opus-4-7", name: "Claude Opus 4.7", api: "anthropic-messages", baseUrl: "https://api.anthropic.com", provider: "anthropic", reasoning: true, input: ["text", "image"], cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25, }, contextWindow: 1000000, maxTokens: 128000, }); }
// Add missing Claude Sonnet 4.6 if (!allModels.some(m => m.provider === "anthropic" && m.id === "claude-sonnet-4-6")) { allModels.push({ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", api: "anthropic-messages", baseUrl: "https://api.anthropic.com", provider: "anthropic", reasoning: true, input: ["text", "image"], cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75, }, contextWindow: 1000000, maxTokens: 64000, }); }
// Add missing Gemini 3.1 Flash Lite Preview until models.dev includes it. if (!allModels.some((m) => m.provider === "google" && m.id === "gemini-3.1-flash-lite-preview")) { allModels.push({ id: "gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview", api: "google-generative-ai", baseUrl: "https://generativelanguage.googleapis.com/v1beta", provider: "google", reasoning: true, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, }, contextWindow: 1048576, maxTokens: 65536, }); }
// Add missing gpt models if (!allModels.some(m => m.provider === "openai" && m.id === "gpt-5-chat-latest")) { allModels.push({ id: "gpt-5-chat-latest", name: "GPT-5 Chat Latest", api: "openai-responses", baseUrl: "https://api.openai.com/v1", provider: "openai", reasoning: false, input: ["text", "image"], cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0, }, contextWindow: 128000, maxTokens: 16384, }); }
if (!allModels.some(m => m.provider === "openai" && m.id === "gpt-5.1-codex")) { allModels.push({ id: "gpt-5.1-codex", name: "GPT-5.1 Codex", api: "openai-responses", baseUrl: "https://api.openai.com/v1", provider: "openai", reasoning: true, input: ["text", "image"], cost: { input: 1.25, output: 5, cacheRead: 0.125, cacheWrite: 1.25, }, contextWindow: 400000, maxTokens: 128000, }); }
if (!allModels.some(m => m.provider === "openai" && m.id === "gpt-5.1-codex-max")) { allModels.push({ id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max", api: "openai-responses", baseUrl: "https://api.openai.com/v1", provider: "openai", reasoning: true, input: ["text", "image"], cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0, }, contextWindow: 400000, maxTokens: 128000, }); }
if (!allModels.some(m => m.provider === "openai" && m.id === "gpt-5.3-codex-spark")) { allModels.push({ id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark", api: "openai-responses", baseUrl: "https://api.openai.com/v1", provider: "openai", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, }, contextWindow: 128000, maxTokens: 16384, }); }
// Add missing GitHub Copilot GPT-5.3 models until models.dev includes them. const copilotBaseModel = allModels.find( (m) => m.provider === "github-copilot" && m.id === "gpt-5.2-codex", ); if (copilotBaseModel) { if (!allModels.some((m) => m.provider === "github-copilot" && m.id === "gpt-5.3-codex")) { allModels.push({ ...copilotBaseModel, id: "gpt-5.3-codex", name: "GPT-5.3 Codex", }); } }
if (!allModels.some((m) => m.provider === "openai" && m.id === "gpt-5.4")) { allModels.push({ id: "gpt-5.4", name: "GPT-5.4", api: "openai-responses", baseUrl: "https://api.openai.com/v1", provider: "openai", reasoning: true, input: ["text", "image"], cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0, }, contextWindow: 272000, maxTokens: 128000, }); }
const deepseekV4Models: Model<"openai-completions">[] = [ { id: "deepseek-flash", name: "DeepSeek Flash", api: "openai-completions", baseUrl: "https://api.deepseek.com", provider: "deepseek", reasoning: true, input: ["text", "image"], cost: { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0, }, contextWindow: 1000000, maxTokens: 384000, compat: DEEPSEEK_V4_COMPAT, }, { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", api: "openai-completions", baseUrl: "https://api.deepseek.com", provider: "deepseek", reasoning: true, input: ["text"], cost: { input: 0.435, output: 0.87, cacheRead: 0.003625, cacheWrite: 0, }, contextWindow: 1000000, maxTokens: 384000, compat: DEEPSEEK_V4_COMPAT, }, ]; allModels.push(...deepseekV4Models);
for (const candidate of allModels) { if ( candidate.api === "openai-completions" && (candidate.id.includes("deepseek-v4") || candidate.id === "deepseek-flash" || candidate.id.endsWith("/deepseek-flash")) ) { candidate.compat = { ...candidate.compat, ...(candidate.provider === "openrouter" ? { requiresReasoningContentOnAssistantMessages: DEEPSEEK_V4_COMPAT.requiresReasoningContentOnAssistantMessages, thinkingFormat: DEEPSEEK_V4_COMPAT.thinkingFormat, } : DEEPSEEK_V4_COMPAT), }; mergeThinkingLevelMap(candidate, DEEPSEEK_V4_THINKING_LEVEL_MAP); } }
const minimaxDirectSupportedIds = new Set(["MiniMax-M2.7", "MiniMax-M2.7-highspeed"]);
for (const candidate of allModels) { if ( (candidate.provider === "minimax" || candidate.provider === "minimax-cn") && minimaxDirectSupportedIds.has(candidate.id) ) { candidate.contextWindow = 204800; candidate.maxTokens = 131072; } }
for (let i = allModels.length - 1; i >= 0; i--) { const candidate = allModels[i]; if ( (candidate.provider === "minimax" || candidate.provider === "minimax-cn") && !minimaxDirectSupportedIds.has(candidate.id) ) { allModels.splice(i, 1); } }
// OpenAI Codex (ChatGPT OAuth) models // NOTE: These are not fetched from models.dev; we keep a small, explicit list to avoid aliases. // Context window is based on observed server limits (400s above ~272k), not marketing numbers. const CODEX_BASE_URL = "https://chatgpt.com/backend-api"; const CODEX_CONTEXT = 272000; const CODEX_MAX_TOKENS = 128000; const codexModels: Model<"openai-codex-responses">[] = [ { id: "gpt-5.1", name: "GPT-5.1", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-5.1-codex-mini", name: "GPT-5.1 Codex Mini", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 0.25, output: 2, cacheRead: 0.025, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-5.2", name: "GPT-5.2", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-5.2-codex", name: "GPT-5.2 Codex", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-5.4", name: "GPT-5.4", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-5.5", name: "GPT-5.5", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 3.125 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 1, output: 6, cacheRead: 0.1, cacheWrite: 1.25 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-6-astra", name: "GPT-6 Astra", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-5.4-mini", name: "GPT-5.4 Mini", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: CODEX_MAX_TOKENS, }, ]; allModels.push(...codexModels);
// Add missing Grok models if (!allModels.some(m => m.provider === "xai" && m.id === "grok-code-fast-1")) { allModels.push({ id: "grok-code-fast-1", name: "Grok Code Fast 1", api: "openai-completions", baseUrl: "https://api.x.ai/v1", provider: "xai", reasoning: false, input: ["text"], cost: { input: 0.2, output: 1.5, cacheRead: 0.02, cacheWrite: 0, }, contextWindow: 32768, maxTokens: 8192, }); }
// Add missing Mistral Medium 3.5 model until models.dev includes it if (!allModels.some(m => m.provider === "mistral" && m.id === "mistral-medium-3.5")) { allModels.push({ id: "mistral-medium-3.5", name: "Mistral Medium 3.5", api: "mistral-conversations", provider: "mistral", baseUrl: "https://api.mistral.ai", reasoning: true, input: ["text", "image"], cost: { input: 1.5, output: 7.5, cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, // 256k tokens maxTokens: 262144, }); }
// Add "auto" alias for openrouter/auto if (!allModels.some(m => m.provider === "openrouter" && m.id === "auto")) { allModels.push({ id: "auto", name: "Auto", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, input: ["text", "image"], cost: { // we dont know about the costs because OpenRouter auto routes to different models // and then charges you for the underlying used model input:0, output:0, cacheRead:0, cacheWrite:0, }, contextWindow: 2000000, maxTokens: 30000, }); }
const VERTEX_BASE_URL = "https://{location}-aiplatform.googleapis.com"; const vertexModels: Model<"google-vertex">[] = [ { id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview (Vertex)", api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 }, contextWindow: 1000000, maxTokens: 64000, }, { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview (Vertex)", api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 }, contextWindow: 1048576, maxTokens: 65536, }, { id: "gemini-3.1-pro-preview-customtools", name: "Gemini 3.1 Pro Preview Custom Tools (Vertex)", api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 }, contextWindow: 1048576, maxTokens: 65536, }, { id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview (Vertex)", api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0 }, contextWindow: 1048576, maxTokens: 65536, }, { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash (Vertex)", api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: false, input: ["text", "image"], cost: { input: 0.15, output: 0.6, cacheRead: 0.0375, cacheWrite: 0 }, contextWindow: 1048576, maxTokens: 8192, }, { id: "gemini-2.0-flash-lite", name: "Gemini 2.0 Flash Lite (Vertex)", api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 0.075, output: 0.3, cacheRead: 0.01875, cacheWrite: 0 }, contextWindow: 1048576, maxTokens: 65536, }, { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro (Vertex)", api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 }, contextWindow: 1048576, maxTokens: 65536, }, { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash (Vertex)", api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0 }, contextWindow: 1048576, maxTokens: 65536, }, { id: "gemini-2.5-flash-lite-preview-09-2025", name: "Gemini 2.5 Flash Lite Preview 09-25 (Vertex)", api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 0.1, output: 0.4, cacheRead: 0.01, cacheWrite: 0 }, contextWindow: 1048576, maxTokens: 65536, }, { id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite (Vertex)", api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 0.1, output: 0.4, cacheRead: 0.01, cacheWrite: 0 }, contextWindow: 1048576, maxTokens: 65536, }, { id: "gemini-1.5-pro", name: "Gemini 1.5 Pro (Vertex)", api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: false, input: ["text", "image"], cost: { input: 1.25, output: 5, cacheRead: 0.3125, cacheWrite: 0 }, contextWindow: 1000000, maxTokens: 8192, }, { id: "gemini-1.5-flash", name: "Gemini 1.5 Flash (Vertex)", api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: false, input: ["text", "image"], cost: { input: 0.075, output: 0.3, cacheRead: 0.01875, cacheWrite: 0 }, contextWindow: 1000000, maxTokens: 8192, }, { id: "gemini-1.5-flash-8b", name: "Gemini 1.5 Flash-8B (Vertex)", api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: false, input: ["text", "image"], cost: { input: 0.0375, output: 0.15, cacheRead: 0.01, cacheWrite: 0 }, contextWindow: 1000000, maxTokens: 8192, }, ]; allModels.push(...vertexModels);
const primeInferenceModels = await fetchPrimeInferenceModels(); allModels.push(...primeInferenceModels);
const azureOpenAiModels: Model<Api>[] = allModels .filter((model) => model.provider === "openai" && model.api === "openai-responses") .map((model) => ({ ...model, api: "azure-openai-responses", provider: "azure-openai-responses", baseUrl: "", })); allModels.push(...azureOpenAiModels);
for (const model of allModels) { applyThinkingLevelMetadata(model); }
// Group by provider and deduplicate by model ID const providers: Record<string, Record<string, Model<Api>>> = {}; for (const model of allModels) { if (!providers[model.provider]) { providers[model.provider] = {}; } // Use model ID as key to automatically deduplicate // Only add if not already present (models.dev takes priority over OpenRouter) if (!providers[model.provider][model.id]) { providers[model.provider][model.id] = model; } }
// Generate TypeScript file. JSON string literals prevent remote catalog // text from becoming executable source code. const output = renderModelsFile(providers);
// Write file writeFileSync(join(packageRoot, "src/models.generated.ts"), output); console.log("Generated src/models.generated.ts");
// Print statistics const totalModels = allModels.length; const reasoningModels = allModels.filter(m => m.reasoning).length;
console.log(`\nModel Statistics:`); console.log(` Total tool-capable models: ${totalModels}`); console.log(` Reasoning-capable models: ${reasoningModels}`);
for (const [provider, models] of Object.entries(providers)) { console.log(` ${provider}: ${Object.keys(models).length} models`); }}
// Run the generatorgenerateModels().catch(console.error);