fix(providers): treat a degraded cached catalog as a failed model sync (#10862)

Obrigado — root cause preciso: a rota sync-models só reconhecia a degradação para local_catalog, não para o fallback de cache com warning, então uma chave expirada (401) virava silenciosamente "Nenhum modelo novo foi adicionado" em vez de um erro visível.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/sync-models-degraded-cached-catalog-9683.test.ts — 6/6 passando (payloads reais do models/route.ts)
- Suítes model-sync/provider-models/sync-models/siliconflow — 181/181 passando, incluindo as 3 asserções pré-existentes #5460/#5465
This commit is contained in:
Nguyen Thanh Dat
2026-08-21 01:19:41 +07:00
committed by GitHub
parent a72dc25c04
commit 80b517edea
4 changed files with 166 additions and 3 deletions

View File

@@ -19,3 +19,44 @@ export function isDegradedLocalCatalog(modelsData: {
typeof modelsData?.source === "string" ? modelsData.source.trim().toLowerCase() : "";
return source === "local_catalog" && modelsData?.intentional !== true;
}
/**
* #9683 — the same degradation, one branch further up.
*
* When remote discovery fails, the models route falls back to the CACHED
* catalog if it has one and only falls back to the local catalog when it does
* not (`buildDiscoveryFallbackResponse`). A provider that was imported
* successfully once therefore has a cache, so an expired key produced
* `source: "cache"` + a warning and HTTP 200 — model-sync treated that as a
* successful discovery, found every cached model already imported, and reported
* "No new models were added" instead of the credential error. Retest, which
* does not go through this path, failed correctly, which is what made the
* import look like a real "nothing to do".
*
* The discriminator is the warning: the fallback builder always attaches one,
* while the ordinary cache hit (`maybeReturnCachedDiscovery`, a non-refresh
* read) attaches none. Model-sync always requests `refresh=true`, so the one
* warning-carrying cache response it can observe is a degraded one.
*/
export function isDegradedCachedCatalog(modelsData: {
source?: unknown;
warning?: unknown;
}): boolean {
const source =
typeof modelsData?.source === "string" ? modelsData.source.trim().toLowerCase() : "";
if (source !== "cache") return false;
return typeof modelsData?.warning === "string" && modelsData.warning.trim().length > 0;
}
/**
* Either degraded shape. Model-sync must refuse to treat these as a successful
* discovery: persisting them would silently pin a stale catalog and hide the
* real failure from the operator.
*/
export function isDegradedDiscovery(modelsData: {
source?: unknown;
intentional?: unknown;
warning?: unknown;
}): boolean {
return isDegradedLocalCatalog(modelsData) || isDegradedCachedCatalog(modelsData);
}

View File

@@ -22,7 +22,7 @@ import { autoSyncCodexProfilesFromLiveCatalog } from "@/lib/cli-helper/codexProf
import { autoSyncClaudeProfilesFromLiveCatalog } from "@/lib/cli-helper/claudeProfileAutoSync";
import { providerUsesCuratedModelsOnly } from "@/lib/providers/modelListingCapability";
import { GET as getProviderModels } from "../models/route";
import { isDegradedLocalCatalog } from "./degradedLocalCatalog";
import { isDegradedDiscovery } from "./degradedLocalCatalog";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
type JsonRecord = Record<string, unknown>;
@@ -465,9 +465,9 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
const modelSource = toNonEmptyString(modelsData.source)?.toLowerCase() || "unknown";
const modelWarning = toNonEmptyString(modelsData.warning);
if (isDegradedLocalCatalog(modelsData)) {
if (isDegradedDiscovery(modelsData)) {
const responseError =
modelWarning || "Remote model discovery failed; local catalog fallback not synced";
modelWarning || "Remote model discovery failed; catalog fallback not synced";
await saveCallLog({
method: "GET",
path: `/api/providers/${id}/models`,