Compare commits
8 Commits
3fccc046b0
...
aade5767b4
| Author | SHA1 | Date | |
|---|---|---|---|
| aade5767b4 | |||
| 03c00126d0 | |||
|
a75a06b4a2
|
|||
|
9f4621883a
|
|||
|
6fe8c370df
|
|||
|
b1dbe714e8
|
|||
|
107f7778e2
|
|||
|
b634eed5af
|
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <glib.h>
|
||||
#include <pthread.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Default providers */
|
||||
@@ -90,9 +91,7 @@ _aiwin_validate(gpointer user_data)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
gboolean win_exists = wins_ai_exists((ProfAiWin*)user_data);
|
||||
pthread_mutex_unlock(&lock);
|
||||
if (!win_exists) {
|
||||
log_warning("[AI-THREAD] aiwin=%p no longer exists — window was closed", (void*)user_data);
|
||||
return NULL;
|
||||
@@ -110,12 +109,12 @@ _aiwin_validate(gpointer user_data)
|
||||
static void
|
||||
_aiwin_display_error(gpointer user_data, const gchar* error_msg)
|
||||
{
|
||||
pthread_mutex_lock(&lock);
|
||||
ProfAiWin* aiwin = _aiwin_validate(user_data);
|
||||
if (!aiwin) {
|
||||
log_warning("[AI-THREAD] Cannot display error: aiwin is invalid or window was closed (msg: %s)", error_msg);
|
||||
return;
|
||||
}
|
||||
pthread_mutex_lock(&lock);
|
||||
aiwin_display_error(aiwin, error_msg);
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
@@ -133,16 +132,14 @@ _aiwin_display_response(gpointer user_data, const gchar* response)
|
||||
return;
|
||||
}
|
||||
|
||||
/* Try to display in AI window if available */
|
||||
pthread_mutex_lock(&lock);
|
||||
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);
|
||||
}
|
||||
@@ -194,12 +191,11 @@ ai_json_escape(const gchar* str)
|
||||
* ======================================================================== */
|
||||
|
||||
static AIProvider*
|
||||
ai_provider_new(const gchar* name, const gchar* api_url, const gchar* org_id)
|
||||
ai_provider_new(const gchar* name, const gchar* api_url)
|
||||
{
|
||||
AIProvider* provider = g_new0(AIProvider, 1);
|
||||
provider->name = g_strdup(name);
|
||||
provider->api_url = g_strdup(api_url ? api_url : "");
|
||||
provider->org_id = g_strdup(org_id);
|
||||
provider->project_id = NULL;
|
||||
provider->default_model = NULL;
|
||||
provider->settings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
||||
@@ -209,7 +205,7 @@ ai_provider_new(const gchar* name, const gchar* api_url, const gchar* org_id)
|
||||
return provider;
|
||||
}
|
||||
|
||||
static AIProvider*
|
||||
AIProvider*
|
||||
ai_provider_ref(AIProvider* provider)
|
||||
{
|
||||
if (provider) {
|
||||
@@ -230,7 +226,6 @@ ai_provider_unref(AIProvider* provider)
|
||||
|
||||
g_free(provider->name);
|
||||
g_free(provider->api_url);
|
||||
g_free(provider->org_id);
|
||||
g_free(provider->project_id);
|
||||
g_free(provider->default_model);
|
||||
g_hash_table_destroy(provider->settings);
|
||||
@@ -288,8 +283,8 @@ ai_client_init(void)
|
||||
providers_ac = autocomplete_new();
|
||||
|
||||
/* Add default providers */
|
||||
ai_add_provider("openai", DEFAULT_OPENAI_URL, NULL);
|
||||
ai_add_provider("perplexity", DEFAULT_PERPLEXITY_URL, NULL);
|
||||
ai_add_provider("openai", DEFAULT_OPENAI_URL);
|
||||
ai_add_provider("perplexity", DEFAULT_PERPLEXITY_URL);
|
||||
|
||||
/* Load saved API keys from config */
|
||||
ai_load_keys();
|
||||
@@ -336,7 +331,7 @@ ai_get_provider(const gchar* name)
|
||||
}
|
||||
|
||||
AIProvider*
|
||||
ai_add_provider(const gchar* name, const gchar* api_url, const gchar* org_id)
|
||||
ai_add_provider(const gchar* name, const gchar* api_url)
|
||||
{
|
||||
if (!name || !api_url)
|
||||
return NULL;
|
||||
@@ -351,14 +346,12 @@ ai_add_provider(const gchar* name, const gchar* api_url, const gchar* org_id)
|
||||
/* Update existing provider */
|
||||
g_free(existing->api_url);
|
||||
existing->api_url = g_strdup(api_url);
|
||||
g_free(existing->org_id);
|
||||
existing->org_id = g_strdup(org_id);
|
||||
log_info("Updated provider: %s", name);
|
||||
return ai_provider_ref(existing);
|
||||
}
|
||||
|
||||
/* Create new provider (ref_count=1 owned by hash table) */
|
||||
AIProvider* provider = ai_provider_new(name, api_url, org_id);
|
||||
AIProvider* provider = ai_provider_new(name, api_url);
|
||||
g_hash_table_insert(providers, g_strdup(name), provider);
|
||||
|
||||
/* Sync autocomplete */
|
||||
@@ -571,7 +564,7 @@ typedef struct
|
||||
} ai_request_t;
|
||||
|
||||
/**
|
||||
* Build curl headers with auth and optional org header.
|
||||
* Build curl headers with auth.
|
||||
* Returns headers list (caller must free with curl_slist_free_all).
|
||||
*/
|
||||
static struct curl_slist*
|
||||
@@ -582,11 +575,6 @@ _build_curl_headers(AIProvider* provider, const gchar* api_key)
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
headers = curl_slist_append(headers, auth_header);
|
||||
|
||||
if (provider && provider->org_id && strlen(provider->org_id) > 0) {
|
||||
auto_gchar gchar* org_header = g_strdup_printf("OpenAI-Organization: %s", provider->org_id);
|
||||
headers = curl_slist_append(headers, org_header);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -638,6 +626,10 @@ _ai_generic_request_thread(gpointer data)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Keep the provider alive for the duration of this request. Without this, /ai remove provider X could free the provider
|
||||
* struct (via the hash table's destroy-notify) while curl_easy_perform is still using it — classic UAF. */
|
||||
ai_provider_ref(provider);
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
log_error("Failed to initialize curl for %s", req->provider_name);
|
||||
@@ -668,6 +660,10 @@ _ai_generic_request_thread(gpointer data)
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
g_free(response.data);
|
||||
|
||||
/* Release the reference taken after lookup — provider may now be freed. */
|
||||
ai_provider_unref(provider);
|
||||
|
||||
g_free(req->provider_name);
|
||||
g_free(req->request_url);
|
||||
g_free(req);
|
||||
@@ -1050,6 +1046,11 @@ ai_session_create(const gchar* provider_name, const gchar* model)
|
||||
}
|
||||
|
||||
AISession* session = g_new0(AISession, 1);
|
||||
if (pthread_mutex_init(&session->lock, NULL) != 0) {
|
||||
log_error("Failed to initialize session mutex");
|
||||
g_free(session);
|
||||
return NULL;
|
||||
}
|
||||
session->provider_name = g_strdup(provider_name);
|
||||
session->provider = ai_provider_ref(provider);
|
||||
session->model = g_strdup(model);
|
||||
@@ -1080,6 +1081,7 @@ ai_session_unref(AISession* session)
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&session->lock);
|
||||
g_free(session->provider_name);
|
||||
ai_provider_unref(session->provider);
|
||||
g_free(session->model);
|
||||
@@ -1094,6 +1096,7 @@ ai_session_unref(AISession* session)
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
g_list_free(session->history);
|
||||
pthread_mutex_unlock(&session->lock);
|
||||
|
||||
g_free(session);
|
||||
log_debug("AI session destroyed");
|
||||
@@ -1105,10 +1108,12 @@ ai_session_add_message(AISession* session, const gchar* role, const gchar* conte
|
||||
if (!session || !role || !content)
|
||||
return;
|
||||
|
||||
pthread_mutex_lock(&session->lock);
|
||||
AIMessage* msg = g_new0(AIMessage, 1);
|
||||
msg->role = g_strdup(role);
|
||||
msg->content = g_strdup(content);
|
||||
session->history = g_list_append(session->history, msg);
|
||||
pthread_mutex_unlock(&session->lock);
|
||||
|
||||
log_debug("Added %s message to session (total: %d)", role, g_list_length(session->history));
|
||||
}
|
||||
@@ -1119,6 +1124,7 @@ ai_session_clear_history(AISession* session)
|
||||
if (!session)
|
||||
return;
|
||||
|
||||
pthread_mutex_lock(&session->lock);
|
||||
GList* curr = session->history;
|
||||
while (curr) {
|
||||
AIMessage* msg = curr->data;
|
||||
@@ -1129,6 +1135,7 @@ ai_session_clear_history(AISession* session)
|
||||
}
|
||||
g_list_free(session->history);
|
||||
session->history = NULL;
|
||||
pthread_mutex_unlock(&session->lock);
|
||||
|
||||
log_info("AI session history cleared");
|
||||
}
|
||||
@@ -1147,24 +1154,64 @@ ai_session_set_model(AISession* session, const gchar* model)
|
||||
if (!session || !model)
|
||||
return;
|
||||
|
||||
pthread_mutex_lock(&session->lock);
|
||||
g_free(session->model);
|
||||
session->model = g_strdup(model);
|
||||
pthread_mutex_unlock(&session->lock);
|
||||
log_info("Session model changed to: %s", model);
|
||||
}
|
||||
|
||||
void
|
||||
ai_session_switch(AISession* session, const gchar* provider_name,
|
||||
const gchar* model, gchar* api_key)
|
||||
{
|
||||
if (!session || !provider_name || !model || !api_key)
|
||||
return;
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider) {
|
||||
log_warning("Provider '%s' not found for session switch", provider_name);
|
||||
g_free(api_key);
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&session->lock);
|
||||
g_free(session->provider_name);
|
||||
session->provider_name = g_strdup(provider_name);
|
||||
|
||||
ai_provider_unref(session->provider);
|
||||
session->provider = ai_provider_ref(provider);
|
||||
|
||||
g_free(session->model);
|
||||
session->model = g_strdup(model);
|
||||
|
||||
g_free(session->api_key);
|
||||
session->api_key = g_strdup(api_key);
|
||||
pthread_mutex_unlock(&session->lock);
|
||||
|
||||
g_free(api_key);
|
||||
log_info("Session switched to %s/%s", provider_name, model);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* API Request Handling
|
||||
* ======================================================================== */
|
||||
|
||||
/**
|
||||
* Build JSON payload from a GList of AIMessage nodes and a prompt string.
|
||||
* This is the thread-safe variant used by _ai_request_thread() which holds
|
||||
* the session lock while calling it.
|
||||
*
|
||||
* @param model The model identifier
|
||||
* @param history GList of AIMessage* (must be held under session lock)
|
||||
* @param prompt The new user prompt to append
|
||||
* @return Newly allocated JSON string (caller must free)
|
||||
*/
|
||||
static gchar*
|
||||
_build_json_payload(AISession* session, const gchar* prompt)
|
||||
_build_json_payload_from_list(const gchar* model, GList* history, const gchar* prompt)
|
||||
{
|
||||
|
||||
/* OpenAI-compatible Responses API format:
|
||||
* {"model": "...", "input": [...], "stream": false, "store": false}
|
||||
* store:false prevents providers from storing/using requests for training */
|
||||
GString* messages_json = g_string_new("");
|
||||
GList* curr = session->history;
|
||||
GList* curr = history;
|
||||
|
||||
while (curr) {
|
||||
AIMessage* msg = curr->data;
|
||||
@@ -1187,7 +1234,7 @@ _build_json_payload(AISession* session, const gchar* prompt)
|
||||
}
|
||||
g_string_append_printf(messages_json, "{\"role\":\"user\",\"content\":\"%s\"}", escaped_prompt);
|
||||
|
||||
auto_gchar gchar* escaped_model = ai_json_escape(session->model);
|
||||
auto_gchar gchar* escaped_model = ai_json_escape(model);
|
||||
gchar* json_payload = g_strdup_printf(
|
||||
"{\"model\":\"%s\",\"input\":[%s],\"stream\":false,\"store\":false}",
|
||||
escaped_model, messages_json->str);
|
||||
@@ -1348,14 +1395,31 @@ _ai_request_thread(gpointer data)
|
||||
auto_gchar gchar* prompt = args[1];
|
||||
gpointer user_data = args[2];
|
||||
|
||||
log_debug("[AI-THREAD] Session: %s/%s", session->provider_name, session->model);
|
||||
log_debug("[AI-THREAD] API key length: %zu", session->api_key ? strlen(session->api_key) : 0);
|
||||
/* =====================================================================
|
||||
* CRITICAL: Copy all session state under lock to prevent UAF races with
|
||||
* cmd_ai_switch() and ai_session_clear_history() which mutate session
|
||||
* fields on the main thread without holding a session ref.
|
||||
*
|
||||
* The worker holds a ref to the session (via ai_session_ref in
|
||||
* ai_send_prompt), which prevents the session struct from being freed,
|
||||
* but does NOT prevent field mutations (g_free/g_strdup of strings,
|
||||
* list modifications). We must snapshot all fields atomically.
|
||||
* ===================================================================== */
|
||||
pthread_mutex_lock(&session->lock);
|
||||
gchar* local_provider_name = g_strdup(session->provider_name);
|
||||
AIProvider* local_provider = ai_provider_ref(session->provider);
|
||||
gchar* local_model = g_strdup(session->model);
|
||||
gchar* local_api_key = g_strdup(session->api_key ? session->api_key : "");
|
||||
pthread_mutex_unlock(&session->lock);
|
||||
|
||||
log_debug("[AI-THREAD] Session: %s/%s", local_provider_name, local_model);
|
||||
log_debug("[AI-THREAD] API key length: %zu", strlen(local_api_key));
|
||||
|
||||
/* Check for API key first */
|
||||
if (!session->api_key || strlen(session->api_key) == 0) {
|
||||
if (!local_api_key || strlen(local_api_key) == 0) {
|
||||
auto_gchar gchar* error_msg = g_strdup_printf("No API key set for provider '%s'. Use '/ai set token %s <key>' to configure.",
|
||||
session->provider_name, session->provider_name);
|
||||
log_error("AI request failed for %s/%s: %s", session->provider_name, session->model, error_msg);
|
||||
local_provider_name, local_provider_name);
|
||||
log_error("AI request failed for %s/%s: %s", local_provider_name, local_model, error_msg);
|
||||
_aiwin_display_error(user_data, error_msg);
|
||||
g_free(args);
|
||||
return NULL;
|
||||
@@ -1365,23 +1429,30 @@ _ai_request_thread(gpointer data)
|
||||
log_debug("[AI-THREAD] Curl initialized: %s", curl ? "OK" : "FAILED");
|
||||
if (!curl) {
|
||||
log_error("AI request failed for %s/%s: Failed to initialize curl",
|
||||
session->provider_name, session->model);
|
||||
local_provider_name, local_model);
|
||||
_aiwin_display_error(user_data, "Failed to initialize curl.");
|
||||
g_free(args);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Add user message to history FIRST */
|
||||
ai_session_add_message(session, "user", prompt);
|
||||
/* Add user message to history FIRST (under lock) */
|
||||
pthread_mutex_lock(&session->lock);
|
||||
AIMessage* user_msg = g_new0(AIMessage, 1);
|
||||
user_msg->role = g_strdup("user");
|
||||
user_msg->content = g_strdup(prompt);
|
||||
session->history = g_list_append(session->history, user_msg);
|
||||
pthread_mutex_unlock(&session->lock);
|
||||
log_debug("[AI-THREAD] Added user message to history");
|
||||
|
||||
/* Build JSON payload (includes the message we just added) */
|
||||
/* Build JSON payload from local history copy (under lock) */
|
||||
log_debug("[AI-THREAD] Building JSON payload...");
|
||||
auto_gchar gchar* json_payload = _build_json_payload(session, prompt);
|
||||
pthread_mutex_lock(&session->lock);
|
||||
auto_gchar gchar* json_payload = _build_json_payload_from_list(local_model, session->history, prompt);
|
||||
pthread_mutex_unlock(&session->lock);
|
||||
log_debug("[AI-THREAD] JSON payload: %s", json_payload);
|
||||
|
||||
/* Build headers using shared helper */
|
||||
struct curl_slist* headers = _build_curl_headers(session->provider, session->api_key);
|
||||
struct curl_slist* headers = _build_curl_headers(local_provider, local_api_key);
|
||||
|
||||
/* Response buffer */
|
||||
struct curl_response_t response;
|
||||
@@ -1389,11 +1460,11 @@ _ai_request_thread(gpointer data)
|
||||
response.size = 0;
|
||||
|
||||
/* Build request URL */
|
||||
const gchar* api_url = session->provider ? session->provider->api_url : DEFAULT_OPENAI_URL;
|
||||
const gchar* api_url = local_provider ? local_provider->api_url : DEFAULT_OPENAI_URL;
|
||||
auto_gchar gchar* request_url = g_strdup_printf("%s%sv1/responses", api_url, g_str_has_suffix(api_url, "/") ? "" : "/");
|
||||
log_debug("[AI-THREAD] API URL: %s", api_url);
|
||||
log_debug("[AI-THREAD] API Request URL: %s", request_url);
|
||||
log_debug("[AI-THREAD] Model: %s", session->model);
|
||||
log_debug("[AI-THREAD] Model: %s", local_model);
|
||||
|
||||
/* Set URL and execute request */
|
||||
curl_easy_setopt(curl, CURLOPT_URL, request_url);
|
||||
@@ -1413,7 +1484,7 @@ _ai_request_thread(gpointer data)
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
auto_gchar gchar* error_msg = g_strdup(curl_easy_strerror(res));
|
||||
log_error("AI request failed for %s/%s: %s", session->provider_name, session->model, error_msg);
|
||||
log_error("AI request failed for %s/%s: %s", local_provider_name, local_model, error_msg);
|
||||
_aiwin_display_error(user_data, error_msg);
|
||||
} else if (http_code >= 400) {
|
||||
/* Handle HTTP errors */
|
||||
@@ -1422,7 +1493,7 @@ _ai_request_thread(gpointer data)
|
||||
/* Try to extract the actual error message from the JSON response */
|
||||
auto_gchar gchar* parsed_error = ai_parse_error_message(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);
|
||||
log_error("AI request failed for %s/%s: %s", local_provider_name, local_model, error_msg);
|
||||
_aiwin_display_error(user_data, error_msg);
|
||||
} else {
|
||||
/* Parse response - transfer ownership to auto_gchar for cleanup */
|
||||
@@ -1431,12 +1502,17 @@ _ai_request_thread(gpointer data)
|
||||
response.data = NULL;
|
||||
auto_gchar gchar* content = ai_parse_response(response_data);
|
||||
if (content) {
|
||||
/* Add assistant response to history */
|
||||
ai_session_add_message(session, "assistant", content);
|
||||
/* Add assistant response to history (under lock) */
|
||||
pthread_mutex_lock(&session->lock);
|
||||
AIMessage* assistant_msg = g_new0(AIMessage, 1);
|
||||
assistant_msg->role = g_strdup("assistant");
|
||||
assistant_msg->content = g_strdup(content);
|
||||
session->history = g_list_append(session->history, assistant_msg);
|
||||
pthread_mutex_unlock(&session->lock);
|
||||
_aiwin_display_response(user_data, content);
|
||||
} else {
|
||||
log_error("AI response parse failed for %s/%s: %.200s...",
|
||||
session->provider_name, session->model, response_data);
|
||||
local_provider_name, local_model, response_data);
|
||||
_aiwin_display_error(user_data, "Failed to parse AI response.");
|
||||
}
|
||||
}
|
||||
@@ -1444,6 +1520,12 @@ _ai_request_thread(gpointer data)
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
/* Cleanup local copies */
|
||||
g_free(local_provider_name);
|
||||
ai_provider_unref(local_provider);
|
||||
g_free(local_model);
|
||||
g_free(local_api_key);
|
||||
|
||||
g_free(args);
|
||||
|
||||
return NULL;
|
||||
|
||||
@@ -19,7 +19,6 @@ typedef struct ai_provider_t
|
||||
{
|
||||
gchar* name; /* Provider name (e.g., "openai", "perplexity") */
|
||||
gchar* api_url; /* API endpoint URL */
|
||||
gchar* org_id; /* Optional organization ID */
|
||||
gchar* project_id; /* Optional project ID (for some providers) */
|
||||
gchar* default_model; /* Default model for this provider */
|
||||
GHashTable* settings; /* Extensible per-provider settings (e.g., tools=enabled, search=disabled) */
|
||||
@@ -33,6 +32,7 @@ typedef struct ai_provider_t
|
||||
*/
|
||||
typedef struct ai_session_t
|
||||
{
|
||||
pthread_mutex_t lock; /* Protects all session fields below */
|
||||
gchar* provider_name; /* Provider name */
|
||||
AIProvider* provider; /* Provider configuration */
|
||||
gchar* model; /* Model identifier (e.g., "gpt-4", "sonar") */
|
||||
@@ -66,10 +66,9 @@ AIProvider* ai_get_provider(const gchar* name);
|
||||
* Add or update a provider configuration.
|
||||
* @param name The provider name
|
||||
* @param api_url The API endpoint URL
|
||||
* @param org_id Optional organization ID (can be NULL)
|
||||
* @return New AIProvider* (caller must unref when done)
|
||||
*/
|
||||
AIProvider* ai_add_provider(const gchar* name, const gchar* api_url, const gchar* org_id);
|
||||
AIProvider* ai_add_provider(const gchar* name, const gchar* api_url);
|
||||
|
||||
/**
|
||||
* Remove a provider by name.
|
||||
@@ -78,6 +77,13 @@ AIProvider* ai_add_provider(const gchar* name, const gchar* api_url, const gchar
|
||||
*/
|
||||
gboolean ai_remove_provider(const gchar* name);
|
||||
|
||||
/**
|
||||
* Increment the reference count of a provider.
|
||||
* @param provider The provider to reference
|
||||
* @return The same provider pointer
|
||||
*/
|
||||
AIProvider* ai_provider_ref(AIProvider* provider);
|
||||
|
||||
/**
|
||||
* Decrement the reference count of a provider.
|
||||
* @param provider The provider to unreference
|
||||
@@ -251,6 +257,19 @@ const gchar* ai_session_get_model(AISession* session);
|
||||
*/
|
||||
void ai_session_set_model(AISession* session, const gchar* model);
|
||||
|
||||
/**
|
||||
* Atomically switch session provider, model, and API key.
|
||||
* All mutations happen under the session lock to prevent races with
|
||||
* _ai_request_thread() which snapshots session state before making requests.
|
||||
*
|
||||
* @param session The session
|
||||
* @param provider_name New provider name
|
||||
* @param model New model identifier
|
||||
* @param api_key New API key (caller must free after calling this)
|
||||
*/
|
||||
void ai_session_switch(AISession* session, const gchar* provider_name,
|
||||
const gchar* model, gchar* api_key);
|
||||
|
||||
/* ========================================================================
|
||||
* Request Handling
|
||||
* ======================================================================== */
|
||||
|
||||
@@ -10839,7 +10839,7 @@ cmd_ai_set(ProfWin* window, const char* const command, gchar** args)
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
if (ai_add_provider(args[2], args[3], NULL)) {
|
||||
if (ai_add_provider(args[2], args[3])) {
|
||||
cons_show("Provider '%s' configured with URL: %s", args[2], args[3]);
|
||||
} else {
|
||||
cons_show_error("Failed to configure provider '%s'.", args[2]);
|
||||
@@ -11074,14 +11074,12 @@ cmd_ai_switch(ProfWin* window, const char* const command, gchar** args)
|
||||
|
||||
const gchar* provider_name;
|
||||
const gchar* model;
|
||||
gboolean changed_provider = FALSE;
|
||||
|
||||
// Check if arg1 is a known provider
|
||||
AIProvider* provider = ai_get_provider(arg1);
|
||||
if (provider) {
|
||||
// arg1 is a provider name
|
||||
provider_name = arg1;
|
||||
changed_provider = TRUE;
|
||||
|
||||
// Get model: arg2 > provider default > error
|
||||
if (arg2) {
|
||||
@@ -11097,6 +11095,10 @@ cmd_ai_switch(ProfWin* window, const char* const command, gchar** args)
|
||||
} else {
|
||||
// arg1 is a model name, keep current provider
|
||||
provider_name = aiwin->session->provider_name;
|
||||
if (!provider_name) {
|
||||
cons_show_error("Session has no provider name.");
|
||||
return TRUE;
|
||||
}
|
||||
model = arg1;
|
||||
}
|
||||
|
||||
@@ -11108,19 +11110,8 @@ cmd_ai_switch(ProfWin* window, const char* const command, gchar** args)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Update the existing session's provider and model
|
||||
if (changed_provider) {
|
||||
AIProvider* new_provider = ai_get_provider(provider_name);
|
||||
g_free(aiwin->session->provider_name);
|
||||
aiwin->session->provider_name = g_strdup(provider_name);
|
||||
ai_provider_unref(aiwin->session->provider);
|
||||
aiwin->session->provider = new_provider;
|
||||
aiwin->session->provider->ref_count++;
|
||||
}
|
||||
g_free(aiwin->session->model);
|
||||
aiwin->session->model = g_strdup(model);
|
||||
g_free(aiwin->session->api_key);
|
||||
aiwin->session->api_key = g_strdup(api_key);
|
||||
// Atomically switch session provider, model, and API key (thread-safe)
|
||||
ai_session_switch(aiwin->session, provider_name, model, g_steal_pointer(&api_key));
|
||||
|
||||
// Update window title
|
||||
win_println((ProfWin*)aiwin, THEME_DEFAULT, "-", "AI Chat: %s/%s", provider_name, model);
|
||||
@@ -11193,58 +11184,6 @@ cmd_ai_clear(ProfWin* window, const char* const command, gchar** args)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_correct(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// /ai correct <message>
|
||||
// Join all arguments from args[1] onwards to support multi-word prompts
|
||||
auto_gchar gchar* prompt = g_strjoinv(" ", &args[1]);
|
||||
if (prompt == NULL || strlen(prompt) == 0) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Get current AI window
|
||||
ProfAiWin* aiwin = wins_get_ai();
|
||||
if (!aiwin) {
|
||||
cons_show("No active AI chat window. Use '/ai start <provider>/<model>' first.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
|
||||
|
||||
if (!aiwin->session) {
|
||||
cons_show("No active session in this chat window.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Get the last user message and replace it
|
||||
GList* history = aiwin->session->history;
|
||||
GList* last_user_msg = NULL;
|
||||
for (GList* curr = history; curr; curr = g_list_next(curr)) {
|
||||
AIMessage* msg = curr->data;
|
||||
if (g_strcmp0(msg->role, "user") == 0) {
|
||||
last_user_msg = curr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!last_user_msg) {
|
||||
cons_show("No user messages in this chat to correct.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Replace the last user message
|
||||
AIMessage* msg = last_user_msg->data;
|
||||
g_free(msg->content);
|
||||
msg->content = g_strdup(prompt);
|
||||
|
||||
// Resend the prompt
|
||||
cons_show("Correcting message...");
|
||||
ai_send_prompt(aiwin->session, prompt, aiwin);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_providers(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
@@ -11270,9 +11209,6 @@ cmd_ai_providers(ProfWin* window, const char* const command, gchar** args)
|
||||
cons_show(" %s", provider->name);
|
||||
cons_show(" URL: %s", provider->api_url);
|
||||
cons_show(" Key: %s", key ? "configured" : "NOT configured");
|
||||
if (provider->org_id && strlen(provider->org_id) > 0) {
|
||||
cons_show(" Org: %s", provider->org_id);
|
||||
}
|
||||
cons_show("");
|
||||
g_free(key);
|
||||
}
|
||||
|
||||
@@ -47,17 +47,15 @@ void
|
||||
test_ai_add_provider(void** state)
|
||||
{
|
||||
/* Add a custom provider (hash table owns ref; caller gets non-owning pointer) */
|
||||
AIProvider* provider = ai_add_provider("custom", "https://custom.api.com/v1", "my-org");
|
||||
AIProvider* provider = ai_add_provider("custom", "https://custom.api.com/v1");
|
||||
assert_non_null(provider);
|
||||
assert_string_equal("custom", provider->name);
|
||||
assert_string_equal("https://custom.api.com/v1", provider->api_url);
|
||||
assert_string_equal("my-org", provider->org_id);
|
||||
|
||||
/* Update existing provider (returns ref; caller owns it) */
|
||||
AIProvider* updated = ai_add_provider("custom", "https://new.api.com/v1", NULL);
|
||||
AIProvider* updated = ai_add_provider("custom", "https://new.api.com/v1");
|
||||
assert_non_null(updated);
|
||||
assert_string_equal("https://new.api.com/v1", updated->api_url);
|
||||
assert_null(updated->org_id);
|
||||
|
||||
ai_provider_unref(updated);
|
||||
}
|
||||
@@ -69,7 +67,7 @@ test_ai_remove_provider(void** state)
|
||||
assert_false(ai_remove_provider("nonexistent"));
|
||||
|
||||
/* Add and remove custom provider */
|
||||
ai_add_provider("temp", "https://temp.api.com/v1", NULL);
|
||||
ai_add_provider("temp", "https://temp.api.com/v1");
|
||||
assert_true(ai_remove_provider("temp"));
|
||||
assert_null(ai_get_provider("temp"));
|
||||
}
|
||||
@@ -85,7 +83,7 @@ test_ai_list_providers(void** state)
|
||||
g_list_free(providers);
|
||||
|
||||
/* Add another provider */
|
||||
ai_add_provider("test", "https://test.api.com/v1", NULL);
|
||||
ai_add_provider("test", "https://test.api.com/v1");
|
||||
providers = ai_list_providers();
|
||||
assert_int_equal(3, g_list_length(providers));
|
||||
|
||||
@@ -304,29 +302,27 @@ void
|
||||
test_ai_add_provider_update_existing(void** state)
|
||||
{
|
||||
/* Add a provider (hash table owns ref) */
|
||||
AIProvider* provider = ai_add_provider("custom", "https://first.api.com/v1", "org1");
|
||||
AIProvider* provider = ai_add_provider("custom", "https://first.api.com/v1");
|
||||
assert_non_null(provider);
|
||||
assert_string_equal("https://first.api.com/v1", provider->api_url);
|
||||
assert_string_equal("org1", provider->org_id);
|
||||
|
||||
/* Update the same provider (returns ref) */
|
||||
provider = ai_add_provider("custom", "https://second.api.com/v1", "org2");
|
||||
provider = ai_add_provider("custom", "https://second.api.com/v1");
|
||||
assert_non_null(provider);
|
||||
assert_string_equal("https://second.api.com/v1", provider->api_url);
|
||||
assert_string_equal("org2", provider->org_id);
|
||||
ai_provider_unref(provider);
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_add_provider_null_name_returns_null(void** state)
|
||||
{
|
||||
assert_null(ai_add_provider(NULL, "https://api.com/v1", NULL));
|
||||
assert_null(ai_add_provider(NULL, "https://api.com/v1"));
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_add_provider_null_url_returns_null(void** state)
|
||||
{
|
||||
assert_null(ai_add_provider("test", NULL, NULL));
|
||||
assert_null(ai_add_provider("test", NULL));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -377,7 +373,7 @@ void
|
||||
test_ai_providers_find_forward_custom(void** state)
|
||||
{
|
||||
/* Add a custom provider and test */
|
||||
ai_add_provider("custom", "https://custom.api.com/v1", NULL);
|
||||
ai_add_provider("custom", "https://custom.api.com/v1");
|
||||
|
||||
auto_gchar gchar* result = ai_providers_find("c", FALSE, NULL);
|
||||
assert_non_null(result);
|
||||
@@ -487,7 +483,7 @@ test_ai_models_find_null_session(void** state)
|
||||
void
|
||||
test_ai_models_find_null_provider(void** state)
|
||||
{
|
||||
ai_add_provider("temp", "https://temp.api.com/v1", NULL);
|
||||
ai_add_provider("temp", "https://temp.api.com/v1");
|
||||
AISession* session = ai_session_create("temp", "gpt-4");
|
||||
assert_non_null(session);
|
||||
ai_session_unref(session);
|
||||
@@ -659,7 +655,7 @@ test_ai_parse_models_openai_format(void** state)
|
||||
/* Format: {"object":"list","data":[{"id":"model1",...},...]} */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL);
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1");
|
||||
|
||||
/* JSON response from Perplexity/OpenAI API */
|
||||
const gchar* json = "{\"object\":\"list\","
|
||||
@@ -695,7 +691,7 @@ test_ai_parse_models_perplexity_format(void** state)
|
||||
/* Test parsing actual Perplexity API response with all models from curl test */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("perplexity", "https://api.perplexity.ai/", NULL);
|
||||
AIProvider* provider = ai_add_provider("perplexity", "https://api.perplexity.ai/");
|
||||
|
||||
const gchar* json = "{\"object\":\"list\","
|
||||
"\"data\":["
|
||||
@@ -768,7 +764,7 @@ test_ai_parse_models_array_format(void** state)
|
||||
/* Test parsing simple array format: ["model1", "model2", ...] */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL);
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1");
|
||||
|
||||
const gchar* json = "[\"model1\", \"model2\", \"model3\"]";
|
||||
|
||||
@@ -793,7 +789,7 @@ test_ai_parse_models_empty_json(void** state)
|
||||
/* Test parsing empty/invalid JSON */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL);
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1");
|
||||
|
||||
ai_parse_models_from_json(provider, "");
|
||||
assert_null(provider->models);
|
||||
@@ -813,7 +809,7 @@ test_ai_parse_models_null_handling(void** state)
|
||||
/* Test NULL handling */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL);
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1");
|
||||
|
||||
/* NULL json should not crash */
|
||||
ai_parse_models_from_json(provider, NULL);
|
||||
@@ -829,7 +825,7 @@ test_ai_parse_models_escaped_quotes(void** state)
|
||||
/* Test parsing model IDs with escaped quotes */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL);
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1");
|
||||
|
||||
const gchar* json = "{\"object\":\"list\","
|
||||
"\"data\":["
|
||||
@@ -853,7 +849,7 @@ test_ai_parse_models_with_whitespace(void** state)
|
||||
/* Test parsing JSON with whitespace after colons */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL);
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1");
|
||||
|
||||
/* JSON with spaces after colons (common in formatted JSON) */
|
||||
const gchar* json = "{ \"object\": \"list\", "
|
||||
|
||||
Reference in New Issue
Block a user