ruvnet/ruflo · error
Failed to fetch /models
Error message
Failed to fetch ${baseURL}/models: ${response.status} ${response.statusText} What it means
Generic non-OK response from the upstream model-list fetch in buildModels(). Covers every failure other than the '401-without-token' case: 401 with a token that was rejected, 403, 404, 429, and 5xx. The message embeds the status code and status text so the operator can see exactly what the gateway replied.
Solutions
- Reproduce the exact request: curl -i -H "Authorization: Bearer $OPENAI_API_KEY" "$OPENAI_BASE_URL/models" and match the status code.
- 404: fix the base URL — most OpenAI-compatible servers expose /models only under the /v1 prefix (or already include it, don't add it twice).
- 401/403: rotate or re-issue the API token and confirm it belongs to the same provider as the base URL.
- 429: wait or reduce refresh frequency; the periodic model refresh will succeed later and the previous cache keeps serving.
- 5xx: check the provider status page and retry after the incident.
Example fix
# before (404 — missing /v1) OPENAI_BASE_URL=https://router.huggingface.co # after OPENAI_BASE_URL=https://router.huggingface.co/v1
Defensive patterns
Strategy: retry
Validate before calling
const url = `${OPENAI_BASE_URL}/models`; // sanity-check shape before fetching
new URL(url); // throws on malformed base
if (!/\/v1\/?$/.test(url)) logger.warn("base URL may be missing the /v1 prefix"); Try / catch
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await buildModels();
break;
} catch (err) {
const msg = String(err);
if (msg.includes(" 429 ") || /\b5\d\d\b/.test(msg)) {
await delay(2 ** attempt * 1000); // transient — retry with backoff
continue;
}
throw err; // 401/403/404 are permanent — fix config instead
}
} Prevention
- Validate the base URL and token with a preflight curl before boot.
- Retry only transient statuses (429, 5xx) with exponential backoff; fail fast on 401/403/404.
- Cache the last good model list so transient upstream failures don't degrade service.
When it happens
Trigger: GET ${OPENAI_BASE_URL}/models returns a non-2xx status: 401/403 with an invalid or expired token; 404 from a base URL missing or adding the /v1 suffix; 429 from rate limiting; 500-503 during provider incidents.
Common situations: Wrong base URL path (https://host instead of https://host/v1, or doubling /v1); expired HF token; gateway rate limits unauthenticated or bursty clients; temporary upstream outage during a scheduled refresh.
Understand the failure class
Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.
Related errors
- meta-llm
- Failed to fetch manifest from
- Failed to get analytics
- Failed to get bulk ratings
- Failed to get ratings
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/2b3f02a1d870d7d5.
Report an issue: GitHub.
Appendix: source
Thrown at ruflo/src/ruvocal/src/lib/server/models.ts:325
logger.info({ baseURL }, "[models] Using OpenAI-compatible base URL");
// Canonical auth token is OPENAI_API_KEY; keep HF_TOKEN as legacy alias
const authToken = config.OPENAI_API_KEY || config.HF_TOKEN;
// Use auth token from the start if available to avoid rate limiting issues
// Some APIs rate-limit unauthenticated requests more aggressively
const response = await fetch(`${baseURL}/models`, {
headers: authToken ? { Authorization: `Bearer ${authToken}` } : undefined,
});
logger.info({ status: response.status }, "[models] First fetch status");
if (!response.ok && response.status === 401 && !authToken) {
// If we get 401 and didn't have a token, there's nothing we can do
throw new Error(
`Failed to fetch ${baseURL}/models: ${response.status} ${response.statusText} (no auth token available)`
);
}
if (!response.ok) {
throw new Error(
`Failed to fetch ${baseURL}/models: ${response.status} ${response.statusText}`
);
}
const json = await response.json();
logger.info({ keys: Object.keys(json || {}) }, "[models] Response keys");
const parsed = listSchema.parse(json);
logger.info({ count: parsed.data.length }, "[models] Parsed models count");
let modelsRaw = parsed.data.map((m) => {
let logoUrl: string | undefined = undefined;
if (isHFRouter && m.id.includes("/")) {
const org = m.id.split("/")[0];
logoUrl = `https://huggingface.co/api/avatars/${encodeURIComponent(org)}`;
}
const inputModalities = (m.architecture?.input_modalities ?? []).map((modality) =>
modality.toLowerCase()View on GitHub (pinned to fa13ee4ad6)