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:
2026-05-13 18:28:35 +00:00
parent 2f1637ca8e
commit b21d537e80
3 changed files with 179 additions and 18 deletions

View File

@@ -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.

View File

@@ -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);