AiResponseEnvelope.h header

#include <ew/app/AiResponseEnvelope.h>

Namespace ew::app

Functions

std::optional< QString > ew::app::ai::apiErrorMessage(const QJsonObject &root)

The error an API response root reports, or nullopt when it reports none.

The message may be an EMPTY string: the caller knows which model it asked and supplies its own wording, rather than this returning prose that would name the wrong one. A present but null or empty "error" is the idiomatic "no error" shape some gateways include alongside a valid result, so it must read as nullopt and never discard that result. A bare string under "error" is accepted beside the documented {"message": "..."} object.

std::optional< QJsonObject > ew::app::ai::apiResponseRoot(const QString &json)

The response object inside an OpenAI-compatible API reply json, or nullopt when the body is not JSON that carries one.

The obvious shape is a bare {...}, and that is what OpenAI itself sends. But the endpoint here is whatever the user configured, and a gateway may wrap the reply in a single-element ARRAY: Google AI Studio's /v1beta/openai/chat/completions answers an error with [{"error":{"code":400,"message":"Please pass a valid API key","status":"INVALID_ARGUMENT"}}]. Read as an object that yields nothing, so the provider's real message – a bad key, an unknown model, an exhausted quota – was replaced by a bare "malformed response", which tells the user nothing about what to change. The first object in the array is taken (there is only ever one reply), so both shapes read the same.

Measured against that host on 2026-07-29: only chat/completions wraps; its embeddings and images/generations answer with a bare object, and it has no audio/speech at all. That split is the argument for handling the shape in ONE place rather than per parser – which of a provider's paths wrap is a detail no response parser should have to know, and all four parsers had independently rejected the array form.

Pure and unit-testable.

QString ew::app::ai::unusableResponseError(const QString &apiMessage, const QString &body, bool transportFailed, const QString &transportError)

Which message to report for a request that came back unusable: apiMessage, what the response parser made of body, or transportError, Qt's description of the HTTP failure (empty and transportFailed false when the request itself succeeded).

Prefers the provider's own message whenever body carried a readable response object – it explains the cause far better than a status line. Falls back to the transport error when it did not: an HTML error page, or an empty 404, parses to wording about the response being "malformed", which describes our parser rather than the user's problem and names neither the status nor the URL. A body that reads fine over a request that ALSO failed keeps the API message, since the provider is then explaining its own refusal.

This replaces a guard that tested apiMessage.isEmpty(). No parser ever leaves the message empty on failure, so that fallback could never run and the status was always thrown away.