From 097e30a71a017f9fbf2ff54ba1c889489abc3a5c Mon Sep 17 00:00:00 2001 From: ImIvanGil Date: Tue, 12 May 2026 18:28:07 -0600 Subject: [PATCH] fix(ai): surface real HTTP error responses instead of "No response received" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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". --- modules/services/Ai.qml | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/modules/services/Ai.qml b/modules/services/Ai.qml index 5e540dc2..fed34e94 100644 --- a/modules/services/Ai.qml +++ b/modules/services/Ai.qml @@ -494,9 +494,16 @@ Singleton { Process { id: curlProcess + // Captures the full stdout when streaming yields nothing — lets us + // surface real HTTP error JSON bodies instead of the generic + // "No response received" placeholder. + property string rawStdoutBuffer: "" + // Use SplitParser for streaming — emits onRead per line stdout: SplitParser { onRead: data => { + curlProcess.rawStdoutBuffer += data + "\n"; + let result = root.currentStrategy.parseStreamChunk(data); if (result.error) { @@ -522,18 +529,41 @@ Singleton { id: curlStderr } + // Try to extract the real error message from a non-streamed HTTP error body. + // Many providers respond to invalid requests with a small JSON like + // {"error":{"message":"...","type":"..."}} + // which the SSE parser correctly ignores (no "data:" prefix) — but that + // leaves the user staring at "No response received from the API" when + // there IS a perfectly clear error sitting in stdout. + 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) { + // Not JSON — fall through; raw body is not informative on its own. + } + return ""; + } + onExited: exitCode => { root.isLoading = false; if (exitCode === 0) { // Check if we got any content during streaming if (root.responseBuffer === "" && root.currentChat.length > 0) { - // No streaming data received — might be non-streaming response or error - // The last message is our placeholder, leave as is let lastMsg = root.currentChat[root.currentChat.length - 1]; if (!lastMsg.content) { let newChat = Array.from(root.currentChat); - newChat[newChat.length - 1].content = "No response received from the API."; + // Try to surface a real API error from the captured stdout + // before falling back to the generic "no response" message. + let apiErr = curlProcess.extractApiError(curlProcess.rawStdoutBuffer); + newChat[newChat.length - 1].content = apiErr + ? "API Error: " + apiErr + : "No response received from the API."; root.currentChat = newChat; } } @@ -551,6 +581,7 @@ Singleton { } root.responseBuffer = ""; + curlProcess.rawStdoutBuffer = ""; } }