mirror of
https://git.jabber.space/devs/cproof.git
synced 2026-07-18 14:26:21 +00:00
feat(ai): persist custom providers to config and move defaults to preferences
Fix /ai set provider <name> <url> not persisting across restarts. - Add prefs_ai_set_provider(), prefs_ai_remove_provider(), prefs_ai_get_provider_url(), prefs_ai_list_providers(), prefs_free_ai_providers(), and prefs_ai_get_default_provider_url() to preferences.h/c for provider persistence. - Move default provider URL definitions (openai, perplexity) from ai_client.c to preferences.c, following the established pattern where defaults are returned as fallbacks when no config value exists. - Modify ai_add_provider() to persist new/updated providers to config. - Modify ai_remove_provider() to remove provider from config. - Add internal _ai_add_provider_nopersist() helper for adding default providers and loading from config without double-persisting. - Update ai_client_init() to load custom providers from config on startup, allowing users to override defaults or add new ones. Users now have full control: default providers remain as fallbacks, user-added providers persist to config, and users can override or remove defaults freely.
This commit is contained in:
@@ -24,10 +24,6 @@
|
||||
#include <pthread.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Default providers */
|
||||
#define DEFAULT_OPENAI_URL "https://api.openai.com/"
|
||||
#define DEFAULT_PERPLEXITY_URL "https://api.perplexity.ai/"
|
||||
|
||||
/* Global state */
|
||||
static GHashTable* providers = NULL;
|
||||
static GHashTable* provider_keys = NULL;
|
||||
@@ -36,6 +32,7 @@ static Autocomplete providers_ac = NULL;
|
||||
/* ========================================================================
|
||||
* Forward declarations
|
||||
* ======================================================================== */
|
||||
static AIProvider* _ai_add_provider_nopersist(const gchar* name, const gchar* api_url);
|
||||
static void _ai_load_models_for_provider(AIProvider* provider);
|
||||
static void _ai_save_models_for_provider(AIProvider* provider);
|
||||
|
||||
@@ -282,24 +279,33 @@ ai_client_init(void)
|
||||
/* Create autocomplete for provider names */
|
||||
providers_ac = autocomplete_new();
|
||||
|
||||
/* Add default providers */
|
||||
ai_add_provider("openai", DEFAULT_OPENAI_URL);
|
||||
ai_add_provider("perplexity", DEFAULT_PERPLEXITY_URL);
|
||||
/* Get all providers (defaults if nothing configured, otherwise config values) */
|
||||
GList* all_providers = prefs_ai_get_providers();
|
||||
for (GList* curr = all_providers; curr; curr = g_list_next(curr)) {
|
||||
/* List contains pairs: name, url, name, url, ... */
|
||||
gchar* name = (gchar*)curr->data;
|
||||
curr = g_list_next(curr);
|
||||
if (!curr)
|
||||
break;
|
||||
gchar* url = (gchar*)curr->data;
|
||||
_ai_add_provider_nopersist(name, url);
|
||||
}
|
||||
prefs_free_ai_providers(all_providers);
|
||||
|
||||
/* 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;
|
||||
GList* models_curr = provider_list;
|
||||
while (models_curr) {
|
||||
AIProvider* provider = (AIProvider*)models_curr->data;
|
||||
_ai_load_models_for_provider(provider);
|
||||
curr = g_list_next(curr);
|
||||
models_curr = g_list_next(models_curr);
|
||||
}
|
||||
g_list_free(provider_list);
|
||||
|
||||
log_info("AI client initialized with default providers: openai, perplexity");
|
||||
log_info("AI client initialized with providers from preferences");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -330,6 +336,29 @@ ai_get_provider(const gchar* name)
|
||||
return g_hash_table_lookup(providers, name);
|
||||
}
|
||||
|
||||
/* Internal helper to add provider without persisting to config.
|
||||
* Used for default providers and loading from config on startup. */
|
||||
static AIProvider*
|
||||
_ai_add_provider_nopersist(const gchar* name, const gchar* api_url)
|
||||
{
|
||||
/* Check if provider already exists */
|
||||
AIProvider* existing = g_hash_table_lookup(providers, name);
|
||||
if (existing) {
|
||||
g_free(existing->api_url);
|
||||
existing->api_url = g_strdup(api_url);
|
||||
return ai_provider_ref(existing);
|
||||
}
|
||||
|
||||
/* Create new provider (ref_count=1 owned by hash table) */
|
||||
AIProvider* provider = ai_provider_new(name, api_url);
|
||||
g_hash_table_insert(providers, g_strdup(name), provider);
|
||||
|
||||
/* Sync autocomplete */
|
||||
autocomplete_add(providers_ac, name);
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
AIProvider*
|
||||
ai_add_provider(const gchar* name, const gchar* api_url)
|
||||
{
|
||||
@@ -346,16 +375,17 @@ ai_add_provider(const gchar* name, const gchar* api_url)
|
||||
/* Update existing provider */
|
||||
g_free(existing->api_url);
|
||||
existing->api_url = g_strdup(api_url);
|
||||
/* Persist the updated URL to config */
|
||||
prefs_ai_set_provider(name, api_url);
|
||||
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);
|
||||
g_hash_table_insert(providers, g_strdup(name), provider);
|
||||
/* Persist the new provider to config */
|
||||
prefs_ai_set_provider(name, api_url);
|
||||
|
||||
/* Sync autocomplete */
|
||||
autocomplete_add(providers_ac, name);
|
||||
/* Add to hash table (reuse internal helper) */
|
||||
AIProvider* provider = _ai_add_provider_nopersist(name, api_url);
|
||||
|
||||
log_info("Added provider: %s (URL: %s)", name, api_url);
|
||||
return provider; /* Caller gets non-owning pointer; hash table owns ref */
|
||||
@@ -367,6 +397,9 @@ ai_remove_provider(const gchar* name)
|
||||
if (!name || !providers)
|
||||
return FALSE;
|
||||
|
||||
/* Remove from config before removing from hash table */
|
||||
prefs_ai_remove_provider(name);
|
||||
|
||||
/* Sync autocomplete before removing */
|
||||
autocomplete_remove(providers_ac, name);
|
||||
|
||||
@@ -1433,7 +1466,7 @@ _ai_request_thread(gpointer data)
|
||||
response.size = 0;
|
||||
|
||||
/* Build request URL */
|
||||
const gchar* api_url = local_provider ? local_provider->api_url : DEFAULT_OPENAI_URL;
|
||||
const gchar* api_url = local_provider->api_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);
|
||||
|
||||
@@ -1863,6 +1863,126 @@ prefs_free_ai_models(GList* models)
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* AI Provider Management
|
||||
* ======================================================================== */
|
||||
|
||||
gboolean
|
||||
prefs_ai_set_provider(const char* const provider, const char* const url)
|
||||
{
|
||||
if (!provider || !url)
|
||||
return FALSE;
|
||||
|
||||
if (!prefs)
|
||||
return FALSE;
|
||||
|
||||
/* Store URL with "_url" suffix to avoid collision with API key storage */
|
||||
g_key_file_set_string(prefs, PREF_GROUP_AI, g_strconcat(provider, "_url", NULL), url);
|
||||
_save_prefs();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
prefs_ai_remove_provider(const char* const provider)
|
||||
{
|
||||
if (!provider)
|
||||
return FALSE;
|
||||
|
||||
if (!prefs)
|
||||
return FALSE;
|
||||
|
||||
auto_gchar gchar* url_key = g_strconcat(provider, "_url", NULL);
|
||||
if (!g_key_file_has_key(prefs, PREF_GROUP_AI, url_key, NULL)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
g_key_file_remove_key(prefs, PREF_GROUP_AI, url_key, NULL);
|
||||
_save_prefs();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
char*
|
||||
prefs_ai_get_provider_url(const char* const provider)
|
||||
{
|
||||
if (!provider || !prefs)
|
||||
return NULL;
|
||||
|
||||
auto_gchar gchar* key = g_strconcat(provider, "_url", NULL);
|
||||
if (!g_key_file_has_key(prefs, PREF_GROUP_AI, key, NULL)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return g_key_file_get_string(prefs, PREF_GROUP_AI, key, NULL);
|
||||
}
|
||||
|
||||
GList*
|
||||
prefs_ai_list_providers(void)
|
||||
{
|
||||
if (!prefs || !g_key_file_has_group(prefs, PREF_GROUP_AI)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
GList* result = NULL;
|
||||
gsize len = 0;
|
||||
auto_gcharv gchar** keys = g_key_file_get_keys(prefs, PREF_GROUP_AI, &len, NULL);
|
||||
|
||||
for (gsize i = 0; i < len; i++) {
|
||||
char* key = keys[i];
|
||||
/* Match keys ending with "_url" that are not "_models" suffix */
|
||||
if (g_str_has_suffix(key, "_url") && !g_str_has_suffix(key, "_models_url")) {
|
||||
/* Extract provider name by removing "_url" suffix */
|
||||
gsize key_len = strlen(key);
|
||||
if (key_len > 4) {
|
||||
gchar* provider_name = g_strndup(key, key_len - 4);
|
||||
result = g_list_append(result, provider_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void
|
||||
prefs_free_ai_providers(GList* providers)
|
||||
{
|
||||
if (providers) {
|
||||
g_list_free_full(providers, g_free);
|
||||
}
|
||||
}
|
||||
|
||||
GList*
|
||||
prefs_ai_get_providers(void)
|
||||
{
|
||||
/* If user has configured any providers, return those */
|
||||
GList* configured = prefs_ai_list_providers();
|
||||
if (configured && g_list_length(configured) > 0) {
|
||||
/* Build result list with name, url pairs */
|
||||
GList* result = NULL;
|
||||
GList* curr = configured;
|
||||
while (curr) {
|
||||
gchar* name = (gchar*)curr->data;
|
||||
gchar* url = prefs_ai_get_provider_url(name);
|
||||
if (url && strlen(url) > 0) {
|
||||
result = g_list_append(result, g_strdup(name));
|
||||
result = g_list_append(result, g_strdup(url));
|
||||
}
|
||||
g_free(url);
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
prefs_free_ai_providers(configured);
|
||||
return result;
|
||||
}
|
||||
prefs_free_ai_providers(configured);
|
||||
|
||||
/* Nothing configured — return built-in defaults */
|
||||
GList* result = NULL;
|
||||
result = g_list_append(result, g_strdup("openai"));
|
||||
result = g_list_append(result, g_strdup("https://api.openai.com/"));
|
||||
result = g_list_append(result, g_strdup("perplexity"));
|
||||
result = g_list_append(result, g_strdup("https://api.perplexity.ai/"));
|
||||
return result;
|
||||
}
|
||||
|
||||
// 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,6 +366,14 @@ char* prefs_ai_get_token(const char* const provider);
|
||||
GList* prefs_ai_list_tokens(void);
|
||||
void prefs_free_ai_tokens(GList* tokens);
|
||||
|
||||
/* AI provider management */
|
||||
gboolean prefs_ai_set_provider(const char* const provider, const char* const url);
|
||||
gboolean prefs_ai_remove_provider(const char* const provider);
|
||||
char* prefs_ai_get_provider_url(const char* const provider);
|
||||
GList* prefs_ai_list_providers(void);
|
||||
void prefs_free_ai_providers(GList* providers);
|
||||
GList* prefs_ai_get_providers(void);
|
||||
|
||||
/* 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);
|
||||
|
||||
Reference in New Issue
Block a user