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:
WHC
2026-10-08 20:34:09 +08:00
parent 0226f417ff
commit 895b47274c
32 changed files with 5825 additions and 429 deletions
+82 -4
View File
@@ -1,5 +1,6 @@
#include "cst_agent.h"
#include "cst_ext.h"
#include "cst_models.h"
#include "cst_tools.h"
#include "cst_version.h"
#include "os_compat.h"
@@ -29,6 +30,8 @@ static void usage(const char *argv0) {
" -N, --new start a new session\n"
" -R, --resume ID open a session by id or unique prefix\n"
" -L, --list-sessions list sessions and exit\n"
" --list-models [P] what is known about each provider's models\n"
" (built-in, <config>/models.json, GET /models)\n"
" -A, --approval MODE ask | auto (default: edits run, shell in\n"
" the sandbox or by safe rules) | yolo\n"
" -y, --yolo same as --approval yolo (this run only)\n"
@@ -81,6 +84,62 @@ static int parse_confirm(const char *s, cst_agent_config_t *cfg) {
return 0;
}
static void list_one_model(const char *provider, const char *id,
const char *where) {
cst_model_info_t mi;
cst_model_info(provider, id, &mi);
char tag[48], price[128] = "";
cst_model_info_label(&mi, tag, sizeof(tag));
if (mi.has_price)
snprintf(price, sizeof(price), " %s%g/%g/%g per 1M (miss/hit/out)",
mi.price.currency, mi.price.miss, mi.price.hit, mi.price.out);
printf(" %-40s %-18s %-8s%s [%s]\n", id, tag, where, price, mi.source);
}
/* --list-models [provider]: the merged registry, to check models.json. */
static int list_models(const char *only) {
char user[1100] = "";
cst_models_user_path(user, sizeof(user));
FILE *uf = user[0] ? fopen(user, "rb") : NULL;
printf("user file: %s%s\n", user[0] ? user : "(no config dir)",
uf ? "" : " (none)");
if (uf)
fclose(uf);
if (cst_models_load_error()[0])
printf("warning: %s\n", cst_models_load_error());
int shown = 0;
static cst_model_meta_t cached[1024];
for (int i = 0; i < cst_provider_count(); i++) {
const cst_provider_t *p = cst_provider_at(i);
if (only && only[0] && strcmp(p->name, only) != 0)
continue;
shown++;
printf("\n%s %s (%s; key %s)\n", p->name, p->api_base,
cst_provider_source(p), p->key_env[0] ? p->key_env : "-");
char listed[16][128];
int nl = cst_provider_models(p, listed, 16);
for (int k = 0; k < nl; k++)
list_one_model(p->name, listed[k],
strcmp(listed[k], p->model) == 0 ? "default" : "listed");
long age = 0;
int nc = cst_models_cache_load(p->name, cached, 1024, &age);
if (nc > 0) {
printf(" -- %d from GET /models, fetched %ldh ago%s\n", nc,
age / 3600, age >= CST_MODELS_CACHE_TTL ? " (stale)" : "");
for (int k = 0; k < nc; k++)
list_one_model(p->name, cached[k].id, "api");
} else if (!nl) {
printf(" (no models listed; the first one GET /models returns is "
"used)\n");
}
}
if (only && only[0] && !shown) {
fprintf(stderr, "unknown provider: %s\n", only);
return 1;
}
return 0;
}
static int list_sessions(const char *ws) {
static char ids[200][128];
int n = cst_agent_store_list(ws, ids, 200);
@@ -286,6 +345,7 @@ int main(int argc, char **argv) {
{"effort", required_argument, 0, 'e'},
{"help", no_argument, 0, 'h'},
{"version", no_argument, 0, 'V'},
{"list-models", optional_argument, 0, 0x100},
{0, 0, 0, 0}};
int c;
while ((c = getopt_long(argc, argv, "w:s:b:k:m:P:r:n:T:Hp:dahVNcR:LyA:C:e:", opts, NULL)) !=
@@ -385,6 +445,14 @@ int main(int argc, char **argv) {
usage(argv[0]);
curl_global_cleanup();
return 0;
case 0x100: {
const char *only = optarg;
if (!only && optind < argc && argv[optind][0] != '-')
only = argv[optind++];
int rc = list_models(only);
curl_global_cleanup();
return rc;
}
case 'V':
printf("cst-agent %s\n", CST_VERSION);
curl_global_cleanup();
@@ -543,10 +611,20 @@ int main(int argc, char **argv) {
/* Router presets have no fixed model: take the first one the API lists
* (the TUI does the same after its /models probe). */
if (!cfg.model[0] && cfg.headless) {
char ids[1][128], err[256] = "";
if (cst_llm_list_models(cfg.api_base, cfg.api_key, ids, 1, err,
sizeof(err)) > 0) {
snprintf(cfg.model, sizeof(cfg.model), "%s", ids[0]);
static cst_model_meta_t list[512];
char err[256] = "";
long age = 0;
int n = cst_models_cache_load(cfg.provider, list, 1, &age);
if (n <= 0 || age >= CST_MODELS_CACHE_TTL) {
int r = cst_llm_list_models_meta(cfg.api_base, cfg.api_key, list, 512,
err, sizeof(err));
if (r >= 0)
cst_models_cache_save(cfg.provider, cfg.api_base, list, r);
if (r >= 0 || n <= 0)
n = r;
}
if (n > 0) {
snprintf(cfg.model, sizeof(cfg.model), "%s", list[0].id);
} else {
fprintf(stderr, "no model for %s: pass --model (%s)\n", cfg.provider,
err[0] ? err : "the API listed none");