[#110] Add AI client with multi-provider support and UI #113

Manually merged
jabber.developer merged 64 commits from feat/ai into master 2026-05-15 02:22:56 +00:00
22 changed files with 3950 additions and 8 deletions
Showing only changes of commit ecb07fd00c - Show all commits

View File

@@ -121,18 +121,27 @@ _aiwin_display_error(gpointer user_data, const gchar* error_msg)
}
/**
* Display a response message in the AI window (thread-safe).
* Display a response message. If aiwin is provided and valid, display in AI window.
* Otherwise, display in console (for commands run from console).
* @param user_data The original user_data pointer (may be NULL)
* @param response The response message to display
*/
static void
_aiwin_display_response(gpointer user_data, const gchar* response)
{
ProfAiWin* aiwin = _aiwin_validate(user_data);
if (!aiwin) {
log_warning("[AI-THREAD] Cannot display response: aiwin is invalid or window was closed");
if (!response || strlen(response) == 0) {
return;
}
/* Try to display in AI window if available */
ProfAiWin* aiwin = _aiwin_validate(user_data);
if (!aiwin) {
pthread_mutex_lock(&lock);
cons_show("%s", response);
pthread_mutex_unlock(&lock);
return;
}
pthread_mutex_lock(&lock);
aiwin_display_response(aiwin, response);
pthread_mutex_unlock(&lock);
@@ -1270,6 +1279,64 @@ _parse_ai_response(const gchar* response_json)
return NULL;
}
/**
* Extract error message from an API error JSON response.
* Handles format: {"error":{"message":"...","type":"...","code":...}}
*/
static gchar*
_parse_error_response(const gchar* error_json)
{
if (!error_json || strlen(error_json) == 0)
return NULL;
/* Look for "message":"..." inside "error" object */
const gchar* error_obj = strstr(error_json, "\"error\":");
if (!error_obj)
return NULL;
const gchar* message_key = strstr(error_obj, "\"message\":\"");
if (!message_key)
return NULL;
message_key += strlen("\"message\":\"");
const gchar* msg_start = message_key;
const gchar* p = msg_start;
while (*p) {
if (*p == '\\' && *(p + 1) == '"') {
p += 2;
continue;
}
if (*p == '"') {
gsize len = p - msg_start;
gchar* result = g_new0(gchar, len + 1);
gchar* out = result;
const gchar* in = msg_start;
while (in < p) {
if (*in == '\\' && *(in + 1) == '"') {
*out++ = '"';
in += 2;
} else if (*in == '\\' && *(in + 1) == 'n') {
*out++ = '\n';
in += 2;
} else if (*in == '\\' && *(in + 1) == '\\') {
*out++ = '\\';
in += 2;
} else if (*in == '\\' && *(in + 1) == 't') {
*out++ = '\t';
in += 2;
} else {
*out++ = *in++;
}
}
*out = '\0';
return result;
}
p++;
}
return NULL;
}
static gpointer
_ai_request_thread(gpointer data)
{
@@ -1328,7 +1395,11 @@ _ai_request_thread(gpointer data)
log_debug("[AI-THREAD] API Request URL: %s", request_url);
log_debug("[AI-THREAD] Model: %s", session->model);
/* Execute request with POST fields */
/* Set URL and execute request */
curl_easy_setopt(curl, CURLOPT_URL, request_url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_payload);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
@@ -1348,8 +1419,9 @@ _ai_request_thread(gpointer data)
/* Handle HTTP errors */
log_debug("[AI-THREAD] HTTP error response body (%zu bytes): %s",
response.size, response.data ? response.data : "NULL");
auto_gchar gchar* error_msg = g_strdup_printf("HTTP %ld: %s", http_code,
response.data ? response.data : "Unknown error");
/* Try to extract the actual error message from the JSON response */
auto_gchar gchar* parsed_error = _parse_error_response(response.data);
auto_gchar gchar* error_msg = parsed_error ? g_strdup_printf("HTTP %ld: %s", http_code, parsed_error) : g_strdup_printf("HTTP %ld: %s", http_code, response.data && strlen(response.data) > 0 ? response.data : "Unknown error");
log_error("AI request failed for %s/%s: %s", session->provider_name, session->model, error_msg);
_aiwin_display_error(user_data, error_msg);
} else {