Stream errors now surface with structured codes instead of silent failure
When the LLM API returned an error during streaming, the chat UI would silently stop -- spinner gone, no message. brauliogusmao fixed the silent failure and then upgraded it to a typed error contract with six classification codes.
ed833e17 fixes two bugs at once. On the backend, chat.ts and projectChat.ts were emitting { type: "error", message: "Stream error" } regardless of what actually failed. The fix adds extractErrorMessage(), which walks the nested Anthropic/OpenAI SDK error structure (err.error.error.message -> err.error.message -> err.message) to get the real underlying text. On the frontend, useAssistantChat.ts was checking for data.type === "error" but not actually throwing -- the error fell through the SSE parse loop silently. Adding throw new Error(data.message) at that point lets the existing catch block display it in the assistant message balloon.
4896b8c extends this to a structured contract. extractErrorMessage() becomes extractError(), returning { code: string; message: string }. The backend pattern-matches the raw message text against six known categories: insufficient_credits, invalid_api_key, rate_limit, context_length, timeout, overloaded. The SSE event now carries both code and message, and the frontend translates by code with a fallback to the raw message and then a generic string. There is also a separate bug fixed here: a throw inside an inner try/catch inside the SSE parse loop was being swallowed. The fix uses a streamError variable set inside the loop and checked after it exits.
The extractError function is duplicated across chat.ts and projectChat.ts rather than shared. Minor, but worth noting if you maintain both files.
The classification approach is string matching on SDK error message text, which is brittle -- Anthropic and OpenAI can reword their error messages. If you adopt this, treat the substring matches as a fallback and prefer matching on SDK error types or HTTP status codes where those are available.
Spotted something wrong? Or know the PR text has fresher detail than the writeup above?