[#110] Add AI client with multi-provider support and UI #113
@@ -626,6 +626,10 @@ _ai_generic_request_thread(gpointer data)
|
|||||||
return NULL;
|
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();
|
CURL* curl = curl_easy_init();
|
||||||
if (!curl) {
|
if (!curl) {
|
||||||
log_error("Failed to initialize curl for %s", req->provider_name);
|
log_error("Failed to initialize curl for %s", req->provider_name);
|
||||||
@@ -656,6 +660,10 @@ _ai_generic_request_thread(gpointer data)
|
|||||||
curl_slist_free_all(headers);
|
curl_slist_free_all(headers);
|
||||||
curl_easy_cleanup(curl);
|
curl_easy_cleanup(curl);
|
||||||
g_free(response.data);
|
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->provider_name);
|
||||||
g_free(req->request_url);
|
g_free(req->request_url);
|
||||||
g_free(req);
|
g_free(req);
|
||||||
@@ -1153,6 +1161,38 @@ ai_session_set_model(AISession* session, const gchar* model)
|
|||||||
log_info("Session model changed to: %s", model);
|
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
|
* API Request Handling
|
||||||
* ======================================================================== */
|
* ======================================================================== */
|
||||||
|
|||||||
@@ -239,32 +239,6 @@ const gchar* ai_session_get_model(AISession* session);
|
|||||||
*/
|
*/
|
||||||
void ai_session_set_model(AISession* session, const gchar* model);
|
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);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Atomically switch session provider, model, and API key.
|
* Atomically switch session provider, model, and API key.
|
||||||
* All mutations happen under the session lock to prevent races with
|
* All mutations happen under the session lock to prevent races with
|
||||||
|
|||||||
@@ -51,7 +51,6 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <langinfo.h>
|
#include <langinfo.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <pthread.h>
|
|
||||||
|
|
||||||
// fork / execl
|
// fork / execl
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
@@ -11075,14 +11074,12 @@ cmd_ai_switch(ProfWin* window, const char* const command, gchar** args)
|
|||||||
|
|
||||||
const gchar* provider_name;
|
const gchar* provider_name;
|
||||||
const gchar* model;
|
const gchar* model;
|
||||||
gboolean changed_provider = FALSE;
|
|
||||||
|
|
||||||
// Check if arg1 is a known provider
|
// Check if arg1 is a known provider
|
||||||
AIProvider* provider = ai_get_provider(arg1);
|
AIProvider* provider = ai_get_provider(arg1);
|
||||||
if (provider) {
|
if (provider) {
|
||||||
// arg1 is a provider name
|
// arg1 is a provider name
|
||||||
provider_name = arg1;
|
provider_name = arg1;
|
||||||
changed_provider = TRUE;
|
|
||||||
|
|
||||||
// Get model: arg2 > provider default > error
|
// Get model: arg2 > provider default > error
|
||||||
if (arg2) {
|
if (arg2) {
|
||||||
@@ -11097,10 +11094,7 @@ cmd_ai_switch(ProfWin* window, const char* const command, gchar** args)
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// arg1 is a model name, keep current provider
|
// arg1 is a model name, keep current provider
|
||||||
// Read provider_name under lock to prevent UAF on session->provider_name
|
|
||||||
pthread_mutex_lock(&aiwin->session->lock);
|
|
||||||
provider_name = aiwin->session->provider_name;
|
provider_name = aiwin->session->provider_name;
|
||||||
pthread_mutex_unlock(&aiwin->session->lock);
|
|
||||||
if (!provider_name) {
|
if (!provider_name) {
|
||||||
cons_show_error("Session has no provider name.");
|
cons_show_error("Session has no provider name.");
|
||||||
return TRUE;
|
return TRUE;
|
||||||
@@ -11116,21 +11110,8 @@ cmd_ai_switch(ProfWin* window, const char* const command, gchar** args)
|
|||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the existing session's provider and model (all under lock)
|
// Atomically switch session provider, model, and API key (thread-safe)
|
||||||
pthread_mutex_lock(&aiwin->session->lock);
|
ai_session_switch(aiwin->session, provider_name, model, g_steal_pointer(&api_key));
|
||||||
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;
|
|
||||||
ai_provider_ref(aiwin->session->provider);
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
pthread_mutex_unlock(&aiwin->session->lock);
|
|
||||||
|
|
||||||
// Update window title
|
// Update window title
|
||||||
win_println((ProfWin*)aiwin, THEME_DEFAULT, "-", "AI Chat: %s/%s", provider_name, model);
|
win_println((ProfWin*)aiwin, THEME_DEFAULT, "-", "AI Chat: %s/%s", provider_name, model);
|
||||||
|
|||||||
Reference in New Issue
Block a user