feat(ai): add model caching, settings, and commands

- Introduce model caching with persistence to preferences
- Add provider default model and custom settings management
- Implement `/ai switch`, `/ai models`, and improve `/ai start`
- Add model name autocomplete for chat commands
- Update command definitions and help text
- Add unit tests for new functionality
This commit is contained in:
2026-05-09 13:15:58 +00:00
parent 731b55fa19
commit acae543057
11 changed files with 1255 additions and 98 deletions

View File

@@ -10795,21 +10795,28 @@ cmd_ai(ProfWin* window, const char* const command, gchar** args)
cons_show("AI Chat - OpenAI-compatible API client");
cons_show("");
// List available providers
// List configured providers with full details
GList* providers = ai_list_providers();
cons_show("Available providers:");
cons_show("Configured providers:");
for (GList* curr = providers; curr; curr = g_list_next(curr)) {
AIProvider* provider = curr->data;
auto_gchar gchar* key = ai_get_provider_key(provider->name);
cons_show(" %s (URL: %s, Key: %s)",
provider->name,
provider->api_url,
key ? "set" : "not set");
const gchar* default_model = ai_get_provider_default_model(provider->name);
cons_show(" %s", provider->name);
cons_show(" URL: %s", provider->api_url);
cons_show(" Key: %s", key ? "configured" : "NOT configured");
if (default_model) {
cons_show(" Default model: %s", default_model);
}
if (provider->models && g_list_length(provider->models) > 0) {
cons_show(" Cached models: %d", g_list_length(provider->models));
}
cons_show("");
}
g_list_free(providers);
cons_show("");
cons_show("Use '/ai start <provider>/<model>' to begin a chat.");
cons_show("Use '/ai start' to begin a chat (uses default provider/model).");
cons_show("Use '/ai models <provider>' to fetch available models.");
cons_show("Available models: https://models.litellm.ai/");
return TRUE;
}
@@ -10849,20 +10856,41 @@ cmd_ai_set(ProfWin* window, const char* const command, gchar** args)
cons_show("API token set for provider: %s", args[2]);
cons_show("");
return TRUE;
} else if (g_strcmp0(args[1], "org") == 0) {
// /ai set org <provider> <org_id>
} else if (g_strcmp0(args[1], "default-model") == 0) {
// /ai set default-model <provider> <model>
if (g_strv_length(args) < 4) {
cons_bad_cmd_usage(command);
return TRUE;
}
AIProvider* provider = ai_get_provider(args[2]);
if (provider) {
g_free(provider->org_id);
provider->org_id = g_strdup(args[3]);
cons_show("Organization ID set for provider: %s", args[2]);
} else {
cons_show("Provider '%s' not found. Add it first with '/ai set provider'.", args[2]);
ai_set_provider_default_model(args[2], args[3]);
cons_show("Default model for provider '%s' set to: %s", args[2], args[3]);
cons_show("");
return TRUE;
} else if (g_strcmp0(args[1], "custom") == 0) {
// /ai set custom <provider> <setting> <value>
if (g_strv_length(args) < 5) {
cons_bad_cmd_usage(command);
return TRUE;
}
ai_set_provider_setting(args[2], args[3], args[4]);
cons_show("Setting '%s' for provider '%s' set to: %s", args[3], args[2], args[4]);
cons_show("");
return TRUE;
} else if (g_strcmp0(args[1], "default-provider") == 0) {
// /ai set default-provider <provider>
if (g_strv_length(args) < 3) {
cons_bad_cmd_usage(command);
return TRUE;
}
const gchar* provider_name = args[2];
AIProvider* provider = ai_get_provider(provider_name);
if (!provider) {
cons_show_error("Provider '%s' not found. Use '/ai set provider %s <url>' to add it.",
provider_name, provider_name);
return TRUE;
}
prefs_set_string(PREF_AI_PROVIDER, provider_name);
cons_show("Default provider set to: %s", provider_name);
cons_show("");
return TRUE;
}
@@ -10897,26 +10925,31 @@ cmd_ai_remove(ProfWin* window, const char* const command, gchar** args)
gboolean
cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
{
// /ai start [<provider>/<model>]
const gchar* provider_model = (g_strv_length(args) >= 2) ? args[1] : NULL;
// /ai start [<provider>] [<model>]
// Space-separated, no slash syntax
const gchar* provider_name = NULL;
const gchar* model = NULL;
auto_gchar gchar* owned_provider_name = NULL;
const gchar* provider_name = "openai";
const gchar* model = "gpt-4o";
if (provider_model) {
const gchar* slash = strchr(provider_model, '/');
if (slash) {
owned_provider_name = g_strndup(provider_model, slash - provider_model);
provider_name = owned_provider_name;
model = slash + 1;
} else {
// Just a model name, use default provider
model = provider_model;
}
if (g_strv_length(args) >= 2) {
provider_name = args[1];
}
if (g_strv_length(args) >= 3) {
model = args[2];
}
// Resolve defaults
auto_gchar gchar* owned_provider_name = NULL;
if (!provider_name) {
// Use default provider from preferences
auto_gchar gchar* default_provider = prefs_get_string(PREF_AI_PROVIDER);
if (default_provider && strlen(default_provider) > 0) {
provider_name = default_provider;
}
}
if (!provider_name) {
provider_name = "openai"; // Fallback
}
// Check if provider exists
AIProvider* provider = ai_get_provider(provider_name);
if (!provider) {
cons_show_error("Provider '%s' not found. Use '/ai set provider %s <url>' to add it.",
@@ -10924,6 +10957,14 @@ cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
return TRUE;
}
// Get model: explicit > provider default > hardcoded fallback
if (!model) {
model = ai_get_provider_default_model(provider_name);
}
if (!model) {
model = "gpt-4o"; // Fallback
}
// Check for API key
gchar* api_key = ai_get_provider_key(provider_name);
if (!api_key || strlen(api_key) == 0) {
@@ -10972,6 +11013,161 @@ cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
return TRUE;
}
gboolean
cmd_ai_model(ProfWin* window, const char* const command, gchar** args)
{
// /ai model <model>
if (args[1] == NULL) {
cons_bad_cmd_usage(command);
return TRUE;
}
const gchar* model = args[1];
// Get current AI window
ProfAiWin* aiwin = (window && window->type == WIN_AI) ? (ProfAiWin*)window : wins_get_ai();
if (!aiwin) {
cons_show("No active AI chat window. Use '/ai start' first.");
return TRUE;
}
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
if (!aiwin->session) {
cons_show("No active session in this chat window.");
return TRUE;
}
ai_session_set_model(aiwin->session, model);
cons_show("Session model changed to: %s", model);
cons_show("");
return TRUE;
}
gboolean
cmd_ai_switch(ProfWin* window, const char* const command, gchar** args)
{
// /ai switch <provider> [<model>] - Change provider and optionally model
// /ai switch <model> - Change model only (keeps current provider)
// Modifies the existing session's provider and model instead of recreating it
if (g_strv_length(args) < 2) {
cons_bad_cmd_usage(command);
return TRUE;
}
// Get current AI window
ProfAiWin* aiwin = (window && window->type == WIN_AI) ? (ProfAiWin*)window : wins_get_ai();
if (!aiwin) {
cons_show("No active AI chat window. Use '/ai start' first.");
return TRUE;
}
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
if (!aiwin->session) {
cons_show("No active session in this chat window.");
return TRUE;
}
const gchar* arg1 = args[1];
const gchar* arg2 = (g_strv_length(args) >= 3) ? args[2] : NULL;
const gchar* provider_name;
const gchar* model;
gboolean changed_provider = FALSE;
// Check if arg1 is a known provider
AIProvider* provider = ai_get_provider(arg1);
if (provider) {
// arg1 is a provider name
provider_name = arg1;
changed_provider = TRUE;
// Get model: arg2 > provider default > error
if (arg2) {
model = arg2;
} else {
model = ai_get_provider_default_model(provider_name);
}
if (!model) {
cons_show_error("No model specified and no default model set for provider '%s'.", provider_name);
cons_show("Use '/ai set default-model %s <model>' or '/ai switch %s <model>'.", provider_name, provider_name);
return TRUE;
}
} else {
// arg1 is a model name, keep current provider
provider_name = aiwin->session->provider_name;
model = arg1;
}
// Check for API key
auto_gchar gchar* api_key = ai_get_provider_key(provider_name);
if (!api_key || strlen(api_key) == 0) {
cons_show_error("No API key set for provider '%s'. Use '/ai set token %s <key>' first.",
provider_name, provider_name);
return TRUE;
}
// Update the existing session's provider and model
if (changed_provider) {
AIProvider* new_provider = ai_get_provider(provider_name);
g_free(aiwin->session->provider_name);
aiwin->session->provider_name = g_strdup(provider_name);
ai_provider_unref(aiwin->session->provider);
aiwin->session->provider = new_provider;
aiwin->session->provider->ref_count++;
}
g_free(aiwin->session->model);
aiwin->session->model = g_strdup(model);
g_free(aiwin->session->api_key);
aiwin->session->api_key = g_strdup(api_key);
// Update window title
win_println((ProfWin*)aiwin, THEME_DEFAULT, "-", "AI Chat: %s/%s", provider_name, model);
cons_show("Switched to %s/%s", provider_name, model);
cons_show("");
return TRUE;
}
gboolean
cmd_ai_models(ProfWin* window, const char* const command, gchar** args)
{
// /ai models <provider> [--refresh]
if (args[1] == NULL) {
cons_bad_cmd_usage(command);
return TRUE;
}
const gchar* provider_name = args[1];
gboolean refresh = (g_strv_length(args) >= 3) && (g_strcmp0(args[2], "--refresh") == 0);
AIProvider* provider = ai_get_provider(provider_name);
if (!provider) {
cons_show_error("Provider '%s' not found.", provider_name);
return TRUE;
}
if (refresh || !ai_models_are_fresh(provider_name)) {
// Fetch models from API
ProfAiWin* aiwin = (window && window->type == WIN_AI) ? (ProfAiWin*)window : wins_get_ai();
ai_fetch_models(provider_name, aiwin);
} else {
// Display cached models
cons_show("Cached models for provider '%s':", provider_name);
GList* curr = provider->models;
while (curr) {
cons_show(" %s", (gchar*)curr->data);
curr = g_list_next(curr);
}
cons_show("");
cons_show("Use '/ai models %s --refresh' to fetch fresh models from the API.", provider_name);
}
return TRUE;
}
gboolean
cmd_ai_clear(ProfWin* window, const char* const command, gchar** args)
{