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