{"record":{"id":"f0bebf9dc64ed4c7","repo":"abhigyanpatwari/GitNexus","slug":"llm-endpoint-circuit-open-retry-in-math-ceil-er","errorCode":null,"errorMessage":"LLM endpoint circuit open: retry in ${Math.ceil(err.retryAfterMs / 1000)}s. ${err.message}","messagePattern":"LLM endpoint circuit open: retry in (.+?)s\\. (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"gitnexus/src/core/wiki/llm-client.ts","lineNumber":409,"sourceCode":"          ...authHeaders,\n        },\n        body: JSON.stringify(body),\n        // Request timeout is opt-in for wiki generation. Large local\n        // model runs can legitimately take well over a minute, so the\n        // default runtime path must not impose a hidden 60s ceiling.\n        signal:\n          config.requestTimeoutMs !== undefined\n            ? AbortSignal.timeout(config.requestTimeoutMs)\n            : undefined,\n      },\n      {\n        breakerKey: `wiki-llm-${new URL(url).host}`,\n        retry: { maxAttempts: config.maxAttempts ?? 3, baseDelayMs: 2_000, capDelayMs: 30_000 },\n      },\n    );\n  } catch (err) {\n    if (err instanceof CircuitOpenError) {\n      throw new Error(\n        `LLM endpoint circuit open: retry in ${Math.ceil(err.retryAfterMs / 1000)}s. ${err.message}`,\n      );\n    }\n    if (err instanceof ResilientFetchExhaustedError) {\n      const errorText = await err.response.text().catch(() => 'unknown error');\n      throw new Error(\n        `LLM API error (${err.response.status} after retries): ${errorText.slice(0, 500)}`,\n      );\n    }\n    if (config.requestTimeoutMs !== undefined && isTimeoutLikeError(err)) {\n      throw new Error(\n        `LLM request timed out after ${formatTimeoutDuration(config.requestTimeoutMs)}. ` +\n          'Increase --timeout or omit it to disable the request timeout.',\n      );\n    }\n    throw err;\n  }\n","sourceCodeStart":391,"sourceCodeEnd":427,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/gitnexus/src/core/wiki/llm-client.ts#L391-L427","documentation":"Thrown when callLLM() catches a CircuitOpenError from resilientFetch(). GitNexus wraps each LLM host in an in-process circuit breaker keyed 'wiki-llm-<host>'; after enough consecutive failures the breaker opens and fails fast instead of hammering a sick endpoint. The message surfaces the remaining cool-down (retryAfterMs rounded up to seconds) plus the underlying cause. This protects both your run and the upstream provider from a retry storm.","triggerScenarios":"Three or more consecutive failed LLM calls to the same host within the breaker window (e.g. repeated 5xx, repeated auth failures, or repeated timeouts). The next callLLM() short-circuits before any network request and you see 'circuit open: retry in Ns'. Reproducible by pointing at an endpoint with a bad API key and forcing several calls quickly.","commonSituations":"LLM provider regional outage; wrong API key returning 401/403 every attempt; sustained 429 rate limiting that exhausts retries; local model server (Ollama) crashed but baseUrl still points at it; CI run hammering the endpoint after maxAttempts=3 each.","solutions":["Wait the indicated cool-down (retryAfterMs) before retrying — the breaker auto-closes after it elapses.","Check the underlying err.message echoed in the text to fix the root cause (API key, model name, endpoint health).","Verify the provider status page and the endpoint reachability (curl the /models endpoint).","Restart a crashed local model server, or point baseUrl at a healthy host (different breaker key).","If the breaker opens during long-running generation, lower config.maxAttempts or add requestTimeoutMs to surface timeouts sooner rather than burning attempts."],"exampleFix":"// before\nfor (const p of prompts) {\n  await callLLM(p, config); // hammers a sick endpoint, trips breaker\n}\n\n// after: respect cool-down + jitter\nasync function callWithBreaker(p) {\n  try { return await callLLM(p, config); }\n  catch (e) {\n    const m = /retry in (\\d+)s/.exec(e.message);\n    if (m) await sleep((+m[1]) * 1000 + 500);\n    throw e;\n  }\n}","handlingStrategy":"retry","validationCode":"// No pre-call check exists; circuit state is internal. Probe endpoint health first:\nasync function endpointHealthy(baseUrl) {\n  try {\n    const r = await fetch(new URL('/models', baseUrl), { headers: { Authorization: 'Bearer ' + key } });\n    return r.ok || r.status === 404; // 404 = wrong path but reachable\n  } catch { return false; }\n}","typeGuard":"function isCircuitOpenError(e) {\n  return e instanceof Error && /circuit open: retry in \\d+s/.test(e.message);\n}\nfunction circuitRetrySeconds(e) {\n  const m = /retry in (\\d+)s/.exec(e.message || '');\n  return m ? +m[1] : null;\n}","tryCatchPattern":"try { return await callLLM(prompt, config); }\ncatch (e) {\n  if (isCircuitOpenError(e)) {\n    const s = circuitRetrySeconds(e);\n    if (s) await new Promise(r => setTimeout(r, s * 1000 + 500));\n    return await callLLM(prompt, config); // one retry after cool-down\n  }\n  throw e;\n}","preventionTips":["Fix auth/endpoint issues fast so consecutive failures never reach the breaker threshold.","Keep maxAttempts low (3) so each failed call does not widen the failure window.","Watch for the breaker message in logs and back off the whole job rather than spawning more calls."],"tags":["llm","network","resilience","circuit-breaker","retry","wiki"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}