v0.7.0: image input, model registry, prefix-cache savings
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
This commit is contained in:
+20
-130
@@ -1,120 +1,32 @@
|
||||
#include "cst_agent.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MODEL_MAX_LEN 128
|
||||
/* Provider and model data live in model_registry.c (cst_models.h). */
|
||||
|
||||
static const cst_provider_t PROVIDERS[] = {
|
||||
{"minimax", "https://api.minimax.cn/v1", "MiniMax-M2.7", "MINIMAX_API_KEY"},
|
||||
{"deepseek", "https://api.deepseek.com/v1", "deepseek-flash",
|
||||
"DEEPSEEK_API_KEY"},
|
||||
{"openai", "https://api.openai.com/v1", "gpt-4.1-mini", "OPENAI_API_KEY"},
|
||||
/* Router: no fixed model; the first id from GET /models is picked. */
|
||||
{"teamorouter", "https://api.teamorouter.com/v1", "",
|
||||
"TEAMOROUTER_API_KEY"},
|
||||
};
|
||||
|
||||
const cst_provider_t *cst_provider_find(const char *name) {
|
||||
if (!name || !name[0])
|
||||
return NULL;
|
||||
for (size_t i = 0; i < sizeof(PROVIDERS) / sizeof(PROVIDERS[0]); i++) {
|
||||
if (strcmp(PROVIDERS[i].name, name) == 0)
|
||||
return &PROVIDERS[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const cst_provider_t *cst_provider_default(void) {
|
||||
const cst_provider_t *p = cst_provider_find(getenv("CST_PROVIDER"));
|
||||
return p ? p : &PROVIDERS[0];
|
||||
}
|
||||
|
||||
static int already(char out[][MODEL_MAX_LEN], int n, const char *name) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (strcmp(out[i], name) == 0)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int push(char out[][MODEL_MAX_LEN], int *n, int max, const char *name) {
|
||||
if (!name || !name[0] || *n >= max)
|
||||
int cst_price_is_peak(time_t when) {
|
||||
time_t bj = when + 8 * 3600;
|
||||
struct tm tmv;
|
||||
#ifdef _WIN32
|
||||
gmtime_s(&tmv, &bj);
|
||||
#else
|
||||
gmtime_r(&bj, &tmv);
|
||||
#endif
|
||||
if (tmv.tm_wday == 0 || tmv.tm_wday == 6)
|
||||
return 0;
|
||||
if (already(out, *n, name))
|
||||
return 0;
|
||||
snprintf(out[*n], MODEL_MAX_LEN, "%s", name);
|
||||
(*n)++;
|
||||
return 1;
|
||||
return (tmv.tm_hour >= 9 && tmv.tm_hour < 12) ||
|
||||
(tmv.tm_hour >= 14 && tmv.tm_hour < 18);
|
||||
}
|
||||
|
||||
int cst_models_list(const char *current, char out[][128], int max_out) {
|
||||
if (!out || max_out <= 0)
|
||||
double cst_price_cost(const cst_price_t *p, long miss, long hit, long write,
|
||||
long out, time_t when) {
|
||||
if (!p)
|
||||
return 0;
|
||||
int n = 0;
|
||||
|
||||
const char *env = getenv("CST_MODELS");
|
||||
if (env && env[0]) {
|
||||
char buf[1024];
|
||||
snprintf(buf, sizeof(buf), "%s", env);
|
||||
char *p = buf;
|
||||
while (*p && n < max_out) {
|
||||
while (*p == ':' || *p == ' ' || *p == ',')
|
||||
p++;
|
||||
if (!*p)
|
||||
break;
|
||||
char *start = p;
|
||||
while (*p && *p != ':' && *p != ',')
|
||||
p++;
|
||||
char save = *p;
|
||||
*p = 0;
|
||||
char *end = start + strlen(start);
|
||||
while (end > start && (end[-1] == ' ' || end[-1] == '\t'))
|
||||
*--end = 0;
|
||||
push(out, &n, max_out, start);
|
||||
*p = save;
|
||||
if (*p)
|
||||
p++;
|
||||
}
|
||||
} else {
|
||||
push(out, &n, max_out, "deepseek-flash");
|
||||
push(out, &n, max_out, "deepseek-v4-pro");
|
||||
push(out, &n, max_out, "MiniMax-M2.7");
|
||||
push(out, &n, max_out, "MiniMax-M2.5");
|
||||
push(out, &n, max_out, "gpt-4.1-mini");
|
||||
push(out, &n, max_out, "gpt-4o");
|
||||
}
|
||||
|
||||
if (current && current[0])
|
||||
push(out, &n, max_out, current);
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Context window in tokens (CST_CTX_TOKENS overrides). Prefix match. */
|
||||
long cst_model_context_tokens(const char *model) {
|
||||
const char *e = getenv("CST_CTX_TOKENS");
|
||||
if (e && atol(e) > 0)
|
||||
return atol(e);
|
||||
static const struct {
|
||||
const char *prefix;
|
||||
long tokens;
|
||||
} T[] = {{"deepseek", 128000}, {"MiniMax", 196608}, {"minimax", 196608},
|
||||
{"gpt-4.1", 1047576}, {"gpt-4o", 128000}, {"gpt-5", 400000},
|
||||
{"o3", 200000}, {"o4", 200000}, {"claude", 200000},
|
||||
{"qwen", 131072}, {"glm", 128000}, {"kimi", 131072}};
|
||||
if (model) {
|
||||
/* routers name models "vendor/model" */
|
||||
const char *slash = strrchr(model, '/');
|
||||
if (slash && slash[1])
|
||||
model = slash + 1;
|
||||
for (size_t i = 0; i < sizeof(T) / sizeof(T[0]); i++) {
|
||||
if (strncmp(model, T[i].prefix, strlen(T[i].prefix)) == 0)
|
||||
return T[i].tokens;
|
||||
}
|
||||
}
|
||||
return 128000;
|
||||
double c = (miss * p->miss + hit * p->hit + write * p->write +
|
||||
out * p->out) / 1e6;
|
||||
if (p->offpeak_half && !cst_price_is_peak(when))
|
||||
c /= 2;
|
||||
return c;
|
||||
}
|
||||
|
||||
int cst_effort_valid(const char *effort) {
|
||||
@@ -125,25 +37,3 @@ int cst_effort_valid(const char *effort) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *cst_effort_wire(const char *provider, const char *model,
|
||||
const char *effort) {
|
||||
if (!cst_effort_valid(effort) || !provider || !model)
|
||||
return NULL;
|
||||
if (strcmp(provider, "deepseek") == 0)
|
||||
return effort;
|
||||
/* OpenAI reasoning models take low | medium | high. */
|
||||
if (strcmp(provider, "openai") == 0 &&
|
||||
(strncmp(model, "o1", 2) == 0 || strncmp(model, "o3", 2) == 0 ||
|
||||
strncmp(model, "o4", 2) == 0 || strncmp(model, "gpt-5", 5) == 0))
|
||||
return strcmp(effort, "max") == 0 ? "high" : effort;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int cst_provider_count(void) {
|
||||
return (int)(sizeof(PROVIDERS) / sizeof(PROVIDERS[0]));
|
||||
}
|
||||
|
||||
const cst_provider_t *cst_provider_at(int i) {
|
||||
return i >= 0 && i < cst_provider_count() ? &PROVIDERS[i] : NULL;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user