fix(api): provider-models route — redirect→local-catalog fallback + custom-model merge (#6267, #6247) (#6449)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-06 18:17:02 -03:00
committed by GitHub
parent cd4a720b7e
commit 2f4b793c3d
4 changed files with 248 additions and 1 deletions

View File

@@ -71,7 +71,7 @@ import {
parseGeminiModelsList,
type GeminiDiscoveryModel,
} from "@/lib/providerModels/geminiModelsParser";
import { getSyncedAvailableModels } from "@/lib/db/models";
import { getSyncedAvailableModels, getCustomModels } from "@/lib/db/models";
import { fetchCursorAgentModels } from "@/lib/providerModels/cursorAgent";
import {
type JsonRecord,
@@ -212,7 +212,38 @@ export async function GET(
// Resolve proxy for this provider (provider-level → global → direct)
const proxy = await resolveProxyForProvider(provider);
// #6247 — user-added custom models live in key_value namespace `customModels`
// (getCustomModels). The live REST /api/v1/models merges them, but this
// per-connection route (used by MCP list_models_catalog + the dashboard
// import view) never did, so custom models were dropped on both the
// discovery-success and local_catalog paths. Read them once here and fold
// them into every models response via buildResponse below (dedup by id).
let customModelsForProvider: Array<{ id: string; name?: string }> = [];
try {
const custom = await getCustomModels(provider);
if (Array.isArray(custom)) {
customModelsForProvider = custom as Array<{ id: string; name?: string }>;
}
} catch {
// DB unavailable — proceed without custom models.
}
const mergeCustomModels = (models: any[]) => {
if (customModelsForProvider.length === 0) return models;
const base = Array.isArray(models) ? models : [];
const existing = new Set(
base.map((m) => (m && typeof m.id === "string" ? m.id : null)).filter(Boolean)
);
const extra = customModelsForProvider
.filter((m) => m && typeof m.id === "string" && m.id.length > 0 && !existing.has(m.id))
.map((m) => ({ id: m.id, name: m.name || m.id, owned_by: provider }));
return extra.length > 0 ? [...base, ...extra] : base;
};
const buildResponse = (payload: any, statusConfig?: ResponseInit) => {
if (payload.models && Array.isArray(payload.models)) {
payload.models = mergeCustomModels(payload.models);
}
if (excludeHidden && payload.models && Array.isArray(payload.models)) {
payload.models = payload.models.filter((m: any) => !getModelIsHidden(provider, m.id));
}
@@ -316,6 +347,16 @@ export async function GET(
localWarning?: string;
}
) => {
// #6267 — a models-endpoint redirect (307/308) is not a fixable-config
// error. safeOutboundFetch throws REDIRECT_BLOCKED which
// getSafeOutboundFetchErrorStatus maps to 503, but unlike the other 503
// cases (URL_GUARD_BLOCKED / INVALID_URL, which are genuinely
// unrecoverable and stay hard errors) a blocked redirect should degrade to
// the local/cached catalog OmniRoute ships instead of surfacing a raw 503.
// General fix — covers any config-driven provider that 307s (e.g. qwen-web).
if (error instanceof SafeOutboundFetchError && error.code === "REDIRECT_BLOCKED") {
return buildDiscoveryFallbackResponse(warnings);
}
const status = getSafeOutboundFetchErrorStatus(error);
if (status === 400 || status === 503 || status === 504) return null;
return buildDiscoveryFallbackResponse(warnings);