{"record":{"id":"998d1541c8ec47cf","repo":"ruvnet/ruflo","slug":"failed-to-load-any-models-from-upstream","errorCode":null,"errorMessage":"Failed to load any models from upstream","messagePattern":"Failed to load any models from upstream","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"ruflo/src/ruvocal/src/lib/server/models.ts","lineNumber":246,"sourceCode":"\t\tprepromptUrl: model.prepromptUrl,\n\t\tendpoints:\n\t\t\tmodel.endpoints?.map((endpoint) => {\n\t\t\t\tif (endpoint.type === \"openai\") {\n\t\t\t\t\tconst { type, baseURL } = endpoint;\n\t\t\t\t\treturn { type, baseURL };\n\t\t\t\t}\n\t\t\t\treturn { type: endpoint.type };\n\t\t\t}) ?? null,\n\t\tmultimodal: model.multimodal,\n\t\tmultimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes,\n\t\tsupportsTools: (model as unknown as { supportsTools?: boolean }).supportsTools ?? false,\n\t\tisRouter: model.isRouter,\n\t\thasInferenceAPI: model.hasInferenceAPI,\n\t});\n\nconst applyModelState = (newModels: ProcessedModel[], startedAt: number): ModelsRefreshSummary => {\n\tif (newModels.length === 0) {\n\t\tthrow new Error(\"Failed to load any models from upstream\");\n\t}\n\n\tconst previousIds = new Set(models.map((m) => m.id));\n\tconst previousSignatures = new Map(models.map((m) => [m.id, signatureForModel(m)]));\n\tconst refreshedAt = new Date();\n\tconst durationMs = Date.now() - startedAt;\n\n\tmodels = newModels;\n\tdefaultModel = models[0];\n\ttaskModel = resolveTaskModel(models);\n\tvalidModelIdSchema = createValidModelIdSchema(models);\n\tlastModelRefresh = refreshedAt;\n\tlastModelRefreshDurationMs = durationMs;\n\n\tconst added = newModels.map((m) => m.id).filter((id) => !previousIds.has(id));\n\tconst removed = Array.from(previousIds).filter(\n\t\t(id) => !newModels.some((model) => model.id === id)\n\t);","sourceCodeStart":228,"sourceCodeEnd":264,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/ruflo/src/ruvocal/src/lib/server/models.ts#L228-L264","documentation":"Thrown by applyModelState when buildModels returned an empty array. applyModelState is the single guard that prevents the global models array from being replaced with []; if the freshly-built list is empty, it refuses to swap state and throws. Empty builds happen when the upstream /models response yields zero usable entries, or when MODELS overrides filter the upstream list down to nothing.","triggerScenarios":"OPENAI_BASE_URL points at an OpenAI-compatible service whose /models returns {data: []}; MODELS overrides all reference model ids that do not exist upstream AND filteredAndOrdered ends up empty (note: when no override matches, buildModels falls back to applying overrides on top of all upstream models, so a truly empty result usually means the upstream list itself was empty); listSchema parses but parsed.data is [].","commonSituations":"Fresh gateway deployment with no published models; wrong base URL (e.g. pointing at an auth or management endpoint that returns an empty data array); upstream temporarily returning empty during maintenance; MODELS override that references only retired/renamed model ids in a config branch that also disables the fallback.","solutions":["Confirm the upstream /models endpoint actually returns models: curl -H \"Authorization: Bearer $TOKEN\" $OPENAI_BASE_URL/models.","If using MODELS overrides, ensure at least one override id matches a real upstream model id, or remove overrides temporarily to load all upstream models.","Point OPENAI_BASE_URL at a known-good OpenAI-compatible router (e.g. https://router.huggingface.co/v1) and verify a non-empty data array.","Check logs for the '[models] Parsed models count' line to see what buildModels actually received."],"exampleFix":"// before\nOPENAI_BASE_URL=https://internal-gateway/v1\nMODELS='[{\"id\":\"retired-model\"}]'\n\n// after\nOPENAI_BASE_URL=https://router.huggingface.co/v1\n# remove or update MODELS to include a currently-available model","handlingStrategy":"validation","validationCode":"async function canFetchModels(): Promise<{ ok: boolean; reason?: string }> {\n  if (!config.OPENAI_BASE_URL) return { ok: false, reason: \"missing-base-url\" };\n  try {\n    const r = await fetch(`${config.OPENAI_BASE_URL.replace(/\\/$/, \"\")}/models`, {\n      headers: config.OPENAI_API_KEY ? { Authorization: `Bearer ${config.OPENAI_API_KEY}` } : undefined,\n    });\n    if (!r.ok) return { ok: false, reason: `http-${r.status}` };\n    const j = await r.json();\n    return { ok: Array.isArray(j?.data) && j.data.length > 0, reason: j?.data?.length ? undefined : \"empty-data\" };\n  } catch (e) {\n    return { ok: false, reason: String(e) };\n  }\n}","typeGuard":"function isNonEmptyModelList(list: unknown): list is { id: string }[] {\n  return Array.isArray(list) && list.length > 0 && list.every((m) => m && typeof m.id === \"string\");\n}","tryCatchPattern":"try { await rebuildModels(); }\ncatch (e) {\n  if (e instanceof Error && /Failed to load any models from upstream/.test(e.message)) {\n    // keep previous models if present, alert operator, do not crash on refresh\n    if (models.length === 0) process.exit(1);\n  } else throw e;\n}","preventionTips":["Smoke-test OPENAI_BASE_URL/models before deploying (curl with the configured token).","Ensure MODELS overrides reference at least one currently-available upstream model id.","Monitor the '[models] Parsed models count' log line; alert on zero.","Keep the previous models array intact on refresh failure instead of clearing it."],"tags":["models","startup","config","upstream","empty-state"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}