fix(ai): repair custom setting name autocompletion
All checks were successful
CI Code / Check spelling (pull_request) Successful in 13s
CI Code / Check coding style (pull_request) Successful in 23s
CI Code / Code Coverage (pull_request) Successful in 3m45s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m24s
CI Code / Linux (arch) (pull_request) Successful in 7m7s
CI Code / Linux (debian) (pull_request) Successful in 8m41s
All checks were successful
CI Code / Check spelling (pull_request) Successful in 13s
CI Code / Check coding style (pull_request) Successful in 23s
CI Code / Code Coverage (pull_request) Successful in 3m45s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m24s
CI Code / Linux (arch) (pull_request) Successful in 7m7s
CI Code / Linux (debian) (pull_request) Successful in 8m41s
The setting-name branch for /ai set custom never fired: it passed the prefix "/ai set custom " with a trailing space to autocomplete_param_with_ac(), which appends a space itself, so the matched prefix contained a double space no real input ever has. Even without that, the primitive completes the first token after the command while the setting name is the second, and the num_args guard only held before the name was started. The suggestion list (tools/search/memory/ plugins) predated the /ai set custom backend and matched no real payload parameter. Replace the branch with token-position completion (arg 5 via autocomplete_param_no_with_func) fed by a list rebuilt on demand from common payload parameters plus the keys already set on the provider, exposed through the new ai_get_provider_setting_keys() (snapshot taken under settings_lock). Drop the now-unused parse_args() call whose NULL result was fed to g_strv_length() unguarded, spamming a glib CRITICAL on TAB at /ai with zero or five-plus arguments.
This commit is contained in:
@@ -990,6 +990,25 @@ ai_get_provider_setting(const gchar* provider_name, const gchar* setting)
|
|||||||
return value;
|
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
|
* Generic HTTP request handling
|
||||||
* ======================================================================== */
|
* ======================================================================== */
|
||||||
|
|||||||
@@ -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);
|
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.
|
* Fetch available models from a provider's API.
|
||||||
* @param provider_name The provider name
|
* @param provider_name The provider name
|
||||||
|
|||||||
@@ -1192,11 +1192,6 @@ cmd_ac_init(void)
|
|||||||
autocomplete_add_unsorted(ai_api_types_ac, "responses", FALSE);
|
autocomplete_add_unsorted(ai_api_types_ac, "responses", FALSE);
|
||||||
autocomplete_add_unsorted(ai_api_types_ac, "auto", 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_unsorted(ai_remove_subcommands_ac, "provider", FALSE);
|
||||||
|
|
||||||
autocomplete_add(avatar_ac, "set");
|
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);
|
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 <provider> <setting>.
|
||||||
|
* 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) <provider>(4) <setting>(5) */
|
||||||
|
return autocomplete_param_no_with_func(input, "/ai set custom", 5, _ai_custom_setting_find, previous, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
static char*
|
static char*
|
||||||
_ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
_ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||||
{
|
{
|
||||||
char* result = NULL;
|
char* result = NULL;
|
||||||
|
|
||||||
/* Parse once for reuse - /ai <subcommand> [<arg1>] [<arg2>] */
|
// /ai set provider <name> <url> - autocomplete provider names (for updating)
|
||||||
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 <subcommand> autocomplete (e.g., /ai s<tab> -> /ai set) */
|
|
||||||
result = autocomplete_param_with_func(input, "/ai set provider", ai_providers_find, previous, NULL);
|
result = autocomplete_param_with_func(input, "/ai set provider", ai_providers_find, previous, NULL);
|
||||||
if (result) {
|
if (result) {
|
||||||
return result;
|
return result;
|
||||||
@@ -4591,18 +4630,12 @@ _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// /ai set custom <provider> <setting> - autocomplete settings
|
// /ai set custom <provider> <setting> - autocomplete setting names
|
||||||
/* args[0]="set", args[1]="custom", args[2]=provider (if typed), args[3]=setting (if typed) */
|
result = _ai_custom_setting_autocomplete(input, previous);
|
||||||
if (num_args == 3 && g_strcmp0(args[1], "custom") == 0) {
|
if (result) {
|
||||||
/* /ai set custom <provider> - check if provider is valid */
|
return result;
|
||||||
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 <subcommand> - autocomplete subcommands
|
// /ai set <subcommand> - autocomplete subcommands
|
||||||
result = autocomplete_param_with_ac(input, "/ai set", ai_set_subcommands_ac, TRUE, previous);
|
result = autocomplete_param_with_ac(input, "/ai set", ai_set_subcommands_ac, TRUE, previous);
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|||||||
@@ -637,6 +637,34 @@ test_ai_set_provider_setting_reserved_key(void** state)
|
|||||||
assert_string_equal("0.7", temp);
|
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
|
void
|
||||||
test_ai_set_provider_api_type(void** state)
|
test_ai_set_provider_api_type(void** state)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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_get_provider_default_model(void** state);
|
||||||
void test_ai_set_provider_setting(void** state);
|
void test_ai_set_provider_setting(void** state);
|
||||||
void test_ai_set_provider_setting_reserved_key(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_set_provider_api_type(void** state);
|
||||||
void test_ai_api_type_string_round_trip(void** state);
|
void test_ai_api_type_string_round_trip(void** state);
|
||||||
void test_ai_get_provider_setting(void** state);
|
void test_ai_get_provider_setting(void** state);
|
||||||
|
|||||||
@@ -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_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, 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_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_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(test_ai_api_type_string_round_trip),
|
||||||
cmocka_unit_test_setup_teardown(test_ai_get_provider_setting, ai_client_setup, ai_client_teardown),
|
cmocka_unit_test_setup_teardown(test_ai_get_provider_setting, ai_client_setup, ai_client_teardown),
|
||||||
|
|||||||
Reference in New Issue
Block a user