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
+61 -3
View File
@@ -6,6 +6,8 @@
#endif
#include "cst_mcp.h"
#include "cst_agent.h"
#include "cst_attach.h"
#include <pthread.h>
#include <stdatomic.h>
@@ -602,7 +604,56 @@ static int auto_approved(const mcp_srv_t *s, const char *tool) {
return 0;
}
static void put_content(cst_buf_t *b, const cJSON *item) {
/* Images a tool call returned, stored beside the session. */
typedef struct {
const char *session;
const char *server;
cst_attachment_t *list;
int n, cap;
} mcp_imgs_t;
/* An image content item goes into the session store and rides along with the
* tool result; the text keeps a one-line description. 0 when stored. */
static int put_image(cst_buf_t *b, const cJSON *item, mcp_imgs_t *im) {
const cJSON *d = cJSON_GetObjectItemCaseSensitive(item, "data");
if (!im || !cJSON_IsString(d))
return -1;
char err[256] = "no session to store the image in";
if (im->n >= CST_ATTACH_MAX_COUNT) {
snprintf(err, sizeof(err), "more than %d images", CST_ATTACH_MAX_COUNT);
} else if (im->session && im->session[0]) {
size_t n = 0;
char *raw = cst_base64_decode(d->valuestring, strlen(d->valuestring), &n);
char name[96];
snprintf(name, sizeof(name), "%s-%d", im->server, im->n + 1);
cst_attachment_t *a =
raw ? cst_attach_import_bytes(im->session, name, raw, n, err,
sizeof(err))
: NULL;
if (!raw)
snprintf(err, sizeof(err), "bad base64");
free(raw);
if (a) {
snprintf(a->origin, sizeof(a->origin), "mcp");
int pushed = cst_attachments_push(&im->list, &im->n, &im->cap, a) == 0;
char desc[128];
cst_attach_describe(a, desc, sizeof(desc));
cst_attach_free(a);
if (pushed) {
cst_buf_printf(b, "[image %s (shown to you as an image)]", desc);
return 0;
}
snprintf(err, sizeof(err), "out of memory");
}
}
const cJSON *mt = cJSON_GetObjectItemCaseSensitive(item, "mimeType");
cst_buf_printf(b, "[image %s, %zu bytes base64, not shown: %s]",
cJSON_IsString(mt) ? mt->valuestring : "",
strlen(d->valuestring), err);
return -1;
}
static void put_content(cst_buf_t *b, const cJSON *item, mcp_imgs_t *im) {
const cJSON *type = cJSON_GetObjectItemCaseSensitive(item, "type");
const char *t = cJSON_IsString(type) ? type->valuestring : "";
if (b->len && b->buf[b->len - 1] != '\n')
@@ -611,6 +662,9 @@ static void put_content(cst_buf_t *b, const cJSON *item) {
const cJSON *x = cJSON_GetObjectItemCaseSensitive(item, "text");
if (cJSON_IsString(x))
cst_buf_puts(b, x->valuestring);
} else if (strcmp(t, "image") == 0 &&
cJSON_IsString(cJSON_GetObjectItemCaseSensitive(item, "data"))) {
put_image(b, item, im);
} else if (strcmp(t, "image") == 0 || strcmp(t, "audio") == 0) {
const cJSON *mt = cJSON_GetObjectItemCaseSensitive(item, "mimeType");
const cJSON *d = cJSON_GetObjectItemCaseSensitive(item, "data");
@@ -660,7 +714,6 @@ static int set_out(cst_tool_result_t *r, int code, const char *fmt, ...) {
static int mcp_tool_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_tool_result_t *r, void *ud) {
(void)ctx;
mtool_t *mt = ud;
mcp_srv_t *s = mt->srv;
pthread_mutex_lock(&s->mu);
@@ -690,9 +743,12 @@ static int mcp_tool_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
const cJSON *res = cJSON_GetObjectItemCaseSensitive(resp, "result");
const cJSON *content = cJSON_GetObjectItemCaseSensitive(res, "content");
cst_buf_t b = {0};
mcp_imgs_t im = {0};
im.session = ctx && ctx->agent ? cst_agent_session_path(ctx->agent) : NULL;
im.server = s->name;
const cJSON *it = NULL;
cJSON_ArrayForEach(it, content) {
put_content(&b, it);
put_content(&b, it, &im);
if (b.len > MCP_OUT_MAX)
break;
}
@@ -716,6 +772,8 @@ static int mcp_tool_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cJSON_Delete(resp);
r->out_len = b.len;
r->out = cst_buf_steal(&b);
r->attach = im.list; /* one block, freed by cst_tool_result_free */
r->nattach = im.n;
return 0;
}