feat(ai): add model caching, settings, and commands
Some checks failed
CI Code / Check spelling (pull_request) Successful in 23s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Code Coverage (pull_request) Successful in 2m28s
CI Code / Linux (debian) (pull_request) Failing after 6m18s
CI Code / Linux (arch) (pull_request) Failing after 8m46s
CI Code / Linux (ubuntu) (pull_request) Failing after 11m4s
Some checks failed
CI Code / Check spelling (pull_request) Successful in 23s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Code Coverage (pull_request) Successful in 2m28s
CI Code / Linux (debian) (pull_request) Failing after 6m18s
CI Code / Linux (arch) (pull_request) Failing after 8m46s
CI Code / Linux (ubuntu) (pull_request) Failing after 11m4s
- Introduce model caching with persistence to preferences - Add provider default model and custom settings management - Implement `/ai switch`, `/ai models`, and improve `/ai start` - Add model name autocomplete for chat commands - Update command definitions and help text - Add unit tests for new functionality
This commit is contained in:
@@ -32,6 +32,12 @@ static GHashTable* providers = NULL;
|
||||
static GHashTable* provider_keys = NULL;
|
||||
static Autocomplete providers_ac = NULL;
|
||||
|
||||
/* ========================================================================
|
||||
* Forward declarations
|
||||
* ======================================================================== */
|
||||
static void _ai_load_models_for_provider(AIProvider* provider);
|
||||
static void _ai_save_models_for_provider(AIProvider* provider);
|
||||
|
||||
/* ========================================================================
|
||||
* Curl helpers
|
||||
* ======================================================================== */
|
||||
@@ -183,7 +189,10 @@ ai_provider_new(const gchar* name, const gchar* api_url, const gchar* org_id)
|
||||
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);
|
||||
provider->models = NULL;
|
||||
provider->models_fresh = FALSE;
|
||||
provider->ref_count = 1;
|
||||
return provider;
|
||||
}
|
||||
@@ -211,6 +220,8 @@ ai_provider_unref(AIProvider* provider)
|
||||
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);
|
||||
|
||||
GList* curr = provider->models;
|
||||
while (curr) {
|
||||
@@ -271,6 +282,16 @@ ai_client_init(void)
|
||||
/* Load saved API keys from config */
|
||||
ai_load_keys();
|
||||
|
||||
/* Load cached models for all providers */
|
||||
GList* provider_list = ai_list_providers();
|
||||
GList* curr = provider_list;
|
||||
while (curr) {
|
||||
AIProvider* provider = (AIProvider*)curr->data;
|
||||
_ai_load_models_for_provider(provider);
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
g_list_free(provider_list);
|
||||
|
||||
log_info("AI client initialized with default providers: openai, perplexity");
|
||||
}
|
||||
|
||||
@@ -387,6 +408,37 @@ ai_providers_find(const char* const search_str, gboolean previous, void* context
|
||||
return autocomplete_complete(providers_ac, effective_search, FALSE, previous);
|
||||
}
|
||||
|
||||
/* Stateful model name finder for autocomplete.
|
||||
* Uses the current session's provider models for completion. */
|
||||
gchar*
|
||||
ai_models_find(const char* const search_str, gboolean previous, void* context)
|
||||
{
|
||||
ProfAiWin* aiwin = (ProfAiWin*)context;
|
||||
if (!aiwin || !aiwin->session) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
AIProvider* provider = aiwin->session->provider;
|
||||
if (!provider || !provider->models) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Create a temporary autocomplete for models */
|
||||
Autocomplete models_ac = autocomplete_new();
|
||||
GList* curr = provider->models;
|
||||
while (curr) {
|
||||
autocomplete_add(models_ac, curr->data);
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
|
||||
/* NULL search_str is treated as empty string for cycling */
|
||||
const char* effective_search = (search_str != NULL) ? search_str : "";
|
||||
|
||||
gchar* result = autocomplete_complete(models_ac, effective_search, FALSE, previous);
|
||||
autocomplete_free(models_ac);
|
||||
return result;
|
||||
}
|
||||
|
||||
gchar*
|
||||
ai_get_provider_key(const gchar* provider_name)
|
||||
{
|
||||
@@ -419,6 +471,456 @@ ai_set_provider_key(const gchar* provider_name, const gchar* api_key)
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Provider default model and settings
|
||||
* ======================================================================== */
|
||||
|
||||
void
|
||||
ai_set_provider_default_model(const gchar* provider_name, const gchar* model)
|
||||
{
|
||||
if (!provider_name || !model)
|
||||
return;
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider) {
|
||||
log_warning("Provider '%s' not found for default model setting", provider_name);
|
||||
return;
|
||||
}
|
||||
|
||||
g_free(provider->default_model);
|
||||
provider->default_model = g_strdup(model);
|
||||
log_info("Default model for provider '%s' set to: %s", provider_name, model);
|
||||
}
|
||||
|
||||
const gchar*
|
||||
ai_get_provider_default_model(const gchar* provider_name)
|
||||
{
|
||||
if (!provider_name)
|
||||
return NULL;
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider)
|
||||
return NULL;
|
||||
|
||||
return provider->default_model;
|
||||
}
|
||||
|
||||
void
|
||||
ai_set_provider_setting(const gchar* provider_name, const gchar* setting, const gchar* value)
|
||||
{
|
||||
if (!provider_name || !setting)
|
||||
return;
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider) {
|
||||
log_warning("Provider '%s' not found for setting '%s'", provider_name, setting);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!provider->settings) {
|
||||
provider->settings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
g_hash_table_insert(provider->settings, g_strdup(setting), g_strdup(value));
|
||||
log_info("Setting '%s' for provider '%s' set to: %s", setting, provider_name, value);
|
||||
} else {
|
||||
g_hash_table_remove(provider->settings, setting);
|
||||
log_info("Setting '%s' removed for provider '%s'", setting, provider_name);
|
||||
}
|
||||
}
|
||||
|
||||
gchar*
|
||||
ai_get_provider_setting(const gchar* provider_name, const gchar* setting)
|
||||
{
|
||||
if (!provider_name || !setting)
|
||||
return NULL;
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider || !provider->settings)
|
||||
return NULL;
|
||||
|
||||
gchar* value = g_hash_table_lookup(provider->settings, setting);
|
||||
return g_strdup(value);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Generic HTTP request handling
|
||||
* ======================================================================== */
|
||||
|
||||
typedef void (*ai_response_handler_t)(AIProvider* provider, const gchar* response, gpointer user_data);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
gchar* provider_name;
|
||||
gpointer user_data;
|
||||
gchar* request_url;
|
||||
ai_response_handler_t handler;
|
||||
} ai_request_t;
|
||||
|
||||
/**
|
||||
* Build curl headers with auth and optional org header.
|
||||
* Returns headers list (caller must free with curl_slist_free_all).
|
||||
*/
|
||||
static struct curl_slist*
|
||||
_build_curl_headers(AIProvider* provider, const gchar* api_key)
|
||||
{
|
||||
struct curl_slist* headers = NULL;
|
||||
auto_gchar gchar* auth_header = g_strdup_printf("Authorization: Bearer %s", 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a curl request and call handler on success.
|
||||
* Returns CURLcode.
|
||||
*/
|
||||
static CURLcode
|
||||
_curl_exec_and_handle(CURL* curl, const gchar* url, struct curl_slist* headers,
|
||||
struct curl_response_t* response, ai_response_handler_t handler,
|
||||
AIProvider* provider, gpointer user_data)
|
||||
{
|
||||
curl_easy_setopt(curl, CURLOPT_URL, 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_TIMEOUT, 30L);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
auto_gchar gchar* error_msg = g_strdup(curl_easy_strerror(res));
|
||||
log_error("Request failed for '%s': %s", provider->name, error_msg);
|
||||
_aiwin_display_error(user_data, error_msg);
|
||||
} else if (http_code >= 400) {
|
||||
auto_gchar gchar* error_msg = g_strdup_printf("HTTP %ld: %s", http_code,
|
||||
response->data ? response->data : "Unknown error");
|
||||
log_error("Request HTTP error for '%s': %s", provider->name, error_msg);
|
||||
_aiwin_display_error(user_data, error_msg);
|
||||
} else {
|
||||
handler(provider, response->data, user_data);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static gpointer
|
||||
_ai_generic_request_thread(gpointer data)
|
||||
{
|
||||
ai_request_t* req = (ai_request_t*)data;
|
||||
AIProvider* provider = ai_get_provider(req->provider_name);
|
||||
if (!provider) {
|
||||
log_error("Provider '%s' not found for request", req->provider_name);
|
||||
g_free(req->provider_name);
|
||||
g_free(req->request_url);
|
||||
g_free(req);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
log_error("Failed to initialize curl for %s", req->provider_name);
|
||||
g_free(req->provider_name);
|
||||
g_free(req->request_url);
|
||||
g_free(req);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto_gchar gchar* api_key = ai_get_provider_key(req->provider_name);
|
||||
if (!api_key || strlen(api_key) == 0) {
|
||||
auto_gchar gchar* error_msg = g_strdup_printf("No API key set for provider '%s'.", req->provider_name);
|
||||
_aiwin_display_error(req->user_data, error_msg);
|
||||
curl_easy_cleanup(curl);
|
||||
g_free(req->provider_name);
|
||||
g_free(req->request_url);
|
||||
g_free(req);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct curl_slist* headers = _build_curl_headers(provider, api_key);
|
||||
struct curl_response_t response;
|
||||
response.data = g_new0(gchar, 1);
|
||||
response.size = 0;
|
||||
|
||||
_curl_exec_and_handle(curl, req->request_url, headers, &response, req->handler, provider, req->user_data);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
g_free(response.data);
|
||||
g_free(req->provider_name);
|
||||
g_free(req->request_url);
|
||||
g_free(req);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Model fetching
|
||||
* ======================================================================== */
|
||||
|
||||
gboolean
|
||||
ai_models_are_fresh(const gchar* provider_name)
|
||||
{
|
||||
if (!provider_name)
|
||||
return FALSE;
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider)
|
||||
return FALSE;
|
||||
|
||||
return provider->models_fresh;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_model_exists(AIProvider* provider, const gchar* model)
|
||||
{
|
||||
GList* curr = provider->models;
|
||||
while (curr) {
|
||||
if (g_strcmp0(curr->data, model) == 0) {
|
||||
return TRUE;
|
||||
}
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
_add_model_if_new(AIProvider* provider, const gchar* model)
|
||||
{
|
||||
if (!_model_exists(provider, model)) {
|
||||
provider->models = g_list_append(provider->models, g_strdup(model));
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Model cache persistence
|
||||
* ======================================================================== */
|
||||
|
||||
/**
|
||||
* Load cached models for a provider from preferences.
|
||||
*/
|
||||
static void
|
||||
_ai_load_models_for_provider(AIProvider* provider)
|
||||
{
|
||||
if (!provider)
|
||||
return;
|
||||
|
||||
GList* models = prefs_ai_get_models(provider->name);
|
||||
if (!models) {
|
||||
log_info("No cached models for provider '%s'", provider->name);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Add loaded models to provider */
|
||||
GList* curr = models;
|
||||
while (curr) {
|
||||
_add_model_if_new(provider, curr->data);
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
|
||||
prefs_free_ai_models(models);
|
||||
|
||||
if (provider->models) {
|
||||
provider->models_fresh = TRUE;
|
||||
log_info("Loaded %d cached models for provider '%s'", g_list_length(provider->models), provider->name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save models for a provider to preferences.
|
||||
*/
|
||||
static void
|
||||
_ai_save_models_for_provider(AIProvider* provider)
|
||||
{
|
||||
if (!provider || !provider->models)
|
||||
return;
|
||||
|
||||
/* Build array of model strings */
|
||||
gint count = g_list_length(provider->models);
|
||||
gchar** models = g_new0(gchar*, count);
|
||||
GList* curr = provider->models;
|
||||
gint i = 0;
|
||||
while (curr) {
|
||||
models[i++] = curr->data;
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
|
||||
prefs_ai_set_models(provider->name, (const gchar* const*)models, count);
|
||||
g_free(models);
|
||||
|
||||
log_debug("Saved %d models for provider '%s' to prefs", count, provider->name);
|
||||
}
|
||||
|
||||
static const gchar*
|
||||
_find_json_field(const gchar* json, const gchar* field)
|
||||
{
|
||||
auto_gchar gchar* key = g_strdup_printf("\"%s\":\"", field);
|
||||
const gchar* pos = strstr(json, key);
|
||||
if (!pos) {
|
||||
return NULL;
|
||||
}
|
||||
return pos + strlen(key);
|
||||
}
|
||||
|
||||
static void
|
||||
_parse_openai_models(AIProvider* provider, const gchar* json)
|
||||
{
|
||||
/* Format: {"data": [{"id": "model1"}, {"id": "model2"}, ...]} */
|
||||
const gchar* data_start = _find_json_field(json, "data");
|
||||
if (!data_start) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gchar* obj_start = strstr(data_start, "{\"id\":");
|
||||
while (obj_start) {
|
||||
const gchar* id_start = _find_json_field(obj_start, "id");
|
||||
if (!id_start) {
|
||||
break;
|
||||
}
|
||||
|
||||
const gchar* id_end = strchr(id_start, '"');
|
||||
if (!id_end) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto_gchar gchar* model = g_strndup(id_start, id_end - id_start);
|
||||
_add_model_if_new(provider, model);
|
||||
|
||||
obj_start = strstr(id_end, "{\"id\":");
|
||||
if (obj_start) {
|
||||
obj_start++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
_parse_array_models(AIProvider* provider, const gchar* json)
|
||||
{
|
||||
/* Format: ["model1", "model2", ...] */
|
||||
const gchar* arr_start = strchr(json, '[');
|
||||
if (!arr_start) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gchar* model_start = strchr(arr_start, '"');
|
||||
while (model_start) {
|
||||
const gchar* model_end = strchr(model_start + 1, '"');
|
||||
if (!model_end) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto_gchar gchar* model = g_strndup(model_start + 1, model_end - model_start - 1);
|
||||
_add_model_if_new(provider, model);
|
||||
|
||||
model_start = strchr(model_end + 1, '"');
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
_parse_and_cache_models(AIProvider* provider, const gchar* response_json)
|
||||
{
|
||||
if (!provider || !response_json) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Try OpenAI format first: {"data": [{"id": "model1"}, ...]} */
|
||||
_parse_openai_models(provider, response_json);
|
||||
|
||||
/* If no models found, try simple array format: ["model1", ...] */
|
||||
if (!provider->models) {
|
||||
_parse_array_models(provider, response_json);
|
||||
}
|
||||
|
||||
provider->models_fresh = TRUE;
|
||||
log_info("Cached %d models for provider '%s'", g_list_length(provider->models), provider->name);
|
||||
|
||||
/* Persist models to preferences */
|
||||
_ai_save_models_for_provider(provider);
|
||||
}
|
||||
|
||||
static void
|
||||
_models_response_handler(AIProvider* provider, const gchar* response, gpointer user_data)
|
||||
{
|
||||
_parse_and_cache_models(provider, response);
|
||||
|
||||
auto_gchar gchar* msg = g_strdup_printf("Cached %d models for provider '%s'. Use '/ai start <provider> <model>' to start a chat.",
|
||||
g_list_length(provider->models), provider->name);
|
||||
_aiwin_display_response(user_data, msg);
|
||||
}
|
||||
|
||||
static gpointer
|
||||
_models_fetch_thread(gpointer data)
|
||||
{
|
||||
ai_request_t* req = (ai_request_t*)data;
|
||||
AIProvider* provider = ai_get_provider(req->provider_name);
|
||||
if (!provider) {
|
||||
g_free(req->provider_name);
|
||||
g_free(req);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Build URL for listing models */
|
||||
auto_gchar gchar* base_url = g_strdup_printf("%s%s", provider->api_url,
|
||||
g_str_has_suffix(provider->api_url, "/") ? "" : "/");
|
||||
auto_gchar gchar* request_url = g_strdup_printf("%sv1/models", base_url);
|
||||
|
||||
req->request_url = g_strdup(request_url);
|
||||
req->handler = _models_response_handler;
|
||||
|
||||
return _ai_generic_request_thread(req);
|
||||
}
|
||||
|
||||
gboolean
|
||||
ai_fetch_models(const gchar* provider_name, gpointer user_data)
|
||||
{
|
||||
if (!provider_name)
|
||||
return FALSE;
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider) {
|
||||
log_error("Provider '%s' not found for model fetch", provider_name);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* If models are fresh, just display them */
|
||||
if (provider->models_fresh && provider->models) {
|
||||
cons_show("Cached %d models for provider '%s':", g_list_length(provider->models), provider_name);
|
||||
GList* curr = provider->models;
|
||||
while (curr) {
|
||||
cons_show(" %s", (gchar*)curr->data);
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
cons_show("");
|
||||
cons_show("Use '/ai models %s --refresh' to fetch fresh models from the API.", provider_name);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Fetch models in a thread */
|
||||
ai_request_t* req = g_new0(ai_request_t, 1);
|
||||
req->provider_name = g_strdup(provider_name);
|
||||
req->user_data = user_data;
|
||||
|
||||
GThread* thread = g_thread_new("ai_models_fetch", _models_fetch_thread, req);
|
||||
if (!thread) {
|
||||
g_free(req->provider_name);
|
||||
g_free(req);
|
||||
log_error("Failed to create thread for model fetch");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
g_thread_unref(thread);
|
||||
cons_show("Fetching models for provider '%s'...", provider_name);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Session Management
|
||||
* ======================================================================== */
|
||||
@@ -708,34 +1210,23 @@ _ai_request_thread(gpointer data)
|
||||
auto_gchar gchar* json_payload = _build_json_payload(session, prompt);
|
||||
log_debug("[AI-THREAD] JSON payload: %s", json_payload);
|
||||
|
||||
/* Set up headers */
|
||||
struct curl_slist* headers = NULL;
|
||||
auto_gchar gchar* auth_header = g_strdup_printf("Authorization: Bearer %s", session->api_key);
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
headers = curl_slist_append(headers, auth_header);
|
||||
|
||||
/* Add organization header if configured */
|
||||
if (session->provider && session->provider->org_id && strlen(session->provider->org_id) > 0) {
|
||||
auto_gchar gchar* org_header = g_strdup_printf("OpenAI-Organization: %s", session->provider->org_id);
|
||||
headers = curl_slist_append(headers, org_header);
|
||||
}
|
||||
/* Build headers using shared helper */
|
||||
struct curl_slist* headers = _build_curl_headers(session->provider, session->api_key);
|
||||
|
||||
/* Response buffer */
|
||||
struct curl_response_t response;
|
||||
response.data = g_new0(gchar, 1);
|
||||
response.size = 0;
|
||||
|
||||
/* Configure request */
|
||||
/* Build request URL */
|
||||
const gchar* api_url = session->provider ? session->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);
|
||||
curl_easy_setopt(curl, CURLOPT_URL, request_url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
|
||||
/* Execute request with POST fields */
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_payload);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
@@ -17,12 +17,15 @@ typedef struct ai_message_t
|
||||
*/
|
||||
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) */
|
||||
GList* models; /* List of available models (gchar*) */
|
||||
gint32 ref_count; /* Reference count (atomic) */
|
||||
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) */
|
||||
GList* models; /* Cached models (gchar*) */
|
||||
gboolean models_fresh; /* Whether models cache is current */
|
||||
gint32 ref_count; /* Reference count (atomic) */
|
||||
} AIProvider;
|
||||
|
||||
/**
|
||||
@@ -97,6 +100,15 @@ GList* ai_list_providers(void);
|
||||
*/
|
||||
gchar* ai_providers_find(const char* const search_str, gboolean previous, void* context);
|
||||
|
||||
/**
|
||||
* Find a model name for autocomplete.
|
||||
* @param search_str The search string
|
||||
* @param previous Whether to go to previous match
|
||||
* @param context ProfAiWin* for the current session
|
||||
* @return Model name, or NULL if not found
|
||||
*/
|
||||
gchar* ai_models_find(const char* const search_str, gboolean previous, void* context);
|
||||
|
||||
/**
|
||||
* Get the API key for a provider.
|
||||
* @param provider_name The provider name
|
||||
@@ -111,6 +123,51 @@ gchar* ai_get_provider_key(const gchar* provider_name);
|
||||
*/
|
||||
void ai_set_provider_key(const gchar* provider_name, const gchar* api_key);
|
||||
|
||||
/**
|
||||
* Set the default model for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @param model The default model name
|
||||
*/
|
||||
void ai_set_provider_default_model(const gchar* provider_name, const gchar* model);
|
||||
|
||||
/**
|
||||
* Get the default model for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @return The default model name (caller must not free), or NULL if not set
|
||||
*/
|
||||
const gchar* ai_get_provider_default_model(const gchar* provider_name);
|
||||
|
||||
/**
|
||||
* Set a custom setting for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @param setting The setting key (e.g., "tools", "search")
|
||||
* @param value The setting value
|
||||
*/
|
||||
void ai_set_provider_setting(const gchar* provider_name, const gchar* setting, const gchar* value);
|
||||
|
||||
/**
|
||||
* Get a custom setting for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @param setting The setting key
|
||||
* @return The setting value (caller must free), or NULL if not set
|
||||
*/
|
||||
gchar* ai_get_provider_setting(const gchar* provider_name, const gchar* setting);
|
||||
|
||||
/**
|
||||
* Fetch available models from a provider's API.
|
||||
* @param provider_name The provider name
|
||||
* @param user_data User data (ProfAiWin* for UI display)
|
||||
* @return TRUE if the request was successfully queued, FALSE otherwise
|
||||
*/
|
||||
gboolean ai_fetch_models(const gchar* provider_name, gpointer user_data);
|
||||
|
||||
/**
|
||||
* Check if models cache is fresh for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @return TRUE if models are fresh, FALSE otherwise
|
||||
*/
|
||||
gboolean ai_models_are_fresh(const gchar* provider_name);
|
||||
|
||||
/* ========================================================================
|
||||
* Session Management
|
||||
* ======================================================================== */
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
|
||||
static char* _sub_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _notify_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _ai_model_autocomplete(ProfWin* window, const char* const input, gboolean previous, const char* prefix);
|
||||
static char* _theme_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _autoaway_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _autoconnect_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
@@ -285,12 +286,14 @@ static Autocomplete correction_ac;
|
||||
static Autocomplete avatar_ac;
|
||||
static Autocomplete ai_subcommands_ac;
|
||||
static Autocomplete ai_set_subcommands_ac;
|
||||
static Autocomplete ai_set_custom_subcommands_ac;
|
||||
static Autocomplete ai_remove_subcommands_ac;
|
||||
static Autocomplete url_ac;
|
||||
static Autocomplete executable_ac;
|
||||
static Autocomplete executable_param_ac;
|
||||
static Autocomplete intype_ac;
|
||||
static Autocomplete mood_ac;
|
||||
static Autocomplete ai_models_ac;
|
||||
static Autocomplete mood_type_ac;
|
||||
static Autocomplete strophe_ac;
|
||||
static Autocomplete strophe_sm_ac;
|
||||
@@ -465,6 +468,7 @@ static Autocomplete* all_acs[] = {
|
||||
&force_encryption_policy_ac,
|
||||
&ai_subcommands_ac,
|
||||
&ai_set_subcommands_ac,
|
||||
&ai_set_custom_subcommands_ac,
|
||||
&ai_remove_subcommands_ac
|
||||
};
|
||||
|
||||
@@ -1178,18 +1182,26 @@ cmd_ac_init(void)
|
||||
autocomplete_add(correction_ac, "off");
|
||||
autocomplete_add(correction_ac, "char");
|
||||
|
||||
autocomplete_add(ai_subcommands_ac, "set");
|
||||
autocomplete_add(ai_subcommands_ac, "remove");
|
||||
autocomplete_add(ai_subcommands_ac, "start");
|
||||
autocomplete_add(ai_subcommands_ac, "clear");
|
||||
autocomplete_add(ai_subcommands_ac, "correct");
|
||||
autocomplete_add(ai_subcommands_ac, "providers");
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "set", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "remove", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "start", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "clear", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "providers", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "switch", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "models", FALSE);
|
||||
|
||||
autocomplete_add(ai_set_subcommands_ac, "provider");
|
||||
autocomplete_add(ai_set_subcommands_ac, "token");
|
||||
autocomplete_add(ai_set_subcommands_ac, "org");
|
||||
autocomplete_add_unsorted(ai_set_subcommands_ac, "provider", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_subcommands_ac, "token", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_subcommands_ac, "default-provider", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_subcommands_ac, "default-model", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_subcommands_ac, "custom", FALSE);
|
||||
|
||||
autocomplete_add(ai_remove_subcommands_ac, "provider");
|
||||
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "tools", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "search", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "memory", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "plugins", FALSE);
|
||||
|
||||
autocomplete_add_unsorted(ai_remove_subcommands_ac, "provider", FALSE);
|
||||
|
||||
autocomplete_add(avatar_ac, "set");
|
||||
autocomplete_add(avatar_ac, "disable");
|
||||
@@ -4480,6 +4492,12 @@ _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
char* result = NULL;
|
||||
|
||||
/* Parse once for reuse - /ai <subcommand> [<arg1>] [<arg2>] */
|
||||
gboolean parse_result = FALSE;
|
||||
auto_gcharv gchar** args = parse_args(input, 1, 4, &parse_result);
|
||||
gboolean space_at_end = g_str_has_suffix(input, " ");
|
||||
int num_args = g_strv_length(args);
|
||||
|
||||
/* Top-level /ai <subcommand> autocomplete (e.g., /ai s<tab> -> /ai set) */
|
||||
result = autocomplete_param_with_func(input, "/ai set provider", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
@@ -4492,12 +4510,36 @@ _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set org <provider> <org_id> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai set org", ai_providers_find, previous, NULL);
|
||||
// /ai set default-provider <provider> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai set default-provider", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set default-model <provider> <model> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai set default-model", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set custom <provider> <setting> <value> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai set custom", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set custom <provider> <setting> - autocomplete settings
|
||||
/* args[0]="set", args[1]="custom", args[2]=provider (if typed), args[3]=setting (if typed) */
|
||||
if (num_args == 3 && g_strcmp0(args[1], "custom") == 0) {
|
||||
/* /ai set custom <provider> - check if provider is valid */
|
||||
if (ai_get_provider(args[2])) {
|
||||
/* Valid provider, try settings autocomplete */
|
||||
result = autocomplete_param_with_ac(input, "/ai set custom ", ai_set_custom_subcommands_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
// /ai set <subcommand> - autocomplete subcommands
|
||||
result = autocomplete_param_with_ac(input, "/ai set", ai_set_subcommands_ac, TRUE, previous);
|
||||
if (result) {
|
||||
@@ -4515,25 +4557,95 @@ _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai start [<provider>/<model>] - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai start", ai_providers_find, previous, NULL);
|
||||
// /ai start [<provider>] [<model>] - use shared parse_args
|
||||
if (parse_result && args && args[0] != NULL && g_strcmp0(args[0], "start") == 0) {
|
||||
/* args[0]="start", args[1]=provider (if typed), args[2]=model (if typed) */
|
||||
if (num_args == 1 || (num_args == 2 && !space_at_end)) {
|
||||
result = autocomplete_param_with_func(input, "/ai start", ai_providers_find, previous, NULL);
|
||||
} else {
|
||||
/* /ai start <provider> <model> - model autocomplete */
|
||||
result = _ai_model_autocomplete(window, input, previous, "/ai start ");
|
||||
}
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// /ai switch <provider> <model> - autocomplete model names for specified provider
|
||||
result = _ai_model_autocomplete(window, input, previous, "/ai switch");
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai clear [<provider>] - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai clear", ai_providers_find, previous, NULL);
|
||||
result = autocomplete_param_with_func(input, "/ai switch", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai correct <provider> <text>
|
||||
result = autocomplete_param_with_func(input, "/ai correct", ai_providers_find, previous, NULL);
|
||||
// /ai switch <model> - autocomplete model names from current session's provider
|
||||
ProfAiWin* aiwin = (window && window->type == WIN_AI) ? (ProfAiWin*)window : wins_get_ai();
|
||||
result = autocomplete_param_with_func(input, "/ai switch", ai_models_find, previous, aiwin);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai models <provider> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai models", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai clear - no autocomplete
|
||||
// /ai providers - no autocomplete
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/ai", ai_subcommands_ac, FALSE, previous);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Autocomplete model names for /ai <cmd> <provider> <model> patterns.
|
||||
* Extracts provider from input and provides model completions from that provider's cached models.
|
||||
* Uses static ai_models_ac to preserve cycling state (last_found) across calls.
|
||||
*/
|
||||
static char*
|
||||
_ai_model_autocomplete(ProfWin* window, const char* const input, gboolean previous, const char* cmd_prefix)
|
||||
{
|
||||
if (!g_str_has_prefix(input, cmd_prefix)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Extract provider name from input (after cmd_prefix) */
|
||||
auto_gchar gchar* rest = g_strdup(input + strlen(cmd_prefix));
|
||||
char* space = strchr(rest, ' ');
|
||||
if (space) {
|
||||
*space = '\0';
|
||||
}
|
||||
|
||||
/* Look up the provider by name and get its models */
|
||||
AIProvider* prov = ai_get_provider(rest);
|
||||
if (!prov || !prov->models) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!ai_models_ac) {
|
||||
ai_models_ac = autocomplete_new();
|
||||
}
|
||||
/* Convert GList* to char** for autocomplete_update */
|
||||
int model_count = g_list_length(prov->models);
|
||||
char** model_array = g_new0(char*, model_count + 1);
|
||||
GList* curr = prov->models;
|
||||
int i = 0;
|
||||
while (curr) {
|
||||
model_array[i++] = curr->data;
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
|
||||
autocomplete_update(ai_models_ac, model_array);
|
||||
g_free(model_array);
|
||||
|
||||
auto_gchar gchar* full_prefix = g_strdup_printf("%s%s", cmd_prefix, rest);
|
||||
|
||||
char* result = autocomplete_param_with_ac(input, full_prefix, ai_models_ac, TRUE, previous);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -2794,8 +2794,9 @@ static const struct cmd_t command_defs[] = {
|
||||
{ "remove", cmd_ai_remove },
|
||||
{ "start", cmd_ai_start },
|
||||
{ "clear", cmd_ai_clear },
|
||||
{ "correct", cmd_ai_correct },
|
||||
{ "providers", cmd_ai_providers })
|
||||
{ "providers", cmd_ai_providers },
|
||||
{ "switch", cmd_ai_switch },
|
||||
{ "models", cmd_ai_models })
|
||||
CMD_MAINFUNC(cmd_ai)
|
||||
CMD_TAGS(
|
||||
CMD_TAG_CHAT)
|
||||
@@ -2803,43 +2804,52 @@ static const struct cmd_t command_defs[] = {
|
||||
"/ai",
|
||||
"/ai set provider <name> <url>",
|
||||
"/ai set token <provider> <token>",
|
||||
"/ai set org <provider> <org_id>",
|
||||
"/ai set default-provider <provider>",
|
||||
"/ai set default-model <provider> <model>",
|
||||
"/ai set custom <provider> <setting> <value>",
|
||||
"/ai remove provider <name>",
|
||||
"/ai providers",
|
||||
"/ai providers-list",
|
||||
"/ai start [<provider>/<model>]",
|
||||
"/ai model <model>",
|
||||
"/ai clear",
|
||||
"/ai correct <message>")
|
||||
"/ai start [<provider>] [<model>]",
|
||||
"/ai switch <provider> [<model>]",
|
||||
"/ai switch <model>",
|
||||
"/ai models <provider>",
|
||||
"/ai clear")
|
||||
CMD_DESC(
|
||||
"Interact with AI models via OpenAI-compatible APIs. "
|
||||
"Supports multiple providers (openai, perplexity, custom). "
|
||||
"Each provider has its own API key and endpoint configuration. "
|
||||
"Each provider has its own API key, endpoint, default model, and settings. "
|
||||
"Chat history is maintained per session and not persisted locally.")
|
||||
CMD_ARGS(
|
||||
{ "", "Display current AI settings and configured providers" },
|
||||
{ "set provider <name> <url>", "Add or update a provider with custom API endpoint" },
|
||||
{ "set token <provider> <token>", "Set API token for a provider (e.g., openai, perplexity)" },
|
||||
{ "set org <provider> <org_id>", "Set organization ID for a provider (optional)" },
|
||||
{ "set default-provider <provider>", "Set global default provider for /ai start" },
|
||||
{ "set default-model <provider> <model>", "Set default model for a provider" },
|
||||
{ "set custom <provider> <setting> <value>", "Set provider-level setting (e.g., tools, search)" },
|
||||
{ "remove provider <name>", "Remove a custom provider" },
|
||||
{ "providers", "List all available providers" },
|
||||
{ "providers-list", "List configured providers with keys status" },
|
||||
{ "start [<provider>/<model>]", "Start new AI chat (e.g., openai/gpt-4o)" },
|
||||
{ "model <model>", "Change model in current chat window" },
|
||||
{ "clear", "Clear current chat history" },
|
||||
{ "correct <message>", "Correct last user message and get new response" })
|
||||
{ "providers", "List configured providers with full details" },
|
||||
{ "start [<provider>] [<model>]", "Start new AI chat (space-separated, uses defaults if omitted)" },
|
||||
{ "switch <provider> [<model>]", "Switch to different provider (and optionally model)" },
|
||||
{ "switch <model>", "Change model in current session (keeps provider)" },
|
||||
{ "models <provider>", "Fetch and display available models for provider" },
|
||||
{ "clear", "Clear current chat history" })
|
||||
CMD_EXAMPLES(
|
||||
"/ai",
|
||||
"/ai set token openai sk-xxx",
|
||||
"/ai set token perplexity pplx-xxx",
|
||||
"/ai set provider custom https://my-api.com/v1/chat/completions",
|
||||
"/ai set org openai my-org-id",
|
||||
"/ai set default-provider perplexity",
|
||||
"/ai set default-model perplexity sonar",
|
||||
"/ai set custom perplexity tools enabled",
|
||||
"/ai remove provider custom",
|
||||
"/ai start openai/gpt-4o",
|
||||
"/ai start perplexity/sonar",
|
||||
"/ai providers-list",
|
||||
"/ai clear",
|
||||
"/ai correct I meant something else")
|
||||
"/ai start",
|
||||
"/ai start perplexity",
|
||||
"/ai start perplexity sonar",
|
||||
"/ai start openai gpt-4o",
|
||||
"/ai switch gpt-4o",
|
||||
"/ai switch openai gpt-5.4-nano",
|
||||
"/ai models perplexity",
|
||||
"/ai clear")
|
||||
},
|
||||
|
||||
// NEXT-COMMAND (search helper)
|
||||
|
||||
@@ -10795,21 +10795,28 @@ cmd_ai(ProfWin* window, const char* const command, gchar** args)
|
||||
cons_show("AI Chat - OpenAI-compatible API client");
|
||||
cons_show("");
|
||||
|
||||
// List available providers
|
||||
// List configured providers with full details
|
||||
GList* providers = ai_list_providers();
|
||||
cons_show("Available providers:");
|
||||
cons_show("Configured providers:");
|
||||
for (GList* curr = providers; curr; curr = g_list_next(curr)) {
|
||||
AIProvider* provider = curr->data;
|
||||
auto_gchar gchar* key = ai_get_provider_key(provider->name);
|
||||
cons_show(" %s (URL: %s, Key: %s)",
|
||||
provider->name,
|
||||
provider->api_url,
|
||||
key ? "set" : "not set");
|
||||
const gchar* default_model = ai_get_provider_default_model(provider->name);
|
||||
cons_show(" %s", provider->name);
|
||||
cons_show(" URL: %s", provider->api_url);
|
||||
cons_show(" Key: %s", key ? "configured" : "NOT configured");
|
||||
if (default_model) {
|
||||
cons_show(" Default model: %s", default_model);
|
||||
}
|
||||
if (provider->models && g_list_length(provider->models) > 0) {
|
||||
cons_show(" Cached models: %d", g_list_length(provider->models));
|
||||
}
|
||||
cons_show("");
|
||||
}
|
||||
g_list_free(providers);
|
||||
|
||||
cons_show("");
|
||||
cons_show("Use '/ai start <provider>/<model>' to begin a chat.");
|
||||
cons_show("Use '/ai start' to begin a chat (uses default provider/model).");
|
||||
cons_show("Use '/ai models <provider>' to fetch available models.");
|
||||
cons_show("Available models: https://models.litellm.ai/");
|
||||
return TRUE;
|
||||
}
|
||||
@@ -10849,20 +10856,41 @@ cmd_ai_set(ProfWin* window, const char* const command, gchar** args)
|
||||
cons_show("API token set for provider: %s", args[2]);
|
||||
cons_show("");
|
||||
return TRUE;
|
||||
} else if (g_strcmp0(args[1], "org") == 0) {
|
||||
// /ai set org <provider> <org_id>
|
||||
} else if (g_strcmp0(args[1], "default-model") == 0) {
|
||||
// /ai set default-model <provider> <model>
|
||||
if (g_strv_length(args) < 4) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
AIProvider* provider = ai_get_provider(args[2]);
|
||||
if (provider) {
|
||||
g_free(provider->org_id);
|
||||
provider->org_id = g_strdup(args[3]);
|
||||
cons_show("Organization ID set for provider: %s", args[2]);
|
||||
} else {
|
||||
cons_show("Provider '%s' not found. Add it first with '/ai set provider'.", args[2]);
|
||||
ai_set_provider_default_model(args[2], args[3]);
|
||||
cons_show("Default model for provider '%s' set to: %s", args[2], args[3]);
|
||||
cons_show("");
|
||||
return TRUE;
|
||||
} else if (g_strcmp0(args[1], "custom") == 0) {
|
||||
// /ai set custom <provider> <setting> <value>
|
||||
if (g_strv_length(args) < 5) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
ai_set_provider_setting(args[2], args[3], args[4]);
|
||||
cons_show("Setting '%s' for provider '%s' set to: %s", args[3], args[2], args[4]);
|
||||
cons_show("");
|
||||
return TRUE;
|
||||
} else if (g_strcmp0(args[1], "default-provider") == 0) {
|
||||
// /ai set default-provider <provider>
|
||||
if (g_strv_length(args) < 3) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
const gchar* provider_name = args[2];
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider) {
|
||||
cons_show_error("Provider '%s' not found. Use '/ai set provider %s <url>' to add it.",
|
||||
provider_name, provider_name);
|
||||
return TRUE;
|
||||
}
|
||||
prefs_set_string(PREF_AI_PROVIDER, provider_name);
|
||||
cons_show("Default provider set to: %s", provider_name);
|
||||
cons_show("");
|
||||
return TRUE;
|
||||
}
|
||||
@@ -10897,26 +10925,31 @@ cmd_ai_remove(ProfWin* window, const char* const command, gchar** args)
|
||||
gboolean
|
||||
cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// /ai start [<provider>/<model>]
|
||||
const gchar* provider_model = (g_strv_length(args) >= 2) ? args[1] : NULL;
|
||||
// /ai start [<provider>] [<model>]
|
||||
// Space-separated, no slash syntax
|
||||
const gchar* provider_name = NULL;
|
||||
const gchar* model = NULL;
|
||||
|
||||
auto_gchar gchar* owned_provider_name = NULL;
|
||||
const gchar* provider_name = "openai";
|
||||
const gchar* model = "gpt-4o";
|
||||
|
||||
if (provider_model) {
|
||||
const gchar* slash = strchr(provider_model, '/');
|
||||
if (slash) {
|
||||
owned_provider_name = g_strndup(provider_model, slash - provider_model);
|
||||
provider_name = owned_provider_name;
|
||||
model = slash + 1;
|
||||
} else {
|
||||
// Just a model name, use default provider
|
||||
model = provider_model;
|
||||
}
|
||||
if (g_strv_length(args) >= 2) {
|
||||
provider_name = args[1];
|
||||
}
|
||||
if (g_strv_length(args) >= 3) {
|
||||
model = args[2];
|
||||
}
|
||||
|
||||
// Resolve defaults
|
||||
auto_gchar gchar* owned_provider_name = NULL;
|
||||
if (!provider_name) {
|
||||
// Use default provider from preferences
|
||||
auto_gchar gchar* default_provider = prefs_get_string(PREF_AI_PROVIDER);
|
||||
if (default_provider && strlen(default_provider) > 0) {
|
||||
provider_name = default_provider;
|
||||
}
|
||||
}
|
||||
if (!provider_name) {
|
||||
provider_name = "openai"; // Fallback
|
||||
}
|
||||
|
||||
// Check if provider exists
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider) {
|
||||
cons_show_error("Provider '%s' not found. Use '/ai set provider %s <url>' to add it.",
|
||||
@@ -10924,6 +10957,14 @@ cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Get model: explicit > provider default > hardcoded fallback
|
||||
if (!model) {
|
||||
model = ai_get_provider_default_model(provider_name);
|
||||
}
|
||||
if (!model) {
|
||||
model = "gpt-4o"; // Fallback
|
||||
}
|
||||
|
||||
// Check for API key
|
||||
gchar* api_key = ai_get_provider_key(provider_name);
|
||||
if (!api_key || strlen(api_key) == 0) {
|
||||
@@ -10972,6 +11013,161 @@ cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_model(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// /ai model <model>
|
||||
if (args[1] == NULL) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
const gchar* model = args[1];
|
||||
|
||||
// Get current AI window
|
||||
ProfAiWin* aiwin = (window && window->type == WIN_AI) ? (ProfAiWin*)window : wins_get_ai();
|
||||
if (!aiwin) {
|
||||
cons_show("No active AI chat window. Use '/ai start' first.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
|
||||
|
||||
if (!aiwin->session) {
|
||||
cons_show("No active session in this chat window.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
ai_session_set_model(aiwin->session, model);
|
||||
cons_show("Session model changed to: %s", model);
|
||||
cons_show("");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_switch(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// /ai switch <provider> [<model>] - Change provider and optionally model
|
||||
// /ai switch <model> - Change model only (keeps current provider)
|
||||
// Modifies the existing session's provider and model instead of recreating it
|
||||
if (g_strv_length(args) < 2) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Get current AI window
|
||||
ProfAiWin* aiwin = (window && window->type == WIN_AI) ? (ProfAiWin*)window : wins_get_ai();
|
||||
if (!aiwin) {
|
||||
cons_show("No active AI chat window. Use '/ai start' first.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
|
||||
|
||||
if (!aiwin->session) {
|
||||
cons_show("No active session in this chat window.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
const gchar* arg1 = args[1];
|
||||
const gchar* arg2 = (g_strv_length(args) >= 3) ? args[2] : NULL;
|
||||
|
||||
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) {
|
||||
model = arg2;
|
||||
} else {
|
||||
model = ai_get_provider_default_model(provider_name);
|
||||
}
|
||||
if (!model) {
|
||||
cons_show_error("No model specified and no default model set for provider '%s'.", provider_name);
|
||||
cons_show("Use '/ai set default-model %s <model>' or '/ai switch %s <model>'.", provider_name, provider_name);
|
||||
return TRUE;
|
||||
}
|
||||
} else {
|
||||
// arg1 is a model name, keep current provider
|
||||
provider_name = aiwin->session->provider_name;
|
||||
model = arg1;
|
||||
}
|
||||
|
||||
// Check for API key
|
||||
auto_gchar gchar* api_key = ai_get_provider_key(provider_name);
|
||||
if (!api_key || strlen(api_key) == 0) {
|
||||
cons_show_error("No API key set for provider '%s'. Use '/ai set token %s <key>' first.",
|
||||
provider_name, provider_name);
|
||||
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);
|
||||
|
||||
// Update window title
|
||||
win_println((ProfWin*)aiwin, THEME_DEFAULT, "-", "AI Chat: %s/%s", provider_name, model);
|
||||
|
||||
cons_show("Switched to %s/%s", provider_name, model);
|
||||
cons_show("");
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_models(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// /ai models <provider> [--refresh]
|
||||
if (args[1] == NULL) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
const gchar* provider_name = args[1];
|
||||
gboolean refresh = (g_strv_length(args) >= 3) && (g_strcmp0(args[2], "--refresh") == 0);
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider) {
|
||||
cons_show_error("Provider '%s' not found.", provider_name);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (refresh || !ai_models_are_fresh(provider_name)) {
|
||||
// Fetch models from API
|
||||
ProfAiWin* aiwin = (window && window->type == WIN_AI) ? (ProfAiWin*)window : wins_get_ai();
|
||||
ai_fetch_models(provider_name, aiwin);
|
||||
} else {
|
||||
// Display cached models
|
||||
cons_show("Cached models for provider '%s':", provider_name);
|
||||
GList* curr = provider->models;
|
||||
while (curr) {
|
||||
cons_show(" %s", (gchar*)curr->data);
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
cons_show("");
|
||||
cons_show("Use '/ai models %s --refresh' to fetch fresh models from the API.", provider_name);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_clear(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
|
||||
@@ -171,8 +171,9 @@ gboolean cmd_ai_set(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_remove(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_start(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_clear(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_correct(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_providers(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_switch(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_models(ProfWin* window, const char* const command, gchar** args);
|
||||
|
||||
gboolean cmd_plugins(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_plugins_sourcepath(ProfWin* window, const char* const command, gchar** args);
|
||||
|
||||
@@ -1786,6 +1786,83 @@ prefs_free_ai_tokens(GList* tokens)
|
||||
}
|
||||
}
|
||||
|
||||
gboolean
|
||||
prefs_ai_set_models(const char* const provider, const gchar* const* models, gint count)
|
||||
{
|
||||
if (!provider || count <= 0)
|
||||
return FALSE;
|
||||
|
||||
if (!prefs)
|
||||
return FALSE;
|
||||
|
||||
/* Build semicolon-separated model list */
|
||||
GString* model_str = g_string_new("");
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (i > 0) {
|
||||
g_string_append_c(model_str, ';');
|
||||
}
|
||||
g_string_append(model_str, models[i]);
|
||||
}
|
||||
|
||||
g_key_file_set_string(prefs, PREF_GROUP_AI, g_strconcat(provider, "_models", NULL), model_str->str);
|
||||
g_string_free(model_str, TRUE);
|
||||
_save_prefs();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
prefs_ai_remove_models(const char* const provider)
|
||||
{
|
||||
if (!provider)
|
||||
return FALSE;
|
||||
|
||||
if (!prefs)
|
||||
return FALSE;
|
||||
|
||||
auto_gchar gchar* key = g_strconcat(provider, "_models", NULL);
|
||||
if (!g_key_file_has_key(prefs, PREF_GROUP_AI, key, NULL)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
g_key_file_remove_key(prefs, PREF_GROUP_AI, key, NULL);
|
||||
_save_prefs();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
GList*
|
||||
prefs_ai_get_models(const char* const provider)
|
||||
{
|
||||
if (!provider || !prefs)
|
||||
return NULL;
|
||||
|
||||
auto_gchar gchar* key = g_strconcat(provider, "_models", NULL);
|
||||
if (!g_key_file_has_key(prefs, PREF_GROUP_AI, key, NULL)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto_gchar gchar* model_str = g_key_file_get_string(prefs, PREF_GROUP_AI, key, NULL);
|
||||
if (!model_str || strlen(model_str) == 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
GList* result = NULL;
|
||||
gchar** models = g_strsplit(model_str, ";", -1);
|
||||
for (int i = 0; models[i] != NULL; i++) {
|
||||
result = g_list_append(result, g_strdup(models[i]));
|
||||
}
|
||||
g_strfreev(models);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void
|
||||
prefs_free_ai_models(GList* models)
|
||||
{
|
||||
if (models) {
|
||||
g_list_free_full(models, g_free);
|
||||
}
|
||||
}
|
||||
|
||||
// get the preference group for a specific preference
|
||||
// for example the PREF_BEEP setting ("beep" in .profrc, see _get_key) belongs
|
||||
// to the [ui] section.
|
||||
|
||||
@@ -366,4 +366,10 @@ char* prefs_ai_get_token(const char* const provider);
|
||||
GList* prefs_ai_list_tokens(void);
|
||||
void prefs_free_ai_tokens(GList* tokens);
|
||||
|
||||
/* AI model cache management */
|
||||
gboolean prefs_ai_set_models(const char* const provider, const gchar* const* models, gint count);
|
||||
gboolean prefs_ai_remove_models(const char* const provider);
|
||||
GList* prefs_ai_get_models(const char* const provider);
|
||||
void prefs_free_ai_models(GList* models);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -461,3 +461,186 @@ test_ai_providers_find_case_insensitive(void** state)
|
||||
assert_non_null(result);
|
||||
assert_string_equal("openai", result);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* AI Autocomplete Integration Tests
|
||||
* ======================================================================== */
|
||||
|
||||
void
|
||||
test_ai_start_provider_autocomplete_only_on_exact(void** state)
|
||||
{
|
||||
auto_gchar gchar* result = ai_providers_find("", FALSE, NULL);
|
||||
assert_non_null(result);
|
||||
assert_string_equal("openai", result);
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_models_find_null_session(void** state)
|
||||
{
|
||||
auto_gchar gchar* result = ai_models_find("gpt", FALSE, NULL);
|
||||
assert_null(result);
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_models_find_null_provider(void** state)
|
||||
{
|
||||
ai_add_provider("temp", "https://temp.api.com/v1", NULL);
|
||||
AISession* session = ai_session_create("temp", "gpt-4");
|
||||
assert_non_null(session);
|
||||
ai_session_unref(session);
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_providers_find_cycling(void** state)
|
||||
{
|
||||
/* NULL search_str should cycle through providers */
|
||||
auto_gchar gchar* result1 = ai_providers_find(NULL, FALSE, NULL);
|
||||
assert_non_null(result1);
|
||||
|
||||
auto_gchar gchar* result2 = ai_providers_find(NULL, FALSE, NULL);
|
||||
assert_non_null(result2);
|
||||
|
||||
/* Cycling: consecutive calls with NULL should return different providers */
|
||||
assert_string_not_equal(result1, result2);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Provider Default Model and Settings Tests
|
||||
* ======================================================================== */
|
||||
|
||||
void
|
||||
test_ai_set_provider_default_model(void** state)
|
||||
{
|
||||
/* Set default model for openai provider */
|
||||
ai_set_provider_default_model("openai", "gpt-5");
|
||||
|
||||
/* Verify the model was set */
|
||||
const gchar* model = ai_get_provider_default_model("openai");
|
||||
assert_non_null(model);
|
||||
assert_string_equal("gpt-5", model);
|
||||
|
||||
/* Update default model */
|
||||
ai_set_provider_default_model("openai", "gpt-4o");
|
||||
model = ai_get_provider_default_model("openai");
|
||||
assert_non_null(model);
|
||||
assert_string_equal("gpt-4o", model);
|
||||
|
||||
/* Set default model for perplexity */
|
||||
ai_set_provider_default_model("perplexity", "sonar-pro");
|
||||
model = ai_get_provider_default_model("perplexity");
|
||||
assert_non_null(model);
|
||||
assert_string_equal("sonar-pro", model);
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_get_provider_default_model(void** state)
|
||||
{
|
||||
/* NULL provider returns NULL */
|
||||
assert_null(ai_get_provider_default_model(NULL));
|
||||
|
||||
/* Non-existent provider returns NULL */
|
||||
assert_null(ai_get_provider_default_model("nonexistent"));
|
||||
|
||||
/* Default providers may or may not have default models set initially */
|
||||
/* After setting, they should return the model */
|
||||
ai_set_provider_default_model("openai", "test-model");
|
||||
assert_string_equal("test-model", ai_get_provider_default_model("openai"));
|
||||
|
||||
/* NULL model argument should be ignored (no change) */
|
||||
ai_set_provider_default_model("openai", NULL);
|
||||
/* After setting NULL, the model should still be "test-model" because
|
||||
* ai_set_provider_default_model returns early on NULL model */
|
||||
assert_string_equal("test-model", ai_get_provider_default_model("openai"));
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_set_provider_setting(void** state)
|
||||
{
|
||||
/* NULL provider should be ignored */
|
||||
ai_set_provider_setting(NULL, "tools", "enabled");
|
||||
|
||||
/* NULL setting should be ignored */
|
||||
ai_set_provider_setting("openai", NULL, "enabled");
|
||||
|
||||
/* Non-existent provider should log warning but not crash */
|
||||
ai_set_provider_setting("nonexistent", "tools", "enabled");
|
||||
|
||||
/* Set a setting for openai */
|
||||
ai_set_provider_setting("openai", "tools", "enabled");
|
||||
ai_set_provider_setting("openai", "search", "disabled");
|
||||
|
||||
/* Verify settings were set */
|
||||
gchar* tools = ai_get_provider_setting("openai", "tools");
|
||||
assert_non_null(tools);
|
||||
assert_string_equal("enabled", tools);
|
||||
g_free(tools);
|
||||
|
||||
gchar* search = ai_get_provider_setting("openai", "search");
|
||||
assert_non_null(search);
|
||||
assert_string_equal("disabled", search);
|
||||
g_free(search);
|
||||
|
||||
/* NULL value removes the setting */
|
||||
ai_set_provider_setting("openai", "tools", NULL);
|
||||
tools = ai_get_provider_setting("openai", "tools");
|
||||
assert_null(tools);
|
||||
|
||||
/* Setting that was never set returns NULL */
|
||||
assert_null(ai_get_provider_setting("openai", "nonexistent_setting"));
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_get_provider_setting(void** state)
|
||||
{
|
||||
/* NULL provider returns NULL */
|
||||
assert_null(ai_get_provider_setting(NULL, "tools"));
|
||||
|
||||
/* NULL setting returns NULL */
|
||||
assert_null(ai_get_provider_setting("openai", NULL));
|
||||
|
||||
/* Non-existent provider returns NULL */
|
||||
assert_null(ai_get_provider_setting("nonexistent", "tools"));
|
||||
|
||||
/* Provider without settings returns NULL */
|
||||
assert_null(ai_get_provider_setting("perplexity", "tools"));
|
||||
|
||||
/* Set and get setting */
|
||||
ai_set_provider_setting("openai", "temperature", "0.7");
|
||||
gchar* temp = ai_get_provider_setting("openai", "temperature");
|
||||
assert_non_null(temp);
|
||||
assert_string_equal("0.7", temp);
|
||||
g_free(temp);
|
||||
|
||||
/* Setting value is a copy (caller must free) */
|
||||
ai_set_provider_setting("openai", "max_tokens", "2048");
|
||||
temp = ai_get_provider_setting("openai", "max_tokens");
|
||||
assert_non_null(temp);
|
||||
assert_string_equal("2048", temp);
|
||||
g_free(temp);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Model Caching Tests
|
||||
* ======================================================================== */
|
||||
|
||||
void
|
||||
test_ai_models_are_fresh_initial(void** state)
|
||||
{
|
||||
/* After init, models should be fresh (or at least not crash) */
|
||||
/* The ai_client_init() loads models from prefs, which may be empty initially */
|
||||
/* So we just verify the function works correctly */
|
||||
|
||||
/* NULL provider returns FALSE */
|
||||
assert_false(ai_models_are_fresh(NULL));
|
||||
|
||||
/* Non-existent provider returns FALSE */
|
||||
assert_false(ai_models_are_fresh("nonexistent"));
|
||||
|
||||
/* Default providers - check that function doesn't crash */
|
||||
/* Freshness depends on whether models were loaded from prefs */
|
||||
gboolean fresh_openai = ai_models_are_fresh("openai");
|
||||
assert_true(fresh_openai == TRUE || fresh_openai == FALSE); /* Just verify no crash */
|
||||
|
||||
gboolean fresh_perplexity = ai_models_are_fresh("perplexity");
|
||||
assert_true(fresh_perplexity == TRUE || fresh_perplexity == FALSE); /* Just verify no crash */
|
||||
}
|
||||
|
||||
@@ -40,3 +40,15 @@ void test_ai_providers_find_previous(void** state);
|
||||
void test_ai_providers_find_null_search_str(void** state);
|
||||
void test_ai_providers_find_empty_search_str(void** state);
|
||||
void test_ai_providers_find_case_insensitive(void** state);
|
||||
/* Provider default model and settings tests */
|
||||
void test_ai_set_provider_default_model(void** state);
|
||||
void test_ai_get_provider_default_model(void** state);
|
||||
void test_ai_set_provider_setting(void** state);
|
||||
void test_ai_get_provider_setting(void** state);
|
||||
/* Model caching tests */
|
||||
void test_ai_models_are_fresh_initial(void** state);
|
||||
/* AI autocomplete integration tests */
|
||||
void test_ai_start_provider_autocomplete_only_on_exact(void** state);
|
||||
void test_ai_models_find_null_session(void** state);
|
||||
void test_ai_models_find_null_provider(void** state);
|
||||
void test_ai_providers_find_cycling(void** state);
|
||||
|
||||
@@ -690,6 +690,18 @@ main(int argc, char* argv[])
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_null_search_str, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_empty_search_str, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_case_insensitive, ai_client_setup, ai_client_teardown),
|
||||
/* Provider default model and settings tests */
|
||||
cmocka_unit_test_setup_teardown(test_ai_set_provider_default_model, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_get_provider_default_model, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_set_provider_setting, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_get_provider_setting, ai_client_setup, ai_client_teardown),
|
||||
/* Model caching tests */
|
||||
cmocka_unit_test_setup_teardown(test_ai_models_are_fresh_initial, ai_client_setup, ai_client_teardown),
|
||||
/* AI autocomplete integration tests */
|
||||
cmocka_unit_test_setup_teardown(test_ai_start_provider_autocomplete_only_on_exact, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_models_find_null_session, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_models_find_null_provider, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_cycling, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test(test_ai_json_escape),
|
||||
cmocka_unit_test(test_ai_json_escape_null),
|
||||
cmocka_unit_test(test_ai_json_escape_empty),
|
||||
|
||||
Reference in New Issue
Block a user