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

  1. Reproduce the exact request: curl -i -H "Authorization: Bearer $OPENAI_API_KEY" "$OPENAI_BASE_URL/models" and match the status code.
  2. 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).
  3. 401/403: rotate or re-issue the API token and confirm it belongs to the same provider as the base URL.
  4. 429: wait or reduce refresh frequency; the periodic model refresh will succeed later and the previous cache keeps serving.
  5. 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

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


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)