v0.7.0: image input, model registry, prefix-cache savings
ci / linux (clang) (push) Successful in 4m41s
ci / linux (gcc) (push) Successful in 5m35s
ci / musl (push) Failing after 30s
ci / windows-cross (push) Successful in 9m59s

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
+42
View File
@@ -14,6 +14,7 @@
#include <regex.h>
#include "cst_attach.h"
#include "cst_tools.h"
#include "cst_util.h"
#include "os_compat.h"
@@ -241,6 +242,47 @@ int cst_tool_read_file2(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
return tool_take_buf(r, &b, 0);
}
/* --- view_image ---------------------------------------------------------- */
/* Show an image to the model: the bytes are copied into the session's
* attachment store and travel as a multimodal content part of the result. */
int cst_tool_view_image(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_tool_result_t *r) {
const char *path = tool_arg(c, "path");
if (!path || !path[0])
return tool_set_result(r, "error: missing path", 1);
const char *session = ctx->agent ? cst_agent_session_path(ctx->agent) : NULL;
if (!session || !session[0])
return tool_set_result(r, "error: no session to store the image in", 1);
char full[OS_PATH_MAX];
if (resolve(ctx, path, full, sizeof(full)) != 0)
return tool_set_resultf(r, 1,
"error: path outside workspace or missing parent: "
"%.200s",
path);
if (is_dir(full))
return tool_set_resultf(r, 1, "error: %.200s is a directory", path);
char err[256];
cst_attachment_t *a = cst_attach_import_file(session, full, err, sizeof(err));
if (!a)
return tool_set_resultf(r, 1, "error: cannot show %.200s: %s", path, err);
snprintf(a->origin, sizeof(a->origin), "view_image");
const char *d = tool_arg(c, "detail");
if (d && (strcmp(d, "low") == 0 || strcmp(d, "high") == 0))
snprintf(a->detail, sizeof(a->detail), "%s", d);
char desc[128];
cst_attach_describe(a, desc, sizeof(desc));
cst_buf_t out = {0};
cst_buf_printf(&out, "view_image %.200s: %s\n(shown to you as an image)\n",
path, desc);
int rc = tool_take_buf(r, &out, 0);
r->attach = a; /* one entry; freed as a block by cst_tool_result_free */
r->nattach = 1;
return rc;
}
/* --- edit_file ----------------------------------------------------------- */
static size_t count_occ(const char *hay, size_t hn, const char *needle,
+42 -4
View File
@@ -56,6 +56,15 @@ static const cst_tool_def_t TOOLS[] = {
{"limit", I, 0, "maximum number of lines"},
{"line_numbers", B, 0, "`N|` line prefixes (default true)"}},
CST_TP_READ, 1, run_read_file, NULL, NO_DYN},
{"view_image",
"Show a local image to yourself: PNG, JPEG, GIF, WebP or BMP, checked by "
"content. The file is copied into the session store and comes back as an "
"image you can actually see (not as text). Use it for screenshots, "
"diagrams and UI checks; path resolves like read_file's.",
NULL,
{{"path", S, 1, "image file path relative to the workspace"},
{"detail", S, 0, "low or high (provider hint; default auto)"}},
CST_TP_READ, 0, cst_tool_view_image, NULL, NO_DYN},
{"run_shell",
"Run a non-interactive shell command in the workspace root for builds, "
"tests, git and other CLIs (stdin is empty; long runs are killed). "
@@ -333,8 +342,8 @@ int cst_tool_active(const cst_agent_config_t *cfg, const cst_tool_def_t *t) {
return 1;
if (cfg->tool_profile == CST_TOOLS_MINIMAL && !t->minimal)
return 0;
if ((cfg->plan_mode || cfg->read_only) &&
(t->perm == CST_TP_WRITE || t->perm == CST_TP_SHELL))
/* plan_mode is enforced when a call runs, so tools[] stays the same */
if (cfg->read_only && (t->perm == CST_TP_WRITE || t->perm == CST_TP_SHELL))
return 0;
if (cfg->depth > 0 && strcmp(t->name, "task") == 0)
return 0;
@@ -413,16 +422,44 @@ static int is_minimal(const cst_agent_config_t *cfg) {
return cfg && cfg->tool_profile == CST_TOOLS_MINIMAL;
}
static int is_external(const cst_tool_def_t *t) {
return t->owner && strncmp(t->owner, "builtin:", 8) != 0;
}
static int by_name(const void *x, const void *y) {
const cst_tool_def_t *a = *(const cst_tool_def_t *const *)x;
const cst_tool_def_t *b = *(const cst_tool_def_t *const *)y;
return strcmp(a->name, b->name);
}
void *cst_tools_schema_cfg(const cst_agent_config_t *cfg) {
cJSON *arr = cJSON_CreateArray();
if (!arr)
return NULL;
int legacy = is_minimal(cfg);
int n = cst_tool_count();
/* Built-ins in registry order, then plugin/MCP tools by name: servers
* finish starting in any order, and tools[] is part of the cached prefix. */
const cst_tool_def_t **order = calloc((size_t)(n ? n : 1), sizeof(*order));
if (!order) {
cJSON_Delete(arr);
return NULL;
}
int nb = 0, no = 0;
for (int i = 0; i < n; i++) {
const cst_tool_def_t *t = cst_tool_at(i);
if (!t || !cst_tool_active(cfg, t))
continue;
if (t && cst_tool_active(cfg, t) && !is_external(t))
order[no++] = t;
}
nb = no;
for (int i = 0; i < n; i++) {
const cst_tool_def_t *t = cst_tool_at(i);
if (t && cst_tool_active(cfg, t) && is_external(t))
order[no++] = t;
}
qsort(order + nb, (size_t)(no - nb), sizeof(*order), by_name);
for (int i = 0; i < no; i++) {
const cst_tool_def_t *t = order[i];
cJSON *tool = cJSON_CreateObject();
cJSON_AddStringToObject(tool, "type", "function");
cJSON *fn = cJSON_AddObjectToObject(tool, "function");
@@ -457,6 +494,7 @@ void *cst_tools_schema_cfg(const cst_agent_config_t *cfg) {
}
cJSON_AddItemToArray(arr, tool);
}
free(order);
return arr;
}
+4
View File
@@ -10,6 +10,7 @@
#include "cJSON.h"
#include "cst_agent.h"
#include "cst_attach.h"
#include "cst_sandbox.h"
#include "cst_util.h"
#include "os_compat.h"
@@ -26,6 +27,9 @@ void cst_tool_result_free(cst_tool_result_t *r) {
free(r->out);
r->out = NULL;
r->out_len = 0;
cst_attachments_free(r->attach, r->nattach);
r->attach = NULL;
r->nattach = 0;
}
void cst_tool_call_clear(ctl_tool_call_t *c) {
+2
View File
@@ -48,6 +48,8 @@ int cst_tool_ask_user(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_tool_result_t *r);
int cst_tool_web_fetch(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_tool_result_t *r);
int cst_tool_view_image(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_tool_result_t *r);
int cst_preview_write_file(const cst_tool_ctx_t *ctx,
const ctl_tool_call_t *c, char **out);