Repository navigation
fix(server): diagnose Kitchen AI provider failures - #34
Merged
Merged
Conversation
Log safe upstream failure metadata on /api/ai-conversation so production provider_unavailable failures can be classified: status, reason, fallbackReason, timeoutSource, requestSizeBytes, primaryProvider, timeoutMs and a format-checked upstreamRequestId alongside the existing provider/attempt fields. Upstream codes are normalised to a short identifier pattern and request ids that fail the pattern are dropped. Upstream error bodies, keys, tokens, prompts and conversation content are never logged. No change to provider selection, models, fallback, timeouts, rate limits or the stream events returned to clients. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 of 6 tasks
1395490241lhj-code
marked this pull request as ready for review
October 21, 2026 02:19
…curate S1: /api/ai-conversation now extracts failure diagnostics with its own conversationFailureInfo helper instead of the shared getUpstreamAiErrorInfo. Every field is read through a guarded accessor (throwing getters and Proxy traps read as unreadable), non-string error codes are never stringified, and an outer try/catch falls back to a fixed category. A malformed upstream error can no longer turn a handled provider_unavailable into an unhandled rejection on the no-fallback, fallback-attempt or mid-stream paths. S2: the logged status is only an integer 100-599 the error itself carries (err.response.status or the SDK's err.status). Transport errors, SDK timeouts and mid-stream errors without a status omit it instead of logging a synthetic 502/504. timeoutSource still separates server_sdk_deadline from a real upstream 504. The request id is the first well-formed value of x-groq-id, x-request-id or x-trace-id (err.response.headers or the SDK's err.headers), then err.requestID. Malformed values are skipped. Only the error body's code/type fields are read, never its message text. Unchanged: getUpstreamAiErrorInfo and every other route, the fallback decision (isTransientProviderFailure), provider calls, timeouts, rate limits and the client NDJSON events. Ten new regression tests use the real openai SDK error classes. Eight of them fail on e6cc48c (4 route throws, 4 synthetic statuses) and pass with this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1395490241lhj-code
added a commit
that referenced
this pull request
Oct 22, 2026
Brings PR #31 (iOS review fixes) and PR #34 (Kitchen AI failure diagnostics) under the Phase 1a work. - test/ai-conversation-stream.test.mjs: both sides appended tests. Keep main's file whole (all 43 PR #34 tests, S1/S2 diagnostics included) and append this branch's servings test, rewritten for the protocol below. - src/server/services/ai-conversation.js: keep plannedServings on propose_add_recipe_to_tonight and propose_replace_planned_meal. This branch had removed it because the old client refused it; PR #31's interpreter now accepts 1-12 and refuses anything else, so the schema offers the optional field with that range in its description. The low-risk wording for add-to-tonight is kept: the client executes it after validation without a confirmation step. - The other shared iOS files merged without conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
P0 diagnostics for Kitchen AI. Render production logs show
/api/ai-conversationfailures that come back as HTTP 200 with an in-streamprovider_unavailableerror. Today'sai_conversation_failedlog line carries onlyresultCode: provider_unavailable, so a 400, 413, 429, timeout and 5xx all look the same in production.This PR only adds failure diagnostic metadata to the existing
ai_conversation_completed/ai_conversation_failedlog lines.27c9aade6cc48cmain@1a3d3b9(PR #35); no functional change to this PR966dc97statusaccurate (S1/S2 from review, see below)Fields now logged
provider,attemptprimaryProvidertimeoutMsrequestSizeBytesJSON.stringify(messages); see the note below the tablefallbackReasonstatuserr.response.status, else the SDK'serr.status), integer 100–599. Omitted for transport errors, SDK timeouts and mid-stream errors that have no HTTP status. No synthetic 502/504reasontimeoutSourceserver_sdk_deadline(our SDK deadline;statusis omitted),upstream_response(a real upstream 504), otherwise omittedupstreamRequestId^[A-Za-z0-9_-]{8,128}$; see the order below. Malformed values are skipped, never logged. Omitted when none matchesrequestSizeBytescountsmessagesonly. It excludes tool definitions and model parameters, so it is not the full provider request size.Error codes (
reason,fallbackReason) are normalized like this:^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$, or a finite number, is kept as is;unrecognized, including objects, arrays, symbols, BigInt and unreadable fields;ECONNABORTED;upstream_error.upstreamRequestIdcandidates are checked in this order:x-groq-id,x-request-idandx-trace-id, taken fromerr.response.headersor the SDK'serr.headers;err.requestID.All field names were already in the logger allowlist (
src/server/observability/logger.jsis unchanged).What is read, and what is not
The diagnostics read only these fields of the thrown error:
statusandresponse.status;codeandtype, on the error itself and on its body (error/response.data.error), plusresponse.data.code;constructor.name;requestID.They never read:
message/detail/ body text;Authorizationvalues.Limitation (format check, not an allowlist). Error codes and request ids are third-party values. They are checked for shape only: short, anchored, bounded character classes. They are not checked against a list of known values. So a short identifier-shaped string that an upstream returns as its code or request id would be logged verbatim, including something that looks like a key. In the tested scenarios, a fake key in the error message, the prompt, inventory text and the upstream message never reach stdout. That is not a guarantee against an arbitrarily hostile upstream code.
Review fixes in
966dc97catchcalled the sharedgetUpstreamAiErrorInfo(err). That helperString()s arbitrary codes and reads properties without guards. An error whose code JSON-parses to an object with a non-callabletoString, or a throwing getter, made the async handler reject: Express 4 does not catch that, andserver.jshas nounhandledRejectionhandler. The paths affected were no-fallback, the fallback attempt itself, and mid-stream.conversationFailureInfo(err). It reads every field through a guarded accessor, so throwing getters and Proxy traps read as unreadable. It never stringifies non-primitive codes. An outertry/catchreturns{ reason: 'unrecognized' }as a last resort.statusis only what the error carries.What does not change
isTransientProviderFailure,visibleEventsgating), timeouts, rate limits, auth and business logicThe only control-flow edit is in the existing
catchblock. It storesconversationFailureInfo(err)before the samecontinue/ error-event path that runs today.Known limitations, tracked separately
isTransientProviderFailurestill runsString(err.code)before any event is written. The same malformed code still makes that path reject, exactly as onmainbefore this PR. Fixing it touches fallback determination and is out of scope here.fallbackReason. Its status, timeout source and request id are not logged.Production context (not resolved by this PR)
Render service
kitchenmanager, 2026-10-20 15:43–15:44 America/Toronto: 13/api/ai-conversationrequests, 9 succeeded and 4 failed. Groq failed twice (54 ms, 58 ms) and Gemini failed twice (171 ms, 178 ms), right after successful model calls. Every failure returned HTTP 200 with in-streamprovider_unavailable.The root cause is not confirmed. Leading hypotheses are an incompatible continuation request after multi-step tool calls, or an immediate upstream rejection. This PR exists to tell those apart. Separately,
/api/ai-chatlogs show repeated Gemini upstream HTTP 503s. That should not be assumed to be the same cause.Independence
main@1a3d3b9via "Update branch". It does not depend on PR Kitchen AI Phase 1a: tonight recipe recommendations with deterministic kitchen facts #32 or PR fix: improve Kitchen AI reliability and conversation scrolling #33.20a0de2on PR fix: improve Kitchen AI reliability and conversation scrolling #33's branch, because this PR adds the S1/S2 hardening. PR fix: improve Kitchen AI reliability and conversation scrolling #33's duplicate diagnostics commit has to be reconciled separately after this PR lands.Test plan
Regression tests were written first. On
e6cc48c8 of them fail: 4 route throws (S1) and 4 synthetic 502/504 statuses (S2). With966dc97all of them pass. Locally on Node v26.12.0, with dependencies installed from this branch's lockfile:test/ai-conversation-stream.test.mjs: 43/43. That is 14 diagnostics tests, 10 of them new, using the real openai SDK error classes.ai-server-routes124/124,ai-conversation-lifecycle2/2,ai-conversation-turn-limit21/21full
npm test: 2013/2013npm audit --omit=dev --audit-level=high: 0 vulnerabilities;git diff --cached --checkcleanBehaviour diff against
mainacross 17 scenarios:HTTP status, NDJSON events, provider call count and throw/no-throw are identical to
mainin all 17. No sensitive test string appears in the logs.The extracted helper was fuzzed with 31 hostile inputs (proxies, a revoked proxy, throwing getters, symbol/BigInt/object codes): 0 throws, and every field value was safe.
Two independent read-only reviews: no blockers. The second review's request-id and comment findings are addressed in
966dc97.CI run 39534978344 on
966dc97, which checked outee9a747, the merge of966dc97into1a3d3b9:test (22): Node 22.23.3, 2013/2013test (24): Node 24.22.0, 2013/2013npm audit, with 0 vulnerabilitiesAfter an approved deploy: confirm the new fields show up on the next production
ai_conversation_failedline🤖 Generated with Claude Code