/* Model registry: built-in data, longest/most specific match, the user file, * the fetched cache and the env overrides. */ #define _POSIX_C_SOURCE 200809L #include "cJSON.h" #include "cst_agent.h" #include "cst_models.h" #include "cst_test_util.h" #include "os_compat.h" #include #include #include static int nfail; #define EXPECT(cond, msg) \ do { \ if (!(cond)) { \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \ nfail++; \ } \ } while (0) static void test_builtin(void) { cJSON *doc = cJSON_Parse(cst_models_builtin_json()); EXPECT(cJSON_IsObject(doc), "embedded built-in JSON parses"); cJSON_Delete(doc); EXPECT(cst_models_load_error()[0] == 0, "no load error"); EXPECT(cst_provider_count() >= 4, "built-in providers"); const cst_provider_t *p = cst_provider_at(0); EXPECT(p && strcmp(p->name, "minimax") == 0, "file order kept"); p = cst_provider_find("deepseek"); EXPECT(p && strcmp(p->model, "deepseek-flash") == 0 && strcmp(p->key_env, "DEEPSEEK_API_KEY") == 0 && strcmp(cst_provider_source(p), "built-in") == 0, "deepseek preset"); cst_model_info_t mi; /* most specific rule per field: price from deepseek-v4-pro, context from * deepseek */ cst_model_info("deepseek", "deepseek-v4-pro", &mi); EXPECT(mi.context == 1000000 && mi.vision == 0, "context inherited"); EXPECT(mi.has_price && mi.price.miss == 9.0 && mi.price.offpeak_half, "longer match wins the price"); EXPECT(strcmp(mi.source, "built-in") == 0, "source built-in"); cst_model_info(NULL, "gpt-4o-mini-2024", &mi); EXPECT(mi.price.miss == 0.15 && mi.context == 128000 && mi.vision == 1, "gpt-4o-mini over gpt-4o"); cst_model_info(NULL, "Qwen/Qwen2.5-VL-72B-Instruct", &mi); EXPECT(mi.vision == 1 && mi.context == 131072, "glob *-vl* + qwen prefix"); cst_model_info(NULL, "zhipu/glm-4.5v", &mi); EXPECT(mi.vision == 1 && mi.context == 128000, "glm-*v*"); cst_model_info(NULL, "glm-4-plus", &mi); EXPECT(mi.vision == -1, "glm without v: unknown"); cst_model_info(NULL, "anthropic/some-new-model", &mi); EXPECT(mi.cache_style == CST_CACHE_ANTHROPIC, "vendor rule on the full id"); cst_model_info(NULL, "totally-unknown", &mi); EXPECT(mi.context == 128000 && mi.vision == -1 && !mi.has_price && strcmp(mi.source, "default") == 0, "defaults"); char tag[48]; cst_model_info(NULL, "gpt-4.1", &mi); cst_model_info_label(&mi, tag, sizeof(tag)); EXPECT(strcmp(tag, "1M · vision") == 0, "label"); } static const char *USER_JSON = "{\"providers\": {" " \"deepseek\": {\"api_base\": \"https://proxy.example/v1\"}," " \"local\": {\"api_base\": \"http://127.0.0.1:8000/v1\"," " \"key_env\": \"LOCAL_KEY\", \"default_model\": \"qwen3-32b\"," " \"models\": [\"qwen3-32b\", \"my-vl\"]}" "}," "\"models\": [" " {\"match\": \"qwen3\", \"context\": 32768}," " {\"match\": \"my-vl\", \"vision\": true, \"price\": {\"miss\": 1," " \"out\": 2, \"currency\": \"¥\"}}" "]}"; static void test_user(void) { char err[256]; EXPECT(cst_models_load_json(cst_models_builtin_json(), USER_JSON, err, sizeof(err)) == 0, "load with user file"); const cst_provider_t *ds = cst_provider_find("deepseek"); EXPECT(ds && strcmp(ds->api_base, "https://proxy.example/v1") == 0 && strcmp(ds->model, "deepseek-flash") == 0 && strcmp(cst_provider_source(ds), "user") == 0, "user overrides one field, keeps the rest"); const cst_provider_t *lp = cst_provider_find("local"); EXPECT(lp && strcmp(lp->model, "qwen3-32b") == 0 && strcmp(lp->key_env, "LOCAL_KEY") == 0, "user adds a provider"); EXPECT(cst_provider_at(cst_provider_count() - 1) == lp, "appended last"); cst_model_info_t mi; cst_model_info("local", "qwen3-32b", &mi); EXPECT(mi.context == 32768 && strcmp(mi.source, "user") == 0, "user rule beats built-in qwen"); cst_model_info("local", "my-vl", &mi); EXPECT(mi.vision == 1 && mi.has_price && mi.price.hit == 1 && mi.price.write == 1 && strcmp(mi.price.currency, "¥") == 0, "user price, hit/write default to miss"); /* passback follows the provider name even behind a proxy base */ cst_model_caps_t caps; cst_model_caps("deepseek", "https://proxy.example/v1", "deepseek-flash", &caps); EXPECT(caps.reasoning_passback && caps.prefix_completion, "caps by name"); char names[32][128]; int n = cst_models_list(NULL, names, 32); int seen = 0; for (int i = 0; i < n; i++) seen += strcmp(names[i], "my-vl") == 0; EXPECT(seen == 1, "picker list includes user models"); EXPECT(cst_models_load_json(cst_models_builtin_json(), "{not json", err, sizeof(err)) != 0 && err[0], "bad user file reported"); } static void test_cache(void) { cst_models_load_json(cst_models_builtin_json(), NULL, NULL, 0); cst_model_meta_t m[3]; memset(m, 0, sizeof(m)); snprintf(m[0].id, sizeof(m[0].id), "anthropic/claude-sonnet-4"); m[0].context = 1000000; m[0].vision = -1; snprintf(m[1].id, sizeof(m[1].id), "deepseek-flash"); m[1].vision = 1; /* API claims vision */ m[1].has_price = 1; m[1].miss = 0.5; m[1].hit = 0.1; m[1].write = 0.5; m[1].out = 1; snprintf(m[2].id, sizeof(m[2].id), "brand-new"); m[2].vision = -1; EXPECT(cst_models_cache_save("teamorouter", "https://x/v1", m, 3) == 0, "cache saved"); cst_model_info_t mi; cst_model_info("teamorouter", "anthropic/claude-sonnet-4", &mi); EXPECT(mi.context == 1000000, "API context beats built-in"); EXPECT(mi.vision == 1 && mi.cache_style == CST_CACHE_ANTHROPIC, "missing API fields fall through to built-in"); EXPECT(strcmp(mi.source, "api+built-in") == 0, "source lists both"); cst_model_info("teamorouter", "deepseek-flash", &mi); EXPECT(mi.vision == 1 && mi.price.miss == 0.5 && strcmp(mi.price.currency, "$") == 0, "API vision and USD price"); cst_model_info("deepseek", "deepseek-flash", &mi); EXPECT(mi.vision == 0 && mi.price.miss == 2.0, "another provider's cache is not used"); EXPECT(cst_model_context_tokens("anthropic/claude-sonnet-4") == 1000000, "no provider: any cache, exact id"); cst_model_meta_t back[8]; long age = -1; int n = cst_models_cache_load("teamorouter", back, 8, &age); EXPECT(n == 3 && age >= 0 && age < 60 && strcmp(back[2].id, "brand-new") == 0, "cache reads back"); EXPECT(cst_models_cache_load("openai", back, 8, &age) == -1, "no cache"); EXPECT(cst_models_cache_save("../evil", NULL, m, 1) != 0, "provider name cannot leave the cache dir"); /* a fresh process (reload) sees the file */ cst_models_reload(); cst_model_info("teamorouter", "anthropic/claude-sonnet-4", &mi); EXPECT(mi.context == 1000000, "cache survives a reload"); } static void test_env(void) { cst_models_load_json(cst_models_builtin_json(), NULL, NULL, 0); setenv("CST_CTX_TOKENS", "4242", 1); setenv("CST_PRICE", "1,0.5,3", 1); cst_model_info_t mi; cst_model_info(NULL, "deepseek-flash", &mi); EXPECT(mi.context == 4242 && mi.price.miss == 1 && mi.price.write == 1 && strstr(mi.source, "env"), "env overrides"); unsetenv("CST_CTX_TOKENS"); unsetenv("CST_PRICE"); setenv("CST_NO_CACHE_HINTS", "1", 1); cst_model_caps_t caps; cst_model_caps("openai", NULL, "gpt-4.1", &caps); EXPECT(caps.cache_style == CST_CACHE_AUTO && caps.vision == 1, "no cache hints, vision kept"); unsetenv("CST_NO_CACHE_HINTS"); setenv("CST_MODELS", "a, b:c", 1); char names[8][128]; int n = cst_models_list("cur", names, 8); EXPECT(n == 4 && strcmp(names[1], "b") == 0 && strcmp(names[3], "cur") == 0, "CST_MODELS list"); unsetenv("CST_MODELS"); } int main(void) { char root[1024]; if (test_tmpdir(root, sizeof(root), "cst-models") != 0) { fprintf(stderr, "cannot create a scratch dir\n"); return 1; } setenv("XDG_CONFIG_HOME", root, 1); /* no real models.json or caches */ unsetenv("CST_CTX_TOKENS"); unsetenv("CST_PRICE"); unsetenv("CST_MODELS"); unsetenv("CST_NO_CACHE_HINTS"); test_builtin(); test_user(); test_cache(); test_env(); test_rmrf(root); if (nfail) { fprintf(stderr, "test_model_registry: %d failed\n", nfail); return 1; } printf("test_model_registry: OK\n"); return 0; }