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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user