Merge main into model-registry

This commit is contained in:
Mario Zechner
2026-06-22 14:00:18 +02:00
220 changed files with 10488 additions and 4354 deletions
+179 -182
View File
@@ -68,6 +68,8 @@ const KIMI_STATIC_HEADERS = {
"User-Agent": "KimiCLI/1.5",
} as const;
const MOONSHOT_CN_MIRRORED_MODEL_IDS = new Set(["kimi-k2.7-code", "kimi-k2.7-code-highspeed"]);
const TOGETHER_BASE_URL = "https://api.together.ai/v1";
const TOGETHER_BASE_COMPAT: OpenAICompletionsCompat = {
supportsStore: false,
@@ -121,6 +123,7 @@ const TOGETHER_TOGGLE_REASONING_LEVEL_MAP = {
const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1";
const AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
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",
@@ -154,6 +157,13 @@ const NVIDIA_NIM_UNSUPPORTED_MODELS = new Set([
"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 ZAI_GLM52_THINKING_LEVEL_MAP = {
minimal: null,
low: "high",
medium: "high",
high: "high",
xhigh: "max",
} as const;
const EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_ANTHROPIC_MODELS = new Set([
"github-copilot:claude-haiku-4.5",
"github-copilot:claude-sonnet-4",
@@ -187,6 +197,23 @@ const OPENAI_RESPONSES_NONE_REASONING_MODELS = new Set([
"gpt-5.5",
]);
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",
]);
// 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-sonnet-4.6": { minimal: "low", xhigh: "max" },
} satisfies Record<string, NonNullable<Model<Api>["thinkingLevelMap"]>>;
function mergeThinkingLevelMap(model: Model<any>, map: NonNullable<Model<any>["thinkingLevelMap"]>): void {
model.thinkingLevelMap = { ...model.thinkingLevelMap, ...map };
}
@@ -251,7 +278,8 @@ function isGemini3ProModel(modelId: string): boolean {
}
function isGemini3FlashModel(modelId: string): boolean {
return /gemini-3(?:\.\d+)?-flash/.test(modelId.toLowerCase());
const id = modelId.toLowerCase();
return /gemini-3(?:\.\d+)?-flash/.test(id) || id === "gemini-flash-latest" || id === "gemini-flash-lite-latest";
}
function isGemma4Model(modelId: string): boolean {
@@ -330,6 +358,15 @@ function applyThinkingLevelMetadata(model: Model<any>): void {
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
@@ -337,6 +374,12 @@ function applyThinkingLevelMetadata(model: Model<any>): void {
// 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" && model.id === "accounts/fireworks/models/glm-5p2") {
mergeThinkingLevelMap(model, { off: "none", minimal: null, low: "high", medium: "high", xhigh: "max" });
}
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 });
@@ -349,6 +392,12 @@ function applyThinkingLevelMetadata(model: Model<any>): void {
// 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 {
@@ -372,6 +421,10 @@ function normalizeNvidiaModelId(modelId: string): string {
return modelId.toLowerCase().replaceAll("_", ".");
}
function roundCost(value: number): number {
return Number(value.toFixed(6));
}
async function fetchNvidiaNimModelIds(): Promise<Map<string, string>> {
try {
console.log("Fetching models from NVIDIA NIM API...");
@@ -417,10 +470,10 @@ async function fetchOpenRouterModels(): Promise<Model<any>[]> {
}
// Convert pricing from $/token to $/million tokens
const inputCost = parseFloat(model.pricing?.prompt || "0") * 1_000_000;
const outputCost = parseFloat(model.pricing?.completion || "0") * 1_000_000;
const cacheReadCost = parseFloat(model.pricing?.input_cache_read || "0") * 1_000_000;
const cacheWriteCost = parseFloat(model.pricing?.input_cache_write || "0") * 1_000_000;
const inputCost = roundCost(parseFloat(model.pricing?.prompt || "0") * 1_000_000);
const outputCost = roundCost(parseFloat(model.pricing?.completion || "0") * 1_000_000);
const cacheReadCost = roundCost(parseFloat(model.pricing?.input_cache_read || "0") * 1_000_000);
const cacheWriteCost = roundCost(parseFloat(model.pricing?.input_cache_write || "0") * 1_000_000);
const normalizedModel: Model<any> = {
id: modelKey,
@@ -476,10 +529,10 @@ async function fetchAiGatewayModels(): Promise<Model<any>[]> {
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;
const inputCost = roundCost(toNumber(model.pricing?.input) * 1_000_000);
const outputCost = roundCost(toNumber(model.pricing?.output) * 1_000_000);
const cacheReadCost = roundCost(toNumber(model.pricing?.input_cache_read) * 1_000_000);
const cacheWriteCost = roundCost(toNumber(model.pricing?.input_cache_write) * 1_000_000);
models.push({
id: model.id,
@@ -586,6 +639,13 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
for (const [modelId, model] of Object.entries(data.google.models)) {
const m = model as ModelsDevModel;
if (m.tool_call !== true) continue;
let source = m;
if (modelId === "gemini-flash-latest") {
source = (data.google.models["gemini-3.5-flash"] as ModelsDevModel | undefined) ?? m;
}
if (modelId === "gemini-flash-lite-latest") {
source = (data.google.models["gemini-3.1-flash-lite"] as ModelsDevModel | undefined) ?? m;
}
models.push({
id: modelId,
@@ -593,16 +653,57 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
api: "google-generative-ai",
provider: "google",
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
reasoning: m.reasoning === true,
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
reasoning: source.reasoning === true,
input: source.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
cost: {
input: m.cost?.input || 0,
output: m.cost?.output || 0,
cacheRead: m.cost?.cache_read || 0,
cacheWrite: m.cost?.cache_write || 0,
input: source.cost?.input || 0,
output: source.cost?.output || 0,
cacheRead: source.cost?.cache_read || 0,
cacheWrite: source.cost?.cache_write || 0,
},
contextWindow: m.limit?.context || 4096,
maxTokens: m.limit?.output || 4096,
contextWindow: source.limit?.context || 4096,
maxTokens: source.limit?.output || 4096,
});
}
}
// Process Google Vertex Gemini models. The google-vertex models.dev catalog also includes
// Claude, OpenAI, and other MaaS models that do not use the @google/genai Gemini streaming
// path implemented by our google-vertex provider.
if (data["google-vertex"]?.models) {
for (const [modelId, model] of Object.entries(data["google-vertex"].models)) {
const m = model as ModelsDevModel;
if (m.tool_call !== true) continue;
if (!modelId.startsWith("gemini-")) continue;
if (modelId === "gemini-3.1-flash-lite-preview") continue;
let source = m;
if (modelId === "gemini-flash-latest") {
source = (data["google-vertex"].models["gemini-3.5-flash"] as ModelsDevModel | undefined) ?? m;
}
if (modelId === "gemini-flash-lite-latest") {
source = (data["google-vertex"].models["gemini-3.1-flash-lite"] as ModelsDevModel | undefined) ?? m;
}
// 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: m.name || modelId,
api: "google-vertex",
provider: "google-vertex",
baseUrl: VERTEX_BASE_URL,
reasoning: source.reasoning === true,
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,
});
}
}
@@ -806,6 +907,8 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
if (m.tool_call !== true) continue;
const supportsImage = m.modalities?.input?.includes("image");
const isGlm52 = modelId === "glm-5.2";
models.push({
id: modelId,
name: m.name || modelId,
@@ -813,6 +916,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
provider,
baseUrl,
reasoning: m.reasoning === true,
...(isGlm52 ? { thinkingLevelMap: ZAI_GLM52_THINKING_LEVEL_MAP } : {}),
input: supportsImage ? ["text", "image"] : ["text"],
cost: {
input: m.cost?.input || 0,
@@ -823,6 +927,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
compat: {
supportsDeveloperRole: false,
thinkingFormat: "zai",
...(isGlm52 ? { supportsReasoningEffort: true } : {}),
...(!ZAI_TOOL_STREAM_UNSUPPORTED_MODELS.has(modelId) ? { zaiToolStream: true } : {}),
},
contextWindow: m.limit?.context || 4096,
@@ -849,7 +954,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
cost: {
input: m.cost?.input || 0,
output: m.cost?.output || 0,
cacheRead: m.cost?.cache_read || 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,
@@ -1066,6 +1171,13 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
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 };
}
}
models.push({
@@ -1228,12 +1340,27 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
supportsStrictMode: false,
thinkingFormat: "deepseek",
};
const getMoonshotProviderModels = (key: "moonshotai" | "moonshotai-cn"): Record<string, ModelsDevModel> => {
const providerModels = data[key]?.models as Record<string, ModelsDevModel> | undefined;
return providerModels ? { ...providerModels } : {};
};
const moonshotModels = {
moonshotai: getMoonshotProviderModels("moonshotai"),
"moonshotai-cn": getMoonshotProviderModels("moonshotai-cn"),
};
// models.dev can lag the CN catalog while the global Moonshot catalog already
// has the model. Mirror selected current model IDs into moonshotai-cn until
// upstream CN metadata catches up.
for (const modelId of MOONSHOT_CN_MIRRORED_MODEL_IDS) {
const model = moonshotModels.moonshotai[modelId];
if (model && !moonshotModels["moonshotai-cn"][modelId]) {
moonshotModels["moonshotai-cn"][modelId] = model;
}
}
for (const { key, provider, baseUrl } of moonshotVariants) {
if (!data[key]?.models) continue;
for (const [modelId, model] of Object.entries(data[key].models)) {
const m = model as ModelsDevModel;
for (const [modelId, m] of Object.entries(moonshotModels[key])) {
if (m.tool_call !== true) continue;
models.push({
@@ -1390,7 +1517,11 @@ async function generateModels() {
candidate.cost.output = 1.9;
candidate.cost.cacheRead = 0.119;
}
if (candidate.provider === "fireworks" && candidate.id === "accounts/fireworks/models/glm-5p2") {
candidate.api = "openai-completions";
candidate.baseUrl = "https://api.fireworks.ai/inference/v1";
candidate.compat = { supportsStore: false, supportsDeveloperRole: false };
}
}
@@ -1731,9 +1862,10 @@ async function generateModels() {
for (const candidate of allModels) {
if (candidate.api === "openai-completions" && candidate.id.includes("deepseek-v4")) {
const preservesNativeReasoningEffort = candidate.provider === "openrouter" || candidate.provider === "opencode";
candidate.compat = {
...candidate.compat,
...(candidate.provider === "openrouter"
...(preservesNativeReasoningEffort
? {
requiresReasoningContentOnAssistantMessages:
deepseekCompat.requiresReasoningContentOnAssistantMessages,
@@ -1908,166 +2040,31 @@ async function generateModels() {
});
}
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,
// 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", "image"],
cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 },
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: 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);
maxTokens: 30000,
});
}
// Azure Foundry deploys these with larger context windows than OpenAI's own API,
// which caps gpt-5.4/gpt-5.5 at 272k. See models-sold-directly-by-azure docs.