diff --git a/src/ai/ai_client.c b/src/ai/ai_client.c index 1187ebc1..f0bc3e25 100644 --- a/src/ai/ai_client.c +++ b/src/ai/ai_client.c @@ -352,16 +352,31 @@ ai_client_init(void) /* Load saved API keys from config */ ai_load_keys(); - /* Load cached models and persisted default-model for all providers */ + /* Load cached models, persisted default-model and custom settings for + * all providers. */ GList* provider_list = ai_list_providers(); for (GList* curr = provider_list; curr; curr = g_list_next(curr)) { AIProvider* provider = (AIProvider*)curr->data; _ai_load_models_for_provider(provider); + auto_gchar gchar* dm = prefs_ai_get_default_model(provider->name); if (dm && dm[0] != '\0') { g_free(provider->default_model); provider->default_model = g_strdup(dm); } + + GList* settings = prefs_ai_list_settings(provider->name); + for (GList* s = settings; s; s = g_list_next(s)) { + const gchar* name = (const gchar*)s->data; + auto_gchar gchar* val = prefs_ai_get_setting(provider->name, name); + if (val) { + if (!provider->settings) { + provider->settings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + } + g_hash_table_insert(provider->settings, g_strdup(name), g_strdup(val)); + } + } + prefs_free_ai_settings(settings); } g_list_free(provider_list); @@ -457,11 +472,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); - prefs_ai_remove_default_model(name); - prefs_ai_remove_models(name); - prefs_ai_remove_token(name); + /* Wipe the whole [ai/] subsection from config — url, key, + * default_model, models, custom settings — in one call. */ + prefs_ai_remove_section(name); /* Sync autocomplete before removing */ autocomplete_remove(providers_ac, name); @@ -598,9 +611,11 @@ ai_set_provider_setting(const gchar* provider_name, const gchar* setting, const if (value) { g_hash_table_insert(provider->settings, g_strdup(setting), g_strdup(value)); + prefs_ai_set_setting(provider_name, setting, value); log_info("Setting '%s' for provider '%s' set to: %s", setting, provider_name, value); } else { g_hash_table_remove(provider->settings, setting); + prefs_ai_remove_setting(provider_name, setting); log_info("Setting '%s' removed for provider '%s'", setting, provider_name); } } diff --git a/src/config/preferences.c b/src/config/preferences.c index c1415b1d..b39fc26b 100644 --- a/src/config/preferences.c +++ b/src/config/preferences.c @@ -67,6 +67,8 @@ #define PREF_GROUP_PLUGINS "plugins" #define PREF_GROUP_EXECUTABLES "executables" #define PREF_GROUP_AI "ai" +#define PREF_GROUP_AI_PREFIX "ai/" /* per-provider subsection prefix: [ai/] */ +#define PREF_AI_SETTING_PREFIX "setting." /* per-provider custom setting key prefix */ #define INPBLOCK_DEFAULT 1000 @@ -243,6 +245,69 @@ _prefs_load(void) g_key_file_remove_key(prefs, PREF_GROUP_UI, "titlebar.muc.title.jid", NULL); g_key_file_remove_key(prefs, PREF_GROUP_UI, "titlebar.muc.title.name", NULL); } + + /* Migrate legacy flat [ai] layout + * openai_url=..., openai=, openai_models=..., openai_default_model=... + * into per-provider subsections + * [ai/openai] url=..., key=..., models=..., default_model=... + * Idempotent: once the flat keys are gone, this is a no-op. */ + if (g_key_file_has_group(prefs, PREF_GROUP_AI)) { + gsize key_count = 0; + auto_gcharv gchar** all_keys = g_key_file_get_keys(prefs, PREF_GROUP_AI, &key_count, NULL); + + /* Collect provider names from _url keys first so we can migrate + * the whole bundle (url + token + models + default_model) atomically + * per provider, without misclassifying unrelated flat keys. */ + GHashTable* to_migrate = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + for (gsize i = 0; i < key_count; i++) { + const gchar* k = all_keys[i]; + if (g_str_has_suffix(k, "_url") && !g_str_has_suffix(k, "_models_url")) { + gsize klen = strlen(k); + if (klen > 4) { + g_hash_table_add(to_migrate, g_strndup(k, klen - 4)); + } + } + } + + GList* provider_names = g_hash_table_get_keys(to_migrate); + for (GList* curr = provider_names; curr; curr = g_list_next(curr)) { + const gchar* name = (const gchar*)curr->data; + auto_gchar gchar* group = g_strconcat(PREF_GROUP_AI_PREFIX, name, NULL); + + const struct + { + const gchar* flat_suffix; + const gchar* new_key; + } map[] = { + { "_url", "url" }, + { "_models", "models" }, + { "_default_model", "default_model" }, + { NULL, NULL }, + }; + for (int j = 0; map[j].flat_suffix; j++) { + auto_gchar gchar* flat_key = g_strconcat(name, map[j].flat_suffix, NULL); + if (g_key_file_has_key(prefs, PREF_GROUP_AI, flat_key, NULL)) { + auto_gchar gchar* val = g_key_file_get_string(prefs, PREF_GROUP_AI, flat_key, NULL); + if (val) { + g_key_file_set_string(prefs, group, map[j].new_key, val); + } + g_key_file_remove_key(prefs, PREF_GROUP_AI, flat_key, NULL); + } + } + + /* Token was stored as the bare provider name in [ai]. */ + if (g_key_file_has_key(prefs, PREF_GROUP_AI, name, NULL)) { + auto_gchar gchar* val = g_key_file_get_string(prefs, PREF_GROUP_AI, name, NULL); + if (val) { + g_key_file_set_string(prefs, group, "key", val); + } + g_key_file_remove_key(prefs, PREF_GROUP_AI, name, NULL); + } + } + g_list_free(provider_names); + g_hash_table_destroy(to_migrate); + } + _save_prefs(); boolean_choice_ac = autocomplete_new(); @@ -1708,73 +1773,103 @@ _save_prefs(void) } /* ======================================================================== - * AI Token Management + * AI provider preferences + * + * Each provider lives in its own [ai/] subsection of the keyfile with: + * url=... + * key= + * default_model=... + * models=model1;model2;... + * setting.=value (e.g. setting.tools=enabled) + * The bare [ai] section keeps only the global "defaults_initialized" sentinel. + * Legacy flat-key layouts are migrated on first load in _prefs_load(). * ======================================================================== */ +static gchar* +_ai_section(const char* const provider) +{ + return g_strconcat(PREF_GROUP_AI_PREFIX, provider, NULL); +} + +static gboolean +_ai_set_string(const char* const provider, const char* const key, const char* const value) +{ + if (!provider || !key || !value || !prefs) + return FALSE; + + auto_gchar gchar* group = _ai_section(provider); + g_key_file_set_string(prefs, group, key, value); + _save_prefs(); + return TRUE; +} + +static char* +_ai_get_string(const char* const provider, const char* const key) +{ + if (!provider || !key || !prefs) + return NULL; + + auto_gchar gchar* group = _ai_section(provider); + if (!g_key_file_has_key(prefs, group, key, NULL)) + return NULL; + return g_key_file_get_string(prefs, group, key, NULL); +} + +static gboolean +_ai_remove_string(const char* const provider, const char* const key) +{ + if (!provider || !key || !prefs) + return FALSE; + + auto_gchar gchar* group = _ai_section(provider); + if (!g_key_file_has_key(prefs, group, key, NULL)) + return FALSE; + + g_key_file_remove_key(prefs, group, key, NULL); + _save_prefs(); + return TRUE; +} + +/* --- Token --- */ + gboolean prefs_ai_set_token(const char* const provider, const char* const token) { - if (!provider) + if (!provider || !prefs) return FALSE; - - if (!prefs) - return FALSE; - - if (token) { - g_key_file_set_string(prefs, PREF_GROUP_AI, provider, token); - _save_prefs(); - return TRUE; - } else { + if (!token) return prefs_ai_remove_token(provider); - } + return _ai_set_string(provider, "key", token); } gboolean prefs_ai_remove_token(const char* const provider) { - if (!provider) - return FALSE; - - if (!prefs) - return FALSE; - - if (!g_key_file_has_key(prefs, PREF_GROUP_AI, provider, NULL)) { - return FALSE; - } - - g_key_file_remove_key(prefs, PREF_GROUP_AI, provider, NULL); - _save_prefs(); - return TRUE; + return _ai_remove_string(provider, "key"); } char* prefs_ai_get_token(const char* const provider) { - if (!provider || !prefs) - return NULL; - - return g_key_file_get_string(prefs, PREF_GROUP_AI, provider, NULL); + return _ai_get_string(provider, "key"); } GList* prefs_ai_list_tokens(void) { - if (!prefs || !g_key_file_has_group(prefs, PREF_GROUP_AI)) { + if (!prefs) return NULL; - } GList* result = NULL; - gsize len; - auto_gcharv gchar** keys = g_key_file_get_keys(prefs, PREF_GROUP_AI, &len, NULL); - - for (gsize i = 0; i < len; i++) { - char* name = keys[i]; - auto_gchar gchar* value = g_key_file_get_string(prefs, PREF_GROUP_AI, name, NULL); - if (value) { + GList* providers = prefs_ai_list_providers(); + for (GList* curr = providers; curr; curr = g_list_next(curr)) { + const gchar* name = (const gchar*)curr->data; + auto_gchar gchar* tok = prefs_ai_get_token(name); + if (tok && tok[0] != '\0') { result = g_list_append(result, g_strdup(name)); } } - + prefs_free_ai_providers(providers); return result; } @@ -1786,72 +1881,45 @@ prefs_free_ai_tokens(GList* tokens) } } +/* --- Models cache --- */ + gboolean prefs_ai_set_models(const char* const provider, const gchar* const* models, gint count) { - if (!provider || count <= 0) + if (!provider || count <= 0 || !prefs) 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) { + 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); + gboolean ok = _ai_set_string(provider, "models", model_str->str); g_string_free(model_str, TRUE); - _save_prefs(); - return TRUE; + return ok; } 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; + return _ai_remove_string(provider, "models"); } GList* prefs_ai_get_models(const char* const provider) { - if (!provider || !prefs) + auto_gchar gchar* model_str = _ai_get_string(provider, "models"); + if (!model_str || model_str[0] == '\0') 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; } @@ -1863,125 +1931,89 @@ prefs_free_ai_models(GList* models) } } -/* ======================================================================== - * AI Default Model (per provider) - * ======================================================================== */ +/* --- Default model --- */ gboolean prefs_ai_set_default_model(const char* const provider, const char* const model) { if (!provider || !model || !prefs) return FALSE; - if (model[0] == '\0') return prefs_ai_remove_default_model(provider); - - auto_gchar gchar* key = g_strconcat(provider, "_default_model", NULL); - g_key_file_set_string(prefs, PREF_GROUP_AI, key, model); - _save_prefs(); - return TRUE; + return _ai_set_string(provider, "default_model", model); } gboolean prefs_ai_remove_default_model(const char* const provider) { - if (!provider || !prefs) - return FALSE; - - auto_gchar gchar* key = g_strconcat(provider, "_default_model", 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; + return _ai_remove_string(provider, "default_model"); } char* prefs_ai_get_default_model(const char* const provider) { - if (!provider || !prefs) - return NULL; - - auto_gchar gchar* key = g_strconcat(provider, "_default_model", 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); + return _ai_get_string(provider, "default_model"); } -/* ======================================================================== - * AI Provider Management - * ======================================================================== */ +/* --- Provider URL --- */ 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; + return _ai_set_string(provider, "url", url); } 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; + /* Provider presence is identified by the url key; removing it is what + * makes prefs_ai_list_providers stop returning this name. The remaining + * sub-keys are wiped through prefs_ai_remove_section(). */ + return _ai_remove_string(provider, "url"); } char* prefs_ai_get_provider_url(const char* const provider) +{ + return _ai_get_string(provider, "url"); +} + +gboolean +prefs_ai_remove_section(const char* const provider) { if (!provider || !prefs) - return NULL; + return FALSE; - auto_gchar gchar* key = g_strconcat(provider, "_url", NULL); - if (!g_key_file_has_key(prefs, PREF_GROUP_AI, key, NULL)) { - return NULL; - } + auto_gchar gchar* group = _ai_section(provider); + if (!g_key_file_has_group(prefs, group)) + return FALSE; - return g_key_file_get_string(prefs, PREF_GROUP_AI, key, NULL); + g_key_file_remove_group(prefs, group, NULL); + _save_prefs(); + return TRUE; } GList* prefs_ai_list_providers(void) { - if (!prefs || !g_key_file_has_group(prefs, PREF_GROUP_AI)) { + if (!prefs) return NULL; - } GList* result = NULL; gsize len = 0; - auto_gcharv gchar** keys = g_key_file_get_keys(prefs, PREF_GROUP_AI, &len, NULL); + auto_gcharv gchar** groups = g_key_file_get_groups(prefs, &len); + if (!groups) + return NULL; + gsize prefix_len = strlen(PREF_GROUP_AI_PREFIX); 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); + const gchar* g = groups[i]; + if (g_str_has_prefix(g, PREF_GROUP_AI_PREFIX) && strlen(g) > prefix_len) { + /* Only count groups that actually identify a provider (have url). */ + if (g_key_file_has_key(prefs, g, "url", NULL)) { + result = g_list_append(result, g_strdup(g + prefix_len)); } } } @@ -2004,7 +2036,7 @@ prefs_ai_get_providers(void) GList* result = NULL; for (GList* curr = configured; curr; curr = g_list_next(curr)) { - gchar* name = (gchar*)curr->data; + const gchar* name = (const gchar*)curr->data; auto_gchar gchar* url = prefs_ai_get_provider_url(name); if (url && strlen(url) > 0) { result = g_list_append(result, g_strdup(name)); @@ -2032,6 +2064,67 @@ prefs_ai_get_providers(void) return result; } +/* --- Custom per-provider settings (setting.) --- */ + +gboolean +prefs_ai_set_setting(const char* const provider, const char* const setting, const char* const value) +{ + if (!provider || !setting) + return FALSE; + auto_gchar gchar* key = g_strconcat(PREF_AI_SETTING_PREFIX, setting, NULL); + if (!value) + return _ai_remove_string(provider, key); + return _ai_set_string(provider, key, value); +} + +gboolean +prefs_ai_remove_setting(const char* const provider, const char* const setting) +{ + if (!provider || !setting) + return FALSE; + auto_gchar gchar* key = g_strconcat(PREF_AI_SETTING_PREFIX, setting, NULL); + return _ai_remove_string(provider, key); +} + +char* +prefs_ai_get_setting(const char* const provider, const char* const setting) +{ + if (!provider || !setting) + return NULL; + auto_gchar gchar* key = g_strconcat(PREF_AI_SETTING_PREFIX, setting, NULL); + return _ai_get_string(provider, key); +} + +GList* +prefs_ai_list_settings(const char* const provider) +{ + if (!provider || !prefs) + return NULL; + + auto_gchar gchar* group = _ai_section(provider); + if (!g_key_file_has_group(prefs, group)) + return NULL; + + GList* result = NULL; + gsize len = 0; + auto_gcharv gchar** keys = g_key_file_get_keys(prefs, group, &len, NULL); + gsize prefix_len = strlen(PREF_AI_SETTING_PREFIX); + for (gsize i = 0; i < len; i++) { + if (g_str_has_prefix(keys[i], PREF_AI_SETTING_PREFIX) && strlen(keys[i]) > prefix_len) { + result = g_list_append(result, g_strdup(keys[i] + prefix_len)); + } + } + return result; +} + +void +prefs_free_ai_settings(GList* settings) +{ + if (settings) { + g_list_free_full(settings, 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. diff --git a/src/config/preferences.h b/src/config/preferences.h index 16a5dab6..fffc2082 100644 --- a/src/config/preferences.h +++ b/src/config/preferences.h @@ -374,6 +374,10 @@ GList* prefs_ai_list_providers(void); void prefs_free_ai_providers(GList* providers); GList* prefs_ai_get_providers(void); +/* Wipe the whole [ai/] subsection (url, key, models, default_model, + * custom settings) in one call. */ +gboolean prefs_ai_remove_section(const char* const provider); + /* 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); @@ -385,4 +389,12 @@ gboolean prefs_ai_set_default_model(const char* const provider, const char* cons gboolean prefs_ai_remove_default_model(const char* const provider); char* prefs_ai_get_default_model(const char* const provider); +/* AI per-provider custom settings (e.g. tools, search) — persisted as + * setting.= inside [ai/]. Pass NULL value to remove. */ +gboolean prefs_ai_set_setting(const char* const provider, const char* const setting, const char* const value); +gboolean prefs_ai_remove_setting(const char* const provider, const char* const setting); +char* prefs_ai_get_setting(const char* const provider, const char* const setting); +GList* prefs_ai_list_settings(const char* const provider); +void prefs_free_ai_settings(GList* settings); + #endif