Images - vision gate: text-only models get a note instead of image parts and no view_image tool; unknown models retry once without images on HTTP 400 - paste: Ctrl+V and /attach paste read the clipboard (wl-paste, xclip, pngpaste/osascript, Win32 PNG or CF_DIB converted to PNG); pasting image file paths attaches them - MCP image content is stored in the session and sent with the tool result - attach: rel path validation, base64 decode, /retry keeps images Model registry - built-in src/models_builtin.json embedded at build time, merged with <config>/models.json and cached GET /models metadata (context, vision, pricing); most specific rule wins per field - /model labels, /status source, --list-models [provider] Cache and cost - stable request prefix (frozen tools, notes as messages), DeepSeek prefix completion, OpenAI/Anthropic cache hints, /cost and hit rate in the status
819 lines
23 KiB
C
819 lines
23 KiB
C
#include "cst_models.h"
|
|
|
|
#include "cJSON.h"
|
|
#include "os_compat.h"
|
|
|
|
#include <ctype.h>
|
|
#include <pthread.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
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;
|
|
}
|