feat(ai): add AI client with multi-provider chat support
All checks were successful
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 29s
CI Code / Code Coverage (push) Successful in 2m44s
CI Code / Linux (debian) (push) Successful in 4m46s
CI Code / Linux (ubuntu) (push) Successful in 4m59s
CI Code / Linux (arch) (push) Successful in 5m56s
All checks were successful
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 29s
CI Code / Code Coverage (push) Successful in 2m44s
CI Code / Linux (debian) (push) Successful in 4m46s
CI Code / Linux (ubuntu) (push) Successful in 4m59s
CI Code / Linux (arch) (push) Successful in 5m56s
Add an AI client module that integrates with OpenAI-compatible API providers (OpenAI, Perplexity, and custom endpoints) to provide AI-assisted chat within CProof. Users can start sessions with /ai start, send prompts, receive responses in a dedicated AI window, switch between providers and models, and manage API keys — all with tab-completion. Providers are configured via /ai set commands with per-provider API keys, endpoints, default models, and custom settings. Two default providers (openai, perplexity) are seeded on first use. Provider state persists in [ai/<name>] sections of the preferences keyfile with automatic migration from the previous flat-key format. The /ai command integrates into the existing command system with 8 subcommands covering provider management, session lifecycle, model fetching, and conversation clearing. Autocomplete uses the standard flat prefix-matching chain for reliable tab-completion at every nesting level. A new ProfAiWin window type is added to the window system. Architecture: Async design: HTTP requests run on a background thread (pthread) to avoid blocking the ncurses UI loop; results are displayed on the main thread via direct function calls Thread safety: AIProvider and AISession use atomic ref-counting and mutex-protected session state; the request thread snapshots all session data before making the HTTP call Window validation: wins_ai_exists() prevents use-after-free when the user closes the AI window during an in-flight HTTP request (~60s) Privacy: store:false is sent with every request to prevent providers from persisting conversations or using them for training Response size limit: 10MB cap with immediate curl abort via CURL_WRITEFUNC_ERROR to prevent OOM JSON parsing uses unified helpers for both chat responses and error envelopes with consistent escape decoding. The response parser tries Perplexity /v1/responses "text" field first, then falls back to OpenAI "content". Error parsing extracts provider error.message from the standard envelope format. Model parsing handles multiple API response formats (OpenAI list, Perplexity, array) including edge cases. Tests include 470+ lines of unit tests covering provider management, session lifecycle, JSON parsing (multiple formats), autocomplete cycling, and error handling, plus functional tests for /ai command dispatch. A stub_ai.c module isolates unit tests from UI dependencies.
This commit is contained in:
@@ -66,6 +66,9 @@
|
||||
#define PREF_GROUP_MUC "muc"
|
||||
#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/<provider>] */
|
||||
#define PREF_AI_SETTING_PREFIX "setting." /* per-provider custom setting key prefix */
|
||||
|
||||
#define INPBLOCK_DEFAULT 1000
|
||||
|
||||
@@ -242,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=<token>, 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 <name>_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();
|
||||
@@ -1706,6 +1772,366 @@ _save_prefs(void)
|
||||
save_keyfile(&prefs_prof_keyfile);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* AI provider preferences
|
||||
*
|
||||
* Each provider lives in its own [ai/<name>] subsection of the keyfile with:
|
||||
* url=...
|
||||
* key=<api token>
|
||||
* default_model=...
|
||||
* models=model1;model2;...
|
||||
* setting.<custom>=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 || !prefs)
|
||||
return FALSE;
|
||||
if (!token)
|
||||
return prefs_ai_remove_token(provider);
|
||||
return _ai_set_string(provider, "key", token);
|
||||
}
|
||||
|
||||
gboolean
|
||||
prefs_ai_remove_token(const char* const provider)
|
||||
{
|
||||
return _ai_remove_string(provider, "key");
|
||||
}
|
||||
|
||||
char*
|
||||
prefs_ai_get_token(const char* const provider)
|
||||
{
|
||||
return _ai_get_string(provider, "key");
|
||||
}
|
||||
|
||||
GList*
|
||||
prefs_ai_list_tokens(void)
|
||||
{
|
||||
if (!prefs)
|
||||
return NULL;
|
||||
|
||||
GList* result = NULL;
|
||||
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;
|
||||
}
|
||||
|
||||
void
|
||||
prefs_free_ai_tokens(GList* tokens)
|
||||
{
|
||||
if (tokens) {
|
||||
g_list_free_full(tokens, g_free);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Models cache --- */
|
||||
|
||||
gboolean
|
||||
prefs_ai_set_models(const char* const provider, const gchar* const* models, gint count)
|
||||
{
|
||||
if (!provider || count <= 0 || !prefs)
|
||||
return FALSE;
|
||||
|
||||
GString* model_str = g_string_new("");
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (i > 0)
|
||||
g_string_append_c(model_str, ';');
|
||||
g_string_append(model_str, models[i]);
|
||||
}
|
||||
|
||||
gboolean ok = _ai_set_string(provider, "models", model_str->str);
|
||||
g_string_free(model_str, TRUE);
|
||||
return ok;
|
||||
}
|
||||
|
||||
gboolean
|
||||
prefs_ai_remove_models(const char* const provider)
|
||||
{
|
||||
return _ai_remove_string(provider, "models");
|
||||
}
|
||||
|
||||
GList*
|
||||
prefs_ai_get_models(const char* const provider)
|
||||
{
|
||||
auto_gchar gchar* model_str = _ai_get_string(provider, "models");
|
||||
if (!model_str || model_str[0] == '\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;
|
||||
}
|
||||
|
||||
void
|
||||
prefs_free_ai_models(GList* models)
|
||||
{
|
||||
if (models) {
|
||||
g_list_free_full(models, g_free);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- 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);
|
||||
return _ai_set_string(provider, "default_model", model);
|
||||
}
|
||||
|
||||
gboolean
|
||||
prefs_ai_remove_default_model(const char* const provider)
|
||||
{
|
||||
return _ai_remove_string(provider, "default_model");
|
||||
}
|
||||
|
||||
char*
|
||||
prefs_ai_get_default_model(const char* const provider)
|
||||
{
|
||||
return _ai_get_string(provider, "default_model");
|
||||
}
|
||||
|
||||
/* --- Provider URL --- */
|
||||
|
||||
gboolean
|
||||
prefs_ai_set_provider(const char* const provider, const char* const url)
|
||||
{
|
||||
if (!provider || !url)
|
||||
return FALSE;
|
||||
return _ai_set_string(provider, "url", url);
|
||||
}
|
||||
|
||||
gboolean
|
||||
prefs_ai_remove_provider(const char* const provider)
|
||||
{
|
||||
/* 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 FALSE;
|
||||
|
||||
auto_gchar gchar* group = _ai_section(provider);
|
||||
if (!g_key_file_has_group(prefs, group))
|
||||
return FALSE;
|
||||
|
||||
g_key_file_remove_group(prefs, group, NULL);
|
||||
_save_prefs();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
GList*
|
||||
prefs_ai_list_providers(void)
|
||||
{
|
||||
if (!prefs)
|
||||
return NULL;
|
||||
|
||||
GList* result = NULL;
|
||||
gsize len = 0;
|
||||
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++) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void
|
||||
prefs_free_ai_providers(GList* providers)
|
||||
{
|
||||
if (providers) {
|
||||
g_list_free_full(providers, g_free);
|
||||
}
|
||||
}
|
||||
|
||||
GList*
|
||||
prefs_ai_get_providers(void)
|
||||
{
|
||||
GList* configured = prefs_ai_list_providers();
|
||||
GList* result = NULL;
|
||||
|
||||
for (GList* curr = configured; curr; curr = g_list_next(curr)) {
|
||||
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));
|
||||
result = g_list_append(result, g_strdup(url));
|
||||
}
|
||||
}
|
||||
prefs_free_ai_providers(configured);
|
||||
|
||||
/* Seed openai/perplexity into the in-memory result on very first init
|
||||
* only. In production the "defaults_initialized" sentinel in [ai] is what
|
||||
* tells us we've already seeded — a user who removes every provider must
|
||||
* not get them silently re-added on the next start. When prefs is NULL
|
||||
* (e.g. unit tests that bring up ai_client without prefs_load) we still
|
||||
* seed the in-memory list so behaviour matches a fresh first run, but
|
||||
* skip the persistence write. */
|
||||
gboolean already_seeded = prefs && g_key_file_get_boolean(prefs, PREF_GROUP_AI, "defaults_initialized", NULL);
|
||||
if (!already_seeded && !result) {
|
||||
if (prefs) {
|
||||
prefs_ai_set_provider("openai", "https://api.openai.com/");
|
||||
prefs_ai_set_provider("perplexity", "https://api.perplexity.ai/");
|
||||
}
|
||||
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/"));
|
||||
}
|
||||
if (prefs && !already_seeded) {
|
||||
g_key_file_set_boolean(prefs, PREF_GROUP_AI, "defaults_initialized", TRUE);
|
||||
_save_prefs();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* --- Custom per-provider settings (setting.<name>) --- */
|
||||
|
||||
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.
|
||||
@@ -1865,6 +2291,9 @@ _get_group(preference_t pref)
|
||||
return PREF_GROUP_OMEMO;
|
||||
case PREF_OX_LOG:
|
||||
return PREF_GROUP_OX;
|
||||
case PREF_AI_PROVIDER:
|
||||
case PREF_AI_API_KEY:
|
||||
return PREF_GROUP_AI;
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
@@ -2142,6 +2571,10 @@ _get_key(preference_t pref)
|
||||
return "stamp.incoming";
|
||||
case PREF_OX_LOG:
|
||||
return "log";
|
||||
case PREF_AI_PROVIDER:
|
||||
return "provider";
|
||||
case PREF_AI_API_KEY:
|
||||
return "api_key";
|
||||
case PREF_MOOD:
|
||||
return "mood";
|
||||
case PREF_VCARD_PHOTO_CMD:
|
||||
@@ -2319,6 +2752,10 @@ _get_default_string(preference_t pref)
|
||||
return "on";
|
||||
case PREF_FORCE_ENCRYPTION_MODE:
|
||||
return "resend-to-confirm";
|
||||
case PREF_AI_PROVIDER:
|
||||
return "openai";
|
||||
case PREF_AI_API_KEY:
|
||||
return "";
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -183,6 +183,9 @@ typedef enum {
|
||||
PREF_OX_LOG,
|
||||
PREF_MOOD,
|
||||
PREF_STROPHE_VERBOSITY,
|
||||
PREF_AI_PROVIDER,
|
||||
PREF_AI_API_KEY,
|
||||
PREF_AI_DEFAULT_MODEL,
|
||||
PREF_STROPHE_SM_ENABLED,
|
||||
PREF_STROPHE_SM_RESEND,
|
||||
PREF_VCARD_PHOTO_CMD,
|
||||
@@ -356,4 +359,42 @@ gboolean prefs_get_room_notify(const char* const roomjid);
|
||||
gboolean prefs_get_room_notify_mention(const char* const roomjid);
|
||||
gboolean prefs_get_room_notify_trigger(const char* const roomjid);
|
||||
|
||||
/* AI token management */
|
||||
gboolean prefs_ai_set_token(const char* const provider, const char* const token);
|
||||
gboolean prefs_ai_remove_token(const char* const provider);
|
||||
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);
|
||||
|
||||
/* Wipe the whole [ai/<provider>] 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);
|
||||
GList* prefs_ai_get_models(const char* const provider);
|
||||
void prefs_free_ai_models(GList* models);
|
||||
|
||||
/* AI default model per provider */
|
||||
gboolean prefs_ai_set_default_model(const char* const provider, const char* const model);
|
||||
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.<name>=<value> inside [ai/<provider>]. 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
|
||||
|
||||
Reference in New Issue
Block a user