santifer/career-ops · error · Error
HTTP
Error message
HTTP ${resp.status}: ${t.slice(0, 120)} What it means
Thrown inside callOpenRouter()'s request loop when the chat-completions endpoint returns a non-ok HTTP status. Unlike the models-list check, this includes the first 120 characters of the response body, which usually contains OpenRouter's JSON error explaining the model/status. Each failed model in the rotation is typically blacklisted and the next candidate tried.
Solutions
- Read the 120-char body snippet in the message — it names the actual problem (auth, credits, model id, moderation).
- 401/402: fix OPENROUTER_API_KEY or add credits; 429: the rotation should try the next model — if all fail, slow down.
- 404: the model was removed from OpenRouter; let loadFreeModels refresh the list on a fresh run.
- 400: check the request body — some free models reject params like temperature or system role.
- Check OpenRouter status page on persistent 5xx.
Example fix
// before
if (!resp.ok) {
const t = await resp.text();
throw new Error(`HTTP ${resp.status}: ${t.slice(0, 120)}`);
}
// after
if (!resp.ok) {
const t = await resp.text();
let detail = t.slice(0, 120);
try { detail = JSON.parse(t)?.error?.message ?? detail; } catch {}
const err = new Error(`HTTP ${resp.status}: ${detail}`);
err.status = resp.status;
throw err; // caller can branch on 429 vs 401 vs 404
} Defensive patterns
Strategy: retry
Try / catch
try {
result = await callOpenRouter(systemPrompt, userMessage);
} catch (err) {
const m = /HTTP (\d{3})/.exec(err.message);
const status = m ? Number(m[1]) : null;
if (status === 429 || (status && status >= 500)) {
await sleep(backoff(attempt++)); // transient — retry; rotation blacklists the model
} else {
throw err; // 401/402/400 need human action
}
} Prevention
- Always read the 120-char body snippet — OpenRouter names the failing model and cause there.
- Retry 429/5xx automatically with backoff; never retry 401/402 blindly.
- Keep the model rotation healthy so a dead model is skipped automatically.
- Log status codes separately to spot per-model patterns.
When it happens
Trigger: POST to the completions endpoint returns 400 (malformed request/unsupported param for that model), 401/402 (key or credits), 404 (model id no longer exists), 429 (rate limited), or 5xx — with body text captured via resp.text().
Common situations: A free model was deprecated mid-session (404); per-model rate limits on free tier (429); provider upstream error surfaced as 502; moderation flagging the prompt (400).
Related errors
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/88d829cdf9e72fb0.
Report an issue: GitHub.
Appendix: source
Thrown at openrouter-runner.mjs:259
max_tokens: MAX_TOKENS,
});
const ctrl = new AbortController();
const timerId = setTimeout(() => ctrl.abort(), MODEL_TIMEOUT_MS);
try {
const resp = await fetch(OPENROUTER_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${key}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://github.com/career-ops-hq/career-ops',
'X-Title': 'career-ops',
},
body,
signal: ctrl.signal,
});
if (!resp.ok) {
const t = await resp.text();
throw new Error(`HTTP ${resp.status}: ${t.slice(0, 120)}`);
}
const data = await resp.json();
if (data.error) throw new Error(data.error.message);
const content = data.choices?.[0]?.message?.content ?? '';
if (!content) throw new Error('Empty response');
console.log('OK');
const usage = normalizeOpenAIUsage(data.usage);
return { content, usage };
} catch (e) {
if (e.name === 'AbortError') throw new Error(`Pinned model timed out after ${MODEL_TIMEOUT_MS / 1000}s`);
throw e;
} finally {
clearTimeout(timerId);
}
}
const models = await loadFreeModels();
let lastError;View on GitHub (pinned to aac998c7ed)