diff --git a/src/app/api/v1/models/route.js b/src/app/api/v1/models/route.js index 26c9d010..40adc696 100644 --- a/src/app/api/v1/models/route.js +++ b/src/app/api/v1/models/route.js @@ -485,6 +485,27 @@ export async function buildModelsList(kindFilter, options = {}) { || capabilitiesFromServiceKind(customKind || liveKind) || (kind === LLM_KIND ? getCapabilitiesForModel(providerId, modelId) : null); if (caps) model.capabilities = caps; + // Token limits under the snake_case names the OpenAI/OpenRouter + // convention uses. `capabilities.contextWindow` is camelCase and nested, + // so clients matching context_length find nothing, fall back to guessing + // the window from the model name, and guess high — a 372k model read as + // 1.05M never reaches its compaction threshold and hard-fails upstream. + // Emitted at top level because not every client recurses into nested + // objects; the camelCase `capabilities` block stays for compatibility. + if (kind === LLM_KIND || allowAsLlm) { + let contextWindow = caps?.contextWindow; + let maxOutput = caps?.maxOutput; + // Live-catalog and service-kind capabilities are usually partial + // (often just { tools: true }), so fill the gaps from the static + // table rather than emitting null and leaving clients to guess. + if (!Number.isFinite(contextWindow) || !Number.isFinite(maxOutput)) { + const fallback = getCapabilitiesForModel(providerId, modelId); + if (!Number.isFinite(contextWindow)) contextWindow = fallback.contextWindow; + if (!Number.isFinite(maxOutput)) maxOutput = fallback.maxOutput; + } + if (Number.isFinite(contextWindow)) model.context_length = contextWindow; + if (Number.isFinite(maxOutput)) model.max_completion_tokens = maxOutput; + } models.push(model); }