[#110] Add AI client with multi-provider support and UI #113
@@ -762,44 +762,133 @@ _ai_save_models_for_provider(AIProvider* provider)
|
||||
log_debug("Saved %d models for provider '%s' to prefs", count, provider->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a JSON field value, handling both string and non-string values.
|
||||
* For string values: "field":"value" -> returns pointer to "value"
|
||||
* For array values: "field":[ -> returns pointer to [
|
||||
* For object values: "field":{ -> returns pointer to {
|
||||
* Handles optional whitespace after colon.
|
||||
* Returns pointer to the value start, or NULL if not found.
|
||||
*/
|
||||
static const gchar*
|
||||
_find_json_field(const gchar* json, const gchar* field)
|
||||
{
|
||||
auto_gchar gchar* key = g_strdup_printf("\"%s\":\"", field);
|
||||
if (!json || !field) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto_gchar gchar* key = g_strdup_printf("\"%s\"", field);
|
||||
const gchar* pos = strstr(json, key);
|
||||
if (!pos) {
|
||||
return NULL;
|
||||
}
|
||||
return pos + strlen(key);
|
||||
|
||||
/* Skip past the key */
|
||||
pos += strlen(key);
|
||||
|
||||
/* Skip whitespace and find the colon */
|
||||
while (*pos == ' ' || *pos == '\t' || *pos == '\n' || *pos == '\r') {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (*pos != ':') {
|
||||
return NULL;
|
||||
}
|
||||
pos++; /* skip colon */
|
||||
|
||||
/* Skip whitespace after colon */
|
||||
while (*pos == ' ' || *pos == '\t' || *pos == '\n' || *pos == '\r') {
|
||||
pos++;
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse models from OpenAI-compatible API response format.
|
||||
* Expected format: {"object":"list","data":[{"id":"model1","object":"model",...},...]}
|
||||
* Extracts only the "id" field from each model object.
|
||||
*/
|
||||
static void
|
||||
_parse_openai_models(AIProvider* provider, const gchar* json)
|
||||
{
|
||||
/* Format: {"data": [{"id": "model1"}, {"id": "model2"}, ...]} */
|
||||
const gchar* data_start = _find_json_field(json, "data");
|
||||
if (!data_start) {
|
||||
if (!json || !provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gchar* obj_start = strstr(data_start, "{\"id\":");
|
||||
/* Find the "data" array start */
|
||||
const gchar* data_start = _find_json_field(json, "data");
|
||||
if (!data_start || *data_start != '[') {
|
||||
log_debug("No data array found in API response");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Find all {"id":"..."} objects within the data array */
|
||||
const gchar* obj_start = data_start;
|
||||
while (obj_start) {
|
||||
const gchar* id_start = _find_json_field(obj_start, "id");
|
||||
if (!id_start) {
|
||||
/* Find next "id" field (handle both {"id": and { "id": formats) */
|
||||
obj_start = strstr(obj_start, "\"id\"");
|
||||
if (!obj_start) {
|
||||
break;
|
||||
}
|
||||
|
||||
const gchar* id_end = strchr(id_start, '"');
|
||||
if (!id_end) {
|
||||
break;
|
||||
/* Verify this "id" is within an object (preceded by { or ,) */
|
||||
const gchar* before_id = obj_start - 1;
|
||||
while (before_id >= data_start && (*before_id == ' ' || *before_id == '\t' || *before_id == '\n' || *before_id == '\r')) {
|
||||
before_id--;
|
||||
}
|
||||
if (before_id < data_start || (*before_id != '{' && *before_id != ',')) {
|
||||
/* This "id" is not at the start of an object in the array, skip it */
|
||||
obj_start = obj_start + 2; /* move past this "id" */
|
||||
continue;
|
||||
}
|
||||
|
||||
auto_gchar gchar* model = g_strndup(id_start, id_end - id_start);
|
||||
/* Find the colon after "id" */
|
||||
const gchar* id_key = obj_start + 4; /* skip "id" */
|
||||
/* Skip whitespace */
|
||||
while (*id_key == ' ' || *id_key == '\t' || *id_key == '\n' || *id_key == '\r') {
|
||||
id_key++;
|
||||
}
|
||||
if (*id_key != ':') {
|
||||
obj_start = id_key;
|
||||
continue;
|
||||
}
|
||||
id_key++; /* skip : */
|
||||
/* Skip whitespace */
|
||||
while (*id_key == ' ' || *id_key == '\t' || *id_key == '\n' || *id_key == '\r') {
|
||||
id_key++;
|
||||
}
|
||||
if (*id_key != '"') {
|
||||
/* "id" value is not a string, skip */
|
||||
obj_start = id_key;
|
||||
continue;
|
||||
}
|
||||
id_key++; /* skip opening quote */
|
||||
|
||||
/* Find the closing quote (handling escaped quotes) */
|
||||
const gchar* id_end = id_key;
|
||||
while (*id_end && *id_end != '"') {
|
||||
if (*id_end == '\\' && *(id_end + 1)) {
|
||||
id_end += 2; /* skip escaped character */
|
||||
} else {
|
||||
id_end++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!*id_end) {
|
||||
break; /* No closing quote found */
|
||||
}
|
||||
|
||||
/* Extract the model ID */
|
||||
auto_gchar gchar* model = g_strndup(id_key, id_end - id_key);
|
||||
_add_model_if_new(provider, model);
|
||||
|
||||
obj_start = strstr(id_end, "{\"id\":");
|
||||
if (obj_start) {
|
||||
obj_start++;
|
||||
/* Move past this object to find the next one */
|
||||
obj_start = id_end + 1;
|
||||
|
||||
/* Safety: stop if we've gone past the data array */
|
||||
if (obj_start - json > 5 * 1024 * 1024) {
|
||||
break; /* 5MB safety limit */
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -849,6 +938,30 @@ _parse_and_cache_models(AIProvider* provider, const gchar* response_json)
|
||||
_ai_save_models_for_provider(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public API: Parse model IDs from JSON response.
|
||||
* Used by tests and internal model fetching.
|
||||
*/
|
||||
void
|
||||
ai_parse_models_from_json(AIProvider* provider, const gchar* json)
|
||||
{
|
||||
if (!provider || !json) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Clear existing models before parsing */
|
||||
GList* curr = provider->models;
|
||||
while (curr) {
|
||||
g_free(curr->data);
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
g_list_free(provider->models);
|
||||
provider->models = NULL;
|
||||
|
||||
/* Parse using the same logic as the internal handler */
|
||||
_parse_and_cache_models(provider, json);
|
||||
}
|
||||
|
||||
static void
|
||||
_models_response_handler(AIProvider* provider, const gchar* response, gpointer user_data)
|
||||
{
|
||||
@@ -893,20 +1006,7 @@ ai_fetch_models(const gchar* provider_name, gpointer user_data)
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* If models are fresh, just display them */
|
||||
if (provider->models_fresh && provider->models) {
|
||||
cons_show("Cached %d models for provider '%s':", g_list_length(provider->models), 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;
|
||||
}
|
||||
|
||||
/* Fetch models in a thread */
|
||||
/* Always fetch fresh models from API */
|
||||
ai_request_t* req = g_new0(ai_request_t, 1);
|
||||
req->provider_name = g_strdup(provider_name);
|
||||
req->user_data = user_data;
|
||||
@@ -920,7 +1020,7 @@ ai_fetch_models(const gchar* provider_name, gpointer user_data)
|
||||
}
|
||||
|
||||
g_thread_unref(thread);
|
||||
cons_show("Fetching models for provider '%s'...", provider_name);
|
||||
cons_show("Fetching fresh models for provider '%s'...", provider_name);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,18 @@ gboolean ai_fetch_models(const gchar* provider_name, gpointer user_data);
|
||||
*/
|
||||
gboolean ai_models_are_fresh(const gchar* provider_name);
|
||||
|
||||
/* ========================================================================
|
||||
* Model Parsing (for testing)
|
||||
* ======================================================================== */
|
||||
|
||||
/**
|
||||
* Parse model IDs from an OpenAI-compatible API response.
|
||||
* Expected format: {"object":"list","data":[{"id":"model1",...},...]}
|
||||
* @param provider The provider to add models to
|
||||
* @param json The JSON response from the API
|
||||
*/
|
||||
void ai_parse_models_from_json(AIProvider* provider, const gchar* json);
|
||||
|
||||
/* ========================================================================
|
||||
* Session Management
|
||||
* ======================================================================== */
|
||||
|
||||
@@ -11134,14 +11134,16 @@ cmd_ai_switch(ProfWin* window, const char* const command, gchar** args)
|
||||
gboolean
|
||||
cmd_ai_models(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// /ai models <provider> [--refresh]
|
||||
// /ai models <provider> [--cached]
|
||||
// Default: fetch fresh models from API
|
||||
// --cached: display cached models (if available)
|
||||
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);
|
||||
gboolean use_cached = (g_strv_length(args) >= 3) && (g_strcmp0(args[2], "--cached") == 0);
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider) {
|
||||
@@ -11149,12 +11151,12 @@ cmd_ai_models(ProfWin* window, const char* const command, gchar** args)
|
||||
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
|
||||
if (use_cached) {
|
||||
// Display cached models (if available)
|
||||
if (!provider->models) {
|
||||
cons_show("No cached models for provider '%s'. Use '/ai models %s' to fetch from API.", provider_name, provider_name);
|
||||
return TRUE;
|
||||
}
|
||||
cons_show("Cached models for provider '%s':", provider_name);
|
||||
GList* curr = provider->models;
|
||||
while (curr) {
|
||||
@@ -11162,7 +11164,11 @@ cmd_ai_models(ProfWin* window, const char* const command, gchar** args)
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
cons_show("");
|
||||
cons_show("Use '/ai models %s --refresh' to fetch fresh models from the API.", provider_name);
|
||||
cons_show("Use '/ai models %s' to fetch fresh models from the API.", provider_name);
|
||||
} else {
|
||||
// Default: fetch fresh models from API
|
||||
ProfAiWin* aiwin = (window && window->type == WIN_AI) ? (ProfAiWin*)window : wins_get_ai();
|
||||
ai_fetch_models(provider_name, aiwin);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
|
||||
@@ -644,3 +644,226 @@ test_ai_models_are_fresh_initial(void** state)
|
||||
gboolean fresh_perplexity = ai_models_are_fresh("perplexity");
|
||||
assert_true(fresh_perplexity == TRUE || fresh_perplexity == FALSE); /* Just verify no crash */
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Model Parsing Tests
|
||||
* ======================================================================== */
|
||||
|
||||
void
|
||||
test_ai_parse_models_openai_format(void** state)
|
||||
{
|
||||
/* Test parsing OpenAI-compatible API response format */
|
||||
/* Format: {"object":"list","data":[{"id":"model1",...},...]} */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL);
|
||||
|
||||
/* JSON response from Perplexity/OpenAI API */
|
||||
const gchar* json = "{\"object\":\"list\","
|
||||
"\"data\":["
|
||||
"{\"id\":\"anthropic/claude-haiku-4-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"},"
|
||||
"{\"id\":\"anthropic/claude-opus-4-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"},"
|
||||
"{\"id\":\"openai/gpt-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"},"
|
||||
"{\"id\":\"google/gemini-3-flash-preview\",\"object\":\"model\",\"created\":0,\"owned_by\":\"google\"}"
|
||||
"]}";
|
||||
|
||||
ai_parse_models_from_json(provider, json);
|
||||
|
||||
/* Verify models were parsed correctly */
|
||||
assert_int_equal(4, g_list_length(provider->models));
|
||||
|
||||
/* Check specific models */
|
||||
GList* models = provider->models;
|
||||
assert_string_equal("anthropic/claude-haiku-4-5", models->data);
|
||||
models = g_list_next(models);
|
||||
assert_string_equal("anthropic/claude-opus-4-5", models->data);
|
||||
models = g_list_next(models);
|
||||
assert_string_equal("openai/gpt-5", models->data);
|
||||
models = g_list_next(models);
|
||||
assert_string_equal("google/gemini-3-flash-preview", models->data);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_parse_models_perplexity_format(void** state)
|
||||
{
|
||||
/* Test parsing actual Perplexity API response with all models from curl test */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("perplexity", "https://api.perplexity.ai/", NULL);
|
||||
|
||||
const gchar* json = "{\"object\":\"list\","
|
||||
"\"data\":["
|
||||
"{\"id\":\"anthropic/claude-haiku-4-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"},"
|
||||
"{\"id\":\"anthropic/claude-opus-4-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"},"
|
||||
"{\"id\":\"anthropic/claude-opus-4-6\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"},"
|
||||
"{\"id\":\"anthropic/claude-opus-4-7\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"},"
|
||||
"{\"id\":\"anthropic/claude-sonnet-4-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"},"
|
||||
"{\"id\":\"anthropic/claude-sonnet-4-6\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"},"
|
||||
"{\"id\":\"google/gemini-3-flash-preview\",\"object\":\"model\",\"created\":0,\"owned_by\":\"google\"},"
|
||||
"{\"id\":\"google/gemini-3.1-pro-preview\",\"object\":\"model\",\"created\":0,\"owned_by\":\"google\"},"
|
||||
"{\"id\":\"nvidia/nemotron-3-super-120b-a12b\",\"object\":\"model\",\"created\":0,\"owned_by\":\"nvidia\"},"
|
||||
"{\"id\":\"openai/gpt-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"},"
|
||||
"{\"id\":\"openai/gpt-5-mini\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"},"
|
||||
"{\"id\":\"openai/gpt-5.1\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"},"
|
||||
"{\"id\":\"openai/gpt-5.2\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"},"
|
||||
"{\"id\":\"openai/gpt-5.4\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"},"
|
||||
"{\"id\":\"openai/gpt-5.4-mini\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"},"
|
||||
"{\"id\":\"openai/gpt-5.4-nano\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"},"
|
||||
"{\"id\":\"openai/gpt-5.5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"},"
|
||||
"{\"id\":\"perplexity/sonar\",\"object\":\"model\",\"created\":0,\"owned_by\":\"perplexity\"},"
|
||||
"{\"id\":\"xai/grok-4-1-fast-non-reasoning\",\"object\":\"model\",\"created\":0,\"owned_by\":\"xai\"},"
|
||||
"{\"id\":\"xai/grok-4.20-multi-agent\",\"object\":\"model\",\"created\":0,\"owned_by\":\"xai\"},"
|
||||
"{\"id\":\"xai/grok-4.20-non-reasoning\",\"object\":\"model\",\"created\":0,\"owned_by\":\"xai\"},"
|
||||
"{\"id\":\"xai/grok-4.20-reasoning\",\"object\":\"model\",\"created\":0,\"owned_by\":\"xai\"},"
|
||||
"{\"id\":\"xai/grok-4.3\",\"object\":\"model\",\"created\":0,\"owned_by\":\"xai\"}"
|
||||
"]}";
|
||||
|
||||
ai_parse_models_from_json(provider, json);
|
||||
|
||||
/* Verify all 23 models were parsed correctly */
|
||||
assert_int_equal(23, g_list_length(provider->models));
|
||||
|
||||
/* Verify NO field names or owned_by values are in the model list */
|
||||
/* The bug was that "id", "object", "model", "created", "owned_by" and provider names
|
||||
* like "anthropic", "google", "openai", "nvidia", "perplexity", "xai" were extracted */
|
||||
GList* models = provider->models;
|
||||
while (models) {
|
||||
const gchar* model = (const gchar*)models->data;
|
||||
/* These should never be model IDs */
|
||||
assert_false(g_strcmp0(model, "id") == 0);
|
||||
assert_false(g_strcmp0(model, "object") == 0);
|
||||
assert_false(g_strcmp0(model, "model") == 0);
|
||||
assert_false(g_strcmp0(model, "created") == 0);
|
||||
assert_false(g_strcmp0(model, "owned_by") == 0);
|
||||
assert_false(g_strcmp0(model, "list") == 0);
|
||||
/* These are owned_by values, not model IDs */
|
||||
assert_false(g_strcmp0(model, "anthropic") == 0);
|
||||
assert_false(g_strcmp0(model, "google") == 0);
|
||||
assert_false(g_strcmp0(model, "openai") == 0);
|
||||
assert_false(g_strcmp0(model, "nvidia") == 0);
|
||||
assert_false(g_strcmp0(model, "perplexity") == 0);
|
||||
assert_false(g_strcmp0(model, "xai") == 0);
|
||||
models = g_list_next(models);
|
||||
}
|
||||
|
||||
/* Verify first and last model IDs */
|
||||
models = provider->models;
|
||||
assert_string_equal("anthropic/claude-haiku-4-5", models->data);
|
||||
models = g_list_last(models);
|
||||
assert_string_equal("xai/grok-4.3", models->data);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_parse_models_array_format(void** state)
|
||||
{
|
||||
/* Test parsing simple array format: ["model1", "model2", ...] */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL);
|
||||
|
||||
const gchar* json = "[\"model1\", \"model2\", \"model3\"]";
|
||||
|
||||
ai_parse_models_from_json(provider, json);
|
||||
|
||||
assert_int_equal(3, g_list_length(provider->models));
|
||||
|
||||
GList* models = provider->models;
|
||||
assert_string_equal("model1", models->data);
|
||||
models = g_list_next(models);
|
||||
assert_string_equal("model2", models->data);
|
||||
models = g_list_next(models);
|
||||
assert_string_equal("model3", models->data);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_parse_models_empty_json(void** state)
|
||||
{
|
||||
/* Test parsing empty/invalid JSON */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL);
|
||||
|
||||
ai_parse_models_from_json(provider, "");
|
||||
assert_null(provider->models);
|
||||
assert_int_equal(0, g_list_length(provider->models));
|
||||
|
||||
ai_parse_models_from_json(provider, "{\"invalid\": true}");
|
||||
assert_null(provider->models);
|
||||
assert_int_equal(0, g_list_length(provider->models));
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_parse_models_null_handling(void** state)
|
||||
{
|
||||
/* Test NULL handling */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL);
|
||||
|
||||
/* NULL json should not crash */
|
||||
ai_parse_models_from_json(provider, NULL);
|
||||
assert_null(provider->models);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_parse_models_escaped_quotes(void** state)
|
||||
{
|
||||
/* Test parsing model IDs with escaped quotes */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL);
|
||||
|
||||
const gchar* json = "{\"object\":\"list\","
|
||||
"\"data\":["
|
||||
"{\"id\":\"test/model\\\"with\\\"quotes\",\"object\":\"model\",\"created\":0,\"owned_by\":\"test\"}"
|
||||
"]}";
|
||||
|
||||
ai_parse_models_from_json(provider, json);
|
||||
|
||||
assert_int_equal(1, g_list_length(provider->models));
|
||||
/* Model ID should have the escaped quotes handled */
|
||||
GList* models = provider->models;
|
||||
assert_non_null(models);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_parse_models_with_whitespace(void** state)
|
||||
{
|
||||
/* Test parsing JSON with whitespace after colons */
|
||||
ai_client_init();
|
||||
|
||||
AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL);
|
||||
|
||||
/* JSON with spaces after colons (common in formatted JSON) */
|
||||
const gchar* json = "{ \"object\": \"list\", "
|
||||
"\"data\": [ "
|
||||
"{ \"id\": \"model-with-spaces\", \"object\": \"model\", \"created\": 0, \"owned_by\": \"test\" }"
|
||||
"]}";
|
||||
|
||||
ai_parse_models_from_json(provider, json);
|
||||
|
||||
assert_int_equal(1, g_list_length(provider->models));
|
||||
GList* models = provider->models;
|
||||
assert_string_equal("model-with-spaces", models->data);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -47,6 +47,14 @@ void test_ai_set_provider_setting(void** state);
|
||||
void test_ai_get_provider_setting(void** state);
|
||||
/* Model caching tests */
|
||||
void test_ai_models_are_fresh_initial(void** state);
|
||||
/* Model parsing tests */
|
||||
void test_ai_parse_models_openai_format(void** state);
|
||||
void test_ai_parse_models_perplexity_format(void** state);
|
||||
void test_ai_parse_models_array_format(void** state);
|
||||
void test_ai_parse_models_empty_json(void** state);
|
||||
void test_ai_parse_models_null_handling(void** state);
|
||||
void test_ai_parse_models_escaped_quotes(void** state);
|
||||
void test_ai_parse_models_with_whitespace(void** state);
|
||||
/* AI autocomplete integration tests */
|
||||
void test_ai_start_provider_autocomplete_only_on_exact(void** state);
|
||||
void test_ai_models_find_null_session(void** state);
|
||||
|
||||
@@ -697,6 +697,14 @@ main(int argc, char* argv[])
|
||||
cmocka_unit_test_setup_teardown(test_ai_get_provider_setting, ai_client_setup, ai_client_teardown),
|
||||
/* Model caching tests */
|
||||
cmocka_unit_test_setup_teardown(test_ai_models_are_fresh_initial, ai_client_setup, ai_client_teardown),
|
||||
/* Model parsing tests */
|
||||
cmocka_unit_test(test_ai_parse_models_openai_format),
|
||||
cmocka_unit_test(test_ai_parse_models_perplexity_format),
|
||||
cmocka_unit_test(test_ai_parse_models_array_format),
|
||||
cmocka_unit_test(test_ai_parse_models_empty_json),
|
||||
cmocka_unit_test(test_ai_parse_models_null_handling),
|
||||
cmocka_unit_test(test_ai_parse_models_escaped_quotes),
|
||||
cmocka_unit_test(test_ai_parse_models_with_whitespace),
|
||||
/* AI autocomplete integration tests */
|
||||
cmocka_unit_test_setup_teardown(test_ai_start_provider_autocomplete_only_on_exact, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_models_find_null_session, ai_client_setup, ai_client_teardown),
|
||||
|
||||
Reference in New Issue
Block a user