santifer/career-ops · error · Error

HTTP

Error message

HTTP ${resp.status}

What it means

Thrown by loadFreeModels() in openrouter-runner.mjs when the OpenRouter models endpoint (`OPENROUTER_MODELS_URL`) returns a non-ok HTTP status. The function caches the free-model list, so this only fires on the first fetch of the session (or after cache invalidation). The raw status code is surfaced verbatim so callers can distinguish auth problems (401/402) from server errors (5xx).

Solutions

  1. Read the status: 401/402 means the key — verify OPENROUTER_API_KEY in .env matches a valid key at openrouter.ai/keys (no quotes/whitespace).
  2. On 429, wait and retry; the list request is rate-limited per account.
  3. On 5xx, check https://openrouter.statuspage.io or retry later — it is server-side.
  4. Confirm network/proxy access to openrouter.ai (try `curl -H "Authorization: Bearer $OPENROUTER_API_KEY" https://openrouter.ai/api/v1/models`).
  5. The wrapped catch at line 152 will rethrow with a hint (key vs no key) — use it to diagnose.

Example fix

// before
const resp = await fetch(OPENROUTER_MODELS_URL, {
  headers: { 'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}` }
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
// after
const resp = await fetch(OPENROUTER_MODELS_URL, {
  headers: { 'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}` }
});
if (!resp.ok) {
  if (resp.status === 401 || resp.status === 402) throw new Error(`OpenRouter auth failed (${resp.status}) — check OPENROUTER_API_KEY`);
  if (resp.status === 429) throw new Error('OpenRouter rate limit hit — retry shortly');
  throw new Error(`OpenRouter models endpoint returned HTTP ${resp.status}`);
}
Defensive patterns

Strategy: retry

Validate before calling

const key = process.env.OPENROUTER_API_KEY;
if (!key) throw new Error('OPENROUTER_API_KEY is not set');
const pre = await fetch('https://openrouter.ai/api/v1/models', { headers: { Authorization: `Bearer ${key}` } });
if (!pre.ok) throw new Error(`OpenRouter models endpoint unhealthy: HTTP ${pre.status}`);

Try / catch

try {
  models = await loadFreeModels();
} catch (err) {
  if (/HTTP 429/.test(err.message)) {
    await sleep(30_000);
    models = await loadFreeModels();
  } else {
    console.error('Model list load failed:', err.message);
    process.exit(1);
  }
}

Prevention

When it happens

Trigger: First call that triggers loadFreeModels() when GET to the OpenRouter models API returns 401 (bad API key), 402 (credits/billing gate), 429 (rate limit), or 5xx from OpenRouter.

Common situations: Expired or revoked OPENROUTER_API_KEY in .env; typo'd key with stray whitespace; OpenRouter incident/maintenance returning 503; hitting rate limits after many rapid evaluations.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/75550f5f54ca8315. Report an issue: GitHub.

Appendix: source

Thrown at openrouter-runner.mjs:123

// Models that failed permanently (403, timeout, persistent 429) — never retry
const blacklistedModels = new Set(loadPersistedBlacklist());
if (blacklistedModels.size > 0) {
  console.log(`[blacklist] Loaded ${blacklistedModels.size} pre-blacklisted model(s) from disk.`);
}
// 429 failure count per model — auto-blacklist after 3 consecutive 429s
const rateLimitCounts = {};

// ---------------------------------------------------------------------------
// Fetch free models from OpenRouter API
// ---------------------------------------------------------------------------
async function loadFreeModels() {
  if (freeModels !== null) return freeModels;

  try {
    const resp = await fetch(OPENROUTER_MODELS_URL, {
      headers: { 'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}` }
    });
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    const data = await resp.json();

    // A model is free when its prompt and completion pricing are both "0"
    const list = (data.data ?? [])
      .filter(m => {
        const p = m.pricing ?? {};
        return String(p.prompt) === '0' && String(p.completion) === '0';
      })
      .map(m => m.id);

    if (list.length === 0) throw new Error('No free models found in API response');

    // Sort by provider priority; within the same provider sort alphabetically
    function providerOf(id) { return id.split('/')[0]; }
    function priorityOf(id) {
      const idx = PROVIDER_PRIORITY.indexOf(providerOf(id));
      return idx === -1 ? PROVIDER_PRIORITY.length : idx;
    }

View on GitHub (pinned to aac998c7ed)