mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-19 21:52:21 +03:00
fix(api): expose models.dev context windows in /v1/models (#1972)
Integrated into release/v3.7.9
This commit is contained in:
@@ -338,10 +338,6 @@ export async function getUnifiedModelsResponse(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get default context length from registry (provider-level default)
|
||||
const registryEntry = REGISTRY[alias] || REGISTRY[canonicalProviderId];
|
||||
const defaultContextLength = registryEntry?.defaultContextLength;
|
||||
|
||||
for (const model of providerModels) {
|
||||
if (!providerSupportsModel(canonicalProviderId, model.id)) continue;
|
||||
const aliasId = `${alias}/${model.id}`;
|
||||
@@ -349,8 +345,6 @@ export async function getUnifiedModelsResponse(
|
||||
|
||||
const visionFields =
|
||||
getVisionCapabilityFields(aliasId) || getVisionCapabilityFields(model.id);
|
||||
// Model-level context length overrides provider default
|
||||
const contextLength = model.contextLength || defaultContextLength;
|
||||
|
||||
models.push({
|
||||
id: aliasId,
|
||||
@@ -360,7 +354,6 @@ export async function getUnifiedModelsResponse(
|
||||
permission: [],
|
||||
root: model.id,
|
||||
parent: null,
|
||||
...(contextLength ? { context_length: contextLength } : {}),
|
||||
...(visionFields || {}),
|
||||
});
|
||||
|
||||
@@ -378,7 +371,6 @@ export async function getUnifiedModelsResponse(
|
||||
permission: [],
|
||||
root: model.id,
|
||||
parent: aliasId,
|
||||
...(contextLength ? { context_length: contextLength } : {}),
|
||||
...(providerVisionFields || {}),
|
||||
});
|
||||
}
|
||||
@@ -813,7 +805,24 @@ export async function getUnifiedModelsResponse(
|
||||
finalModels = filtered;
|
||||
}
|
||||
|
||||
const enrichedModels = finalModels.map((model) => enrichCatalogModelEntry(model));
|
||||
const getDefaultContextFallback = (model: any): number | undefined => {
|
||||
if (typeof model.context_length === "number") return undefined;
|
||||
if (model.owned_by === "combo") return undefined;
|
||||
if (model.type && model.type !== "chat") return undefined;
|
||||
|
||||
const provider = typeof model.owned_by === "string" ? model.owned_by : null;
|
||||
if (!provider) return undefined;
|
||||
const canonicalId = aliasToProviderId[provider] || provider;
|
||||
return REGISTRY[canonicalId]?.defaultContextLength;
|
||||
};
|
||||
|
||||
const enrichedModels = finalModels.map((model) => {
|
||||
const enriched = enrichCatalogModelEntry(model);
|
||||
const fallbackContextLength = getDefaultContextFallback(enriched);
|
||||
return fallbackContextLength
|
||||
? { ...enriched, context_length: fallbackContextLength }
|
||||
: enriched;
|
||||
});
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
|
||||
@@ -183,16 +183,21 @@ function resolveVisionCapability(
|
||||
modalitiesInput: string[],
|
||||
modalitiesOutput: string[]
|
||||
): boolean | null {
|
||||
if (typeof spec?.supportsVision === "boolean") return spec.supportsVision;
|
||||
if (typeof registryModel?.supportsVision === "boolean") return registryModel.supportsVision;
|
||||
|
||||
const allModalities = [...modalitiesInput, ...modalitiesOutput].map((entry) =>
|
||||
String(entry).toLowerCase()
|
||||
);
|
||||
|
||||
if (typeof synced?.attachment === "boolean") {
|
||||
return synced.attachment;
|
||||
}
|
||||
|
||||
if (allModalities.some((entry) => entry.includes("image"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof registryModel?.supportsVision === "boolean") return registryModel.supportsVision;
|
||||
if (typeof spec?.supportsVision === "boolean") return spec.supportsVision;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -215,18 +220,22 @@ export function getResolvedModelCapabilities(input: CapabilityInput): ResolvedMo
|
||||
) || "";
|
||||
|
||||
const supportsTools =
|
||||
typeof spec?.supportsTools === "boolean"
|
||||
? spec.supportsTools
|
||||
: typeof registryModel?.toolCalling === "boolean"
|
||||
? registryModel.toolCalling
|
||||
: (synced?.tool_call ?? null);
|
||||
synced?.tool_call ??
|
||||
(typeof registryModel?.toolCalling === "boolean" ? registryModel.toolCalling : null) ??
|
||||
(typeof spec?.supportsTools === "boolean" ? spec.supportsTools : null);
|
||||
|
||||
const supportsThinking =
|
||||
typeof spec?.supportsThinking === "boolean"
|
||||
? spec.supportsThinking
|
||||
: typeof registryModel?.supportsReasoning === "boolean"
|
||||
? registryModel.supportsReasoning
|
||||
: (synced?.reasoning ?? null);
|
||||
synced?.reasoning ??
|
||||
(typeof registryModel?.supportsReasoning === "boolean"
|
||||
? registryModel.supportsReasoning
|
||||
: null) ??
|
||||
(typeof spec?.supportsThinking === "boolean" ? spec.supportsThinking : null);
|
||||
|
||||
const contextWindow =
|
||||
synced?.limit_context ??
|
||||
(typeof registryModel?.contextLength === "number" ? registryModel.contextLength : null) ??
|
||||
spec?.contextWindow ??
|
||||
null;
|
||||
|
||||
return {
|
||||
provider: resolved.provider,
|
||||
@@ -247,14 +256,10 @@ export function getResolvedModelCapabilities(input: CapabilityInput): ResolvedMo
|
||||
attachment: synced?.attachment ?? null,
|
||||
structuredOutput: synced?.structured_output ?? null,
|
||||
temperature: synced?.temperature ?? null,
|
||||
contextWindow:
|
||||
spec?.contextWindow ??
|
||||
(typeof registryModel?.contextLength === "number" ? registryModel.contextLength : null) ??
|
||||
synced?.limit_context ??
|
||||
null,
|
||||
maxInputTokens: synced?.limit_input ?? spec?.contextWindow ?? null,
|
||||
contextWindow,
|
||||
maxInputTokens: synced?.limit_input ?? contextWindow,
|
||||
maxOutputTokens:
|
||||
spec?.maxOutputTokens ?? synced?.limit_output ?? MODEL_SPECS.__default__.maxOutputTokens,
|
||||
synced?.limit_output ?? spec?.maxOutputTokens ?? MODEL_SPECS.__default__.maxOutputTokens,
|
||||
defaultThinkingBudget: spec?.defaultThinkingBudget ?? 0,
|
||||
thinkingBudgetCap: spec?.thinkingBudgetCap ?? null,
|
||||
thinkingOverhead: spec?.thinkingOverhead ?? null,
|
||||
|
||||
@@ -154,12 +154,16 @@ const MODELS_DEV_PROVIDER_MAP: Record<string, string[]> = {
|
||||
openai: ["openai", "cx"], // cx = Codex (uses OpenAI models)
|
||||
anthropic: ["anthropic", "cc"], // cc = Claude Code
|
||||
google: ["gemini", "gemini-cli"],
|
||||
"google-vertex": ["gemini", "vertex"],
|
||||
"google-vertex-anthropic": ["anthropic", "cc", "vertex"],
|
||||
vertex_ai: ["gemini", "vertex"],
|
||||
deepseek: ["deepseek", "if"], // if = Qoder (routes through DeepSeek)
|
||||
groq: ["groq"],
|
||||
xai: ["xai"],
|
||||
mistral: ["mistral"],
|
||||
togetherai: ["together", "openrouter"],
|
||||
together_ai: ["together", "openrouter"],
|
||||
"fireworks-ai": ["fireworks"],
|
||||
fireworks: ["fireworks"],
|
||||
cerebras: ["cerebras"],
|
||||
cohere: ["cohere"],
|
||||
@@ -172,11 +176,25 @@ const MODELS_DEV_PROVIDER_MAP: Record<string, string[]> = {
|
||||
perplexity: ["pplx", "perplexity"],
|
||||
// OAuth / special providers
|
||||
bedrock: ["kiro", "kr"], // kr = Kiro (AWS Bedrock)
|
||||
"github-copilot": ["github", "gh"],
|
||||
"github-models": ["github", "gh"],
|
||||
kilo: ["kilocode", "kc", "kilo-gateway"],
|
||||
kilocode: ["kilocode", "kc", "kilo-gateway"],
|
||||
"kimi-for-coding": ["kimi-coding", "kmc", "kimi-coding-apikey", "kmca"],
|
||||
opencode: ["opencode-zen"],
|
||||
"opencode-go": ["opencode-go"],
|
||||
// Additional providers that may overlap with OmniRoute
|
||||
alibaba: ["ali", "alibaba", "bcp", "alicode", "alicode-intl"],
|
||||
"alibaba-cn": ["ali", "alibaba", "bcp"],
|
||||
"alibaba-coding-plan": ["alicode", "alicode-intl"],
|
||||
"alibaba-coding-plan-cn": ["alicode"],
|
||||
zai: ["zai", "glm"], // GLM models via Z.AI
|
||||
"zai-coding-plan": ["zai", "glm"],
|
||||
moonshotai: ["moonshot", "kimi"],
|
||||
"moonshotai-cn": ["moonshot", "kimi"],
|
||||
moonshot: ["moonshot", "kimi", "kimi-coding", "kmc", "kmca"],
|
||||
minimax: ["minimax", "minimax-cn"],
|
||||
"minimax-cn": ["minimax-cn"],
|
||||
longcat: ["lc", "longcat"],
|
||||
pollinations: ["pol", "pollinations"],
|
||||
puter: ["pu", "puter"],
|
||||
@@ -184,7 +202,6 @@ const MODELS_DEV_PROVIDER_MAP: Record<string, string[]> = {
|
||||
scaleway: ["scw"],
|
||||
ollama: ["ollamacloud", "ollama-cloud"],
|
||||
blackbox: ["bb", "blackbox"],
|
||||
kilocode: ["kc", "kilocode"],
|
||||
cline: ["cl", "cline"],
|
||||
cursor: ["cu", "cursor"],
|
||||
github: ["gh", "github"],
|
||||
|
||||
@@ -49,7 +49,7 @@ test.after(() => {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("canonical model capability resolver merges models.dev data and keeps static overrides authoritative", () => {
|
||||
test("canonical model capability resolver lets exact synced metadata override global specs", () => {
|
||||
modelsDevSync.saveModelsDevCapabilities({
|
||||
openai: {
|
||||
"gpt-4o": buildCapability({
|
||||
@@ -92,11 +92,11 @@ test("canonical model capability resolver merges models.dev data and keeps stati
|
||||
const geminiHigh = modelCapabilities.getResolvedModelCapabilities(
|
||||
"antigravity/gemini-3.1-pro-high"
|
||||
);
|
||||
assert.equal(geminiHigh.toolCalling, true);
|
||||
assert.equal(geminiHigh.reasoning, true);
|
||||
assert.equal(geminiHigh.supportsThinking, true);
|
||||
assert.equal(geminiHigh.contextWindow, 1048576);
|
||||
assert.equal(geminiHigh.maxOutputTokens, 65535);
|
||||
assert.equal(geminiHigh.toolCalling, false);
|
||||
assert.equal(geminiHigh.reasoning, false);
|
||||
assert.equal(geminiHigh.supportsThinking, false);
|
||||
assert.equal(geminiHigh.contextWindow, 1024);
|
||||
assert.equal(geminiHigh.maxOutputTokens, 9999);
|
||||
assert.equal(geminiHigh.defaultThinkingBudget, 24576);
|
||||
assert.equal(
|
||||
modelCapabilities.capThinkingBudget("antigravity/gemini-3.1-pro-high", 40000),
|
||||
|
||||
@@ -14,6 +14,7 @@ const modelsDb = await import("../../src/lib/db/models.ts");
|
||||
const combosDb = await import("../../src/lib/db/combos.ts");
|
||||
const settingsDb = await import("../../src/lib/db/settings.ts");
|
||||
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
||||
const modelsDevSync = await import("../../src/lib/modelsDevSync.ts");
|
||||
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
@@ -689,6 +690,99 @@ test("v1 models catalog exposes provider-prefixed custom models, filters by raw
|
||||
assert.equal(providerAlias.parent, "cl/demo-custom");
|
||||
});
|
||||
|
||||
test("v1 models catalog uses synced models.dev limits instead of provider defaults", async () => {
|
||||
await seedConnection("openai", { name: "openai-models-dev" });
|
||||
|
||||
try {
|
||||
modelsDevSync.saveModelsDevCapabilities({
|
||||
openai: {
|
||||
"gpt-5.5": {
|
||||
tool_call: true,
|
||||
reasoning: true,
|
||||
attachment: true,
|
||||
structured_output: true,
|
||||
temperature: true,
|
||||
modalities_input: JSON.stringify(["text", "image"]),
|
||||
modalities_output: JSON.stringify(["text"]),
|
||||
knowledge_cutoff: null,
|
||||
release_date: null,
|
||||
last_updated: null,
|
||||
status: null,
|
||||
family: "gpt-5",
|
||||
open_weights: false,
|
||||
limit_context: 1050000,
|
||||
limit_input: 1050000,
|
||||
limit_output: 128000,
|
||||
interleaved_field: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const body = (await response.json()) as any;
|
||||
const model = body.data.find((item) => item.id === "openai/gpt-5.5");
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(model);
|
||||
assert.equal(model.context_length, 1050000);
|
||||
assert.equal(model.max_input_tokens, 1050000);
|
||||
assert.equal(model.max_output_tokens, 128000);
|
||||
} finally {
|
||||
modelsDevSync.saveModelsDevCapabilities({});
|
||||
}
|
||||
});
|
||||
|
||||
test("v1 models catalog lets provider-specific synced limits beat global static specs", async () => {
|
||||
await seedConnection("github", {
|
||||
authType: "oauth",
|
||||
name: "github-copilot-models-dev",
|
||||
apiKey: null,
|
||||
accessToken: "github-access",
|
||||
});
|
||||
|
||||
try {
|
||||
modelsDevSync.saveModelsDevCapabilities({
|
||||
github: {
|
||||
"gpt-5.5": {
|
||||
tool_call: true,
|
||||
reasoning: true,
|
||||
attachment: true,
|
||||
structured_output: true,
|
||||
temperature: true,
|
||||
modalities_input: JSON.stringify(["text", "image"]),
|
||||
modalities_output: JSON.stringify(["text"]),
|
||||
knowledge_cutoff: null,
|
||||
release_date: null,
|
||||
last_updated: null,
|
||||
status: null,
|
||||
family: "gpt-5",
|
||||
open_weights: false,
|
||||
limit_context: 400000,
|
||||
limit_input: 272000,
|
||||
limit_output: 128000,
|
||||
interleaved_field: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const body = (await response.json()) as any;
|
||||
const model = body.data.find((item) => item.id === "gh/gpt-5.5");
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(model);
|
||||
assert.equal(model.context_length, 400000);
|
||||
assert.equal(model.max_input_tokens, 272000);
|
||||
assert.equal(model.max_output_tokens, 128000);
|
||||
} finally {
|
||||
modelsDevSync.saveModelsDevCapabilities({});
|
||||
}
|
||||
});
|
||||
|
||||
test("v1 models catalog returns 500 when model compatibility lookup crashes", async () => {
|
||||
await seedConnection("openai", { name: "openai-compat-crash" });
|
||||
|
||||
|
||||
@@ -412,6 +412,19 @@ describe("modelsDevSync — mapProviderId", () => {
|
||||
it("maps moonshot to the canonical provider plus Kimi aliases", () => {
|
||||
assert.deepEqual(mapProviderId("moonshot"), ["moonshot", "kimi", "kimi-coding", "kmc", "kmca"]);
|
||||
});
|
||||
|
||||
it("maps current models.dev provider IDs used by OmniRoute-compatible providers", () => {
|
||||
assert.deepEqual(mapProviderId("github-copilot"), ["github", "gh"]);
|
||||
assert.deepEqual(mapProviderId("kilo"), ["kilocode", "kc", "kilo-gateway"]);
|
||||
assert.deepEqual(mapProviderId("kimi-for-coding"), [
|
||||
"kimi-coding",
|
||||
"kmc",
|
||||
"kimi-coding-apikey",
|
||||
"kmca",
|
||||
]);
|
||||
assert.deepEqual(mapProviderId("fireworks-ai"), ["fireworks"]);
|
||||
assert.deepEqual(mapProviderId("togetherai"), ["together", "openrouter"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("modelsDevSync — fetchModelsDev (live API)", () => {
|
||||
|
||||
Reference in New Issue
Block a user