diff --git a/src/ai/ai_client.c b/src/ai/ai_client.c index 8bd48767..221f4e30 100644 --- a/src/ai/ai_client.c +++ b/src/ai/ai_client.c @@ -990,6 +990,25 @@ ai_get_provider_setting(const gchar* provider_name, const gchar* setting) return value; } +GList* +ai_get_provider_setting_keys(const gchar* provider_name) +{ + AIProvider* provider = provider_name ? ai_get_provider(provider_name) : NULL; + if (!provider || !provider->settings) + return NULL; + + GList* keys = NULL; + pthread_mutex_lock(&provider->settings_lock); + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init(&iter, provider->settings); + while (g_hash_table_iter_next(&iter, &key, &value)) { + keys = g_list_prepend(keys, g_strdup((gchar*)key)); + } + pthread_mutex_unlock(&provider->settings_lock); + return keys; +} + /* ======================================================================== * Generic HTTP request handling * ======================================================================== */ diff --git a/src/ai/ai_client.h b/src/ai/ai_client.h index f29dddd5..e8fa5e73 100644 --- a/src/ai/ai_client.h +++ b/src/ai/ai_client.h @@ -207,6 +207,14 @@ gboolean ai_set_provider_setting(const gchar* provider_name, const gchar* settin */ gchar* ai_get_provider_setting(const gchar* provider_name, const gchar* setting); +/** + * List the custom setting keys currently set on a provider. + * @param provider_name The provider name + * @return GList of gchar* (free with g_list_free_full(keys, g_free)), + * or NULL if the provider is unknown or has no settings + */ +GList* ai_get_provider_setting_keys(const gchar* provider_name); + /** * Fetch available models from a provider's API. * @param provider_name The provider name diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c index 3bcb2022..4eae90dd 100644 --- a/src/command/cmd_ac.c +++ b/src/command/cmd_ac.c @@ -1192,11 +1192,6 @@ cmd_ac_init(void) autocomplete_add_unsorted(ai_api_types_ac, "responses", FALSE); autocomplete_add_unsorted(ai_api_types_ac, "auto", FALSE); - 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"); @@ -4545,17 +4540,61 @@ _ai_api_type_find(const char* const search_str, gboolean previous, void* context return autocomplete_complete(ai_api_types_ac, search_str, FALSE, previous); } +static char* +_ai_custom_setting_find(const char* const search_str, gboolean previous, void* context) +{ + return autocomplete_complete(ai_set_custom_subcommands_ac, search_str, FALSE, previous); +} + +/** + * Autocomplete the setting name for /ai set custom . + * Offers a few common payload parameters merged with the keys already set + * on the provider, so existing settings can be retyped or overridden. + */ +static char* +_ai_custom_setting_autocomplete(const char* const input, gboolean previous) +{ + if (!g_str_has_prefix(input, "/ai set custom ")) { + return NULL; + } + + /* Quote-aware parse: args[2] is the provider, args[3] the setting being + * typed; skip the rebuild when the caret is not at the setting position */ + gboolean parse_result = FALSE; + auto_gcharv gchar** args = parse_args(input, 1, 5, &parse_result); + if (!parse_result) { + return NULL; + } + guint num_args = g_strv_length(args); + if (num_args < 3 || num_args > 4 || !ai_get_provider(args[2])) { + return NULL; + } + + /* Rebuild the completion list: common parameters + the provider's keys */ + gchar* suggestions[] = { "temperature", "max_tokens", "top_p" }; + GList* keys = ai_get_provider_setting_keys(args[2]); + char** items = g_new0(char*, ARRAY_SIZE(suggestions) + g_list_length(keys) + 1); + guint i = 0; + for (; i < ARRAY_SIZE(suggestions); i++) { + items[i] = suggestions[i]; + } + for (GList* curr = keys; curr; curr = g_list_next(curr)) { + items[i++] = curr->data; + } + autocomplete_update(ai_set_custom_subcommands_ac, items); + g_free(items); + g_list_free_full(keys, g_free); + + /* /ai(1) set(2) custom(3) (4) (5) */ + return autocomplete_param_no_with_func(input, "/ai set custom", 5, _ai_custom_setting_find, previous, NULL); +} + static char* _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous) { char* result = NULL; - /* Parse once for reuse - /ai [] [] */ - gboolean parse_result = FALSE; - auto_gcharv gchar** args = parse_args(input, 1, 4, &parse_result); - int num_args = g_strv_length(args); - - /* Top-level /ai autocomplete (e.g., /ai s -> /ai set) */ + // /ai set provider - autocomplete provider names (for updating) result = autocomplete_param_with_func(input, "/ai set provider", ai_providers_find, previous, NULL); if (result) { return result; @@ -4591,18 +4630,12 @@ _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous) return result; } - // /ai set custom - 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 - 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 custom - autocomplete setting names + result = _ai_custom_setting_autocomplete(input, previous); + if (result) { + return result; } + // /ai set - autocomplete subcommands result = autocomplete_param_with_ac(input, "/ai set", ai_set_subcommands_ac, TRUE, previous); if (result) { diff --git a/tests/unittests/test_ai_client.c b/tests/unittests/test_ai_client.c index aa6205b4..708447eb 100644 --- a/tests/unittests/test_ai_client.c +++ b/tests/unittests/test_ai_client.c @@ -637,6 +637,34 @@ test_ai_set_provider_setting_reserved_key(void** state) assert_string_equal("0.7", temp); } +void +test_ai_get_provider_setting_keys(void** state) +{ + ai_add_provider("p_setting_keys", "https://example.test/skeys/"); + + /* No settings yet, unknown provider, NULL — all yield NULL */ + assert_null(ai_get_provider_setting_keys("p_setting_keys")); + assert_null(ai_get_provider_setting_keys("nonexistent")); + assert_null(ai_get_provider_setting_keys(NULL)); + + ai_set_provider_setting("p_setting_keys", "temperature", "0.7"); + ai_set_provider_setting("p_setting_keys", "reasoning_effort", "high"); + + /* Both keys listed (order is hash-defined, assert membership) */ + GList* keys = ai_get_provider_setting_keys("p_setting_keys"); + assert_int_equal(2, g_list_length(keys)); + assert_non_null(g_list_find_custom(keys, "temperature", (GCompareFunc)g_strcmp0)); + assert_non_null(g_list_find_custom(keys, "reasoning_effort", (GCompareFunc)g_strcmp0)); + g_list_free_full(keys, g_free); + + /* Removing a setting removes its key */ + ai_set_provider_setting("p_setting_keys", "temperature", NULL); + keys = ai_get_provider_setting_keys("p_setting_keys"); + assert_int_equal(1, g_list_length(keys)); + assert_string_equal("reasoning_effort", keys->data); + g_list_free_full(keys, g_free); +} + void test_ai_set_provider_api_type(void** state) { diff --git a/tests/unittests/test_ai_client.h b/tests/unittests/test_ai_client.h index 6b274475..f35b007d 100644 --- a/tests/unittests/test_ai_client.h +++ b/tests/unittests/test_ai_client.h @@ -44,6 +44,7 @@ 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_set_provider_setting_reserved_key(void** state); +void test_ai_get_provider_setting_keys(void** state); void test_ai_set_provider_api_type(void** state); void test_ai_api_type_string_round_trip(void** state); void test_ai_get_provider_setting(void** state); diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index 523a2321..94ff7965 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -753,6 +753,7 @@ main(int argc, char* argv[]) 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_set_provider_setting_reserved_key, ai_client_setup, ai_client_teardown), + cmocka_unit_test_setup_teardown(test_ai_get_provider_setting_keys, ai_client_setup, ai_client_teardown), cmocka_unit_test_setup_teardown(test_ai_set_provider_api_type, ai_client_setup, ai_client_teardown), cmocka_unit_test(test_ai_api_type_string_round_trip), cmocka_unit_test_setup_teardown(test_ai_get_provider_setting, ai_client_setup, ai_client_teardown),