diff --git a/src/ai/ai_client.c b/src/ai/ai_client.c index f958b39c..e3d09823 100644 --- a/src/ai/ai_client.c +++ b/src/ai/ai_client.c @@ -24,10 +24,6 @@ #include #include -/* 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); diff --git a/src/config/preferences.c b/src/config/preferences.c index acd5b383..5c044a3d 100644 --- a/src/config/preferences.c +++ b/src/config/preferences.c @@ -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. diff --git a/src/config/preferences.h b/src/config/preferences.h index bf4ed6a9..b62d2e36 100644 --- a/src/config/preferences.h +++ b/src/config/preferences.h @@ -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);