#!/usr/bin/env node import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "fs"; import { dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; import { getEffortThinkingLevelMap, type ModelsDevReasoningOption } from "./models-dev-reasoning-options.ts"; import { type AiGatewayPricing, getAiGatewayCost } from "./ai-gateway-pricing.ts"; import { buildOpenRouterCatalog, type OpenRouterCatalog, type OpenRouterModelListItem } from "./openrouter-catalog.ts"; import { CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL, CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL, CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL, CLOUDFLARE_WORKERS_AI_BASE_URL, CLOUDFLARE_WORKERS_AI_REST_BASE_URL, } from "../src/api/cloudflare.ts"; import type { AnthropicMessagesCompat, AnyModel, Api, ClassifierApi, ClassifierModel, ImageApi, ImageModel, KnownProvider, Model, ModelCost, ModelPromptCache, OpenAICompletionsCompat, OpenAIResponsesCompat, } from "../src/types.ts"; import { assertExactModelIds, createModelDataManifest, groupProviderModelData, type ModelDataStructure, MODEL_DATA_MANIFEST_FILE, readModelDataProviderIds, validateGeneratedModelData, validateModelDataDirectory, } from "./model-data.ts"; import { DEFAULT_RADIUS_GATEWAY, getRadiusModelsFromConfig, loadRadiusGatewayConfig, } from "../src/providers/radius-config.ts"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageRoot = join(__dirname, ".."); function readGeneratorOptions(args: string[]): { strict: boolean; dataOnly: boolean; jsonOnly: boolean; jsonOutputDir: string | undefined; pretty: boolean; } { let strict = false; let dataOnly = false; let jsonOnly = false; let jsonOutputDir: string | undefined; let pretty = false; for (let index = 0; index < args.length; index++) { const arg = args[index]; if (arg === "--strict") { strict = true; continue; } if (arg === "--data-only") { dataOnly = true; continue; } if (arg === "--json-only") { jsonOnly = true; continue; } if (arg === "--pretty") { pretty = true; continue; } if (arg === "--json-output") { const value = args[++index]; if (!value) throw new Error("--json-output requires a directory"); jsonOutputDir = resolve(value); continue; } throw new Error(`Unknown argument: ${arg}`); } if (jsonOnly && !jsonOutputDir) throw new Error("--json-only requires --json-output"); if (dataOnly && (jsonOnly || jsonOutputDir)) throw new Error("--data-only cannot be combined with JSON catalog output"); return { strict, dataOnly, jsonOnly, jsonOutputDir, pretty }; } const generatorOptions = readGeneratorOptions(process.argv.slice(2)); interface ModelsDevModel { id: string; name: string; tool_call?: boolean; structured_output?: boolean; reasoning?: boolean; reasoning_options?: ModelsDevReasoningOption[]; status?: string; limit?: { context?: number; output?: number; }; cost?: { input?: number; output?: number; cache_read?: number; cache_write?: number; tiers?: { input?: number; output?: number; cache_read?: number; cache_write?: number; tier?: { type?: string; size?: number; }; }[]; }; modalities?: { input?: string[]; output?: string[]; }; provider?: { npm?: string; }; } interface ModelsDevProvider { models?: Record; } interface ModelsDevMetadata { id: string; type?: string; name: string; limit?: { context?: number }; modalities?: { input?: string[] }; } type ModelsDevCatalog = Record; interface NvidiaNimModelListItem { id: string; } interface AiGatewayModel { id: string; name?: string; type?: string; context_window?: number; max_tokens?: number; tags?: string[]; pricing?: AiGatewayPricing; } const COPILOT_STATIC_HEADERS = { "User-Agent": "GitHubCopilotChat/0.35.0", "Editor-Version": "vscode/1.107.0", "Editor-Plugin-Version": "copilot-chat/0.35.0", "Copilot-Integration-Id": "vscode-chat", } as const; const TOGETHER_BASE_URL = "https://api.together.ai/v1"; const TOGETHER_BASE_COMPAT: OpenAICompletionsCompat = { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: false, maxTokensField: "max_tokens", supportsStrictMode: false, supportsLongCacheRetention: false, }; const TOGETHER_TOGGLE_REASONING_COMPAT: OpenAICompletionsCompat = { ...TOGETHER_BASE_COMPAT, thinkingFormat: "together", }; const TOGETHER_REASONING_EFFORT_COMPAT: OpenAICompletionsCompat = { ...TOGETHER_BASE_COMPAT, supportsReasoningEffort: true, thinkingFormat: "openai", }; const TOGETHER_TOGGLE_REASONING_EFFORT_COMPAT: OpenAICompletionsCompat = { ...TOGETHER_TOGGLE_REASONING_COMPAT, supportsReasoningEffort: true, }; const TOGETHER_REASONING_ONLY_MODELS = new Set([ "deepseek-ai/DeepSeek-R1", "MiniMaxAI/MiniMax-M2.7", ]); const TOGETHER_REASONING_EFFORT_MODELS = new Set(["openai/gpt-oss-20b", "openai/gpt-oss-120b"]); const TOGETHER_TOGGLE_REASONING_EFFORT_MODELS = new Set(["deepseek-ai/DeepSeek-V4-Pro-0813"]); const TOGETHER_FIXED_REASONING_LEVEL_MAP = { off: null, minimal: null, low: null, medium: null, } as const; const TOGETHER_REASONING_EFFORT_LEVEL_MAP = { off: null, minimal: null, } as const; const TOGETHER_DEEPSEEK_V4_THINKING_LEVEL_MAP = { minimal: null, low: null, medium: null, high: "high", xhigh: null, } as const; const TOGETHER_TOGGLE_REASONING_LEVEL_MAP = { minimal: null, low: null, medium: null, } as const; const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1"; const AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh"; // TypeSafe-compatible System One endpoint for evaluation models. // https://vercel.com/docs/ai-gateway/sdks-and-apis/typesafe const AI_GATEWAY_TYPESAFE_BASE_URL = "https://ai-gateway.vercel.sh/typesafe/v1"; const VERTEX_BASE_URL = "https://{location}-aiplatform.googleapis.com"; const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1"; const NVIDIA_HEADERS = { "NVCF-POLL-SECONDS": "3600", } as const; const NVIDIA_OPENAI_COMPAT: OpenAICompletionsCompat = { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: false, maxTokensField: "max_tokens", supportsStrictMode: false, supportsLongCacheRetention: false, }; const NVIDIA_NIM_UNSUPPORTED_MODELS = new Set([ "abacusai/dracarys-llama-3.1-70b-instruct", "bytedance/seed-oss-36b-instruct", "deepseek-ai/deepseek-v4-flash", "deepseek-ai/deepseek-v4-pro", "google/gemma-2-2b-it", "google/gemma-3n-e2b-it", "google/gemma-3n-e4b-it", "google/gemma-4-31b-it", "meta/llama-3.2-1b-instruct", "meta/llama-4-maverick-17b-128e-instruct", "microsoft/phi-4-mini-instruct", "minimaxai/minimax-m2.7", "mistralai/mistral-nemotron", "nvidia/nemotron-mini-4b-instruct", "qwen/qwen3-next-80b-a3b-instruct", "qwen/qwen3.5-397b-a17b", "sarvamai/sarvam-m", "upstage/solar-10.7b-instruct", ]); const ZAI_TOOL_STREAM_UNSUPPORTED_MODELS = new Set(["glm-4.5", "glm-4.5-air", "glm-4.5-flash", "glm-4.5v"]); const OPENCODE_GO_GLM52_THINKING_LEVEL_MAP = { off: null, minimal: null, low: null, medium: null, high: "high", max: "max", } as const; 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 ANTHROPIC_ALLOWED_FALLBACK_MODELS = { "claude-fable-5": ["claude-opus-4-8", "claude-opus-5"], "claude-opus-5": ["claude-opus-4-8"], } satisfies Record; const DEEPSEEK_V4_THINKING_LEVEL_MAP = { minimal: null, low: null, medium: null, high: "high", max: "max", } as const; const DEEPSEEK_V4_FLASH_THINKING_LEVEL_MAP = { ...DEEPSEEK_V4_THINKING_LEVEL_MAP, low: "low", } as const; // Azure Foundry rejects DeepSeek's own max effort. const AZURE_DEEPSEEK_V4_THINKING_LEVEL_MAP = { minimal: null, low: "low", medium: "medium", high: "high", xhigh: null, max: null, } as const; // Verified against Fireworks Messages raw_output on 2026-09-10 (#9323). // Fall back to verified support when models.dev omits effort metadata; this is // not an allowlist. Any Fireworks Messages model advertising effort uses adaptive thinking. const FIREWORKS_ADAPTIVE_THINKING_FALLBACK_MODELS = new Set([ "accounts/fireworks/models/deepseek-v4-flash-0731", "accounts/fireworks/models/deepseek-v4-flash-vision-exp", "accounts/fireworks/models/deepseek-v4-pro-0813", "accounts/fireworks/models/qwen3p8-max", "accounts/fireworks/models/qwen3p8-2p4t-a95b", ]); const QWEN_TOKEN_PLAN_FALLBACK_THINKING_LEVEL_MAP = { minimal: null, low: null, medium: null, high: "high", xhigh: null, max: "max", } as const; const QWEN_TOKEN_PLAN_REASONING_EFFORT_FALLBACK_MODEL_IDS = new Set(["glm-5", "glm-5.1"]); // Retired preview id — models.dev may still list it after GA ships. const QWEN_TOKEN_PLAN_EXCLUDED_MODEL_IDS = new Set(["qwen3.8-max-preview"]); const QWEN_TOKEN_PLAN_PROVIDER_IDS = new Set([ "qwen-token-plan", "qwen-token-plan-cn", "qwen-token-plan-individual", ]); // QwenCloud Token Plan Individual text-model allowlist, verified 2026-09-03. // Retired models remain excluded above even if the public catalog lags. // https://docs.qwencloud.com/token-plan/personal/token-plan-personal-overview const QWEN_TOKEN_PLAN_INDIVIDUAL_MODEL_IDS = new Set([ "deepseek-v4-flash-0731", "deepseek-v4-pro", "deepseek-v4-pro-0813", "glm-5.2", "qwen3.6-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.8-flash", "qwen3.8-max", ]); const KIMI_K3_MAX_TOKENS = 131072; const KIMI_K3_COST = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0, } as const; // Kimi Chat Completions settings shared by Moonshot and Kimi Coding. const KIMI_OPENAI_COMPAT: OpenAICompletionsCompat = { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: false, maxTokensField: "max_tokens", supportsStrictMode: false, thinkingFormat: "deepseek", }; // Kimi K3-style models take reasoning_effort and need earlier reasoning sent back. const KIMI_K3_OPENAI_COMPAT: OpenAICompletionsCompat = { ...KIMI_OPENAI_COMPAT, requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "openai", supportsReasoningEffort: true, }; const OPENROUTER_KIMI_K3_MODEL_IDS = new Set(["moonshotai/kimi-k3", "~moonshotai/kimi-latest"]); const ANT_LING_RING_THINKING_LEVEL_MAP = { off: null, minimal: null, low: null, medium: null, high: "high", xhigh: "xhigh", } as const; const BEDROCK_INFERENCE_PROFILE_ONLY_MODEL_IDS = new Set(["anthropic.claude-opus-5"]); const MODELS_DEV_OPENAI_UNSUPPORTED_MODEL_IDS = new Set(["gpt-5.6"]); const OPENAI_TOOL_SEARCH_MODEL_IDS = new Set([ "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-pro", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol", ]); const OPENAI_ADDITIONAL_TOOLS_MODEL_IDS = OPENAI_TOOL_SEARCH_MODEL_IDS; const OPENAI_MID_CONVO_SYSTEM_MESSAGE_MODEL_IDS = OPENAI_TOOL_SEARCH_MODEL_IDS; const OPENAI_CODEX_ADDITIONAL_TOOLS_MODEL_IDS = new Set([ "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol", ]); // OpenAI documents configuration_update for the GPT-6 family. // https://developers.openai.com/api/docs/guides/reasoning const OPENAI_MID_CONVO_EFFORT_MODEL_IDS = new Set(["gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol"]); const OPENAI_LONG_CONTEXT_INPUT_THRESHOLD = 272000; const OPENAI_SHORT_CONTEXT_CAPPED_MODEL_IDS = new Set([ "gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol", ]); const OPENAI_LONG_CONTEXT_PRICING_MODEL_IDS = new Set([ "gpt-5.4", "gpt-5.4-pro", "gpt-5.5", "gpt-5.5-pro", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol", ]); // Keep the generated default no less restrictive than coding-agent's historical // image preprocessing. Provider limits can narrow this profile, but unknown // providers retain the cache-safe 2000px / 4.5 MiB behavior. const DEFAULT_IMAGE_RESIZE = { maxWidth: 2000, maxHeight: 2000, maxBytes: 4.5 * 1024 * 1024, jpegQuality: 80, } as const; function withOpenAiLongContextPricing(cost: Model["cost"]): Model["cost"] { return { ...cost, tiers: [ { inputTokensAbove: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD, input: roundCost(cost.input * 2), output: roundCost(cost.output * 1.5), cacheRead: roundCost(cost.cacheRead * 2), cacheWrite: roundCost(cost.cacheWrite * 2), }, ], }; } // Keep current OpenAI prices authoritative until models.dev and passthrough // catalogs catch up. // https://developers.openai.com/api/docs/pricing const OPENAI_STANDARD_COSTS: Record = { "gpt-5.6-luna": { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 }, "gpt-5.6-sol": { input: 4, output: 20, cacheRead: 0.4, cacheWrite: 5 }, "gpt-5.6-terra": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 }, "gpt-6-astra": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 }, "gpt-6-luna": { input: 0.1, output: 0.5, cacheRead: 0.01, cacheWrite: 0.125 }, "gpt-6-sol": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 }, "gpt-6.1-sol": { input: 2, output: 10, cacheRead: 0.1, cacheWrite: 2.5 }, }; 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-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-sol", "gpt-6-luna", ]); const XAI_BUILTIN_EXCLUDED_MODEL_IDS = new Set([ "grok-3", "grok-3-fast", "grok-4.20-0309-non-reasoning", "grok-4.20-0309-reasoning", "grok-build-0.1", "grok-code-fast-1", ]); const XAI_RESPONSES_COMPAT: OpenAIResponsesCompat = { supportsLongCacheRetention: false, }; const OPENCODE_OPENAI_COMPLETIONS_LONG_CACHE_RETENTION_UNSUPPORTED_MODELS = new Set([ "opencode:deepseek-v4-flash", "opencode:deepseek-v4-pro", "opencode:kimi-k2.5", "opencode:kimi-k2.6", "opencode:minimax-m2.7", "opencode-go:kimi-k2.6", ]); // GitHub's "Models with extended capabilities" table lists these Copilot models as supporting // the extended 1 million token context window. const GITHUB_COPILOT_EXTENDED_CONTEXT_MODELS = new Set([ "claude-fable-5", "claude-opus-4.6", "claude-opus-4.7", "claude-opus-4.8", "claude-opus-5", "claude-opus-5.5", "claude-sonnet-4.6", "claude-sonnet-5", "gpt-5.3-codex", "gpt-5.4", "gpt-5.5", "gpt-6-astra", "gpt-6-luna", "gpt-6-sol", ]); // Checked manually against the authenticated GitHub Copilot /models endpoint on 2026-06-15. // Keep this to narrow corrections over models.dev metadata instead of snapshotting Copilot's catalog. const GITHUB_COPILOT_THINKING_LEVEL_OVERRIDES = { "claude-opus-4.7": { minimal: "low" }, "claude-opus-4.8": { minimal: "low" }, "claude-opus-5": { minimal: "low" }, "claude-sonnet-4.6": { minimal: "low", max: "max" }, } satisfies Record["thinkingLevelMap"]>>; function mergeThinkingLevelMap(model: Model, map: NonNullable["thinkingLevelMap"]>): void { model.thinkingLevelMap = { ...model.thinkingLevelMap, ...map }; } const modelsDevReasoningOptions = new Map(); function getModelKey(model: Pick, "provider" | "id">): string { return `${model.provider}:${model.id}`; } function recordModelsDevReasoningOptions(provider: string, id: string, sourceModel: ModelsDevModel): void { if (sourceModel.reasoning_options !== undefined) { modelsDevReasoningOptions.set(`${provider}:${id}`, sourceModel.reasoning_options); } } function supportsDirectReasoningEffort(model: Model): boolean { if (model.api === "anthropic-messages") return model.compat?.forceAdaptiveThinking === true; if ( model.api === "openai-responses" || model.api === "azure-openai-responses" || model.api === "openai-codex-responses" ) { return true; } if (model.api !== "openai-completions") return false; const compat = { ...detectOpenAICompletionsCompat(model as Model<"openai-completions">), ...(model.compat as OpenAICompletionsCompat | undefined), }; return compat.thinkingFormat === "openai" && compat.supportsReasoningEffort; } function applyModelsDevReasoningOptionMetadata(model: Model): void { const reasoningOptions = modelsDevReasoningOptions.get(getModelKey(model)); if (!reasoningOptions || !supportsDirectReasoningEffort(model)) return; const thinkingLevelMap = getEffortThinkingLevelMap(reasoningOptions); if (thinkingLevelMap) mergeThinkingLevelMap(model, thinkingLevelMap); } function getTogetherCompat(modelId: string, reasoning: boolean): OpenAICompletionsCompat { if (!reasoning) return TOGETHER_BASE_COMPAT; if (TOGETHER_REASONING_EFFORT_MODELS.has(modelId)) return TOGETHER_REASONING_EFFORT_COMPAT; if (TOGETHER_TOGGLE_REASONING_EFFORT_MODELS.has(modelId)) return TOGETHER_TOGGLE_REASONING_EFFORT_COMPAT; if (TOGETHER_REASONING_ONLY_MODELS.has(modelId)) return TOGETHER_BASE_COMPAT; return TOGETHER_TOGGLE_REASONING_COMPAT; } function getTogetherThinkingLevelMap( modelId: string, reasoning: boolean, ): NonNullable["thinkingLevelMap"]> | undefined { if (!reasoning) return undefined; if (TOGETHER_REASONING_EFFORT_MODELS.has(modelId)) return { ...TOGETHER_REASONING_EFFORT_LEVEL_MAP }; if (TOGETHER_TOGGLE_REASONING_EFFORT_MODELS.has(modelId)) return { ...TOGETHER_DEEPSEEK_V4_THINKING_LEVEL_MAP }; if (TOGETHER_REASONING_ONLY_MODELS.has(modelId)) return { ...TOGETHER_FIXED_REASONING_LEVEL_MAP }; return { ...TOGETHER_TOGGLE_REASONING_LEVEL_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 supportsOpenAiMax(model: Model): boolean { return ( (model.id.includes("gpt-5.6") || model.id.includes("gpt-6")) && (model.api === "openai-responses" || model.api === "azure-openai-responses" || model.api === "openai-codex-responses" || model.api === "openai-completions") ); } const VERIFIED_ANTHROPIC_MID_CONVO_EFFORT_PROVIDERS = new Set(["anthropic", "openrouter"]); // OpenRouter rejects `configuration_update` system messages on Opus 5 ("Mid-conversation // reasoning effort (configuration_update) is not supported on anthropic/claude-opus-5-20260723") // while accepting them on Fable 5.1, so gate that model there. const MID_CONVO_EFFORT_UNSUPPORTED_ANTHROPIC_MODELS = new Set(["openrouter:anthropic/claude-opus-5"]); function supportsAnthropicMidConvoEffort(modelId: string): boolean { const id = modelId.toLowerCase().replace(/^~?anthropic\//, ""); return ( /^claude-opus-(?:5|5[.-]5)(?:-\d{8})?$/.test(id) || /^claude-(?:sonnet|haiku)-5[.-]5(?:-\d{8})?$/.test(id) || /^claude-(?:fable|mythos)-5(?:[.-]1)(?:-\d{8})?$/.test(id) ); } // Claude models that accept `thinking: {type: "disabled"}` alongside per-message effort. // Opus 5.5, Sonnet 5.5, Fable 5.1, and Mythos 5.1 reject it. // https://platform.claude.com/docs/en/build-with-claude/thinking#configuring-thinking const ANTHROPIC_DISABLED_THINKING_MODELS = new Set(["claude-opus-5", "claude-haiku-5-5", "claude-haiku-5.5"]); function supportsAnthropicDisabledThinkingWithMidConvoEffort(modelId: string): boolean { // Strip OpenRouter's "anthropic/" or "~anthropic/" prefix and any dated snapshot suffix. const id = modelId .toLowerCase() .replace(/^~?anthropic\//, "") .replace(/-\d{8}$/, ""); return ANTHROPIC_DISABLED_THINKING_MODELS.has(id); } function supportsAnthropicMidConvoSystemMessages(modelId: string): boolean { return ( /^claude-opus-(?:4[.-]8|5(?:[.-]5)?)(?:-\d{8})?$/.test(modelId) || /^claude-(?:sonnet|haiku)-5[.-]5(?:-\d{8})?$/.test(modelId) || /^claude-(?:fable|mythos)-5(?:[.-]1)?(?:-\d{8})?$/.test(modelId) ); } function isAnthropicAdaptiveThinkingModel(modelId: string): boolean { return ( modelId.includes("opus-4-6") || modelId.includes("opus-4.6") || modelId.includes("opus-4-7") || modelId.includes("opus-4.7") || modelId.includes("opus-4-8") || modelId.includes("opus-4.8") || modelId.includes("opus-5") || modelId.includes("opus.5") || modelId.includes("sonnet-4-6") || modelId.includes("sonnet-4.6") || modelId.includes("sonnet-5") || modelId.includes("sonnet.5") || modelId.includes("haiku-5") || modelId.includes("haiku.5") || modelId.includes("fable-5") || modelId.includes("mythos-5") ); } function isAnthropicTemperatureUnsupportedModel(modelId: string): boolean { const id = modelId.toLowerCase(); return ( id.includes("opus-4-7") || id.includes("opus-4.7") || id.includes("opus-4-8") || id.includes("opus-4.8") || id.includes("opus-5") || id.includes("opus.5") || id.includes("sonnet-5-5") || id.includes("sonnet-5.5") || id.includes("haiku-5-5") || id.includes("haiku-5.5") ); } const OPENAI_COMPLETIONS_DEFAULT_COMPAT = { supportsStore: true, supportsDeveloperRole: true, supportsReasoningEffort: true, supportsUsageInStreaming: true, supportsFinishReason: true, maxTokensField: "max_completion_tokens", requiresToolResultName: false, requiresAssistantAfterToolResult: false, requiresThinkingAsText: false, requiresReasoningContentOnAssistantMessages: false, thinkingFormat: "openai", openRouterRouting: {}, vercelGatewayRouting: {}, chatTemplateKwargs: {}, chatTemplateArgs: {}, zaiToolStream: false, supportsStrictMode: false, supportsOpenAIGrammarTools: false, supportsMidConvoSystemMessages: false, supportsMidConvoToolAdditions: false, sendSessionAffinityHeaders: false, supportsLongCacheRetention: true, } satisfies Required< Omit< OpenAICompletionsCompat, "cacheControlFormat" | "supportsThinkingTokenBudget" | "thinkingTokenBudgetField" > > & { cacheControlFormat?: OpenAICompletionsCompat["cacheControlFormat"]; }; type OpenAICompletionsResolvedCompat = typeof OPENAI_COMPLETIONS_DEFAULT_COMPAT & { cacheControlFormat?: OpenAICompletionsCompat["cacheControlFormat"]; }; function mergeAnthropicMessagesCompat(model: Model, compat: AnthropicMessagesCompat): void { model.compat = { ...(model.compat as AnthropicMessagesCompat | undefined), ...compat }; } function detectOpenAICompletionsCompat(model: Model<"openai-completions">): OpenAICompletionsResolvedCompat { const provider = model.provider; const baseUrl = model.baseUrl; const isZai = provider === "zai" || provider === "zai-coding-cn" || baseUrl.includes("api.z.ai") || baseUrl.includes("open.bigmodel.cn"); const isTogether = provider === "together" || baseUrl.includes("api.together.ai") || baseUrl.includes("api.together.xyz"); const isMoonshot = provider === "moonshotai" || provider === "moonshotai-cn" || baseUrl.includes("api.moonshot."); const isOpenRouter = provider === "openrouter" || baseUrl.includes("openrouter.ai"); const isCloudflareWorkersAI = provider === "cloudflare-workers-ai" || baseUrl.includes("api.cloudflare.com"); const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com"); const isNvidia = provider === "nvidia" || baseUrl.includes("integrate.api.nvidia.com"); const isAntLing = provider === "ant-ling" || baseUrl.includes("api.ant-ling.com"); const isCerebras = provider === "cerebras" || baseUrl.includes("cerebras.ai"); const isTogetherReasoningOnly = isTogether && TOGETHER_REASONING_ONLY_MODELS.has(model.id); const isDeepSeek = provider === "deepseek" || baseUrl.toLowerCase().includes("deepseek.com"); const isNonStandard = isNvidia || isCerebras || provider === "xai" || baseUrl.includes("api.x.ai") || isTogether || baseUrl.includes("chutes.ai") || isDeepSeek || isZai || isMoonshot || provider === "opencode" || baseUrl.includes("opencode.ai") || isCloudflareWorkersAI || isCloudflareAiGateway || isAntLing; const useMaxTokens = baseUrl.includes("chutes.ai") || isDeepSeek || isMoonshot || isCloudflareAiGateway || isTogether || isNvidia || isAntLing || isZai; const isGrok = provider === "xai" || baseUrl.includes("api.x.ai"); const isOpenRouterDeveloperRoleModel = isOpenRouter && (model.id.startsWith("anthropic/") || model.id.startsWith("openai/")); const cacheControlFormat = provider === "openrouter" && /^~?anthropic\//.test(model.id) ? "anthropic" : undefined; return { supportsStore: !isNonStandard, supportsDeveloperRole: isOpenRouterDeveloperRoleModel || (!isNonStandard && !isOpenRouter), supportsReasoningEffort: !isGrok && !isZai && !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia && !isAntLing, supportsUsageInStreaming: true, supportsFinishReason: true, maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens", requiresToolResultName: false, requiresAssistantAfterToolResult: false, requiresThinkingAsText: false, requiresReasoningContentOnAssistantMessages: isDeepSeek, thinkingFormat: isDeepSeek ? "deepseek" : isZai ? "zai" : isTogether && !isTogetherReasoningOnly ? "together" : isAntLing ? "ant-ling" : isOpenRouter ? "openrouter" : "openai", openRouterRouting: {}, vercelGatewayRouting: {}, chatTemplateKwargs: {}, chatTemplateArgs: {}, zaiToolStream: false, // Preserve built-in behavior as explicit metadata against the conservative runtime default. supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia && !isCerebras, supportsOpenAIGrammarTools: false, supportsMidConvoSystemMessages: false, supportsMidConvoToolAdditions: false, ...(cacheControlFormat ? { cacheControlFormat } : {}), sendSessionAffinityHeaders: isOpenRouter, supportsLongCacheRetention: !( isTogether || isCloudflareWorkersAI || isCloudflareAiGateway || isNvidia || isAntLing ), }; } function isPlainEmptyObject(value: unknown): boolean { return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0; } function openAICompletionsCompatDelta(compat: OpenAICompletionsResolvedCompat): OpenAICompletionsCompat { const delta: OpenAICompletionsCompat = {}; for (const [key, value] of Object.entries(compat)) { const defaultValue = OPENAI_COMPLETIONS_DEFAULT_COMPAT[key as keyof typeof OPENAI_COMPLETIONS_DEFAULT_COMPAT]; if (isPlainEmptyObject(value) && isPlainEmptyObject(defaultValue)) continue; if (value !== defaultValue) { (delta as Record)[key] = value; } } return delta; } function mergeOpenAICompletionsCompat(model: Model, compat: OpenAICompletionsCompat): void { model.compat = { ...(model.compat as OpenAICompletionsCompat | undefined), ...compat }; } function applyOpenAICompletionsCompatMetadata(model: Model): void { if (model.api !== "openai-completions") return; const detected = openAICompletionsCompatDelta(detectOpenAICompletionsCompat(model as Model<"openai-completions">)); model.compat = { ...detected, ...(model.compat as OpenAICompletionsCompat | undefined) }; if (Object.keys(model.compat).length === 0) { delete model.compat; } } function applyAnthropicMessagesCompatMetadata(model: Model): void { if (model.api !== "anthropic-messages") return; const compat = getAnthropicMessagesCompat(model.provider, model.id); if (compat) { mergeAnthropicMessagesCompat(model, compat); if (compat.supportsMidConvoEffort && !supportsAnthropicDisabledThinkingWithMidConvoEffort(model.id)) { mergeThinkingLevelMap(model, { off: null }); } } } function isAnthropicFallbackMetadataModel(model: Model): model is Model<"anthropic-messages"> { if (model.provider !== "anthropic" || model.api !== "anthropic-messages") return false; return ( model.id in ANTHROPIC_ALLOWED_FALLBACK_MODELS || Object.values(ANTHROPIC_ALLOWED_FALLBACK_MODELS).some((fallbackModelIds) => fallbackModelIds.includes(model.id)) ); } function applyAnthropicAllowedFallbackModelMetadata(models: readonly Model<"anthropic-messages">[]): void { const modelsById = new Map(models.map((model) => [model.id, model])); for (const [modelId, fallbackModelIds] of Object.entries(ANTHROPIC_ALLOWED_FALLBACK_MODELS)) { const model = modelsById.get(modelId); if (!model) continue; const compatibleFallbackModelIds = model.compat?.supportsMidConvoEffort ? fallbackModelIds.filter(supportsAnthropicMidConvoEffort) : fallbackModelIds; const allowedFallbackModels = compatibleFallbackModelIds.flatMap((fallbackModelId) => { const fallbackModel = modelsById.get(fallbackModelId); return fallbackModel ? [{ provider: fallbackModel.provider, model: fallbackModel.id, cost: fallbackModel.cost }] : []; }); if (allowedFallbackModels.length > 0) { mergeAnthropicMessagesCompat(model, { allowedFallbackModels }); } } } function applyStrictToolCompatMetadata(model: Model): void { if ( (model.provider === "openai" || model.provider === "cloudflare-ai-gateway") && model.api === "openai-responses" ) { model.compat = { ...(model.compat as OpenAIResponsesCompat | undefined), supportsStrictMode: true }; } else if (model.provider === "anthropic" && model.api === "anthropic-messages") { mergeAnthropicMessagesCompat(model, { supportsStrictTools: true }); } } // Responses endpoints verified (OpenAI, ChatGPT Codex backend, GitHub Copilot, // opencode zen) or documented (Azure OpenAI, Cloudflare AI Gateway) to pass // OpenAI custom grammar tools through. OpenAI rejects `type: "custom"` tools // for pre-GPT-5 models (gpt-4.x, gpt-4o, o-series). const OPENAI_GRAMMAR_TOOL_PROVIDERS = new Set([ "openai", "openai-codex", "azure", "github-copilot", "opencode", "cloudflare-ai-gateway", ]); const OPENAI_GRAMMAR_TOOL_APIS = new Set([ "openai-responses", "azure-openai-responses", "openai-codex-responses", ]); function applyOpenAIGrammarToolCompatMetadata(model: Model): void { if (!OPENAI_GRAMMAR_TOOL_APIS.has(model.api) || !OPENAI_GRAMMAR_TOOL_PROVIDERS.has(model.provider)) return; const match = /^gpt-(\d+)/.exec(model.id); if (!match || Number(match[1]) < 5) return; model.compat = { ...(model.compat as OpenAIResponsesCompat | undefined), supportsOpenAIGrammarTools: true }; } function applyOpenAIToolSearchMetadata(model: Model): void { const isOpenAIResponses = model.provider === "openai" && model.api === "openai-responses"; const isOpenAICodex = model.provider === "openai-codex" && model.api === "openai-codex-responses"; if (!(isOpenAIResponses || isOpenAICodex) || !OPENAI_TOOL_SEARCH_MODEL_IDS.has(model.id)) return; const supportsAdditionalTools = (isOpenAIResponses && OPENAI_ADDITIONAL_TOOLS_MODEL_IDS.has(model.id)) || (isOpenAICodex && OPENAI_CODEX_ADDITIONAL_TOOLS_MODEL_IDS.has(model.id)); model.compat = { ...(model.compat as OpenAIResponsesCompat | undefined), ...(supportsAdditionalTools ? { supportsAdditionalTools: true } : {}), supportsToolSearch: true, }; } // Moonshot Kimi K2.6/K2.7 accept system text after the conversation starts but reject // tool-bearing system messages. Kimi K3 accepts both forms; Fireworks and OpenCode pass // its tool-bearing form through. GitHub Copilot forwards K3 text but silently drops its // tool-bearing message. DeepSeek V4 Pro and OpenAI models behind OpenRouter also accept // plain system text in place. Kimi Coding sets these flags in createKimiCodingModels(). function applyOpenAICompletionsTranscriptMetadata(model: Model): void { if (model.api !== "openai-completions") return; const isKimiK3 = (model.provider.startsWith("moonshot") && model.id === "kimi-k3") || (model.provider === "fireworks" && model.id.includes("kimi-k3")) || ((model.provider === "opencode" || model.provider === "opencode-go") && model.id === "kimi-k3"); const isMoonshotKimiK2 = model.provider.startsWith("moonshot") && (model.id === "kimi-k2.6" || model.id === "kimi-k2.7-code" || model.id === "kimi-k2.7-code-highspeed"); const isTextOnly = isMoonshotKimiK2 || (model.provider === "github-copilot" && model.id === "kimi-k3") || (model.provider === "deepseek" && model.id === "deepseek-v4-pro") || (model.provider === "openrouter" && model.id.startsWith("openai/") && OPENAI_MID_CONVO_SYSTEM_MESSAGE_MODEL_IDS.has(model.id.slice("openai/".length))); if (!isKimiK3 && !isTextOnly) return; model.compat = { ...(model.compat as OpenAICompletionsCompat | undefined), supportsMidConvoSystemMessages: true, ...(isKimiK3 ? { supportsMidConvoToolAdditions: true } : {}), }; } // Newer OpenAI Responses models accept developer messages after the conversation has started. // OpenCode Zen, OpenCode Go, and GitHub Copilot pass both those messages and // `additional_tools` items through to OpenAI unchanged; tool search is not verified // through those proxies. const OPENAI_RESPONSES_PROXY_PROVIDERS = new Set(["opencode", "opencode-go", "github-copilot"]); function applyOpenAIResponsesTranscriptMetadata(model: Model): void { const isOpenAIResponses = model.provider === "openai" && model.api === "openai-responses"; const isOpenAICodex = model.provider === "openai-codex" && model.api === "openai-codex-responses"; const isProxiedResponses = OPENAI_RESPONSES_PROXY_PROVIDERS.has(model.provider) && model.api === "openai-responses"; if ( !(isOpenAIResponses || isOpenAICodex || isProxiedResponses) || !OPENAI_MID_CONVO_SYSTEM_MESSAGE_MODEL_IDS.has(model.id) ) { return; } model.compat = { ...(model.compat as OpenAIResponsesCompat | undefined), supportsMidConvoSystemMessages: true, ...(isProxiedResponses ? { supportsAdditionalTools: true } : {}), }; } // OpenAI charges prompt-cache writes starting with the GPT-5.6 family, and exactly // those models accept `prompt_cache_options`; older models reject the parameter. // https://developers.openai.com/api/docs/guides/prompt-caching function applyOpenAIExplicitPromptCacheMetadata(model: Model): void { if (model.provider !== "openai" || model.api !== "openai-responses") return; if (!(model.cost.cacheWrite > 0)) return; model.compat = { ...(model.compat as OpenAIResponsesCompat | undefined), supportsExplicitPromptCacheMode: true, }; } // GPT-6 models accept `configuration_update` input items that change the reasoning effort // without changing the cached prompt prefix. Not enabled for openai-codex (Codex CLI keeps it // behind a flag that is off by default) or proxies. function applyOpenAIMidConvoEffortMetadata(model: Model): void { if (model.provider !== "openai" || model.api !== "openai-responses") return; if (!OPENAI_MID_CONVO_EFFORT_MODEL_IDS.has(model.id)) return; model.compat = { ...(model.compat as OpenAIResponsesCompat | undefined), supportsMidConvoEffort: true, }; } // Anthropic ephemeral entries have a hard five-minute lifetime; `ttl: "1h"` // extends it to one hour. Only direct Anthropic is annotated so cache warming // does not assume equivalent behavior through proxies. // https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching const ANTHROPIC_PROMPT_CACHE: ModelPromptCache = { short: 300, long: 3600 }; function applyPromptCacheMetadata(model: Model): void { if (model.provider === "anthropic" && model.api === "anthropic-messages") { model.promptCache = ANTHROPIC_PROMPT_CACHE; } // Do not add OpenAI lifetimes yet. Before enabling warming for explicit // OpenAI caches, re-evaluate it using observed expiry, replay, and billing // behavior; a documented TTL alone does not establish full cache loss. } function applyImageInputMetadata(model: AnyModel): void { if (!model.input.includes("image")) return; const providerLimits: AnyModel["inputLimits"] = model.provider === "anthropic" ? { maxRequestBytes: 32 * 1024 * 1024, images: { maxPerRequest: model.type !== "image" && model.contextWindow === 200000 ? 100 : 600 }, } : model.provider === "amazon-bedrock" ? { images: { maxPerMessage: 20 } } : model.provider === "openai" ? { maxRequestBytes: 512 * 1024 * 1024, images: { maxPerRequest: 1500 } } : model.provider === "google" ? { maxRequestBytes: 20 * 1024 * 1024, images: { maxPerRequest: 3600 } } : undefined; const configuredImages = model.inputLimits?.images; model.inputLimits = { ...providerLimits, ...model.inputLimits, images: { ...providerLimits?.images, ...configuredImages, resize: { ...DEFAULT_IMAGE_RESIZE, ...configuredImages?.resize }, }, }; } function isGemma4Model(modelId: string): boolean { return /gemma-?4/.test(modelId.toLowerCase()); } function getGoogleThinkingLevelMap( modelId: string, reasoningOptions: readonly ModelsDevReasoningOption[], ): NonNullable["thinkingLevelMap"]> | undefined { const effortMap = getEffortThinkingLevelMap(reasoningOptions); if (effortMap) return effortMap; if (isGemma4Model(modelId)) { return { off: null, minimal: "MINIMAL", low: null, medium: null, high: "HIGH" }; } return undefined; } function applyThinkingLevelMetadata(model: Model): void { if ( (model.api === "openai-responses" || model.api === "azure-openai-responses") && model.id.startsWith("gpt-5") ) { mergeThinkingLevelMap(model, { off: null }); } if ( (model.id === "gpt-6-astra" || model.id === "gpt-6-sol" || model.id === "gpt-6-luna" || model.id === "gpt-6.1-sol") && (model.api === "openai-responses" || model.api === "azure-openai-responses" || model.api === "openai-codex-responses") ) { mergeThinkingLevelMap(model, { // GPT-6 Astra and GPT-6.1 Sol reject reasoning.effort "none". off: model.id === "gpt-6-astra" || model.id === "gpt-6.1-sol" ? null : "none", minimal: null, low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "max", }); } if (model.provider === "github-copilot" && model.id.startsWith("gpt-5")) { mergeThinkingLevelMap(model, { minimal: "low" }); } if ( model.api === "openai-responses" && model.provider === "openai" && OPENAI_RESPONSES_NONE_REASONING_MODELS.has(model.id) ) { mergeThinkingLevelMap(model, { off: "none" }); } // xAI models without verified effort options must not send the undocumented // "none"/"minimal" efforts. if (model.provider === "xai" && model.api === "openai-responses" && model.thinkingLevelMap === undefined) { mergeThinkingLevelMap(model, { off: null, minimal: null }); } if (supportsOpenAiXhigh(model.id)) { mergeThinkingLevelMap(model, { xhigh: "xhigh" }); } if (supportsOpenAiMax(model)) { mergeThinkingLevelMap(model, { max: "max" }); } if (model.provider === "openai" && model.id === "gpt-5.5") { mergeThinkingLevelMap(model, { minimal: null }); } if (model.id.endsWith("gpt-5.5-pro")) { mergeThinkingLevelMap(model, { off: null, minimal: null, low: null }); } // Anthropic adaptive-thinking effort support (per Anthropic adaptive thinking docs): // - "max" is available on all adaptive-thinking Claude models. // - "xhigh" is only available on Opus 4.7/4.8/5, Sonnet 5, Haiku 5.5, and Fable 5. 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("opus.5") || model.id.includes("sonnet-5") || model.id.includes("sonnet.5") || model.id.includes("haiku-5") || model.id.includes("haiku.5") ) { mergeThinkingLevelMap(model, { xhigh: "xhigh", max: "max" }); } if (model.id.includes("fable-5")) { mergeThinkingLevelMap(model, { off: null, xhigh: "xhigh", max: "max" }); } if (model.api === "anthropic-messages" && isAnthropicAdaptiveThinkingModel(model.id)) { mergeAnthropicMessagesCompat(model, { forceAdaptiveThinking: true }); } if (model.api === "anthropic-messages" && isAnthropicTemperatureUnsupportedModel(model.id)) { mergeAnthropicMessagesCompat(model, { supportsTemperature: false }); } if ( model.api === "openai-completions" && model.id.includes("deepseek-v4") && model.thinkingLevelMap === undefined ) { mergeThinkingLevelMap( model, model.provider === "openrouter" ? { ...DEEPSEEK_V4_THINKING_LEVEL_MAP, xhigh: "xhigh", max: null } : model.provider === "azure" ? AZURE_DEEPSEEK_V4_THINKING_LEVEL_MAP : (model.provider === "deepseek" || model.provider === "opencode" || model.provider === "opencode-go") && model.id.includes("deepseek-v4-flash") ? DEEPSEEK_V4_FLASH_THINKING_LEVEL_MAP : DEEPSEEK_V4_THINKING_LEVEL_MAP, ); } if (model.provider === "groq" && model.id === "qwen/qwen3.6-27b") { mergeThinkingLevelMap(model, { minimal: null, low: null, medium: null, high: "default" }); } if (model.provider === "openai-codex" && supportsOpenAiXhigh(model.id)) { mergeThinkingLevelMap(model, { minimal: "low" }); } if ( (model.provider === "moonshotai" || model.provider === "moonshotai-cn") && (model.id === "kimi-k2.7-code" || model.id === "kimi-k2.7-code-highspeed") ) { // Kimi K2.7 Code is always-thinking. Official docs say // `thinking: { type: "disabled" }` is rejected, and callers can omit // the thinking parameter to use the enabled default. mergeThinkingLevelMap(model, { off: null }); } if (model.provider === "openrouter" && model.id.startsWith("inception/mercury-2")) { // Mercury 2 in instant mode (reasoning_effort: "none") disables tool calling. // Mark "off" unsupported so the openai-completions provider omits the reasoning param // instead of defaulting to {reasoning:{effort:"none"}} (see openai-completions.ts:575). // Pi's low/medium/high pass through verbatim; OpenRouter normalizes to Mercury's vocabulary. mergeThinkingLevelMap(model, { off: null }); } if (model.provider === "openrouter" && model.id === "z-ai/glm-5.2") { mergeThinkingLevelMap(model, { xhigh: "xhigh" }); } if (model.provider === "fireworks") { if (model.api === "anthropic-messages" && model.compat?.forceAdaptiveThinking) { // Qwen Max currently advertises only a toggle. Prefer upstream effort // metadata once available instead of replacing it with this fallback. if (model.id === "accounts/fireworks/models/qwen3p8-max" && !model.thinkingLevelMap) { model.thinkingLevelMap = getEffortThinkingLevelMap([ { type: "effort", values: ["low", "medium", "xhigh"] }, ]); } const reasoningOptions = modelsDevReasoningOptions.get(getModelKey(model)); if ( reasoningOptions?.some((option) => option.type === "toggle") || // The 2.4T alias omits the verified toggle in models.dev. model.id === "accounts/fireworks/models/qwen3p8-2p4t-a95b" ) { mergeThinkingLevelMap(model, { off: "none" }); } if (model.id === "accounts/fireworks/models/deepseek-v4-pro-0813") { mergeThinkingLevelMap(model, { low: "low" }); } } if (model.id.includes("glm-5p2")) { // GLM 5.2 and its fast router support off/high/max. Fireworks maps low // and medium to high, so do not expose those aliases as distinct levels. mergeThinkingLevelMap(model, { off: "none", minimal: null, low: null, medium: null, max: "max" }); } if (model.id.includes("kimi-k3")) { // Fireworks maps medium to high on both APIs; do not expose it as a distinct level. mergeThinkingLevelMap(model, { medium: null }); } } if (model.provider === "opencode-go" && model.id === "glm-5.2") { mergeThinkingLevelMap(model, OPENCODE_GO_GLM52_THINKING_LEVEL_MAP); } if (model.provider === "opencode-go" && model.id === "kimi-k2.6") { // OpenCode Go exposes Kimi K2.6 thinking as on/off, not distinct effort tiers. mergeThinkingLevelMap(model, { minimal: null, low: null, medium: null }); } if (model.provider === "opencode" && model.id === "grok-build-0.1") { // OpenCode Zen Grok Build reasons by default but rejects explicit reasoningEffort. mergeThinkingLevelMap(model, { off: null, minimal: null, low: null, medium: null }); } if (model.provider === "ant-ling" && model.reasoning) { // Ring reasons by default. Only high/xhigh have documented explicit effort controls. mergeThinkingLevelMap(model, ANT_LING_RING_THINKING_LEVEL_MAP); } if (model.provider === "github-copilot") { const override = GITHUB_COPILOT_THINKING_LEVEL_OVERRIDES[model.id]; if (override) { mergeThinkingLevelMap(model, override); } } } function getAnthropicMessagesCompat(provider: string, modelId: string): AnthropicMessagesCompat | undefined { const compat: AnthropicMessagesCompat = {}; if ( VERIFIED_ANTHROPIC_MID_CONVO_EFFORT_PROVIDERS.has(provider) && supportsAnthropicMidConvoEffort(modelId) && !MID_CONVO_EFFORT_UNSUPPORTED_ANTHROPIC_MODELS.has(`${provider}:${modelId}`) ) { compat.supportsMidConvoEffort = true; } if (provider === "anthropic" && supportsAnthropicMidConvoSystemMessages(modelId)) { compat.supportsMidConvoSystemMessages = true; compat.supportsMidConvoToolChanges = true; } // OpenCode Zen and GitHub Copilot forward mid-conversation system messages but reject // `tool_addition`/`tool_removal` blocks, so tool changes stay top-level there. if ((provider === "opencode" || provider === "github-copilot") && supportsAnthropicMidConvoSystemMessages(modelId)) { compat.supportsMidConvoSystemMessages = true; } if (EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_ANTHROPIC_MODELS.has(`${provider}:${modelId}`)) { compat.supportsEagerToolInputStreaming = false; } if (provider === "xiaomi" || provider.startsWith("xiaomi-token-plan-")) { compat.allowEmptySignature = true; } // OpenCode Qwen 3.8 Flash emits and accepts thinking blocks with empty signatures. if ((provider === "opencode" || provider === "opencode-go") && modelId === "qwen3.8-flash") { compat.allowEmptySignature = true; } return Object.keys(compat).length > 0 ? compat : undefined; } function getBedrockBaseUrl(modelId: string): string { return modelId.startsWith("eu.") ? "https://bedrock-runtime.eu-central-1.amazonaws.com" : "https://bedrock-runtime.us-east-1.amazonaws.com"; } function normalizeNvidiaModelId(modelId: string): string { return modelId.toLowerCase().replaceAll("_", "."); } function roundCost(value: number): number { return Number(value.toFixed(6)); } function getModelsDevCost(cost: ModelsDevModel["cost"]): ModelCost { const base = { input: cost?.input || 0, output: cost?.output || 0, cacheRead: cost?.cache_read || 0, cacheWrite: cost?.cache_write || 0, }; // Rates a tier does not list keep the base price. const tiers = cost?.tiers?.flatMap((tier) => { const context = tier.tier; if (context?.type !== "context" || context.size === undefined) return []; return [ { inputTokensAbove: context.size, input: tier.input ?? base.input, output: tier.output ?? base.output, cacheRead: tier.cache_read ?? base.cacheRead, cacheWrite: tier.cache_write ?? base.cacheWrite, }, ]; }); return tiers && tiers.length > 0 ? { ...base, tiers } : base; } async function fetchNvidiaNimModelIds(): Promise> { try { console.log("Fetching models from NVIDIA NIM API..."); const response = await fetch(`${NVIDIA_BASE_URL}/models`); if (!response.ok) throw new Error(`NVIDIA NIM API returned ${response.status}`); const data = (await response.json()) as { data?: NvidiaNimModelListItem[] }; const modelIds = new Map(); for (const model of data.data ?? []) { modelIds.set(model.id, model.id); modelIds.set(normalizeNvidiaModelId(model.id), model.id); } console.log(`Fetched ${data.data?.length ?? 0} model IDs from NVIDIA NIM`); return modelIds; } catch (error) { console.error("Failed to fetch NVIDIA NIM models:", error); if (generatorOptions.strict) throw error; return new Map(); } } async function fetchOpenRouterList(query: string): Promise { const response = await fetch(`https://openrouter.ai/api/v1/models${query}`); if (!response.ok) throw new Error(`OpenRouter API returned ${response.status}`); const data = (await response.json()) as { data?: OpenRouterModelListItem[] }; return data.data ?? []; } async function fetchOpenRouterModels(): Promise { try { console.log("Fetching models from OpenRouter API..."); const [listed, imageListed, decisionListed] = await Promise.all([ fetchOpenRouterList(""), fetchOpenRouterList("?output_modalities=image"), fetchOpenRouterList("?output_modalities=decisions"), ]); const catalog = buildOpenRouterCatalog(listed, imageListed, decisionListed); console.log( `Fetched ${catalog.chat.length} tool-capable, ${catalog.images.length} image, and ${catalog.classifiers.length} classifier models from OpenRouter`, ); if (generatorOptions.strict && catalog.images.length === 0) { throw new Error("OpenRouter API returned no usable image models"); } return catalog; } catch (error) { console.error("Failed to fetch OpenRouter models:", error); if (generatorOptions.strict) throw error; return { chat: [], images: [], classifiers: [] }; } } async function fetchRadiusModels(): Promise[]> { try { console.log("Fetching models from Radius API..."); const config = await loadRadiusGatewayConfig(DEFAULT_RADIUS_GATEWAY); const models = getRadiusModelsFromConfig("radius", config); if (models.length === 0) throw new Error("Radius API returned no models"); console.log(`Fetched ${models.length} models from Radius`); return models; } catch (error) { console.error("Failed to fetch Radius models:", error); if (generatorOptions.strict) throw error; return []; } } async function fetchAiGatewayModels(): Promise<{ chat: Model[]; classifiers: ClassifierModel<"typesafe-system-one">[]; }> { try { console.log("Fetching models from Vercel AI Gateway API..."); const response = await fetch(`${AI_GATEWAY_MODELS_URL}/models`); if (!response.ok) throw new Error(`Vercel AI Gateway API returned ${response.status}`); const data = await response.json(); const models: Model[] = []; const classifiers: ClassifierModel<"typesafe-system-one">[] = []; 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) { // Evaluation models such as TypeSafe's Jev are served through the // TypeSafe-compatible System One endpoint. if (model.type === "evaluation") { classifiers.push({ type: "classifier", id: model.id, name: model.name || model.id, api: "typesafe-system-one", provider: "vercel-ai-gateway", baseUrl: AI_GATEWAY_TYPESAFE_BASE_URL, input: ["text"], cost: { input: roundCost(toNumber(model.pricing?.input) * 1_000_000), output: roundCost(toNumber(model.pricing?.output) * 1_000_000), cacheRead: 0, cacheWrite: 0, }, contextWindow: model.context_window || 4096, }); continue; } 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"); } models.push({ id: model.id, name: model.name || model.id, api: "anthropic-messages", baseUrl: AI_GATEWAY_BASE_URL, provider: "vercel-ai-gateway", reasoning: tags.includes("reasoning"), input, compat: { allowEmptySignature: true }, cost: getAiGatewayCost(model.pricing), contextWindow: model.context_window || 4096, maxTokens: model.max_tokens || 4096, }); } console.log( `Fetched ${models.length} tool-capable and ${classifiers.length} classifier models from Vercel AI Gateway`, ); return { chat: models, classifiers }; } catch (error) { console.error("Failed to fetch Vercel AI Gateway models:", error); if (generatorOptions.strict) throw error; return { chat: [], classifiers: [] }; } } function processZaiModels(data: ModelsDevCatalog): Model[] { const variants = [ { source: "zai-coding-plan", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", }, { source: "zhipuai-coding-plan", provider: "zai-coding-cn", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", }, ] as const; const models: Model[] = []; for (const { source, provider, baseUrl } of variants) { for (const [modelId, model] of Object.entries(data[source]?.models ?? {})) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; const supportsImage = m.modalities?.input?.includes("image"); const thinkingLevelMap = getEffortThinkingLevelMap(m.reasoning_options ?? []); const isGlm52 = modelId === "glm-5.2" || modelId === "glm-5.2-highspeed"; if (thinkingLevelMap && isGlm52) { thinkingLevelMap.off = "none"; } const supportsReasoningEffort = thinkingLevelMap !== undefined; const referenceCost = data.zai?.models[modelId]?.cost ?? m.cost; models.push({ id: modelId, name: m.name || modelId, api: "openai-completions", provider, baseUrl, reasoning: m.reasoning === true, ...(thinkingLevelMap ? { thinkingLevelMap } : {}), input: supportsImage ? ["text", "image"] : ["text"], cost: getModelsDevCost(referenceCost), compat: { supportsDeveloperRole: false, thinkingFormat: "zai", ...(supportsReasoningEffort ? { supportsReasoningEffort: true } : {}), ...(!ZAI_TOOL_STREAM_UNSUPPORTED_MODELS.has(modelId) ? { zaiToolStream: true } : {}), }, contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); recordModelsDevReasoningOptions(provider, modelId, m); } } return models; } function processBasetenModels(provider: ModelsDevProvider | undefined): Model[] { if (!provider?.models) return []; const baseUrl = "https://inference.baseten.co/v1"; const baseCompat: OpenAICompletionsCompat = { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: false, supportsUsageInStreaming: true, maxTokensField: "max_tokens", supportsStrictMode: true, // Baseten automatic prompt caching needs session affinity so related // requests land on the same replica. See: // https://docs.baseten.co/inference/model-apis/pricing-and-limits sendSessionAffinityHeaders: true, supportsLongCacheRetention: false, }; const reasoningEffortCompat: OpenAICompletionsCompat = { ...baseCompat, supportsReasoningEffort: true, thinkingFormat: "openai", }; const toggleReasoningCompat: OpenAICompletionsCompat = { ...baseCompat, thinkingFormat: "baseten", chatTemplateArgs: { enable_thinking: { $var: "thinking.enabled" } }, }; const toggleReasoningEffortCompat: OpenAICompletionsCompat = { ...reasoningEffortCompat, thinkingFormat: "baseten", chatTemplateArgs: { enable_thinking: { $var: "thinking.enabled" } }, }; const toggleThinkingLevelMap = { off: "off", minimal: null, low: null, medium: null, high: "high", xhigh: null, max: null, } as const; const glm52ThinkingLevelMap = { off: "none", minimal: null, low: null, medium: null, high: "high", xhigh: null, max: "max", } as const; const models: Model[] = []; for (const [modelId, model] of Object.entries(provider.models)) { if (model.status === "deprecated") continue; const reasoning = model.reasoning === true; const reasoningOptions = model.reasoning_options ?? []; const isGlm52 = modelId === "zai-org/GLM-5.2" || modelId === "zai-org/GLM-5.2-Fast"; const supportsToggle = reasoningOptions.some((option) => option.type === "toggle") || isGlm52; const supportsEffort = reasoningOptions.some((option) => option.type === "effort") || isGlm52; const compat = supportsToggle && supportsEffort ? toggleReasoningEffortCompat : supportsToggle ? toggleReasoningCompat : supportsEffort ? reasoningEffortCompat : baseCompat; const thinkingLevelMap = isGlm52 ? glm52ThinkingLevelMap : supportsToggle ? toggleThinkingLevelMap : getEffortThinkingLevelMap(reasoningOptions); // Baseten's GLM-5.2 endpoints are text-only despite models.dev reporting image input. const supportsImageInput = !isGlm52 && model.modalities?.input?.includes("image"); models.push({ id: modelId, name: model.name || modelId, api: "openai-completions", provider: "baseten", baseUrl, reasoning, ...(thinkingLevelMap ? { thinkingLevelMap } : {}), input: supportsImageInput ? ["text", "image"] : ["text"], cost: getModelsDevCost(model.cost), compat, contextWindow: model.limit?.context || 4096, maxTokens: model.limit?.output || 4096, }); } return models; } function processGoogleModels(data: ModelsDevCatalog): Model[] { const models: Model[] = []; const googleModels = data.google?.models; if (googleModels) { for (const [modelId, model] of Object.entries(googleModels)) { if (model.tool_call !== true) continue; const source = modelId === "gemini-flash-latest" ? (googleModels["gemini-3.5-flash"] ?? model) : modelId === "gemini-flash-lite-latest" ? (googleModels["gemini-3.1-flash-lite"] ?? model) : model; const thinkingLevelMap = getGoogleThinkingLevelMap(modelId, source.reasoning_options ?? []); models.push({ id: modelId, name: model.name || modelId, api: "google-generative-ai", provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", reasoning: source.reasoning === true, ...(thinkingLevelMap ? { thinkingLevelMap } : {}), input: source.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(source.cost), contextWindow: source.limit?.context || 4096, maxTokens: source.limit?.output || 4096, }); } } // The google-vertex models.dev catalog also includes Claude, OpenAI, and other // MaaS models that do not use the @google/genai Gemini streaming path. const vertexModels = data["google-vertex"]?.models; if (vertexModels) { for (const [modelId, model] of Object.entries(vertexModels)) { if (model.tool_call !== true || !modelId.startsWith("gemini-")) continue; if (modelId === "gemini-3.1-flash-lite-preview") continue; const source = modelId === "gemini-flash-latest" ? (vertexModels["gemini-3.5-flash"] ?? model) : modelId === "gemini-flash-lite-latest" ? (vertexModels["gemini-3.1-flash-lite"] ?? model) : model; const thinkingLevelMap = getGoogleThinkingLevelMap(modelId, source.reasoning_options ?? []); // models.dev reports Vertex cache_read/cache_write values for Gemini 2.5 Flash that // do not match the official Gemini API standard pricing table. pi only accounts // cachedContentTokenCount as cacheRead. const cacheRead = modelId === "gemini-2.5-flash" ? 0.03 : source.cost?.cache_read || 0; models.push({ id: modelId, name: model.name || modelId, api: "google-vertex", provider: "google-vertex", baseUrl: VERTEX_BASE_URL, reasoning: source.reasoning === true, ...(thinkingLevelMap ? { thinkingLevelMap } : {}), input: source.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: { input: source.cost?.input || 0, output: source.cost?.output || 0, cacheRead, cacheWrite: 0, }, contextWindow: source.limit?.context || 4096, maxTokens: source.limit?.output || 4096, }); } } return models; } function processFireworksModels(provider: ModelsDevProvider | undefined): Model[] { if (!provider?.models) return []; const anthropicCompat: AnthropicMessagesCompat = { allowEmptySignature: true, sendSessionAffinityHeaders: true, supportsEagerToolInputStreaming: false, supportsCacheControlOnTools: false, supportsLongCacheRetention: false, }; const openAICompat: OpenAICompletionsCompat = { supportsStore: false, supportsDeveloperRole: false, sendSessionAffinityHeaders: true, supportsLongCacheRetention: false, }; const kimiK3Compat: OpenAICompletionsCompat = { ...openAICompat, requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "openai", }; const models: Model[] = []; for (const [modelId, model] of Object.entries(provider.models)) { if (model.tool_call !== true) continue; const input: ("text" | "image")[] = model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"]; const common = { id: modelId, name: model.name || modelId, provider: "fireworks", reasoning: model.reasoning === true, input, cost: getModelsDevCost(model.cost), contextWindow: model.limit?.context || 4096, maxTokens: model.limit?.output || 4096, }; if (modelId.includes("glm-")) { models.push({ ...common, api: "openai-completions", baseUrl: "https://api.fireworks.ai/inference/v1", compat: openAICompat, }); } else if (modelId.includes("kimi-k3")) { models.push({ ...common, api: "openai-completions", baseUrl: "https://api.fireworks.ai/inference/v1", compat: kimiK3Compat, }); } else { models.push({ ...common, api: "anthropic-messages", // Fireworks Anthropic-compatible API - SDK appends /v1/messages. baseUrl: "https://api.fireworks.ai/inference", // Fireworks prompt caching uses automatic prefix matching + session affinity. // x-session-affinity routes requests to the same replica for cache hits. // cache_control on tools and eager_input_streaming are not supported. // See: https://docs.fireworks.ai/tools-sdks/anthropic-compatibility // Use adaptive thinking for cataloged effort controls, with verified // fallbacks where models.dev is incomplete. New models need no allowlist entry. compat: { ...anthropicCompat, ...(model.reasoning_options?.some((option) => option.type === "effort") || FIREWORKS_ADAPTIVE_THINKING_FALLBACK_MODELS.has(modelId) ? { forceAdaptiveThinking: true } : {}), }, }); } recordModelsDevReasoningOptions("fireworks", modelId, model); } return models; } // Kimi Coding is subscription-backed, so models.dev reports zero cost. Use the // equivalent Moonshot API rates to estimate the value of subscription usage. const KIMI_CODING_IMPLIED_COSTS: Record["cost"]> = { k3: KIMI_K3_COST, "kimi-for-coding": { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 }, "kimi-for-coding-highspeed": { input: 1.9, output: 8, cacheRead: 0.38, cacheWrite: 0 }, }; // Models whose https://api.kimi.com/coding/v1/models entry reports effort levels and // dynamic tool loading (tools declared in later system messages). Highspeed reports // neither and rejects tool-bearing system messages with a 400. const KIMI_CODING_K3_STYLE_MODEL_IDS = new Set(["k3", "k3-256k", "kimi-for-coding"]); // Kimi Coding uses Chat Completions, the same protocol as the official Kimi Code client. function createKimiCodingModels(kimiModels: Record): Model<"openai-completions">[] { const models: Model<"openai-completions">[] = []; for (const [modelId, m] of Object.entries(kimiModels)) { if (m.tool_call !== true) continue; const isK3Style = KIMI_CODING_K3_STYLE_MODEL_IDS.has(modelId); const impliedCost = KIMI_CODING_IMPLIED_COSTS[modelId]; // K3-style models send reasoning_effort, so expose the effort values models.dev lists. const effortLevels = isK3Style && m.reasoning_options ? getEffortThinkingLevelMap(m.reasoning_options) : undefined; models.push({ id: modelId, name: m.name || modelId, api: "openai-completions", provider: "kimi-coding", baseUrl: "https://api.kimi.com/coding/v1", compat: { ...(isK3Style ? KIMI_K3_OPENAI_COMPAT : KIMI_OPENAI_COMPAT), // All models accept system text after the conversation starts. supportsMidConvoSystemMessages: true, ...(isK3Style ? { supportsMidConvoToolAdditions: true } : {}), }, reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: { input: m.cost?.input || impliedCost?.input || 0, output: m.cost?.output || impliedCost?.output || 0, cacheRead: m.cost?.cache_read || impliedCost?.cacheRead || 0, cacheWrite: m.cost?.cache_write || impliedCost?.cacheWrite || 0, }, contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, // Kimi Code reports every model as always-thinking. thinkingLevelMap: { ...effortLevels, off: null }, }); } return models; } async function loadModelsDevData(): Promise[]> { try { console.log("Fetching models from models.dev API..."); const response = await fetch("https://models.dev/api.json"); if (!response.ok) throw new Error(`models.dev API returned ${response.status}`); const data = (await response.json()) as ModelsDevCatalog; const models: Model[] = []; const nvidiaNimModelIds = data.nvidia?.models ? await fetchNvidiaNimModelIds() : new Map(); // Process Amazon Bedrock models if (data["amazon-bedrock"]?.models) { for (const [modelId, model] of Object.entries(data["amazon-bedrock"].models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; if (BEDROCK_INFERENCE_PROFILE_ONLY_MODEL_IDS.has(modelId)) continue; let id = modelId; if (id.startsWith("ai21.jamba")) { // These models doesn't support tool use in streaming mode continue; } if (id.startsWith("mistral.mistral-7b-instruct-v0")) { // These models doesn't support system messages continue; } models.push({ id, name: m.name || id, api: "bedrock-converse-stream" as const, provider: "amazon-bedrock" as const, baseUrl: getBedrockBaseUrl(id), reasoning: m.reasoning === true, input: (m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"]) as ("text" | "image")[], // Includes models.dev pricing tiers, e.g. the long-context tier for OpenAI models (#10326). cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, ...(m.structured_output === true && { compat: { supportsStrictMode: true } }), }); recordModelsDevReasoningOptions("amazon-bedrock" as const, id, m); } } // Process Anthropic models if (data.anthropic?.models) { for (const [modelId, model] of Object.entries(data.anthropic.models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; models.push({ id: modelId, name: m.name || modelId, api: "anthropic-messages", provider: "anthropic", baseUrl: "https://api.anthropic.com", reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); recordModelsDevReasoningOptions("anthropic", modelId, m); } } models.push(...processGoogleModels(data)); // Process OpenAI models if (data.openai?.models) { for (const [modelId, model] of Object.entries(data.openai.models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; // models.dev lists this alias, but it is not accepted by OpenAI APIs. if (MODELS_DEV_OPENAI_UNSUPPORTED_MODEL_IDS.has(modelId)) continue; models.push({ id: modelId, name: m.name || modelId, api: "openai-responses", provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); recordModelsDevReasoningOptions("openai", modelId, m); } } // Process Groq models if (data.groq?.models) { for (const [modelId, model] of Object.entries(data.groq.models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; models.push({ id: modelId, name: m.name || modelId, api: "openai-completions", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); recordModelsDevReasoningOptions("groq", modelId, m); } } // Process Cerebras models if (data.cerebras?.models) { for (const [modelId, model] of Object.entries(data.cerebras.models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; models.push({ id: modelId, name: m.name || modelId, api: "openai-completions", provider: "cerebras", baseUrl: "https://api.cerebras.ai/v1", reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); recordModelsDevReasoningOptions("cerebras", modelId, m); } } // Process Cloudflare Workers AI models if (data["cloudflare-workers-ai"]?.models) { for (const [modelId, model] of Object.entries(data["cloudflare-workers-ai"].models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; models.push({ id: modelId, name: m.name || modelId, api: "openai-completions", provider: "cloudflare-workers-ai", baseUrl: CLOUDFLARE_WORKERS_AI_BASE_URL, reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, compat: { sendSessionAffinityHeaders: true }, }); recordModelsDevReasoningOptions("cloudflare-workers-ai", modelId, m); } } // Process Cloudflare AI Gateway models const cloudflareAIGatewayModelIds = new Set(); if (data["cloudflare-ai-gateway"]?.models) { for (const [prefixedId, model] of Object.entries(data["cloudflare-ai-gateway"].models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; const slashIdx = prefixedId.indexOf("/"); if (slashIdx === -1) continue; const upstream = prefixedId.slice(0, slashIdx); const nativeId = prefixedId.slice(slashIdx + 1); let api: "anthropic-messages" | "openai-completions" | "openai-responses"; let baseUrl: string; let id: string; if (upstream === "openai") { api = "openai-responses"; baseUrl = CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL; id = nativeId; } else if (upstream === "anthropic") { api = "anthropic-messages"; baseUrl = CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL; // The /anthropic passthrough forwards the model ID to Anthropic unchanged. // models.dev lists dotted versions (claude-opus-5.5), but Anthropic only // accepts dashed IDs (claude-opus-5-5). id = nativeId.replaceAll(".", "-"); } else if (upstream === "workers-ai") { api = "openai-completions"; baseUrl = CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL; id = prefixedId; } else { continue; } // Gateway passthroughs forward session affinity headers to upstreams that // use them for cache/routing affinity. const compat = upstream === "anthropic" || upstream === "workers-ai" ? { sendSessionAffinityHeaders: true } : undefined; cloudflareAIGatewayModelIds.add(id); models.push({ id, name: m.name || id, api, provider: "cloudflare-ai-gateway", baseUrl, reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, ...(compat ? { compat } : {}), }); recordModelsDevReasoningOptions("cloudflare-ai-gateway", id, m); } } // The gateway proxies Workers AI through its OpenAI-compatible /compat endpoint, // but models.dev may omit or intermittently drop those `workers-ai/*` entries // from the AI Gateway catalog. Mirror the Workers AI catalog under the documented // prefix so the gateway keeps its OpenAI-compatible models stable. if (data["cloudflare-workers-ai"]?.models) { for (const [modelId, model] of Object.entries(data["cloudflare-workers-ai"].models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; const id = `workers-ai/${modelId}`; if (cloudflareAIGatewayModelIds.has(id)) continue; cloudflareAIGatewayModelIds.add(id); models.push({ id, name: m.name || id, api: "openai-completions", provider: "cloudflare-ai-gateway", baseUrl: CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL, reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, compat: { sendSessionAffinityHeaders: true }, }); recordModelsDevReasoningOptions("cloudflare-ai-gateway", id, m); } } // Process xAi models if (data.xai?.models) { for (const [modelId, model] of Object.entries(data.xai.models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; models.push({ id: modelId, name: m.name || modelId, api: "openai-responses", provider: "xai", baseUrl: "https://api.x.ai/v1", compat: { ...XAI_RESPONSES_COMPAT }, reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); recordModelsDevReasoningOptions("xai", modelId, m); } } // Process Meta models if (data.meta?.models) { for (const [modelId, model] of Object.entries(data.meta.models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; models.push({ id: modelId, name: m.name || modelId, api: "openai-responses", provider: "meta", baseUrl: "https://api.meta.ai/v1", reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); recordModelsDevReasoningOptions("meta", modelId, m); } } models.push(...processZaiModels(data)); // Process Mistral models if (data.mistral?.models) { for (const [modelId, model] of Object.entries(data.mistral.models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; // Models with effort values use `reasoning_effort` with these levels. // Reasoning models without them (Magistral) use `prompt_mode`. const thinkingLevelMap = getEffortThinkingLevelMap(m.reasoning_options ?? []); models.push({ id: modelId, name: m.name || modelId, api: "mistral-conversations", provider: "mistral", baseUrl: "https://api.mistral.ai", reasoning: m.reasoning === true, ...(thinkingLevelMap ? { thinkingLevelMap } : {}), input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: { input: m.cost?.input || 0, output: m.cost?.output || 0, cacheRead: m.cost?.cache_read ?? (m.cost?.input ? roundCost(m.cost.input * 0.1) : 0), cacheWrite: m.cost?.cache_write || 0, }, contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); } } // Process Hugging Face models if (data.huggingface?.models) { for (const [modelId, model] of Object.entries(data.huggingface.models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; models.push({ id: modelId, name: m.name || modelId, api: "openai-completions", provider: "huggingface", baseUrl: "https://router.huggingface.co/v1", reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), compat: { supportsDeveloperRole: false, }, contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); recordModelsDevReasoningOptions("huggingface", modelId, m); } } models.push(...processFireworksModels(data["fireworks-ai"])); // Process NVIDIA NIM models if (data.nvidia?.models) { for (const [modelId, model] of Object.entries(data.nvidia.models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; if (!m.modalities?.input?.includes("text")) continue; if (!m.modalities?.output?.includes("text")) continue; const liveModelId = nvidiaNimModelIds.get(modelId) ?? nvidiaNimModelIds.get(normalizeNvidiaModelId(modelId)); if (!liveModelId) continue; if (NVIDIA_NIM_UNSUPPORTED_MODELS.has(liveModelId)) continue; models.push({ id: liveModelId, name: m.name || liveModelId, api: "openai-completions", provider: "nvidia", baseUrl: NVIDIA_BASE_URL, headers: { ...NVIDIA_HEADERS }, reasoning: m.reasoning === true, input: m.modalities.input.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), compat: NVIDIA_OPENAI_COMPAT, contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); recordModelsDevReasoningOptions("nvidia", liveModelId, m); } } // Process Together AI models const togetherProvider = data.together ?? data.togetherai ?? data["together-ai"]; if (togetherProvider?.models) { for (const [modelId, model] of Object.entries(togetherProvider.models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; if (m.status === "deprecated") continue; const reasoning = m.reasoning === true; const thinkingLevelMap = getTogetherThinkingLevelMap(modelId, reasoning); models.push({ id: modelId, name: m.name || modelId, api: "openai-completions", provider: "together", baseUrl: TOGETHER_BASE_URL, reasoning, ...(thinkingLevelMap ? { thinkingLevelMap } : {}), input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), compat: getTogetherCompat(modelId, reasoning), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); recordModelsDevReasoningOptions("together", modelId, m); } } models.push(...processBasetenModels(data.baseten)); // Process OpenCode models (Zen and Go) // API mapping based on provider.npm field: // - @ai-sdk/openai → openai-responses // - @ai-sdk/anthropic → anthropic-messages // - @ai-sdk/google → google-generative-ai // - null/undefined/@ai-sdk/openai-compatible → openai-completions const opencodeVariants = [ { key: "opencode", provider: "opencode", basePath: "https://opencode.ai/inference" }, { key: "opencode-go", provider: "opencode-go", basePath: "https://opencode.ai/inference/go" }, ] as const; for (const variant of opencodeVariants) { if (!data[variant.key]?.models) continue; for (const [modelId, model] of Object.entries(data[variant.key].models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; if (m.status === "deprecated") continue; const npm = m.provider?.npm; let api: Api; let baseUrl: string; let compat: OpenAICompletionsCompat | OpenAIResponsesCompat | undefined; if (npm === "@ai-sdk/openai") { api = "openai-responses"; baseUrl = `${variant.basePath}/v1`; compat = { sessionAffinityFormat: "openai-nosession" }; } else if (npm === "@ai-sdk/anthropic") { api = "anthropic-messages"; // Anthropic SDK appends /v1/messages to baseURL baseUrl = variant.basePath; } else if (npm === "@ai-sdk/google") { api = "google-generative-ai"; baseUrl = `${variant.basePath}/v1`; } else if (npm === "@ai-sdk/alibaba") { api = "openai-completions"; baseUrl = `${variant.basePath}/v1`; compat = { cacheControlFormat: "anthropic" }; } else { // null, undefined, or @ai-sdk/openai-compatible api = "openai-completions"; baseUrl = `${variant.basePath}/v1`; } if (variant.provider === "opencode" && modelId === "grok-build-0.1") { compat = { ...(compat ?? {}), supportsReasoningEffort: false }; } if ((variant.provider === "opencode" || variant.provider === "opencode-go") && modelId === "kimi-k2.6") { // OpenCode Kimi K2.6 accepts Anthropic-style thinking objects // and rejects string thinking values or combined reasoning_effort. compat = { ...(compat ?? {}), thinkingFormat: "deepseek", supportsReasoningEffort: false }; } // Fix known mismatches between models.dev npm data and actual // OpenCode Go endpoint behaviour. models.dev reports these models // as @ai-sdk/anthropic, but OpenCode Go serves them through the // OpenAI-compatible /v1/chat/completions path. if (variant.provider === "opencode-go") { if (modelId === "qwen3.5-plus" || modelId === "qwen3.6-plus") { api = "openai-completions"; baseUrl = `${variant.basePath}/v1`; // Qwen/DashScope uses enable_thinking at the top level. compat = { ...(compat ?? {}), thinkingFormat: "qwen" }; } } if (api === "openai-completions") { compat = { ...(compat ?? {}), maxTokensField: "max_tokens" }; if ( OPENCODE_OPENAI_COMPLETIONS_LONG_CACHE_RETENTION_UNSUPPORTED_MODELS.has( `${variant.provider}:${modelId}`, ) ) { compat = { ...compat, supportsLongCacheRetention: false }; } } let thinkingLevelMap: NonNullable["thinkingLevelMap"]> | undefined; if (api === "google-generative-ai") { thinkingLevelMap = getGoogleThinkingLevelMap(modelId, m.reasoning_options ?? []); } else if (variant.provider === "opencode-go" && modelId === "deepseek-v4.1-flash") { thinkingLevelMap = getEffortThinkingLevelMap(m.reasoning_options ?? []); } models.push({ id: modelId, name: m.name || modelId, api, provider: variant.provider, baseUrl, reasoning: m.reasoning === true, ...(thinkingLevelMap ? { thinkingLevelMap } : {}), input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), ...(compat ? { compat } : {}), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); recordModelsDevReasoningOptions(variant.provider, modelId, m); } } // Process GitHub Copilot models if (data["github-copilot"]?.models) { for (const [modelId, model] of Object.entries(data["github-copilot"].models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; if (m.status === "deprecated") continue; // Claude 4.x and 5.x models route to Anthropic Messages API const isCopilotClaude = /^claude-(haiku|sonnet|opus|fable)-[45]([.\-]|$)/.test(modelId); // GPT, Grok, OSWE, and MAI-Code models are only served through // the Copilot /responses endpoint. const needsResponsesApi = modelId.startsWith("gpt-") || modelId.startsWith("grok-") || modelId.startsWith("oswe") || modelId.startsWith("mai-"); const api: Api = isCopilotClaude ? "anthropic-messages" : needsResponsesApi ? "openai-responses" : "openai-completions"; const anthropicCompat = api === "anthropic-messages" ? getAnthropicMessagesCompat("github-copilot", modelId) : undefined; const copilotModel: Model = { id: modelId, name: m.name || modelId, api, provider: "github-copilot", baseUrl: "https://api.individual.githubcopilot.com", reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 128000, maxTokens: m.limit?.output || 8192, headers: { ...COPILOT_STATIC_HEADERS }, ...(anthropicCompat ? { compat: anthropicCompat } : {}), // compat only applies to openai-completions ...(api === "openai-completions" ? { compat: { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: false, }, } : {}), }; models.push(copilotModel); recordModelsDevReasoningOptions("github-copilot", modelId, m); } } // Process MiniMax models const minimaxVariants = [ { key: "minimax", provider: "minimax", baseUrl: "https://api.minimax.io/anthropic" }, { key: "minimax-cn", provider: "minimax-cn", baseUrl: "https://api.minimaxi.com/anthropic" }, ] as const; for (const { key, provider, baseUrl } of minimaxVariants) { if (data[key]?.models) { for (const [modelId, model] of Object.entries(data[key].models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; models.push({ id: modelId, name: m.name || modelId, api: "anthropic-messages", provider, // MiniMax's Anthropic-compatible API - SDK appends /v1/messages baseUrl, reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); recordModelsDevReasoningOptions(provider, modelId, m); } } } // Process Kimi For Coding models if (data["kimi-code-plan-global"]?.models) { models.push(...createKimiCodingModels(data["kimi-code-plan-global"].models as Record)); } // Process Moonshot AI models const moonshotVariants = [ { key: "moonshotai", provider: "moonshotai", baseUrl: "https://api.moonshot.ai/v1" }, { key: "moonshotai-cn", provider: "moonshotai-cn", baseUrl: "https://api.moonshot.cn/v1" }, ] as const; const getMoonshotProviderModels = (key: "moonshotai" | "moonshotai-cn"): Record => { const providerModels = data[key]?.models as Record | undefined; return providerModels ? { ...providerModels } : {}; }; const moonshotModels = { moonshotai: getMoonshotProviderModels("moonshotai"), "moonshotai-cn": getMoonshotProviderModels("moonshotai-cn"), }; for (const { key, provider, baseUrl } of moonshotVariants) { for (const [modelId, m] of Object.entries(moonshotModels[key])) { if (m.tool_call !== true) continue; const isKimiK3 = modelId === "kimi-k3"; const compat = { ...(isKimiK3 ? KIMI_K3_OPENAI_COMPAT : KIMI_OPENAI_COMPAT) }; models.push({ id: modelId, name: m.name || modelId, api: "openai-completions", provider, baseUrl, reasoning: isKimiK3 || m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], // Moonshot does not bill cache writes; models.dev lists the input rate as cache_write for Kimi K3. cost: isKimiK3 ? { ...KIMI_K3_COST } : { input: m.cost?.input || 0, output: m.cost?.output || 0, cacheRead: m.cost?.cache_read || 0, cacheWrite: m.cost?.cache_write || 0, }, contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, compat, }); recordModelsDevReasoningOptions(provider, modelId, m); } } // Process Xiaomi MiMo models // Built-in `xiaomi` targets the API billing endpoint (single stable URL, // keys from platform.xiaomimimo.com). The three `xiaomi-token-plan-*` // providers cover prepaid Token Plan endpoints in cn / ams / sgp. // MiMo Chat Completions only documents thinking.type enabled/disabled and // rejects some reasoning_effort values (e.g. "minimal"). Xiaomi documents // all effort levels as identical, so expose only off and high. const xiaomiCompat: OpenAICompletionsCompat = { supportsReasoningEffort: false, requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek", }; const xiaomiThinkingLevelMap = { off: "off", minimal: null, low: null, medium: null, high: "high", xhigh: null, max: null, } as const; const xiaomiVariants = [ { source: "xiaomi", provider: "xiaomi", baseUrl: "https://api.xiaomimimo.com/v1" }, { source: "xiaomi-token-plan-cn", provider: "xiaomi-token-plan-cn", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", }, { source: "xiaomi-token-plan-ams", provider: "xiaomi-token-plan-ams", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", }, { source: "xiaomi-token-plan-sgp", provider: "xiaomi-token-plan-sgp", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", }, ] as const; for (const { source, provider, baseUrl } of xiaomiVariants) { const providerModels = data[source]?.models; if (!providerModels) continue; for (const [modelId, model] of Object.entries(providerModels)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; if (m.status === "deprecated") continue; models.push({ id: modelId, name: m.name || modelId, api: "openai-completions", provider, baseUrl, compat: xiaomiCompat, reasoning: m.reasoning === true, ...(m.reasoning === true ? { thinkingLevelMap: xiaomiThinkingLevelMap } : {}), input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); } } // Process Alibaba Cloud Model Studio Token Plan models. International and // China use separate endpoints and API keys (sk-sp- prefix). The Individual // provider reuses the international source and endpoint with a narrower catalog. // models.dev keys are "alibaba-token-plan[-cn]"; pi exposes them as // "qwen-token-plan[-cn]" plus the Individual catalog view. const qwenTokenPlanCompat: OpenAICompletionsCompat = { thinkingFormat: "qwen", supportsDeveloperRole: false, supportsStore: false, supportsReasoningEffort: true, }; const qwenTokenPlanVariants = [ { source: "alibaba-token-plan", provider: "qwen-token-plan", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", modelIds: undefined, }, { source: "alibaba-token-plan", provider: "qwen-token-plan-individual", baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", modelIds: QWEN_TOKEN_PLAN_INDIVIDUAL_MODEL_IDS, }, { source: "alibaba-token-plan-cn", provider: "qwen-token-plan-cn", baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", modelIds: undefined, }, ] as const; for (const { source, provider, baseUrl, modelIds } of qwenTokenPlanVariants) { const providerModels = data[source]?.models; const emittedModelIds = modelIds ? new Set() : undefined; for (const [modelId, model] of Object.entries(providerModels ?? {})) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; if (QWEN_TOKEN_PLAN_EXCLUDED_MODEL_IDS.has(modelId)) continue; if (modelIds && !modelIds.has(modelId)) continue; const thinkingLevelMap = getEffortThinkingLevelMap(m.reasoning_options ?? []) ?? (QWEN_TOKEN_PLAN_REASONING_EFFORT_FALLBACK_MODEL_IDS.has(modelId) ? QWEN_TOKEN_PLAN_FALLBACK_THINKING_LEVEL_MAP : undefined); models.push({ id: modelId, name: m.name || modelId, api: "openai-completions", provider, baseUrl, compat: thinkingLevelMap ? qwenTokenPlanCompat : { ...qwenTokenPlanCompat, supportsReasoningEffort: false }, ...(thinkingLevelMap ? { thinkingLevelMap } : {}), reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); emittedModelIds?.add(modelId); } if (modelIds && emittedModelIds && generatorOptions.strict) { assertExactModelIds(provider, modelIds, emittedModelIds); } } console.log(`Loaded ${models.length} tool-capable models from models.dev`); return models; } catch (error) { console.error("Failed to load models.dev data:", error); if (generatorOptions.strict) throw error; return []; } } async function loadModelsDevClassifierModels(): Promise[]> { try { console.log("Fetching classifier models from models.dev API..."); const response = await fetch("https://models.dev/models.json?type=decision"); if (!response.ok) throw new Error(`models.dev classifier API returned ${response.status}`); const data = (await response.json()) as Record; const metadata = data["typesafe/jev-latest"]; if (!metadata || metadata.type !== "decision") { throw new Error("models.dev did not return decision model typesafe/jev-latest"); } return [ { type: "classifier", id: "jev-latest", name: metadata.name, api: "typesafe-system-one", provider: "typesafe", baseUrl: "https://api.typesafe.ai/v1/", input: metadata.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], // The canonical models.dev entry has no direct-provider pricing. System One reports token usage, // so classify() results carry token counts but price them at zero. cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: metadata.limit?.context || 64000, }, ]; } catch (error) { console.error("Failed to load models.dev classifier data:", error); if (generatorOptions.strict) throw error; return []; } } // Workers AI has no unauthenticated catalog and models.dev does not list its // System One models yet. Cloudflare publishes pricing only in the dashboard. // https://developers.cloudflare.com/ai/models/typesafe/jev/ // OpenCode Zen serves Jev through its TypeSafe-compatible System One endpoint. // Neither its /inference/v1/models listing nor models.dev carries metadata for it. // https://opencode.ai/docs/zen const OPENCODE_CLASSIFIER_MODELS: ClassifierModel<"typesafe-system-one">[] = [ { type: "classifier", id: "jev-1.13", name: "Jev 1.13", api: "typesafe-system-one", provider: "opencode", baseUrl: "https://opencode.ai/inference/v1", input: ["text"], cost: { input: 0.042, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32000, }, { type: "classifier", id: "jev-1.13-free", name: "Jev 1.13 Free", api: "typesafe-system-one", provider: "opencode", baseUrl: "https://opencode.ai/inference/v1", input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32000, }, ]; const CLOUDFLARE_WORKERS_AI_CLASSIFIER_MODELS: ClassifierModel<"cloudflare-workers-ai-system-one">[] = [ // Cloudflare-hosted Clef decision models. They accept images, but classifier // contexts carry text or JSON state only, so the catalog advertises text. // Pricing: https://developers.cloudflare.com/workers-ai/models/clef/ // and https://developers.cloudflare.com/workers-ai/models/clef-flash/ { type: "classifier", id: "@cf/cloudflare/clef", name: "Clef", api: "cloudflare-workers-ai-system-one", provider: "cloudflare-workers-ai", baseUrl: CLOUDFLARE_WORKERS_AI_REST_BASE_URL, input: ["text"], cost: { input: 0.24, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 65536, }, { type: "classifier", id: "@cf/cloudflare/clef-flash", name: "Clef Flash", api: "cloudflare-workers-ai-system-one", provider: "cloudflare-workers-ai", baseUrl: CLOUDFLARE_WORKERS_AI_REST_BASE_URL, input: ["text"], cost: { input: 0.09, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 65536, }, { type: "classifier", id: "typesafe/jev", name: "Jev", api: "cloudflare-workers-ai-system-one", provider: "cloudflare-workers-ai", baseUrl: CLOUDFLARE_WORKERS_AI_REST_BASE_URL, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32000, }, ]; // OpenAI Decisions API (public beta): gpt-6-luna is its only model. It bills input tokens only, // with the same long-context multiplier as chat requests. Only API keys work: Sign in with ChatGPT // tokens are rejected on /v1/decisions, and the Codex backend has no Decisions route. // The endpoint rejects inputs above 922K tokens (the model's documented maximum input), but // requests running longer than about five seconds, currently above roughly 600K input tokens, // fail with a gateway timeout. // https://developers.openai.com/api/docs/guides/decisions const OPENAI_CLASSIFIER_MODELS: ClassifierModel<"openai-decisions">[] = [ { type: "classifier", id: "gpt-6-luna", name: "GPT-6 Luna", api: "openai-decisions", provider: "openai", baseUrl: "https://api.openai.com/v1", input: ["text", "image"], cost: withOpenAiLongContextPricing({ input: 0.1, output: 0, cacheRead: 0, cacheWrite: 0 }), contextWindow: 922000, }, ]; async function generateModels() { // Fetch models from all upstream catalogs. // models.dev: Anthropic, Google, OpenAI, Groq, Cerebras, and others // OpenRouter: its tool-capable routed catalog // AI Gateway: OpenAI-compatible catalog with tool-capable models // Radius: its unauthenticated public catalog; authenticated clients overlay it at runtime const modelsDevModels = await loadModelsDevData(); const modelsDevClassifierModels = await loadModelsDevClassifierModels(); const openRouterCatalog = await fetchOpenRouterModels(); const aiGatewayCatalog = await fetchAiGatewayModels(); const radiusModels = await fetchRadiusModels(); // Combine chat models (models.dev has priority where sources overlap). const allModels = [...modelsDevModels, ...openRouterCatalog.chat, ...aiGatewayCatalog.chat, ...radiusModels].filter( (model) => !(model.provider === "xai" && XAI_BUILTIN_EXCLUDED_MODEL_IDS.has(model.id)) && !((model.provider === "opencode" || model.provider === "opencode-go") && model.id === "gpt-5.3-codex-spark"), ); // The authenticated Copilot catalog advertised these models on 2026-09-22, // but models.dev did not include them yet. const missingCopilotModels: Model[] = [ { id: "claude-opus-5.5", name: "Claude Opus 5.5", api: "anthropic-messages", provider: "github-copilot", baseUrl: "https://api.individual.githubcopilot.com", reasoning: true, thinkingLevelMap: { off: null, minimal: null, low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "max", }, input: ["text", "image"], cost: { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 5 }, contextWindow: 1000000, maxTokens: 128000, headers: { ...COPILOT_STATIC_HEADERS }, }, ...(["gpt-6-sol", "gpt-6-luna"] as const).map((id) => ({ id, name: id === "gpt-6-sol" ? "GPT-6 Sol" : "GPT-6 Luna", api: "openai-responses" as const, provider: "github-copilot" as const, baseUrl: "https://api.individual.githubcopilot.com", reasoning: true, input: ["text", "image"] as ("text" | "image")[], cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS[id]), contextWindow: 1000000, maxTokens: 128000, headers: { ...COPILOT_STATIC_HEADERS }, })), ]; for (const model of missingCopilotModels) { if (!allModels.some((candidate) => candidate.provider === model.provider && candidate.id === model.id)) { allModels.push(model); } } // Temporary overrides until upstream model metadata is corrected. for (const candidate of allModels) { if (candidate.provider === "github-copilot" && GITHUB_COPILOT_EXTENDED_CONTEXT_MODELS.has(candidate.id)) { candidate.contextWindow = 1000000; } // models.dev may list Opus 5.5, Sonnet 5.5, and Haiku 5.5 before their effort metadata is complete. if ( (candidate.provider === "anthropic" && (candidate.id === "claude-opus-5-5" || candidate.id === "claude-sonnet-5-5" || candidate.id === "claude-haiku-5-5")) || (candidate.provider === "github-copilot" && candidate.id === "claude-opus-5.5") ) { mergeThinkingLevelMap(candidate, { // Haiku 5.5 accepts disabled thinking; Opus 5.5 and Sonnet 5.5 reject it. ...(candidate.id === "claude-haiku-5-5" ? {} : { off: null }), minimal: null, low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "max", }); } if ( (candidate.provider === "anthropic" || candidate.provider === "opencode" || candidate.provider === "opencode-go") && (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; } // Keep direct OpenAI requests in the short-context pricing tier by default. Users can opt into the // larger context through model overrides, so retain long-context cost metadata on the capped models. if (candidate.provider === "openai" && OPENAI_SHORT_CONTEXT_CAPPED_MODEL_IDS.has(candidate.id)) { candidate.contextWindow = OPENAI_LONG_CONTEXT_INPUT_THRESHOLD; candidate.maxTokens = 128000; } if (candidate.provider === "openai" && OPENAI_LONG_CONTEXT_PRICING_MODEL_IDS.has(candidate.id)) { const standardCost = OPENAI_STANDARD_COSTS[candidate.id]; candidate.cost = withOpenAiLongContextPricing(standardCost ?? candidate.cost); } // Cloudflare AI Gateway passes OpenAI usage through at OpenAI list prices. if (candidate.provider === "cloudflare-ai-gateway") { const standardCost = OPENAI_STANDARD_COSTS[candidate.id]; if (standardCost) candidate.cost = withOpenAiLongContextPricing(standardCost); } // models.dev reports gpt-5-pro output as 272000 (a duplicate of the input sub-limit); // the actual max output is 128000. Also propagates to the derived Azure clone. if (candidate.provider === "openai" && candidate.id === "gpt-5-pro") { candidate.maxTokens = 128000; } // Keep Kimi K3's canonical output limit when gateway metadata is missing or incorrect. if ( (candidate.provider === "openrouter" && OPENROUTER_KIMI_K3_MODEL_IDS.has(candidate.id)) || (candidate.provider === "vercel-ai-gateway" && candidate.id === "moonshotai/kimi-k3") ) { candidate.maxTokens = KIMI_K3_MAX_TOKENS; } // 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.startsWith("moonshotai/kimi-k2.6")) { candidate.compat = { ...candidate.compat, supportsDeveloperRole: false, requiresReasoningContentOnAssistantMessages: true, }; } 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 gpt models const missingOpenAiModels: Model<"openai-responses">[] = [ { id: "gpt-6.1-sol", name: "GPT-6.1 Sol", api: "openai-responses", baseUrl: "https://api.openai.com/v1", provider: "openai", reasoning: true, input: ["text", "image"], cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-6.1-sol"]), contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD, maxTokens: 128000, }, { id: "gpt-6-astra", name: "GPT-6 Astra", api: "openai-responses", baseUrl: "https://api.openai.com/v1", provider: "openai", reasoning: true, input: ["text", "image"], cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-6-astra"]), contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD, maxTokens: 128000, }, { id: "gpt-6-sol", name: "GPT-6 Sol", api: "openai-responses", baseUrl: "https://api.openai.com/v1", provider: "openai", reasoning: true, input: ["text", "image"], cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-6-sol"]), contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD, maxTokens: 128000, }, { id: "gpt-6-luna", name: "GPT-6 Luna", api: "openai-responses", baseUrl: "https://api.openai.com/v1", provider: "openai", reasoning: true, input: ["text", "image"], cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-6-luna"]), contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD, maxTokens: 128000, }, { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", api: "openai-responses", baseUrl: "https://api.openai.com/v1", provider: "openai", reasoning: true, input: ["text", "image"], cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-5.6-sol"]), contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD, maxTokens: 128000, }, { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", api: "openai-responses", baseUrl: "https://api.openai.com/v1", provider: "openai", reasoning: true, input: ["text", "image"], cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-5.6-terra"]), contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD, maxTokens: 128000, }, { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", api: "openai-responses", baseUrl: "https://api.openai.com/v1", provider: "openai", reasoning: true, input: ["text", "image"], cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-5.6-luna"]), contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD, maxTokens: 128000, }, { 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, }, ]; for (const model of missingOpenAiModels) { if (!allModels.some((m) => m.provider === model.provider && m.id === model.id)) { allModels.push(model); } } const deepseekCompat: OpenAICompletionsCompat = { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek", }; const deepseekModels: Model<"openai-completions">[] = [ { id: "deepseek-flash", name: "DeepSeek V4.1 Flash", api: "openai-completions", baseUrl: "https://api.deepseek.com", provider: "deepseek", reasoning: true, thinkingLevelMap: DEEPSEEK_V4_FLASH_THINKING_LEVEL_MAP, input: ["text", "image"], cost: { // DeepSeek also offers time-based off-peak rates, which the cost schema cannot represent yet. input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, }, contextWindow: 1000000, maxTokens: 384000, compat: deepseekCompat, }, { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", api: "openai-completions", baseUrl: "https://api.deepseek.com", provider: "deepseek", reasoning: true, input: ["text"], cost: { // DeepSeek also offers time-based off-peak rates, which the cost schema cannot represent yet. input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0, }, contextWindow: 1000000, maxTokens: 384000, compat: deepseekCompat, }, ]; allModels.push(...deepseekModels); const antLingCompat: OpenAICompletionsCompat = { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: false, maxTokensField: "max_tokens", supportsLongCacheRetention: false, }; const antLingModels: Model<"openai-completions">[] = [ { id: "Ling-2.6-flash", name: "Ling 2.6 Flash", api: "openai-completions", baseUrl: "https://api.ant-ling.com/v1", provider: "ant-ling", reasoning: false, input: ["text"], cost: { input: 0.01, output: 0.02, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262144, maxTokens: 65536, compat: antLingCompat, }, { id: "Ling-2.6-1T", name: "Ling 2.6 1T", api: "openai-completions", baseUrl: "https://api.ant-ling.com/v1", provider: "ant-ling", reasoning: false, input: ["text"], cost: { input: 0.06, output: 0.25, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262144, maxTokens: 65536, compat: antLingCompat, }, { id: "Ring-2.6-1T", name: "Ring 2.6 1T", api: "openai-completions", baseUrl: "https://api.ant-ling.com/v1", provider: "ant-ling", reasoning: true, input: ["text"], cost: { input: 0.06, output: 0.25, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262144, maxTokens: 65536, compat: { ...antLingCompat, thinkingFormat: "ant-ling" }, }, ]; allModels.push(...antLingModels); for (const candidate of allModels) { if ( candidate.api === "openai-completions" && candidate.id.includes("deepseek-v4") && !QWEN_TOKEN_PLAN_PROVIDER_IDS.has(candidate.provider) ) { const preservesNativeReasoningEffort = candidate.provider === "openrouter" || candidate.provider === "opencode"; candidate.compat = { ...candidate.compat, ...(preservesNativeReasoningEffort ? { requiresReasoningContentOnAssistantMessages: deepseekCompat.requiresReasoningContentOnAssistantMessages, } : deepseekCompat), }; } } const minimaxDirectSupportedIds = new Set(["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M3"]); 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. // Older model limits are based on observed server behavior; GPT-5.6 and GPT-6 use Codex's 272k default catalog limit. // GPT-6.1 Sol uses the 500k default returned by /backend-api/codex/models. const CODEX_BASE_URL = "https://chatgpt.com/backend-api"; const CODEX_CONTEXT = 272000; const CODEX_GPT_61_SOL_CONTEXT = 500000; const CODEX_GPT_56_CONTEXT = 272000; const CODEX_SPARK_CONTEXT = 128000; const CODEX_MAX_TOKENS = 128000; const codexModels: Model<"openai-codex-responses">[] = [ { id: "gpt-6.1-sol", name: "GPT-6.1 Sol", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-6.1-sol"]), contextWindow: CODEX_GPT_61_SOL_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: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-6-astra"]), contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-6-sol", name: "GPT-6 Sol", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-6-sol"]), contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { id: "gpt-6-luna", name: "GPT-6 Luna", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-6-luna"]), 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: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }, contextWindow: CODEX_SPARK_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: withOpenAiLongContextPricing({ input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }), 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: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-5.6-luna"]), contextWindow: CODEX_GPT_56_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: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-5.6-sol"]), contextWindow: CODEX_GPT_56_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: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-5.6-terra"]), contextWindow: CODEX_GPT_56_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, ]; allModels.push(...codexModels); // 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, thinkingLevelMap: getEffortThinkingLevelMap([{ type: "effort", values: ["none", "high"] }]), 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, }); } // Add "fusion" alias for openrouter/fusion. OpenRouter exposes Fusion as a // router alias/plugin entry point; its model metadata does not advertise // tools, but the alias resolves to a concrete model that can invoke caller // tools and has the openrouter:fusion server tool auto-injected. if (!allModels.some(m => m.provider === "openrouter" && m.id === "openrouter/fusion")) { allModels.push({ id: "openrouter/fusion", name: "OpenRouter: Fusion", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, input: ["text"], cost: { // we dont know about the costs because Fusion routes to multiple models // and then charges you for the underlying used models input: 0, output: 0, cacheRead: 0, cacheWrite: 0, }, contextWindow: 1000000, maxTokens: 30000, }); } // Azure Foundry deploys these with larger context windows than OpenAI's own short-tier defaults. // See models-sold-directly-by-azure docs. const AZURE_CONTEXT_WINDOW_OVERRIDES: Record = { "gpt-5.4": 1050000, "gpt-5.5": 1050000, "gpt-5.6-luna": 1050000, "gpt-5.6-sol": 1050000, "gpt-5.6-terra": 1050000, }; const azureOpenAiModels: Model[] = allModels .filter((model) => model.provider === "openai" && model.api === "openai-responses") .map((model) => ({ ...model, api: "azure-openai-responses", provider: "azure", baseUrl: "", cost: { input: model.cost.input, output: model.cost.output, cacheRead: model.cost.cacheRead, cacheWrite: model.cost.cacheWrite, }, contextWindow: AZURE_CONTEXT_WINDOW_OVERRIDES[model.id] ?? model.contextWindow, })); allModels.push(...azureOpenAiModels); // Azure resells DeepSeek at its own rates. US data zone, checked 2026-09-16. // https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/deepseek/ const AZURE_DEEPSEEK_V4_PRO_COST: ModelCost = { input: 1.925, output: 3.828, cacheRead: 0.165, cacheWrite: 0 }; // Azure 400s on DeepSeek's `thinking` field and on every prompt cache parameter, discards a // `developer` system message unbilled once reasoning_effort is set, and honours mid-convo ones (#9645). const azureDeepSeekModels: Model[] = allModels .filter((model) => model.provider === "deepseek" && model.id === "deepseek-v4-pro") .map((model) => ({ ...model, provider: "azure", baseUrl: "", cost: AZURE_DEEPSEEK_V4_PRO_COST, compat: { ...(model.compat as OpenAICompletionsCompat), supportsDeveloperRole: false, supportsMidConvoSystemMessages: true, thinkingFormat: "openai", supportsLongCacheRetention: false, }, })); allModels.push(...azureDeepSeekModels); for (const model of allModels) { applyOpenAICompletionsCompatMetadata(model); applyAnthropicMessagesCompatMetadata(model); applyModelsDevReasoningOptionMetadata(model); applyThinkingLevelMetadata(model); applyStrictToolCompatMetadata(model); applyOpenAIGrammarToolCompatMetadata(model); applyOpenAIToolSearchMetadata(model); applyOpenAICompletionsTranscriptMetadata(model); applyOpenAIResponsesTranscriptMetadata(model); applyOpenAIExplicitPromptCacheMetadata(model); applyOpenAIMidConvoEffortMetadata(model); applyPromptCacheMetadata(model); applyImageInputMetadata(model); } applyAnthropicAllowedFallbackModelMetadata(allModels.filter(isAnthropicFallbackMetadataModel)); // Keep chat and image catalogs separate so one upstream ID can expose both // operations with different API implementations. type ProviderCatalog = { chat: Record>; image: Record>; classifier: Record>; }; const providers: Record = {}; for (const model of allModels) { providers[model.provider] ??= { chat: {}, image: {}, classifier: {} }; // Only add if not already present (models.dev takes priority over OpenRouter). providers[model.provider].chat[model.id] ??= { ...model, type: "chat" }; } for (const model of openRouterCatalog.images) { applyImageInputMetadata(model); providers[model.provider] ??= { chat: {}, image: {}, classifier: {} }; providers[model.provider].image[model.id] ??= model; } const classifierModels: ClassifierModel[] = [ ...modelsDevClassifierModels, ...openRouterCatalog.classifiers, ...aiGatewayCatalog.classifiers, ...OPENCODE_CLASSIFIER_MODELS, ...CLOUDFLARE_WORKERS_AI_CLASSIFIER_MODELS, ...OPENAI_CLASSIFIER_MODELS, ]; for (const model of classifierModels) { providers[model.provider] ??= { chat: {}, image: {}, classifier: {} }; providers[model.provider].classifier[model.id] ??= model; } const sortedProviderIds = Object.keys(providers).sort(); const jsonChatProviders: Record>> = {}; const jsonImageProviders: Record>> = {}; const jsonClassifierProviders: Record>> = {}; const jsonAllProviders: Record = {}; for (const providerId of sortedProviderIds) { jsonChatProviders[providerId] = Object.fromEntries( Object.entries(providers[providerId].chat).sort(([left], [right]) => left.localeCompare(right)), ); jsonImageProviders[providerId] = Object.fromEntries( Object.entries(providers[providerId].image).sort(([left], [right]) => left.localeCompare(right)), ); jsonClassifierProviders[providerId] = Object.fromEntries( Object.entries(providers[providerId].classifier).sort(([left], [right]) => left.localeCompare(right)), ); jsonAllProviders[providerId] = [ ...Object.values(jsonChatProviders[providerId]), ...Object.values(jsonImageProviders[providerId]), ...Object.values(jsonClassifierProviders[providerId]), ]; } const serializeJson = (value: unknown) => `${JSON.stringify(value, null, generatorOptions.pretty ? 2 : undefined)}\n`; const writeJson = (path: string, value: unknown) => writeFileSync(path, serializeJson(value)); const generatedDataProviderIds = generatorOptions.dataOnly ? readModelDataProviderIds(packageRoot) : sortedProviderIds; const missingProviderIds = generatedDataProviderIds.filter((providerId) => !jsonAllProviders[providerId]); if (missingProviderIds.length > 0) { throw new Error(`Cannot hydrate missing providers: ${missingProviderIds.join(", ")}`); } // Only the ignored internal data is grouped by API for type derivation. const generatedDataProviders: Record>> = {}; const modelDataStructure: ModelDataStructure = {}; for (const providerId of generatedDataProviderIds) { const { groups, structure } = groupProviderModelData(providerId, jsonAllProviders[providerId]); generatedDataProviders[providerId] = groups; modelDataStructure[providerId] = structure; } const generatedAt = new Date().toISOString(); if (!generatorOptions.jsonOnly) { // Stage and validate all provider values before replacing the current generated data. const providersDir = join(packageRoot, "src/providers"); const dataDir = join(providersDir, "data"); const stagingRoot = mkdtempSync(join(providersDir, ".model-generation-")); const stagedDataDir = join(stagingRoot, "data"); const previousDataDir = join(stagingRoot, "previous-data"); let restoreGeneratedCatalog: (() => void) | undefined; try { mkdirSync(stagedDataDir, { recursive: true }); const fileContents: Record = {}; for (const providerId of generatedDataProviderIds) { const filename = `${providerId}.json`; const content = serializeJson(generatedDataProviders[providerId]); fileContents[filename] = content; writeFileSync(join(stagedDataDir, filename), content); } writeJson( join(stagedDataDir, MODEL_DATA_MANIFEST_FILE), createModelDataManifest(modelDataStructure, fileContents, generatedAt), ); validateModelDataDirectory(modelDataStructure, stagedDataDir); if (!generatorOptions.dataOnly) { const previousShardContents = new Map( readdirSync(providersDir) .filter((entry) => entry.endsWith(".models.ts")) .map((entry) => [entry, readFileSync(join(providersDir, entry), "utf8")] as const), ); const aggregatorPath = join(packageRoot, "src/models.generated.ts"); const previousAggregator = readFileSync(aggregatorPath, "utf8"); restoreGeneratedCatalog = () => { for (const entry of readdirSync(providersDir)) { if (entry.endsWith(".models.ts")) rmSync(join(providersDir, entry)); } for (const [entry, content] of previousShardContents) { writeFileSync(join(providersDir, entry), content); } writeFileSync(aggregatorPath, previousAggregator); }; const generatedHeader = `// This file is auto-generated by scripts/generate-models.ts // Do not edit manually - run 'npm run generate-models' to update `; const catalogConstName = (providerId: string) => `${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_MODELS`; const imageCatalogConstName = (providerId: string) => `${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_IMAGE_MODELS`; const classifierCatalogConstName = (providerId: string) => `${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_CLASSIFIER_MODELS`; const generatedShardFiles = new Set(); for (const providerId of sortedProviderIds) { let output = generatedHeader; output += `import values from "./data/${providerId}.json" with { type: "json" };\n`; output += `import { flattenChatModelCatalog, flattenClassifierModelCatalog, flattenImageModelCatalog, type ChatModelCatalog, type ClassifierModelCatalog, type ImageModelCatalog } from "../model-catalog.ts";\n\n`; output += `export const ${catalogConstName(providerId)}: ChatModelCatalog =\n`; output += `\tflattenChatModelCatalog(${JSON.stringify(providerId)}, values);\n\n`; output += `export const ${imageCatalogConstName(providerId)}: ImageModelCatalog =\n`; output += `\tflattenImageModelCatalog(${JSON.stringify(providerId)}, values);\n\n`; output += `export const ${classifierCatalogConstName(providerId)}: ClassifierModelCatalog =\n`; output += `\tflattenClassifierModelCatalog(${JSON.stringify(providerId)}, values);\n`; const filename = `${providerId}.models.ts`; generatedShardFiles.add(filename); writeFileSync(join(providersDir, filename), output); } for (const entry of readdirSync(providersDir)) { if (entry.endsWith(".models.ts") && !generatedShardFiles.has(entry)) rmSync(join(providersDir, entry)); } let output = generatedHeader; for (const providerId of sortedProviderIds) { output += `import { ${classifierCatalogConstName(providerId)}, ${imageCatalogConstName(providerId)}, ${catalogConstName(providerId)} } from "./providers/${providerId}.models.ts";\n`; } output += `\nexport const MODELS: {\n`; for (const providerId of sortedProviderIds) { output += `\treadonly ${JSON.stringify(providerId)}: typeof ${catalogConstName(providerId)};\n`; } output += `} = {\n`; for (const providerId of sortedProviderIds) { output += `\t${JSON.stringify(providerId)}: ${catalogConstName(providerId)},\n`; } output += `};\n\nexport const IMAGE_MODELS: {\n`; for (const providerId of sortedProviderIds) { output += `\treadonly ${JSON.stringify(providerId)}: typeof ${imageCatalogConstName(providerId)};\n`; } output += `} = {\n`; for (const providerId of sortedProviderIds) { output += `\t${JSON.stringify(providerId)}: ${imageCatalogConstName(providerId)},\n`; } output += `};\n\nexport const CLASSIFIER_MODELS: {\n`; for (const providerId of sortedProviderIds) { output += `\treadonly ${JSON.stringify(providerId)}: typeof ${classifierCatalogConstName(providerId)};\n`; } output += `} = {\n`; for (const providerId of sortedProviderIds) { output += `\t${JSON.stringify(providerId)}: ${classifierCatalogConstName(providerId)},\n`; } output += `};\n`; writeFileSync(aggregatorPath, output); console.log("Generated provider catalogs and src/models.generated.ts"); } const hadPreviousData = existsSync(dataDir); if (hadPreviousData) renameSync(dataDir, previousDataDir); try { renameSync(stagedDataDir, dataDir); validateGeneratedModelData(packageRoot); } catch (error) { rmSync(dataDir, { recursive: true, force: true }); if (hadPreviousData && existsSync(previousDataDir)) renameSync(previousDataDir, dataDir); throw error; } restoreGeneratedCatalog = undefined; console.log( generatorOptions.dataOnly ? "Hydrated JSON model values under src/providers/data/" : "Generated JSON model values under src/providers/data/", ); } catch (error) { restoreGeneratedCatalog?.(); throw error; } finally { rmSync(stagingRoot, { recursive: true, force: true }); } } if (generatorOptions.jsonOutputDir) { // `models.json` and `providers/{id}.json` retain the legacy keyed chat catalog. // The `.all` variants are arrays so the same upstream id can appear once per type. const providerOutputDir = join(generatorOptions.jsonOutputDir, "providers"); rmSync(generatorOptions.jsonOutputDir, { recursive: true, force: true }); mkdirSync(providerOutputDir, { recursive: true }); writeJson(join(generatorOptions.jsonOutputDir, "models.json"), jsonChatProviders); writeJson(join(generatorOptions.jsonOutputDir, "models.all.json"), jsonAllProviders); writeJson(join(generatorOptions.jsonOutputDir, "providers.json"), sortedProviderIds); for (const providerId of sortedProviderIds) { writeJson(join(providerOutputDir, `${providerId}.json`), jsonChatProviders[providerId]); writeJson(join(providerOutputDir, `${providerId}.all.json`), jsonAllProviders[providerId]); } console.log(`Generated JSON model catalog under ${generatorOptions.jsonOutputDir}`); } // Print statistics const totalModels = allModels.length; const reasoningModels = allModels.filter((model) => model.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.chat).length} chat models, ${Object.keys(models.image).length} image models, ${Object.keys(models.classifier).length} classifier models`, ); } } // Run the generator generateModels().catch((error) => { console.error(error); process.exitCode = 1; });