Skip to content

fix(ai): surface real HTTP error responses instead of "No response received"#178

Open
ImIvanGil wants to merge 1 commit into
Axenide:mainfrom
ImIvanGil:fix/ai-panel-surface-http-errors
Open

fix(ai): surface real HTTP error responses instead of "No response received"#178
ImIvanGil wants to merge 1 commit into
Axenide:mainfrom
ImIvanGil:fix/ai-panel-surface-http-errors

Conversation

@ImIvanGil
Copy link
Copy Markdown

Problem

When the AI panel's request fails for any reason that returns a small non-streamed JSON error body (vs. a streamed SSE response), users see the generic message:

"No response received from the API."

…regardless of what the actual error is. This is the most common failure mode and the most opaque.

Concrete examples of errors that get hidden:

Real cause What server returns What user sees
Invalid API key {"error":{"message":"Incorrect API key provided: sk-***"}} "No response received"
Wrong model name {"error":{"message":"The model 'foo' does not exist"}} "No response received"
Invalid temperature for Kimi thinking {"error":{"message":"invalid temperature: only 1 is allowed for this model"}} "No response received"
Quota / rate limit {"error":{"message":"You exceeded your current quota..."}} "No response received"
Wrong endpoint URL Any non-SSE error body "No response received"

This makes debugging custom providers, network issues, and key misconfiguration enormously harder than it needs to be. The errors are perfectly actionable — they're just being dropped.

Why it happens

curlProcess.stdout is a SplitParser that yields lines to currentStrategy.parseStreamChunk. The stream parser correctly ignores anything that isn't data: ..., so a JSON error body is silently dropped. When curl exits 0 (no network error) and responseBuffer is empty, the existing branch in onExited shows the generic placeholder.

The error body is in stdout the whole time — it just never goes anywhere.

Fix

Capture stdout into a side buffer on curlProcess and, in the empty-buffer branch, try to parse it as a typical OpenAI-style error envelope before falling back to the generic message.

 Process {
     id: curlProcess
+    property string rawStdoutBuffer: ""

     stdout: SplitParser {
         onRead: data => {
+            curlProcess.rawStdoutBuffer += data + "\n";
             let result = root.currentStrategy.parseStreamChunk(data);
             ...
+function extractApiError(raw) {
+    if (!raw || !raw.trim()) return "";
+    try {
+        let json = JSON.parse(raw.trim());
+        if (json && json.error) {
+            if (typeof json.error === "string") return json.error;
+            if (json.error.message) return json.error.message;
+        }
+    } catch (e) {}
+    return "";
+}

 onExited: exitCode => {
     ...
     if (root.responseBuffer === "" && root.currentChat.length > 0) {
         ...
+        let apiErr = curlProcess.extractApiError(curlProcess.rawStdoutBuffer);
+        newChat[newChat.length - 1].content = apiErr
+            ? "API Error: " + apiErr
+            : "No response received from the API.";
         ...
     }

Buffer is reset alongside responseBuffer at the end of each request.

Behavior

  • Happy path (streamed response): unchanged. responseBuffer fills, error branch never runs.
  • Streamed error chunk ({"error":...} inside data: ...): unchanged — still handled by parseStreamChunk via result.error.
  • Network failure (curl exitCode != 0): unchanged — shows existing "Network Request Failed: ..." from stderr.
  • JSON error body in stdout with curl exit=0: new path — extracts and surfaces the real error message.
  • Empty stdout / unparseable stdout: falls back to existing "No response received" placeholder (no regression).

Tested

Reproduced and verified each of the four examples in the table above. Before: every one of them showed "No response received". After: every one shows the real provider message prefixed with "API Error: ".

Diff stats

modules/services/Ai.qml | 35 +++++++++++++++++++++++++++--------
1 file changed, 27 insertions(+), 8 deletions(-)

Related

5th in the series of Ambxst fixes. Related PRs:

This one is independent of the other AI PRs but massively improves the debugging experience for all of them. Without it, users debugging custom providers / thinking models / new API keys are flying blind. With it, they get the provider's own error message in plain text.

…ceived"

When an OpenAI-compatible API rejects a request with a small, non-streamed
JSON error body (e.g. `{"error":{"message":"invalid temperature","type":"..."}}`),
the SSE-style stream parser correctly ignores it (no `data:` prefix). The
existing onExited handler then falls into the "responseBuffer is empty"
branch and shows the generic placeholder **"No response received from the
API."** — discarding the perfectly actionable error message that's sitting
right there in stdout.

This left users with no signal at all when:
- The API key is invalid (most providers return 401 with a JSON body)
- The model name doesn't exist (404 with `model not found` JSON)
- The request body has invalid params (400 with explanatory JSON)
- Rate limits / quota errors (429 with `quota exceeded` JSON)
- Endpoint URL is wrong but DNS-resolvable (returns some HTML/JSON)

Fix: capture the full stdout from the curl SplitParser into a buffer on the
Process, then in onExited's "empty responseBuffer" branch, try to parse
that captured stdout as `{"error":{"message":"..."}}` and surface the
inner message. Falls through to the generic "No response received"
placeholder when parsing fails (preserves backward compat for actual
silent-failure cases).

This is purely additive — no behavior change for the happy path, no change
when curl itself fails (exitCode != 0). Only changes the empty-buffer
branch to be more informative.

Tested with real-world failures:
- Wrong temperature for Kimi thinking model → user sees
  `API Error: invalid temperature: only 1 is allowed for this model`
- Invalid API key for OpenAI → user sees
  `API Error: Incorrect API key provided: sk-***`
- Non-existent model name → user sees
  `API Error: The model 'foo' does not exist`

Before the patch, all three showed the same useless "No response received".
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