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
+54 -1
View File
@@ -7,6 +7,7 @@
#include "cJSON.h"
#include "cst_agent.h"
#include "cst_attach.h"
#include <pthread.h>
#include <stdio.h>
@@ -97,7 +98,11 @@ static void msg_free(cst_message_t *m) {
free(m->content);
free(m->tool_calls_json);
free(m->meta_json);
m->content = m->tool_calls_json = m->meta_json = NULL;
free(m->reasoning);
cst_attachments_free(m->attach, m->nattach);
m->content = m->tool_calls_json = m->meta_json = m->reasoning = NULL;
m->attach = NULL;
m->nattach = 0;
}
static int session_grow(cst_session_t *s) {
@@ -127,6 +132,16 @@ static int session_push_mem(cst_session_t *s, const cst_message_t *src) {
d->tool_calls_json = xstrdup(src->tool_calls_json);
if (src->meta_json && src->meta_json[0])
d->meta_json = xstrdup(src->meta_json);
if (src->reasoning && src->reasoning[0])
d->reasoning = xstrdup(src->reasoning);
if (src->attach && src->nattach > 0) {
d->attach = cst_attachments_dup(src->attach, src->nattach);
if (!d->attach) {
msg_free(d);
return -1;
}
d->nattach = src->nattach;
}
s->nmsgs++;
return 0;
}
@@ -174,9 +189,14 @@ static void load_existing(cst_session_t *s) {
if (cJSON_IsObject(mo))
meta = cJSON_PrintUnformatted(mo);
m.meta_json = meta;
m.reasoning = (char *)jstr(o, "reasoning");
const cJSON *at = cJSON_GetObjectItemCaseSensitive(o, "attachments");
if (cJSON_IsArray(at))
m.attach = cst_attachments_from_json(at, &m.nattach);
session_push_mem(s, &m);
free(tcj);
free(meta);
cst_attachments_free(m.attach, m.nattach);
cJSON_Delete(o);
}
free(line);
@@ -249,6 +269,8 @@ static int write_line(FILE *fp, const cst_message_t *msg) {
cJSON_AddStringToObject(o, "tool_calls_json", msg->tool_calls_json);
}
}
if (msg->reasoning && msg->reasoning[0])
cJSON_AddStringToObject(o, "reasoning", msg->reasoning);
if (msg->meta_json && msg->meta_json[0]) {
cJSON *mo = cJSON_Parse(msg->meta_json);
if (cJSON_IsObject(mo))
@@ -256,6 +278,28 @@ static int write_line(FILE *fp, const cst_message_t *msg) {
else
cJSON_Delete(mo);
}
if (msg->attach && msg->nattach > 0) {
cJSON *arr = cJSON_CreateArray();
for (int i = 0; i < msg->nattach; i++) {
const cst_attachment_t *a = &msg->attach[i];
cJSON *it = cJSON_CreateObject();
cJSON_AddStringToObject(it, "id", a->id);
cJSON_AddStringToObject(it, "name", a->name);
cJSON_AddStringToObject(it, "mime", a->mime);
cJSON_AddStringToObject(it, "rel", a->rel);
cJSON_AddNumberToObject(it, "bytes", (double)a->bytes);
if (a->width > 0 && a->height > 0) {
cJSON_AddNumberToObject(it, "width", a->width);
cJSON_AddNumberToObject(it, "height", a->height);
}
if (a->detail[0])
cJSON_AddStringToObject(it, "detail", a->detail);
if (a->origin[0])
cJSON_AddStringToObject(it, "origin", a->origin);
cJSON_AddItemToArray(arr, it);
}
cJSON_AddItemToObject(o, "attachments", arr);
}
char *txt = cJSON_PrintUnformatted(o);
cJSON_Delete(o);
if (!txt)
@@ -310,6 +354,7 @@ int cst_session_clear(cst_session_t *s, int keep_system) {
if (s->msgs[i].role == CST_ROLE_SYSTEM) {
sys.role = CST_ROLE_SYSTEM;
sys.content = xstrdup(s->msgs[i].content ? s->msgs[i].content : "");
sys.meta_json = xstrdup(s->msgs[i].meta_json);
have_sys = sys.content != NULL;
break;
}
@@ -322,6 +367,7 @@ int cst_session_clear(cst_session_t *s, int keep_system) {
write_line(s->fp, &sys);
}
free(sys.content);
free(sys.meta_json);
pthread_mutex_unlock(&s->mu);
return rc;
}
@@ -339,6 +385,11 @@ int cst_session_rewrite(cst_session_t *s, const cst_message_t *msgs, int n) {
copy[i].tool_calls_json =
msgs[i].tool_calls_json ? strdup(msgs[i].tool_calls_json) : NULL;
copy[i].meta_json = msgs[i].meta_json ? strdup(msgs[i].meta_json) : NULL;
copy[i].reasoning = msgs[i].reasoning ? strdup(msgs[i].reasoning) : NULL;
copy[i].attach = msgs[i].attach && msgs[i].nattach > 0
? cst_attachments_dup(msgs[i].attach, msgs[i].nattach)
: NULL;
copy[i].nattach = copy[i].attach ? msgs[i].nattach : 0;
}
pthread_mutex_lock(&s->mu);
free_all(s);
@@ -352,6 +403,8 @@ int cst_session_rewrite(cst_session_t *s, const cst_message_t *msgs, int n) {
free(copy[i].content);
free(copy[i].tool_calls_json);
free(copy[i].meta_json);
free(copy[i].reasoning);
cst_attachments_free(copy[i].attach, copy[i].nattach);
}
free(copy);
return rc;