Compare commits

..

1 Commits

Author SHA1 Message Date
37218d2e07 fix(ai): bound response parsing and harden AUTO fallback errors
All checks were successful
CI Code / Check spelling (pull_request) Successful in 16s
CI Code / Check coding style (pull_request) Successful in 26s
CI Code / Code Coverage (pull_request) Successful in 9m12s
CI Code / Linux (ubuntu) (pull_request) Successful in 9m18s
CI Code / Linux (debian) (pull_request) Successful in 9m29s
CI Code / Linux (arch) (pull_request) Successful in 13m13s
- bound _parse_responses scans to the output_text part's object and to
  the content array, so a later sibling item's "text" (e.g. a reasoning
  summary) can never be returned as the assistant reply
- do not retry the other flavour on a curl timeout: the request likely
  reached the server and may still be generating, so a re-POST of the
  conversation could trigger a second billed generation
- remember an unparseable 2xx from the first AUTO attempt and surface
  stashed first-attempt errors in both error paths, instead of showing
  only the final attempt's transport or HTTP error
- recognize Ollama's model-not-found wording in _names_model so a model
  typo is not misread as a missing endpoint
- match model errors case-insensitively and recognize the
  'model "x" is not supported' wording, so a wrong-model 400 (seen live
  on Perplexity /v1/responses) fails fast instead of probing the other
  flavour with a doomed request
- say "empty response body" instead of "Unknown error" when an HTTP
  error arrives without a body (e.g. Perplexity's bare 404)
- drop the dead, racy provider-lookup fallback in the generic request
  thread: a missing provider ref is a caller bug and now fails loudly
- fix ai_providers_lists_defaults to expect the header the command
  actually prints ("Configured providers:"); the test was broken since
  its introduction but CI never ran it
- add functional test group 5 (AI command surface) to FUNC_TEST_GROUPS
  so the CI parallel target runs it; proftest.c port ranges already
  account for five groups
2026-07-16 13:00:56 +03:00

View File

@@ -472,12 +472,16 @@ _names_model(const gchar* s)
{ {
if (!s) if (!s)
return FALSE; return FALSE;
/* "does not exist"/"not found" alone also appear in route-missing bodies; /* "does not exist"/"not found"/"not supported" alone also appear in
* require them to talk about a model (covers Ollama's 'model "x" not * route-missing bodies; require them to talk about a model (covers
* found, try pulling it first') */ * Ollama's 'model "x" not found, try pulling it first' and Perplexity's
return strstr(s, "model_not_found") || strstr(s, "invalid_model") * 'model "x" is not supported'). Gateways vary in casing, so compare
|| strstr(s, "unknown_model") * case-insensitively. */
|| (strstr(s, "model") && (strstr(s, "does not exist") || strstr(s, "not found"))); auto_gchar gchar* lower = g_ascii_strdown(s, -1);
return strstr(lower, "model_not_found") || strstr(lower, "invalid_model")
|| strstr(lower, "unknown_model")
|| (strstr(lower, "model")
&& (strstr(lower, "does not exist") || strstr(lower, "not found") || strstr(lower, "not supported")));
} }
/* A missing endpoint answers 404/405/501. A wrong model can also 404/405, but /* A missing endpoint answers 404/405/501. A wrong model can also 404/405, but
@@ -1088,7 +1092,7 @@ _curl_exec_and_handle(CURL* curl, const gchar* url, struct curl_slist* headers,
auto_gchar gchar* error_msg = parsed auto_gchar gchar* error_msg = parsed
? g_strdup_printf("HTTP %ld: %s", http_code, parsed) ? g_strdup_printf("HTTP %ld: %s", http_code, parsed)
: g_strdup_printf("HTTP %ld: %s", http_code, : g_strdup_printf("HTTP %ld: %s", http_code,
response->data && strlen(response->data) > 0 ? response->data : "Unknown error"); response->data && strlen(response->data) > 0 ? response->data : "empty response body");
log_error("Request HTTP error for '%s': %s", provider->name, error_msg); log_error("Request HTTP error for '%s': %s", provider->name, error_msg);
_aiwin_display_error(user_data, error_msg); _aiwin_display_error(user_data, error_msg);
} else { } else {
@@ -2236,7 +2240,7 @@ _ai_request_thread(gpointer data)
response.size, response_data); response.size, response_data);
/* Try to extract the actual error message from the JSON response */ /* Try to extract the actual error message from the JSON response */
auto_gchar gchar* parsed_error = ai_parse_error_message(err_body); auto_gchar gchar* parsed_error = ai_parse_error_message(err_body);
auto_gchar gchar* error_msg = parsed_error ? g_strdup_printf("HTTP %ld: %s", err_code, parsed_error) : g_strdup_printf("HTTP %ld: %s", err_code, err_body && strlen(err_body) > 0 ? err_body : "Unknown error"); auto_gchar gchar* error_msg = parsed_error ? g_strdup_printf("HTTP %ld: %s", err_code, parsed_error) : g_strdup_printf("HTTP %ld: %s", err_code, err_body && strlen(err_body) > 0 ? err_body : "empty response body");
if (first_unparsable_note) { if (first_unparsable_note) {
gchar* with_note = g_strdup_printf("%s (first attempt: %s)", error_msg, first_unparsable_note); gchar* with_note = g_strdup_printf("%s (first attempt: %s)", error_msg, first_unparsable_note);
g_free(error_msg); g_free(error_msg);