Files
Hermit/tests/test_model_registry.c
WHC 50abcf1a91
ci / tcc-check (push) Successful in 1m9s
ci / linux (aarch64, 0) (push) Failing after 58m44s
ci / linux (x86, 1) (push) Successful in 13m47s
ci / linux (x86_64, 1) (push) Failing after 1h0m2s
ci / windows (push) Successful in 10m46s
build: finish cross-platform release tooling
2026-10-10 16:21:00 +08:00

231 lines
9.1 KiB
C

/* 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 <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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("deepseek", "deepseek-flash", &mi);
EXPECT(mi.context == 1000000 && mi.vision == 1, "deepseek-flash takes images");
cst_model_info("deepseek", "deepseek-chat", &mi);
EXPECT(mi.vision == 1, "deepseek-chat is deepseek-flash");
cst_model_info(NULL, "gpt-4o-mini-2024", &mi);
/* x87 (i686) compares the literal at long double precision: no == here */
EXPECT(fabs(mi.price.miss - 0.15) < 1e-12 && 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.price.miss == 2.0 && strcmp(mi.price.currency, "¥") == 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;
}