#include "cst_models.h" #include "cJSON.h" #include "os_compat.h" #include #include #include #include #include #include static const char BUILTIN[] = { #include "models_builtin.inc" 0}; const char *cst_models_builtin_json(void) { return BUILTIN; } #define MAX_PROV 32 #define MAX_PMODELS 16 #define MAX_RULES 256 typedef struct { cst_provider_t pub; /* points into the buffers below */ char name[64]; char api_base[256]; char model[128]; char key_env[64]; int passback, prefix; char effort[16]; /* "", "passthrough", "openai" */ char models[MAX_PMODELS][128]; int nmodels; const char *source; } prov_t; enum { F_CONTEXT = 1, F_VISION = 2, F_CACHE = 4, F_REASONING = 8, F_PRICE = 16 }; typedef struct { char match[128]; /* lowercase */ int spec; /* literal characters: higher is more specific */ unsigned has; /* F_* set by this rule */ long context; int vision, cache, reasoning; cst_price_t price; } rule_t; typedef struct { char provider[64]; long fetched_at; cst_model_meta_t m; } cached_t; static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; static struct { int loaded; prov_t prov[MAX_PROV]; int nprov; rule_t builtin[MAX_RULES]; int nbuiltin; rule_t user[MAX_RULES]; int nuser; cached_t *cache; int ncache; char err[1400]; } R; /* --- parsing ------------------------------------------------------------- */ static void lower_copy(char *out, size_t n, const char *s) { size_t i = 0; for (; s && s[i] && i + 1 < n; i++) out[i] = (char)tolower((unsigned char)s[i]); out[i] = 0; } static const char *jstr(const cJSON *o, const char *k) { const cJSON *v = cJSON_GetObjectItemCaseSensitive(o, k); return cJSON_IsString(v) ? v->valuestring : NULL; } static void fix_pub(prov_t *p) { p->pub.name = p->name; p->pub.api_base = p->api_base; p->pub.model = p->model; p->pub.key_env = p->key_env; } static prov_t *prov_by_name(const char *name) { for (int i = 0; i < R.nprov; i++) if (strcmp(R.prov[i].name, name) == 0) return &R.prov[i]; return NULL; } static void parse_providers(const cJSON *obj, const char *source) { const cJSON *it = NULL; cJSON_ArrayForEach(it, obj) { if (!cJSON_IsObject(it) || !it->string || !it->string[0]) continue; prov_t *p = prov_by_name(it->string); if (!p) { if (R.nprov >= MAX_PROV) continue; p = &R.prov[R.nprov++]; memset(p, 0, sizeof(*p)); snprintf(p->name, sizeof(p->name), "%s", it->string); fix_pub(p); } p->source = source; const char *s; if ((s = jstr(it, "api_base"))) snprintf(p->api_base, sizeof(p->api_base), "%s", s); if ((s = jstr(it, "key_env"))) snprintf(p->key_env, sizeof(p->key_env), "%s", s); if ((s = jstr(it, "default_model"))) snprintf(p->model, sizeof(p->model), "%s", s); if ((s = jstr(it, "effort"))) snprintf(p->effort, sizeof(p->effort), "%s", s); const cJSON *b = cJSON_GetObjectItemCaseSensitive(it, "reasoning_passback"); if (cJSON_IsBool(b)) p->passback = cJSON_IsTrue(b); b = cJSON_GetObjectItemCaseSensitive(it, "prefix_completion"); if (cJSON_IsBool(b)) p->prefix = cJSON_IsTrue(b); const cJSON *ms = cJSON_GetObjectItemCaseSensitive(it, "models"); if (cJSON_IsArray(ms)) { p->nmodels = 0; const cJSON *m = NULL; cJSON_ArrayForEach(m, ms) { if (cJSON_IsString(m) && m->valuestring[0] && p->nmodels < MAX_PMODELS) snprintf(p->models[p->nmodels++], 128, "%s", m->valuestring); } } } } static int parse_price(const cJSON *o, cst_price_t *pr) { const cJSON *miss = cJSON_GetObjectItemCaseSensitive(o, "miss"); const cJSON *out = cJSON_GetObjectItemCaseSensitive(o, "out"); if (!cJSON_IsNumber(miss) || !cJSON_IsNumber(out)) return -1; memset(pr, 0, sizeof(*pr)); pr->miss = miss->valuedouble; pr->out = out->valuedouble; const cJSON *v = cJSON_GetObjectItemCaseSensitive(o, "hit"); pr->hit = cJSON_IsNumber(v) ? v->valuedouble : pr->miss; v = cJSON_GetObjectItemCaseSensitive(o, "write"); pr->write = cJSON_IsNumber(v) ? v->valuedouble : pr->miss; const char *c = jstr(o, "currency"); snprintf(pr->currency, sizeof(pr->currency), "%s", c ? c : "$"); pr->offpeak_half = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(o, "offpeak_half")); return 0; } static int parse_rule(const cJSON *o, rule_t *r) { const char *m = jstr(o, "match"); if (!m || !m[0]) return -1; memset(r, 0, sizeof(*r)); lower_copy(r->match, sizeof(r->match), m); for (const char *q = r->match; *q; q++) if (*q != '*') r->spec++; const cJSON *v = cJSON_GetObjectItemCaseSensitive(o, "context"); if (cJSON_IsNumber(v) && v->valuedouble > 0) { r->context = (long)v->valuedouble; r->has |= F_CONTEXT; } v = cJSON_GetObjectItemCaseSensitive(o, "vision"); if (cJSON_IsBool(v)) { r->vision = cJSON_IsTrue(v); r->has |= F_VISION; } v = cJSON_GetObjectItemCaseSensitive(o, "reasoning"); if (cJSON_IsBool(v)) { r->reasoning = cJSON_IsTrue(v); r->has |= F_REASONING; } const char *c = jstr(o, "cache"); if (c) { if (strcmp(c, "openai") == 0) r->cache = CST_CACHE_OPENAI; else if (strcmp(c, "anthropic") == 0) r->cache = CST_CACHE_ANTHROPIC; else r->cache = CST_CACHE_AUTO; r->has |= F_CACHE; } v = cJSON_GetObjectItemCaseSensitive(o, "price"); if (cJSON_IsObject(v) && parse_price(v, &r->price) == 0) r->has |= F_PRICE; return 0; } static int parse_doc(const char *json, int user, char *err, size_t err_sz) { cJSON *doc = cJSON_Parse(json); if (!cJSON_IsObject(doc)) { cJSON_Delete(doc); snprintf(err, err_sz, "not a JSON object"); return -1; } parse_providers(cJSON_GetObjectItemCaseSensitive(doc, "providers"), user ? "user" : "built-in"); rule_t *rules = user ? R.user : R.builtin; int *n = user ? &R.nuser : &R.nbuiltin; const cJSON *it = NULL; cJSON_ArrayForEach(it, cJSON_GetObjectItemCaseSensitive(doc, "models")) { if (*n < MAX_RULES && parse_rule(it, &rules[*n]) == 0) (*n)++; } cJSON_Delete(doc); return 0; } static char *read_file(const char *path) { FILE *f = fopen(path, "rb"); if (!f) return NULL; char *buf = NULL; size_t len = 0; char chunk[8192]; size_t got; while ((got = fread(chunk, 1, sizeof(chunk), f)) > 0 && len < (8u << 20)) { char *nb = realloc(buf, len + got + 1); if (!nb) break; buf = nb; memcpy(buf + len, chunk, got); len += got; } fclose(f); if (buf) buf[len] = 0; return buf; } static int cache_path(const char *provider, char *out, size_t n) { char dir[1024]; if (!provider || !provider[0] || os_config_dir(dir, sizeof(dir)) != 0) return -1; for (const char *q = provider; *q; q++) if (!isalnum((unsigned char)*q) && !strchr("._-", *q)) return -1; int w = snprintf(out, n, "%s/models-cache/%s.json", dir, provider); return w > 0 && (size_t)w < n ? 0 : -1; } static void meta_from_json(const cJSON *o, cst_model_meta_t *m) { memset(m, 0, sizeof(*m)); m->vision = -1; const char *id = jstr(o, "id"); snprintf(m->id, sizeof(m->id), "%s", id ? id : ""); const cJSON *v = cJSON_GetObjectItemCaseSensitive(o, "context"); if (cJSON_IsNumber(v) && v->valuedouble > 0) m->context = (long)v->valuedouble; v = cJSON_GetObjectItemCaseSensitive(o, "vision"); if (cJSON_IsBool(v)) m->vision = cJSON_IsTrue(v); v = cJSON_GetObjectItemCaseSensitive(o, "price"); cst_price_t pr; if (cJSON_IsObject(v) && parse_price(v, &pr) == 0) { m->has_price = 1; m->miss = pr.miss; m->hit = pr.hit; m->write = pr.write; m->out = pr.out; } } /* Appends provider's cache file to R.cache (caller holds the lock). */ static void load_cache(const char *provider) { char path[1100]; if (cache_path(provider, path, sizeof(path)) != 0) return; char *txt = read_file(path); cJSON *doc = txt ? cJSON_Parse(txt) : NULL; free(txt); const cJSON *fa = cJSON_GetObjectItemCaseSensitive(doc, "fetched_at"); long at = cJSON_IsNumber(fa) ? (long)fa->valuedouble : 0; const cJSON *it = NULL; cJSON_ArrayForEach(it, cJSON_GetObjectItemCaseSensitive(doc, "models")) { cst_model_meta_t m; meta_from_json(it, &m); if (!m.id[0]) continue; cached_t *nc = realloc(R.cache, (size_t)(R.ncache + 1) * sizeof(*nc)); if (!nc) break; R.cache = nc; cached_t *c = &R.cache[R.ncache++]; snprintf(c->provider, sizeof(c->provider), "%s", provider); c->fetched_at = at; c->m = m; } cJSON_Delete(doc); } static void drop_cache(const char *provider) { int w = 0; for (int i = 0; i < R.ncache; i++) if (provider && strcmp(R.cache[i].provider, provider) != 0) R.cache[w++] = R.cache[i]; R.ncache = w; } static void reset_locked(void) { R.nprov = 0; R.nbuiltin = 0; R.nuser = 0; drop_cache(NULL); R.err[0] = 0; } static void load_caches_locked(void) { for (int i = 0; i < R.nprov; i++) load_cache(R.prov[i].name); } int cst_models_user_path(char *out, size_t out_sz) { char dir[1024]; if (os_config_dir(dir, sizeof(dir)) != 0) return -1; int w = snprintf(out, out_sz, "%s/models.json", dir); return w > 0 && (size_t)w < out_sz ? 0 : -1; } static void load_default_locked(void) { reset_locked(); char e[128]; if (parse_doc(BUILTIN, 0, e, sizeof(e)) != 0) snprintf(R.err, sizeof(R.err), "built-in model data: %s", e); char path[1100]; char *user = cst_models_user_path(path, sizeof(path)) == 0 ? read_file(path) : NULL; if (user && parse_doc(user, 1, e, sizeof(e)) != 0) snprintf(R.err, sizeof(R.err), "%s: %s", path, e); free(user); load_caches_locked(); R.loaded = 1; } static void ensure_locked(void) { if (!R.loaded) load_default_locked(); } void cst_models_reload(void) { pthread_mutex_lock(&g_mu); load_default_locked(); pthread_mutex_unlock(&g_mu); } int cst_models_load_json(const char *builtin_json, const char *user_json, char *err, size_t err_sz) { char dummy[8]; if (!err || !err_sz) { err = dummy; err_sz = sizeof(dummy); } err[0] = 0; pthread_mutex_lock(&g_mu); reset_locked(); int rc = 0; if (builtin_json && parse_doc(builtin_json, 0, err, err_sz) != 0) rc = -1; if (rc == 0 && user_json && parse_doc(user_json, 1, err, err_sz) != 0) rc = -1; load_caches_locked(); R.loaded = 1; pthread_mutex_unlock(&g_mu); return rc; } const char *cst_models_load_error(void) { pthread_mutex_lock(&g_mu); ensure_locked(); pthread_mutex_unlock(&g_mu); return R.err; } /* --- providers ----------------------------------------------------------- */ const cst_provider_t *cst_provider_find(const char *name) { if (!name || !name[0]) return NULL; pthread_mutex_lock(&g_mu); ensure_locked(); prov_t *p = prov_by_name(name); pthread_mutex_unlock(&g_mu); return p ? &p->pub : NULL; } const cst_provider_t *cst_provider_default(void) { const cst_provider_t *p = cst_provider_find(getenv("CST_PROVIDER")); return p ? p : cst_provider_at(0); } int cst_provider_count(void) { pthread_mutex_lock(&g_mu); ensure_locked(); int n = R.nprov; pthread_mutex_unlock(&g_mu); return n; } const cst_provider_t *cst_provider_at(int i) { pthread_mutex_lock(&g_mu); ensure_locked(); const cst_provider_t *p = i >= 0 && i < R.nprov ? &R.prov[i].pub : NULL; pthread_mutex_unlock(&g_mu); return p; } const char *cst_provider_source(const cst_provider_t *p) { for (int i = 0; p && i < MAX_PROV; i++) if (p == &R.prov[i].pub) return R.prov[i].source ? R.prov[i].source : "built-in"; return ""; } int cst_provider_models(const cst_provider_t *p, char out[][128], int max) { int n = 0; pthread_mutex_lock(&g_mu); for (int i = 0; p && i < R.nprov; i++) { if (p != &R.prov[i].pub) continue; for (int k = 0; k < R.prov[i].nmodels && n < max; k++) snprintf(out[n++], 128, "%s", R.prov[i].models[k]); } pthread_mutex_unlock(&g_mu); return n; } static int push_name(char out[][128], int *n, int max, const char *name) { if (!name || !name[0] || *n >= max) return 0; for (int i = 0; i < *n; i++) if (strcmp(out[i], name) == 0) return 0; snprintf(out[(*n)++], 128, "%s", name); return 1; } int cst_models_list(const char *current, char out[][128], int max_out) { if (!out || max_out <= 0) return 0; int n = 0; const char *env = getenv("CST_MODELS"); if (env && env[0]) { char buf[1024]; snprintf(buf, sizeof(buf), "%s", env); for (char *tok = buf; *tok;) { while (*tok == ':' || *tok == ',' || *tok == ' ') tok++; char *end = tok; while (*end && *end != ':' && *end != ',') end++; char save = *end; *end = 0; char *t = end; while (t > tok && (t[-1] == ' ' || t[-1] == '\t')) *--t = 0; push_name(out, &n, max_out, tok); *end = save; tok = *end ? end + 1 : end; } } else { pthread_mutex_lock(&g_mu); ensure_locked(); for (int i = 0; i < R.nprov; i++) for (int k = 0; k < R.prov[i].nmodels; k++) push_name(out, &n, max_out, R.prov[i].models[k]); pthread_mutex_unlock(&g_mu); } push_name(out, &n, max_out, current); return n; } /* --- model lookup -------------------------------------------------------- */ /* '*' matches any run; without one the pattern is a prefix. */ static int glob(const char *p, const char *s) { if (!strchr(p, '*')) return strncmp(s, p, strlen(p)) == 0; while (*p) { if (*p == '*') { while (*p == '*') p++; if (!*p) return 1; for (; *s; s++) if (glob(p, s)) return 1; return 0; } if (*p != *s) return 0; p++; s++; } return !*s; } static void set_field(cst_model_info_t *o, unsigned f, const rule_t *r) { switch (f) { case F_CONTEXT: o->context = r->context; break; case F_VISION: o->vision = r->vision; break; case F_CACHE: o->cache_style = r->cache; break; case F_REASONING: o->reasoning = r->reasoning; break; case F_PRICE: o->price = r->price; o->has_price = 1; break; } } static void add_source(cst_model_info_t *o, const char *src) { if (strstr(o->source, src)) return; size_t l = strlen(o->source); snprintf(o->source + l, sizeof(o->source) - l, "%s%s", l ? "+" : "", src); } static void apply_rules(const rule_t *rules, int n, const char *full, const char *bare, unsigned *have, cst_model_info_t *o, const char *src) { for (unsigned f = 1; f <= F_PRICE; f <<= 1) { if (*have & f) continue; const rule_t *best = NULL; for (int i = 0; i < n; i++) { const rule_t *r = &rules[i]; if (!(r->has & f) || (best && r->spec <= best->spec)) continue; if (glob(r->match, strchr(r->match, '/') ? full : bare)) best = r; } if (best) { set_field(o, f, best); *have |= f; add_source(o, src); } } } static void apply_cache(const char *provider, const char *model, unsigned *have, cst_model_info_t *o) { for (int i = 0; i < R.ncache; i++) { const cached_t *c = &R.cache[i]; if ((provider && strcmp(c->provider, provider) != 0) || strcmp(c->m.id, model) != 0) continue; unsigned before = *have; if (!(*have & F_CONTEXT) && c->m.context > 0) { o->context = c->m.context; *have |= F_CONTEXT; } if (!(*have & F_VISION) && c->m.vision >= 0) { o->vision = c->m.vision; *have |= F_VISION; } if (!(*have & F_PRICE) && c->m.has_price) { memset(&o->price, 0, sizeof(o->price)); snprintf(o->price.currency, sizeof(o->price.currency), "$"); o->price.miss = c->m.miss; o->price.hit = c->m.hit; o->price.write = c->m.write; o->price.out = c->m.out; o->has_price = 1; *have |= F_PRICE; } if (*have != before) add_source(o, "api"); return; } } static int env_price(cst_price_t *out) { const char *e = getenv("CST_PRICE"); if (!e || !e[0]) return -1; memset(out, 0, sizeof(*out)); char cur[8] = ""; int n = sscanf(e, "%lf,%lf,%lf,%lf,%7s", &out->miss, &out->hit, &out->out, &out->write, cur); if (n < 3) return -1; if (n < 4) out->write = out->miss; snprintf(out->currency, sizeof(out->currency), "%s", cur); return 0; } void cst_model_info(const char *provider, const char *model, cst_model_info_t *out) { memset(out, 0, sizeof(*out)); out->vision = -1; unsigned have = 0; const char *e = getenv("CST_CTX_TOKENS"); if (e && atol(e) > 0) { out->context = atol(e); have |= F_CONTEXT; add_source(out, "env"); } if (env_price(&out->price) == 0) { out->has_price = 1; have |= F_PRICE; add_source(out, "env"); } if (model && model[0]) { char full[256], bare[256]; lower_copy(full, sizeof(full), model); const char *slash = strrchr(full, '/'); snprintf(bare, sizeof(bare), "%s", slash && slash[1] ? slash + 1 : full); pthread_mutex_lock(&g_mu); ensure_locked(); apply_rules(R.user, R.nuser, full, bare, &have, out, "user"); if (provider && !prov_by_name(provider)) provider = NULL; apply_cache(provider, model, &have, out); apply_rules(R.builtin, R.nbuiltin, full, bare, &have, out, "built-in"); pthread_mutex_unlock(&g_mu); } if (!(have & F_CONTEXT)) { out->context = 128000; add_source(out, "default"); } } void cst_model_info_label(const cst_model_info_t *mi, char *out, size_t n) { char ctx[32]; if (mi->context >= 1000000) snprintf(ctx, sizeof(ctx), "%.0fM", (double)mi->context / 1e6); else snprintf(ctx, sizeof(ctx), "%ldK", (mi->context + 500) / 1000); snprintf(out, n, "%s%s", ctx, mi->vision == 1 ? " · vision" : mi->vision == 0 ? " · text only" : ""); } /* --- wrappers kept for the rest of the code ------------------------------ */ long cst_model_context_tokens(const char *model) { cst_model_info_t i; cst_model_info(NULL, model, &i); return i.context; } int cst_model_price(const char *model, cst_price_t *out) { cst_model_info_t i; cst_model_info(NULL, model, &i); *out = i.price; if (!i.has_price) memset(out, 0, sizeof(*out)); return i.has_price ? 0 : -1; } /* "https://api.deepseek.com/v1" -> "api.deepseek.com" */ static void host_of(const char *url, char *out, size_t n) { const char *s = strstr(url, "://"); s = s ? s + 3 : url; size_t i = 0; for (; s[i] && s[i] != '/' && i + 1 < n; i++) out[i] = s[i]; out[i] = 0; } void cst_model_caps(const char *provider, const char *api_base, const char *model, cst_model_caps_t *out) { memset(out, 0, sizeof(*out)); pthread_mutex_lock(&g_mu); ensure_locked(); /* provider extensions follow the provider, or its host behind any name */ for (int i = 0; i < R.nprov; i++) { const prov_t *p = &R.prov[i]; if (!p->passback && !p->prefix) continue; char host[256]; host_of(p->api_base, host, sizeof(host)); if ((provider && strcmp(provider, p->name) == 0) || (api_base && host[0] && strstr(api_base, host))) { out->reasoning_passback |= p->passback; out->prefix_completion |= p->prefix; } } pthread_mutex_unlock(&g_mu); cst_model_info_t mi; cst_model_info(provider, model, &mi); out->vision = mi.vision; const char *e = getenv("CST_NO_CACHE_HINTS"); if ((e && e[0] == '1') || !model) return; out->cache_style = mi.cache_style; } const char *cst_effort_wire(const char *provider, const char *model, const char *effort) { if (!cst_effort_valid(effort) || !provider || !model) return NULL; char mode[16] = ""; pthread_mutex_lock(&g_mu); ensure_locked(); const prov_t *p = prov_by_name(provider); if (p) snprintf(mode, sizeof(mode), "%s", p->effort); pthread_mutex_unlock(&g_mu); if (strcmp(mode, "passthrough") == 0) return effort; if (strcmp(mode, "openai") == 0) { cst_model_info_t mi; cst_model_info(provider, model, &mi); if (mi.reasoning) return strcmp(effort, "max") == 0 ? "high" : effort; } return NULL; } /* --- fetched cache ------------------------------------------------------- */ int cst_models_cache_save(const char *provider, const char *api_base, const cst_model_meta_t *list, int n) { char path[1100]; if (!list || n < 0 || cache_path(provider, path, sizeof(path)) != 0) return -1; char dir[1100]; snprintf(dir, sizeof(dir), "%s", path); char *sl = strrchr(dir, '/'); if (sl) *sl = 0; os_mkdir_p(dir, 0700); long now = (long)time(NULL); cJSON *doc = cJSON_CreateObject(); cJSON_AddNumberToObject(doc, "fetched_at", (double)now); if (api_base) cJSON_AddStringToObject(doc, "api_base", api_base); cJSON *arr = cJSON_AddArrayToObject(doc, "models"); for (int i = 0; i < n; i++) { const cst_model_meta_t *m = &list[i]; cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "id", m->id); if (m->context > 0) cJSON_AddNumberToObject(o, "context", (double)m->context); if (m->vision >= 0) cJSON_AddBoolToObject(o, "vision", m->vision); if (m->has_price) { cJSON *p = cJSON_AddObjectToObject(o, "price"); cJSON_AddStringToObject(p, "currency", "$"); cJSON_AddNumberToObject(p, "miss", m->miss); cJSON_AddNumberToObject(p, "hit", m->hit); cJSON_AddNumberToObject(p, "write", m->write); cJSON_AddNumberToObject(p, "out", m->out); } cJSON_AddItemToArray(arr, o); } char *txt = cJSON_Print(doc); cJSON_Delete(doc); if (!txt) return -1; char tmp[1200]; snprintf(tmp, sizeof(tmp), "%s.tmp", path); FILE *f = fopen(tmp, "wb"); int rc = -1; if (f) { size_t len = strlen(txt); rc = fwrite(txt, 1, len, f) == len ? 0 : -1; if (fclose(f) != 0) rc = -1; if (rc == 0) rc = os_rename_replace(tmp, path); if (rc != 0) remove(tmp); } free(txt); if (rc == 0) { pthread_mutex_lock(&g_mu); ensure_locked(); drop_cache(provider); load_cache(provider); pthread_mutex_unlock(&g_mu); } return rc; } int cst_models_cache_load(const char *provider, cst_model_meta_t *out, int max, long *age_s) { if (age_s) *age_s = 0; if (!provider) return -1; pthread_mutex_lock(&g_mu); ensure_locked(); int n = 0, found = 0; for (int i = 0; i < R.ncache; i++) { const cached_t *c = &R.cache[i]; if (strcmp(c->provider, provider) != 0) continue; if (!found && age_s) *age_s = (long)time(NULL) - c->fetched_at; found = 1; if (out && n < max) out[n++] = c->m; } pthread_mutex_unlock(&g_mu); return found ? n : -1; }