Skip to content

fix(ai): a total LLM outage was reporting itself as success - #151

Merged
github-actions[bot] merged 1 commit into
mainfrom
fix/llm-outage-is-visible
Aug 28, 2026
Merged

fix(ai): a total LLM outage was reporting itself as success#151
github-actions[bot] merged 1 commit into
mainfrom
fix/llm-outage-is-visible

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

Found by driving the Lex demo in production. The chat answered "I'm sorry, I encountered an error processing your request." Behind it: every AI feature on the site is down, and has been invisible.

The outage

The box has an invalid Groq key, no other provider key, and no Ollama:

Groq API error: 401 (model openai/gpt-oss-120b)  {"code":"invalid_api_key"}
Groq API error: 401 (model openai/gpt-oss-20b)   {"code":"invalid_api_key"}

Every endpoint answered HTTP 200 anyway

/api/demo/chat          {"success":true,"data":{"response":""}}
/api/quick-chat         {"success":true,"data":{"response":"I'm having a moment..."}}
/api/professional-chat  same
/api/health             {"success":true,"data":{"status":"healthy"}}

Green everywhere, product dead.

demo/chat was the worst of the three: its catch returns content: context, and a bot demo has no context — so a failed generation became a successful empty string.

What changed

  • demo/chat throws LLMUnavailableError when there's nothing to fall back on; the knowledge-base demo still serves its retrieved passage
  • quick-chat and professional-chat return 503 LLM_UNAVAILABLE instead of apologising with 200
  • lib/llm-health.ts records what generations actually did. /api/health reports it and returns 503 once the chain is consistently failing — so "healthy" stops meaning "the database answered"

A test was pinning the bug

professional-chat.test.ts asserted 200, the word "moment", and provider: 'fallback'. It now asserts 503 LLM_UNAVAILABLE. A test that pins broken behaviour is worse than no test.

Never-twice

tests/__tests__/lib/llm-health.test.ts covers the tracker's state machine and asserts at source level that no chat route answers an LLM failure with success, and that health consults the LLM chain.

⚠️ This makes the outage visible. It does not fix it.

The actual fix needs a valid GROQ_API_KEY (or OPENROUTER_API_KEY) in /opt/botsmann/shared/.env on the box. Until then these endpoints will correctly return 503 and /api/health will report degraded/down.

Verify

format:check, lint, typecheck, 268 tests, build — all green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HVwg8DKHQktxJuHeLM3xpG

Found by driving the Lex demo in production: the chat answered "I'm sorry,
I encountered an error processing your request." Behind it, every AI feature
on the site is down and has been invisible.

The box has an invalid Groq key, no other provider key, and no Ollama:

  Groq API error: 401 (model openai/gpt-oss-120b)  {"code":"invalid_api_key"}
  Groq API error: 401 (model openai/gpt-oss-20b)   {"code":"invalid_api_key"}

Every endpoint answered HTTP 200 anyway:

  /api/demo/chat          {"success":true,"data":{"response":""}}
  /api/quick-chat         {"success":true,"data":{"response":"I'm having a moment..."}}
  /api/professional-chat  same
  /api/health             {"success":true,"data":{"status":"healthy"}}

Green everywhere, product dead. demo/chat was the worst of the three: its
catch returns `content: context`, and a bot demo has no context, so a failed
generation became a successful empty string.

  - demo/chat throws LLMUnavailableError when there is nothing to fall back
    on; the knowledge-base demo still serves its retrieved passage
  - quick-chat and professional-chat return 503 LLM_UNAVAILABLE instead of
    apologising with 200
  - lib/llm-health.ts records what generations actually did; /api/health
    reports it and returns 503 once the chain is consistently failing, so
    "healthy" stops meaning "the database answered"

tests/__tests__/api/professional-chat.test.ts asserted the bug -- 200, the
word "moment", provider 'fallback'. It now asserts 503 LLM_UNAVAILABLE. A
test that pins broken behaviour is worse than no test.

Never-twice: tests/__tests__/lib/llm-health.test.ts covers the tracker's
state machine and asserts at source level that no chat route answers an LLM
failure with success, and that health consults the LLM chain.

This makes the outage visible. It does NOT fix it -- that needs a valid
GROQ_API_KEY (or OPENROUTER_API_KEY) in /opt/botsmann/shared/.env on the box.
Until then these endpoints will correctly return 503.

verify: format, lint, typecheck, 268 tests, build -- all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVwg8DKHQktxJuHeLM3xpG
@github-actions
github-actions Bot merged commit eb29bc5 into main Aug 28, 2026
1 check passed
@github-actions
github-actions Bot deleted the fix/llm-outage-is-visible branch August 28, 2026 11:57
github-actions Bot pushed a commit that referenced this pull request Aug 28, 2026
…#153)

#151 made /api/health return 503 whenever the LLM chain was down. That is
right for alerting and wrong for liveness: restarting botsmann does not
repair an expired Groq key, so the only effect of failing there is to get a
healthy process killed, and to fail deploy gates on a problem no deploy
caused. That is the failure class that teaches people to bypass the gate.

  GET /api/health            liveness -- 200 while the process serves and the
                             database answers. The body still carries
                             status: healthy | degraded | down and the full
                             llm block, so the outage stays visible.
  GET /api/health?strict=1   readiness -- 503 once the chain is consistently
                             failing. Point alerting here.

'unknown' (nothing has exercised the chain since this process started) is not
evidence of a problem, so it reports healthy rather than degraded.

The deploy gate was never at risk -- selfhost-deploy.yml curls the homepage,
not this endpoint -- but the shape was wrong regardless, and the guidance in
CLAUDE.md does point a post-deploy check at /api/health.

The guard now asserts the 503-on-LLM path stays behind `strict`, so this
cannot quietly become unconditional again.

verify: format, lint, typecheck, 269 tests, build -- all green.


Claude-Session: https://claude.ai/code/session_01HVwg8DKHQktxJuHeLM3xpG

Co-authored-by: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant