{"record":{"id":"fdf0f0964e31b008","repo":"santifer/career-ops","slug":"timeout-after-model-timeout-ms-1000-s","errorCode":null,"errorMessage":"Timeout after ${MODEL_TIMEOUT_MS / 1000}s","messagePattern":"Timeout after (.+?)s","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"openrouter-runner.mjs","lineNumber":324,"sourceCode":"      try {\n        const resp = await fetch(OPENROUTER_API_URL, {\n          method: 'POST',\n          headers: {\n            'Authorization': `Bearer ${key}`,\n            'Content-Type':  'application/json',\n            'HTTP-Referer':  'https://github.com/career-ops-hq/career-ops',\n            'X-Title':       'career-ops',\n          },\n          body,\n          signal: controller.signal,\n        });\n        if (!resp.ok) {\n          const t = await resp.text();\n          throw new Error(`HTTP ${resp.status}: ${t.slice(0, 120)}`);\n        }\n        data = await resp.json();\n      } catch (e) {\n        if (e.name === 'AbortError') throw new Error(`Timeout after ${MODEL_TIMEOUT_MS / 1000}s`);\n        throw e;\n      } finally {\n        clearTimeout(timerId);\n      }\n      if (data.error) throw new Error(data.error.message);\n\n      const content = data.choices?.[0]?.message?.content ?? '';\n      if (!content) throw new Error('Empty response');\n\n      const usage = normalizeOpenAIUsage(data.usage);\n\n      modelIndex = (modelIndex + attempt + 1) % active.length;\n      console.log('OK');\n      return { content, usage };\n\n    } catch (e) {\n      lastError = e;\n      const msg = e.message.split('\\n')[0];","sourceCodeStart":306,"sourceCodeEnd":342,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/openrouter-runner.mjs#L306-L342","documentation":"Each model attempt is wrapped in an AbortController with a timer of MODEL_TIMEOUT_MS; if the fetch is aborted because the timer fired, the AbortError is converted into this clearer error. It means the OpenRouter model did not return a response within the configured per-model timeout.","triggerScenarios":"A callOpenRouter request whose fetch neither resolves nor rejects before MODEL_TIMEOUT_MS elapses — slow/overloaded free model, network stall, or a hung streaming connection.","commonSituations":"Free models under heavy load queueing requests for minutes; no internet or DNS hang; MODEL_TIMEOUT_MS set too low for long generations (large max_tokens).","solutions":["Raise MODEL_TIMEOUT_MS to a value that accommodates slow free models (e.g. 60–120s)","Rely on rotation: the runner tries the next model; verify timeout errors actually lead to blacklisting/rotation","Check network connectivity and any proxy/firewall stalling outbound HTTPS to openrouter.ai","Reduce max_tokens/prompt size so generation finishes within the timeout","Implement an explicit retry with backoff for timeout failures, which are often transient"],"exampleFix":"// before\nif (e.name === 'AbortError') throw new Error(`Timeout after ${MODEL_TIMEOUT_MS / 1000}s`);\n// after\nif (e.name === 'AbortError') {\n  if (attempt < active.length - 1) { console.log(`timeout after ${MODEL_TIMEOUT_MS / 1000}s, next model`); continue; }\n  throw new Error(`Timeout after ${MODEL_TIMEOUT_MS / 1000}s`);\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  return await callOpenRouter(prompt);\n} catch (e) {\n  if (/^Timeout after/.test(e.message)) {\n    // transient: retry with next model / backoff\n    return withNextModelOrThrow();\n  }\n  throw e;\n}","preventionTips":["Set MODEL_TIMEOUT_MS generously for slow free models (60s+)","Treat timeouts as transient and rotate models rather than aborting the job","Keep prompts/max_tokens modest so generations finish in time","Monitor network path to openrouter.ai (DNS, proxy) in CI environments"],"tags":["openrouter","timeout","network","abort-controller"],"backgroundTag":"request-timeout","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}