ruvnet/ruflo · critical · Error

Failed to fetch ${baseURL}/models: ${response.status} ${resp

Error message

Failed to fetch ${baseURL}/models: ${response.status} ${response.statusText}

What it means

Thrown by buildModels when GET ${baseURL}/models returns any non-ok status that is not the '401 with no token' case. This is the catch-all for 403 Forbidden, 404 Not Found, 429 Too Many Requests, 5xx server errors, and other HTTP failures. The message includes the status code, status text, and base URL so the operator can localize the problem.

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 6b01dc5a68)

Solutions

  1. curl -i -H "Authorization: Bearer $OPENAI_API_KEY" $OPENAI_BASE_URL/models to reproduce and inspect the response.
  2. For 404: verify the base URL ends with the OpenAI-compatible path (commonly /v1) and that /models exists.
  3. For 403: confirm the token has the required scope/entitlement for the gateway.
  4. For 429/5xx: wait and retry; if recurring, contact the gateway provider or raise limits.
  5. Strip trailing slashes from OPENAI_BASE_URL (the code already strips one trailing slash, but verify the URL shape).

Example fix

# before
OPENAI_BASE_URL=https://router.huggingface.co

# after (add /v1)
OPENAI_BASE_URL=https://router.huggingface.co/v1
Defensive patterns

Strategy: retry

Validate before calling

async function probeModelsEndpoint(baseURL: string, token?: string) {
  const r = await fetch(`${baseURL.replace(/\/$/, "")}/models`, {
    headers: token ? { Authorization: `Bearer ${token}` } : undefined,
  });
  return { ok: r.ok, status: r.status, statusText: r.statusText };
}

Type guard

function isModelsFetchFailure(e: unknown, statusRe?: RegExp): e is Error {
  if (!(e instanceof Error)) return false;
  if (!/Failed to fetch .*\/models:/.test(e.message)) return false;
  return statusRe ? statusRe.test(e.message) : true;
}

Try / catch

try { await rebuildModels(); }
catch (e) {
  if (e instanceof Error && /Failed to fetch .*\/models/.test(e.message)) {
    const m = /:(\d{3})\s/.exec(e.message);
    const status = m ? Number(m[1]) : 0;
    if (status === 429 || status >= 500) {
      await new Promise((r) => setTimeout(r, 5000));
      return rebuildModels(); // one retry
    }
  }
  throw e;
}

Prevention

When it happens

Trigger: OPENAI_BASE_URL points at a URL whose /models path returns 403 (forbidden/wrong key scope), 404 (wrong base path), 429 (rate limited at boot), 500/502/503 (upstream incident), or a redirect that resolves to a non-ok response. Also thrown when a valid token is present but the gateway still rejects (e.g. 403 with a wrong-scope token).

Common situations: Wrong base URL (missing /v1 suffix, typo, pointing at a non-OpenAI API); token with insufficient scope (403); gateway outage (5xx); aggressive boot-time rate limiting (429) when many instances start at once; reverse proxy returning 502 because the upstream is down; base URL with a trailing slash or path that 404s on /models.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/2b3f02a1d870d7d5. Report an issue: GitHub.