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
+543
View File
@@ -0,0 +1,543 @@
#define _POSIX_C_SOURCE 200809L
#include "cJSON.h"
#include "cst_attach.h"
#include "cst_agent.h"
#include "cst_clipboard.h"
#include "cst_test_util.h"
#include "os_compat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int nfail;
#define EXPECT(cond, msg) \
do { \
if (!(cond)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \
nfail++; \
} \
} while (0)
static void put_be32(unsigned char *p, unsigned v) {
p[0] = (unsigned char)(v >> 24);
p[1] = (unsigned char)(v >> 16);
p[2] = (unsigned char)(v >> 8);
p[3] = (unsigned char)v;
}
static void put_le16(unsigned char *p, unsigned v) {
p[0] = (unsigned char)(v & 0xFF);
p[1] = (unsigned char)((v >> 8) & 0xFF);
}
/* 8 signature + 4 length + "IHDR" + w + h (what cst_attach_dimensions reads). */
static size_t png_hdr(unsigned char *b, unsigned w, unsigned h) {
static const unsigned char sig[8] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'};
memcpy(b, sig, 8);
b[8] = b[9] = b[10] = 0;
b[11] = 13;
memcpy(b + 12, "IHDR", 4);
put_be32(b + 16, w);
put_be32(b + 20, h);
return 32;
}
static size_t gif_hdr(unsigned char *b, unsigned w, unsigned h) {
memcpy(b, "GIF89a", 6);
put_le16(b + 6, w);
put_le16(b + 8, h);
return 16;
}
static void put_le32(unsigned char *p, unsigned v) {
p[0] = (unsigned char)(v & 0xFF);
p[1] = (unsigned char)((v >> 8) & 0xFF);
p[2] = (unsigned char)((v >> 16) & 0xFF);
p[3] = (unsigned char)((v >> 24) & 0xFF);
}
static size_t bmp_hdr(unsigned char *b, int w, int h) {
memset(b, 0, 64);
b[0] = 'B';
b[1] = 'M';
put_le32(b + 18, (unsigned)w);
put_le32(b + 22, (unsigned)h);
return 64;
}
/* FFD8 + a filler APP0 segment + SOF0 carrying the frame size. */
static size_t jpeg_hdr(unsigned char *b, unsigned w, unsigned h) {
size_t i = 0;
b[i++] = 0xFF;
b[i++] = 0xD8;
b[i++] = 0xFF;
b[i++] = 0xE0;
b[i++] = 0x00;
b[i++] = 0x04;
b[i++] = 0x00;
b[i++] = 0x00;
b[i++] = 0xFF;
b[i++] = 0xC0;
b[i++] = 0x00;
b[i++] = 0x0B;
b[i++] = 0x08;
b[i++] = (unsigned char)(h >> 8);
b[i++] = (unsigned char)h;
b[i++] = (unsigned char)(w >> 8);
b[i++] = (unsigned char)w;
b[i++] = 0x01; /* one component */
b[i++] = 0x01; /* component id */
b[i++] = 0x11; /* sampling */
b[i++] = 0x00; /* quant table */
return i;
}
static void test_sniff(void) {
unsigned char b[64];
char mime[32];
size_t n;
n = png_hdr(b, 32, 24);
EXPECT(cst_attach_sniff(b, n, mime, sizeof(mime)) == 0 &&
strcmp(mime, "image/png") == 0, "PNG sniffed");
n = jpeg_hdr(b, 20, 10);
EXPECT(cst_attach_sniff(b, n, mime, sizeof(mime)) == 0 &&
strcmp(mime, "image/jpeg") == 0, "JPEG sniffed");
n = gif_hdr(b, 3, 5);
EXPECT(cst_attach_sniff(b, n, mime, sizeof(mime)) == 0 &&
strcmp(mime, "image/gif") == 0, "GIF sniffed");
memcpy(b, "RIFF\0\0\0\0WEBPVP8 ", 16);
EXPECT(cst_attach_sniff(b, 30, mime, sizeof(mime)) == 0 &&
strcmp(mime, "image/webp") == 0, "WebP sniffed");
n = bmp_hdr(b, 7, 9);
EXPECT(cst_attach_sniff(b, n, mime, sizeof(mime)) == 0 &&
strcmp(mime, "image/bmp") == 0, "BMP sniffed");
/* an extension never decides: text and a PDF are not images */
EXPECT(cst_attach_sniff("hello, world", 12, mime, sizeof(mime)) != 0,
"text rejected");
EXPECT(cst_attach_sniff("%PDF-1.7\n", 9, mime, sizeof(mime)) != 0,
"PDF rejected");
EXPECT(cst_attach_sniff("\x89PNG", 4, mime, sizeof(mime)) != 0,
"truncated PNG rejected");
EXPECT(cst_attach_sniff("", 0, mime, sizeof(mime)) != 0, "empty rejected");
}
static void test_dimensions(void) {
unsigned char b[64];
int w = 0, h = 0;
size_t n;
n = png_hdr(b, 1280, 720);
EXPECT(cst_attach_dimensions(b, n, &w, &h) == 0 && w == 1280 && h == 720,
"PNG dimensions");
n = gif_hdr(b, 16, 40);
EXPECT(cst_attach_dimensions(b, n, &w, &h) == 0 && w == 16 && h == 40,
"GIF dimensions");
n = bmp_hdr(b, 7, 9);
EXPECT(cst_attach_dimensions(b, n, &w, &h) == 0 && w == 7 && h == 9,
"BMP dimensions");
n = jpeg_hdr(b, 640, 480);
EXPECT(cst_attach_dimensions(b, n, &w, &h) == 0 && w == 640 && h == 480,
"JPEG dimensions");
w = h = -1;
EXPECT(cst_attach_dimensions("nope", 4, &w, &h) != 0 && w == 0 && h == 0,
"unknown format gives 0x0");
}
static void test_base64(void) {
char *s = cst_base64_encode("Man", 3);
EXPECT(s && strcmp(s, "TWFu") == 0, "base64 <Man>");
free(s);
s = cst_base64_encode("Ma", 2);
EXPECT(s && strcmp(s, "TWE=") == 0, "base64 padding 1");
free(s);
s = cst_base64_encode("M", 1);
EXPECT(s && strcmp(s, "TQ==") == 0, "base64 padding 2");
free(s);
s = cst_base64_encode("\x00\xff\xfe", 3);
EXPECT(s && strcmp(s, "AP/+") == 0, "base64 binary");
free(s);
size_t n = 0;
char *d = cst_base64_decode("TWFu", 4, &n);
EXPECT(d && n == 3 && memcmp(d, "Man", 3) == 0, "decode <Man>");
free(d);
d = cst_base64_decode("TQ==", 4, &n);
EXPECT(d && n == 1 && d[0] == 'M', "decode with padding");
free(d);
d = cst_base64_decode("AP/+\n", 5, &n);
EXPECT(d && n == 3 && memcmp(d, "\x00\xff\xfe", 3) == 0,
"decode binary, newline ignored");
free(d);
d = cst_base64_decode("AP_-", 4, &n);
EXPECT(d && n == 3 && memcmp(d, "\x00\xff\xfe", 3) == 0, "URL-safe alphabet");
free(d);
EXPECT(cst_base64_decode("A*B=", 4, &n) == NULL, "bad character refused");
unsigned char raw[200];
for (int i = 0; i < 200; i++)
raw[i] = (unsigned char)(i * 37);
for (size_t len = 0; len < 7; len++) {
char *e = cst_base64_encode(raw, 190 + len);
d = e ? cst_base64_decode(e, strlen(e), &n) : NULL;
EXPECT(d && n == 190 + len && memcmp(d, raw, n) == 0, "round trip");
free(e);
free(d);
}
}
static void test_rel_ok(const char *root) {
EXPECT(cst_attach_rel_ok("img-0a1b2c3d.png"), "normal name accepted");
EXPECT(!cst_attach_rel_ok("../x.png"), "parent dir refused");
EXPECT(!cst_attach_rel_ok("a/../../b.png"), "slash refused");
EXPECT(!cst_attach_rel_ok("/etc/passwd"), "absolute refused");
EXPECT(!cst_attach_rel_ok("a\\b.png"), "backslash refused");
EXPECT(!cst_attach_rel_ok(".hidden"), "dot file refused");
EXPECT(!cst_attach_rel_ok(""), "empty refused");
cJSON *arr = cJSON_Parse("[{\"id\":\"x\",\"rel\":\"../../secret.png\"},"
"{\"id\":\"y\",\"rel\":\"img-1.png\"}]");
int n = 0;
cst_attachment_t *back = cst_attachments_from_json(arr, &n);
EXPECT(n == 1 && back && strcmp(back[0].id, "y") == 0,
"escaping rel dropped on load");
cst_attachments_free(back, n);
cJSON_Delete(arr);
/* a stored file swapped for non-image bytes is not sent */
char session[1024], full[1024], err[256];
snprintf(session, sizeof(session), "%.900s/sessions/r.jsonl", root);
unsigned char b[64];
size_t bn = png_hdr(b, 4, 4);
cst_attachment_t *a =
cst_attach_import_bytes(session, "x.png", (const char *)b, bn, err,
sizeof(err));
EXPECT(a != NULL, "stored");
if (!a)
return;
EXPECT(cst_attach_full_path(session, a, full, sizeof(full)) == 0, "path");
FILE *f = fopen(full, "wb");
if (f) {
fputs("plain text now", f);
fclose(f);
}
size_t rn = 0;
EXPECT(cst_attach_read(session, a, &rn, err, sizeof(err)) == NULL &&
strstr(err, "no longer an image"),
"read re-checks the bytes");
EXPECT(cst_attach_data_url(session, a) == NULL, "no data url for it");
snprintf(a->rel, sizeof(a->rel), "../r.jsonl");
EXPECT(cst_attach_full_path(session, a, full, sizeof(full)) != 0,
"full path refuses an escaping rel");
cst_attach_free(a);
}
static void test_store(const char *root) {
char session[1024], dir[1024];
snprintf(session, sizeof(session), "%.900s/sessions/a.jsonl", root);
unsigned char b[64];
size_t n = png_hdr(b, 32, 24);
char err[256];
cst_attachment_t *a = cst_attach_import_bytes(session, "shot.png",
(const char *)b, n, err,
sizeof(err));
EXPECT(a != NULL, "import stored");
if (!a) {
fprintf(stderr, " err: %s\n", err);
return;
}
EXPECT(strcmp(a->mime, "image/png") == 0, "stored mime");
EXPECT(a->width == 32 && a->height == 24, "stored dimensions");
EXPECT(a->bytes == (long long)n, "stored size");
EXPECT(strcmp(a->name, "shot.png") == 0, "stored name");
EXPECT(strcmp(a->origin, "user") == 0, "origin=user");
EXPECT(strcmp(a->rel, "img-") != 0 && strstr(a->rel, ".png") != NULL,
"rel has a .png extension");
EXPECT(cst_attach_dir(session, dir, sizeof(dir)) == 0, "attach dir");
char full[1024];
EXPECT(cst_attach_full_path(session, a, full, sizeof(full)) == 0 &&
strstr(full, "attachments/") != NULL,
"full path inside attachments/");
FILE *f = fopen(full, "rb");
EXPECT(f != NULL, "stored file exists");
if (f)
fclose(f);
char desc[128];
cst_attach_describe(a, desc, sizeof(desc));
EXPECT(strstr(desc, "32x24") && strstr(desc, "png"), "describe has size");
size_t rn = 0;
char *data = cst_attach_read(session, a, &rn, err, sizeof(err));
EXPECT(data && rn == n && memcmp(data, b, n) == 0, "read back exact bytes");
free(data);
char *url = cst_attach_data_url(session, a);
EXPECT(url && strncmp(url, "data:image/png;base64,", 22) == 0,
"data url prefix");
if (url)
EXPECT(strlen(url) == 22 + ((n + 2) / 3) * 4, "data url length");
free(url);
cst_attach_free(a);
/* a text file named .png is still refused */
EXPECT(cst_attach_import_bytes(session, "fake.png", "not an image", 12, err,
sizeof(err)) == NULL,
"content decides, not the name");
EXPECT(strstr(err, "not a supported image") != NULL, "err explains why");
EXPECT(cst_attach_import_bytes(session, "empty.png", "", 0, err,
sizeof(err)) == NULL,
"empty data refused");
}
static void test_import_file(const char *root) {
char session[1024], src[1024];
snprintf(session, sizeof(session), "%.900s/sessions/b.jsonl", root);
snprintf(src, sizeof(src), "%.900s/logo.gif", root);
unsigned char b[64];
size_t n = gif_hdr(b, 8, 6);
FILE *f = fopen(src, "wb");
EXPECT(f != NULL, "write source image");
if (f) {
fwrite(b, 1, n, f);
fclose(f);
}
char err[256];
cst_attachment_t *a =
cst_attach_import_file(session, src, err, sizeof(err));
EXPECT(a != NULL, "file imported");
if (a) {
EXPECT(strcmp(a->mime, "image/gif") == 0, "imported mime");
EXPECT(strcmp(a->name, "logo.gif") == 0, "imported keeps the leaf name");
EXPECT(a->width == 8 && a->height == 6, "imported dimensions");
cst_attach_free(a);
}
EXPECT(cst_attach_import_file(session, "/nonexistent/x.png", err,
sizeof(err)) == NULL,
"missing file refused");
}
static void test_json_roundtrip(void) {
cst_attachment_t list[3];
memset(list, 0, sizeof(list));
snprintf(list[0].id, sizeof(list[0].id), "img-1");
snprintf(list[0].name, sizeof(list[0].name), "a.png");
snprintf(list[0].mime, sizeof(list[0].mime), "image/png");
snprintf(list[0].rel, sizeof(list[0].rel), "img-1.png");
list[0].bytes = 12345;
list[0].width = 640;
list[0].height = 480;
snprintf(list[0].origin, sizeof(list[0].origin), "user");
snprintf(list[1].id, sizeof(list[1].id), "img-2");
snprintf(list[1].name, sizeof(list[1].name), "b.jpg");
snprintf(list[1].mime, sizeof(list[1].mime), "image/jpeg");
snprintf(list[1].rel, sizeof(list[1].rel), "img-2.jpg");
list[1].bytes = 4096;
snprintf(list[1].detail, sizeof(list[1].detail), "high");
snprintf(list[1].origin, sizeof(list[1].origin), "view_image");
snprintf(list[2].id, sizeof(list[2].id), "img-3");
snprintf(list[2].name, sizeof(list[2].name), "c.webp");
snprintf(list[2].mime, sizeof(list[2].mime), "image/webp");
snprintf(list[2].rel, sizeof(list[2].rel), "img-3.webp");
list[2].bytes = 7;
char *json = cst_attachments_to_json(list, 3);
EXPECT(json != NULL, "to_json");
int n = 0;
cJSON *parsed = cJSON_Parse(json);
cst_attachment_t *back = cst_attachments_from_json(parsed, &n);
cJSON_Delete(parsed);
EXPECT(back != NULL && n == 3, "from_json count");
if (back && n == 3) {
EXPECT(strcmp(back[0].id, "img-1") == 0 && back[0].bytes == 12345 &&
back[0].width == 640 && back[0].height == 480,
"first entry survives");
EXPECT(strcmp(back[1].detail, "high") == 0 &&
strcmp(back[1].origin, "view_image") == 0,
"detail and origin survive");
EXPECT(strcmp(back[2].mime, "image/webp") == 0 && back[2].width == 0,
"entry without dimensions");
EXPECT(strcmp(back[0].detail, "auto") == 0, "missing detail defaults");
cst_attachments_free(back, n);
}
free(json);
/* entries without rel/id are dropped; a non-array is NULL */
cJSON *arr = cJSON_Parse("[{\"id\":\"x\"},{\"id\":\"y\",\"rel\":\"y.png\"}]");
back = cst_attachments_from_json(arr, &n);
EXPECT(back != NULL && n == 1 && strcmp(back[0].id, "y") == 0,
"incomplete entries skipped");
cst_attachments_free(back, n);
cJSON_Delete(arr);
cJSON *obj = cJSON_Parse("{}");
n = 7;
EXPECT(cst_attachments_from_json(obj, &n) == NULL && n == 0,
"object is not an attachment array");
cJSON_Delete(obj);
EXPECT(cst_attachments_to_json(NULL, 0) == NULL, "empty list is NULL");
}
static void test_lists(void) {
cst_attachment_t src;
memset(&src, 0, sizeof(src));
snprintf(src.id, sizeof(src.id), "img-x");
snprintf(src.mime, sizeof(src.mime), "image/png");
cst_attachment_t *list = NULL;
int n = 0, cap = 0;
for (int i = 0; i < 10; i++)
EXPECT(cst_attachments_push(&list, &n, &cap, &src) == 0, "push");
EXPECT(n == 10 && cap >= 10, "list grew");
EXPECT(strcmp(list[9].id, "img-x") == 0, "last entry copied");
cst_attachment_t *dup = cst_attachments_dup(list, n);
EXPECT(dup && strcmp(dup[3].mime, "image/png") == 0, "dup copies fields");
dup[3].id[0] = 'Z';
EXPECT(list[3].id[0] == 'i', "dup is a deep copy");
cst_attachments_free(dup, n);
EXPECT(cst_attachments_dup(NULL, 0) == NULL, "dup of nothing is NULL");
cst_attachments_free(list, n);
}
static void test_agent_pending(const char *root) {
cst_agent_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", root);
snprintf(cfg.session_path, sizeof(cfg.session_path), "%s/sessions/c.jsonl",
root);
cst_agent_t *a = cst_agent_create(&cfg);
EXPECT(a != NULL, "agent created");
if (!a)
return;
char err[256], line[128];
EXPECT(cst_agent_pending_count(a) == 0, "no pending at start");
EXPECT(cst_agent_pending_describe(a, 0, line, sizeof(line)) == -1,
"describe out of range");
unsigned char b[32];
size_t n = png_hdr(b, 4, 2);
char src[1024];
snprintf(src, sizeof(src), "%.900s/pic.png", root);
FILE *f = fopen(src, "wb");
EXPECT(f != NULL, "write the source png");
if (f) {
fwrite(b, 1, n, f);
fclose(f);
}
EXPECT(cst_agent_attach_file(a, src, err, sizeof(err)) == 0,
"attach through the agent");
EXPECT(cst_agent_pending_count(a) == 1, "one pending");
EXPECT(cst_agent_pending_describe(a, 0, line, sizeof(line)) == 0 &&
strstr(line, "4x2") != NULL,
"pending describe shows the size");
EXPECT(cst_agent_attach_file(a, "/nonexistent/x.png", err, sizeof(err)) == -1,
"missing file refused");
EXPECT(cst_agent_pending_count(a) == 1, "a failure keeps the queue");
for (int i = 1; i < CST_ATTACH_MAX_COUNT; i++)
EXPECT(cst_agent_attach_file(a, src, err, sizeof(err)) == 0, "attach more");
EXPECT(cst_agent_pending_count(a) == CST_ATTACH_MAX_COUNT, "queue full");
EXPECT(cst_agent_attach_file(a, src, err, sizeof(err)) == -1 &&
strstr(err, "at most") != NULL,
"one over the limit is refused");
cst_agent_pending_clear(a);
EXPECT(cst_agent_pending_count(a) == 0, "cleared");
cst_agent_destroy(a);
}
static void test_paste_paths(void) {
static char p[CST_PASTE_MAX_PATHS][CST_PASTE_PATH_MAX];
const char *s = "/tmp/a.png";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == 1 &&
strcmp(p[0], "/tmp/a.png") == 0,
"single path");
s = " '/home/u/my shot.png' \"/x/y z.jpg\"\n/plain.gif\r\n";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == 3, "quoted paths count");
EXPECT(strcmp(p[0], "/home/u/my shot.png") == 0, "single quotes");
EXPECT(strcmp(p[1], "/x/y z.jpg") == 0, "double quotes");
EXPECT(strcmp(p[2], "/plain.gif") == 0, "newline separated");
s = "file:///home/u/a%20b.png file://localhost/c.png";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == 2, "file uris");
EXPECT(strcmp(p[0], "/home/u/a b.png") == 0, "percent decoded");
EXPECT(strcmp(p[1], "/c.png") == 0, "localhost host");
s = "file://evil.example/c.png";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == -1, "remote host refused");
#ifndef _WIN32
s = "/home/u/my\\ shot.png";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == 1 &&
strcmp(p[0], "/home/u/my shot.png") == 0,
"backslash escape");
#endif
s = "'/unterminated.png";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == -1, "unbalanced quote");
s = " \n ";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == -1, "blank");
s = "a b c";
EXPECT(cst_paste_paths(s, strlen(s), p, 2) == -1, "too many");
}
static void test_dib(void) {
/* 3x2, 24-bit, bottom-up: stride 12 (9 bytes + pad). */
unsigned char dib[40 + 24];
memset(dib, 0, sizeof(dib));
put_le32(dib, 40);
put_le32(dib + 4, 3);
put_le32(dib + 8, 2);
put_le16(dib + 12, 1);
put_le16(dib + 14, 24);
dib[40] = 0xff; /* bottom-left pixel blue (BGR) */
size_t n = 0;
char *img = cst_dib_to_image(dib, sizeof(dib), &n);
char mime[32];
int w = 0, h = 0;
EXPECT(img && cst_attach_sniff(img, n, mime, sizeof(mime)) == 0 &&
strcmp(mime, "image/png") == 0,
"24-bit DIB becomes a PNG");
EXPECT(img && cst_attach_dimensions(img, n, &w, &h) == 0 && w == 3 && h == 2,
"PNG keeps the size");
free(img);
put_le16(dib + 14, 8); /* palette needs 256 entries: past the end */
EXPECT(cst_dib_to_image(dib, sizeof(dib), &n) == NULL, "short DIB refused");
put_le32(dib, 4);
EXPECT(cst_dib_to_image(dib, sizeof(dib), &n) == NULL, "bad header size");
}
int main(void) {
char root[1024];
if (test_tmpdir(root, sizeof(root), "cst-attach") != 0) {
fprintf(stderr, "cannot create a scratch dir\n");
return 1;
}
test_sniff();
test_dimensions();
test_base64();
test_rel_ok(root);
test_paste_paths();
test_dib();
test_store(root);
test_import_file(root);
test_json_roundtrip();
test_lists();
test_agent_pending(root);
test_rmrf(root);
if (nfail) {
fprintf(stderr, "test_attach: %d failed\n", nfail);
return 1;
}
printf("test_attach: OK\n");
return 0;
}
+170
View File
@@ -159,7 +159,177 @@ static void test_tool_cap_head_tail(void) {
free(big);
}
static cJSON *build_req(const cst_llm_request_t *r) {
char *body = cst_llm_build_body(r);
EXPECT(body != NULL, "body built");
cJSON *j = body ? cJSON_Parse(body) : NULL;
EXPECT(j != NULL, "body is valid JSON");
free(body);
return j;
}
static const cJSON *msg_at(const cJSON *root, int i) {
return cJSON_GetArrayItem(cJSON_GetObjectItemCaseSensitive(root, "messages"),
i);
}
static const cJSON *cache_mark(const cJSON *m) {
const cJSON *c = cJSON_GetObjectItemCaseSensitive(m, "content");
if (!cJSON_IsArray(c))
return NULL;
const cJSON *last = cJSON_GetArrayItem(c, cJSON_GetArraySize(c) - 1);
return cJSON_GetObjectItemCaseSensitive(last, "cache_control");
}
static void test_reasoning_and_prefix(void) {
cst_message_t m[4];
m[0] = msg(CST_ROLE_SYSTEM, "sys");
m[1] = msg(CST_ROLE_USER, "go");
m[2] = msg(CST_ROLE_ASSISTANT, "partial answer");
m[2].reasoning = "thinking it over";
m[3] = msg(CST_ROLE_ASSISTANT, "cut off here");
m[3].prefix = 1;
cst_llm_request_t r;
memset(&r, 0, sizeof(r));
r.model = "deepseek-flash";
r.msgs = m;
r.nmsgs = 4;
cJSON *j = build_req(&r);
EXPECT(!cJSON_GetObjectItemCaseSensitive(msg_at(j, 2), "reasoning_content"),
"reasoning only when asked for");
const cJSON *p = cJSON_GetObjectItemCaseSensitive(msg_at(j, 3), "prefix");
EXPECT(cJSON_IsTrue(p), "prefix:true on the continued message");
EXPECT(!cJSON_GetObjectItemCaseSensitive(msg_at(j, 2), "prefix"),
"no prefix on history");
cJSON_Delete(j);
r.send_reasoning = 1;
j = build_req(&r);
const cJSON *rc =
cJSON_GetObjectItemCaseSensitive(msg_at(j, 2), "reasoning_content");
EXPECT(cJSON_IsString(rc) && strcmp(rc->valuestring, "thinking it over") == 0,
"reasoning_content passed back");
EXPECT(!cJSON_GetObjectItemCaseSensitive(j, "prompt_cache_key"),
"no cache key without a cache style");
cJSON_Delete(j);
}
static void test_cache_hints(void) {
cst_message_t m[4];
m[0] = msg(CST_ROLE_SYSTEM, "sys");
m[1] = msg(CST_ROLE_USER, "first");
m[2] = msg(CST_ROLE_ASSISTANT, "reply");
m[3] = msg(CST_ROLE_USER, "second");
cst_llm_request_t r;
memset(&r, 0, sizeof(r));
r.model = "gpt-4.1";
r.msgs = m;
r.nmsgs = 4;
r.cache_style = CST_CACHE_OPENAI;
r.cache_key = "sess-1";
cJSON *j = build_req(&r);
const cJSON *k = cJSON_GetObjectItemCaseSensitive(j, "prompt_cache_key");
EXPECT(cJSON_IsString(k) && strcmp(k->valuestring, "sess-1") == 0,
"prompt_cache_key");
EXPECT(content_at(j, 3) && !cache_mark(msg_at(j, 3)),
"OpenAI style leaves content as strings");
cJSON_Delete(j);
r.model = "anthropic/claude-sonnet-4";
r.cache_style = CST_CACHE_ANTHROPIC;
r.cache_ttl = "1h";
j = build_req(&r);
EXPECT(!cJSON_GetObjectItemCaseSensitive(j, "prompt_cache_key"),
"no prompt_cache_key for Anthropic");
const cJSON *cs = cache_mark(msg_at(j, 0));
const cJSON *cl = cache_mark(msg_at(j, 3));
const cJSON *cu = cache_mark(msg_at(j, 1));
EXPECT(cs && cl && cu, "breakpoints on system, newest, previous user");
EXPECT(!cache_mark(msg_at(j, 2)), "no breakpoint on the assistant reply");
const cJSON *ttl = cJSON_GetObjectItemCaseSensitive(cl, "ttl");
EXPECT(cJSON_IsString(ttl) && strcmp(ttl->valuestring, "1h") == 0, "ttl");
const cJSON *parts =
cJSON_GetObjectItemCaseSensitive(msg_at(j, 3), "content");
const cJSON *txt =
cJSON_GetObjectItemCaseSensitive(cJSON_GetArrayItem(parts, 0), "text");
EXPECT(cJSON_IsString(txt) && strcmp(txt->valuestring, "second") == 0,
"marked content keeps its text");
cJSON_Delete(j);
}
static void test_tools_snapshot(void) {
cst_message_t m[1] = {msg(CST_ROLE_USER, "go")};
cst_llm_request_t r;
memset(&r, 0, sizeof(r));
r.model = "test-model";
r.msgs = m;
r.nmsgs = 1;
r.enable_tools = 1;
r.tools_json = "[{\"type\":\"function\",\"function\":{\"name\":\"only_me\","
"\"parameters\":{\"type\":\"object\"}}}]";
cJSON *j = build_req(&r);
const cJSON *tools = cJSON_GetObjectItemCaseSensitive(j, "tools");
EXPECT(cJSON_IsArray(tools) && cJSON_GetArraySize(tools) == 1,
"tools[] taken from the snapshot");
const cJSON *nm = cJSON_GetObjectItemCaseSensitive(
cJSON_GetObjectItemCaseSensitive(cJSON_GetArrayItem(tools, 0),
"function"),
"name");
EXPECT(cJSON_IsString(nm) && strcmp(nm->valuestring, "only_me") == 0,
"snapshot tool name");
cJSON_Delete(j);
}
static void test_images_off(void) {
cst_attachment_t img;
memset(&img, 0, sizeof(img));
snprintf(img.id, sizeof(img.id), "img-1");
snprintf(img.rel, sizeof(img.rel), "img-1.png");
snprintf(img.name, sizeof(img.name), "shot.png");
snprintf(img.mime, sizeof(img.mime), "image/png");
img.width = 640;
img.height = 480;
img.bytes = 2048;
cst_message_t m[4];
m[0] = msg(CST_ROLE_USER, "what is this");
m[0].attach = &img;
m[0].nattach = 1;
m[1] = msg(CST_ROLE_ASSISTANT, "");
m[1].tool_calls_json = "[{\"id\":\"c1\",\"type\":\"function\",\"function\":"
"{\"name\":\"view_image\",\"arguments\":\"{}\"}}]";
m[2] = msg(CST_ROLE_TOOL, "view_image shot.png");
snprintf(m[2].tool_call_id, sizeof(m[2].tool_call_id), "c1");
m[2].attach = &img;
m[2].nattach = 1;
m[3] = msg(CST_ROLE_USER, "and now?");
cst_llm_request_t r;
memset(&r, 0, sizeof(r));
r.model = "deepseek-flash";
r.msgs = m;
r.nmsgs = 4;
r.images_off = 1;
r.session_path = "/nonexistent/s.jsonl";
cJSON *j = build_req(&r);
char *s = cJSON_PrintUnformatted(j);
EXPECT(s && !strstr(s, "image_url"), "no image parts when images are off");
free(s);
const char *c0 = content_at(j, 0);
EXPECT(c0 && strstr(c0, "what is this") &&
strstr(c0, "[image \"shot.png\" (640x480") &&
strstr(c0, "not sent: this model does not take images"),
"user image becomes a plain-text note");
const char *c2 = content_at(j, 2);
EXPECT(c2 && strstr(c2, "view_image shot.png") && strstr(c2, "not sent"),
"tool image noted inside the tool result");
EXPECT(nmsgs(j) == 4, "no extra user message for deferred tool images");
cJSON_Delete(j);
}
int main(void) {
test_images_off();
test_reasoning_and_prefix();
test_cache_hints();
test_tools_snapshot();
test_escaping();
test_invalid_utf8();
test_tool_calls_and_null_content();
+54 -2
View File
@@ -5,6 +5,8 @@
#define _POSIX_C_SOURCE 200809L
#include "cJSON.h"
#include "cst_agent.h"
#include "cst_attach.h"
#include "cst_mcp.h"
#include "cst_tools.h"
#include "os_compat.h"
@@ -18,6 +20,7 @@
static int nfail;
static char g_tmp[OS_PATH_MAX];
static cst_agent_t *g_agent;
#define CHECK(cond, msg) \
do { \
@@ -99,6 +102,7 @@ static int server(void) {
cJSON_AddItemToArray(tools, tool("mixed", "Mixed content."));
cJSON_AddItemToArray(tools, tool("hang", "Never answers."));
cJSON_AddItemToArray(tools, tool("crash", "Exits."));
cJSON_AddItemToArray(tools, tool("pic", "Returns images."));
}
reply(id, r);
} else if (strcmp(mt, "tools/call") == 0) {
@@ -136,6 +140,21 @@ static int server(void) {
cJSON_AddStringToObject(rr, "text", "resource text");
cJSON_AddItemToArray(c, res);
reply(id, r);
} else if (strcmp(name, "pic") == 0) {
cJSON *r = text_result("a chart", 0);
cJSON *c = cJSON_GetObjectItemCaseSensitive(r, "content");
const char *datas[] = {
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAC"
"hwGA60e6kgAAAABJRU5ErkJggg==",
"AAAA"};
for (int i = 0; i < 2; i++) {
cJSON *img = cJSON_CreateObject();
cJSON_AddStringToObject(img, "type", "image");
cJSON_AddStringToObject(img, "mimeType", "image/png");
cJSON_AddStringToObject(img, "data", datas[i]);
cJSON_AddItemToArray(c, img);
}
reply(id, r);
} else if (strcmp(name, "crash") == 0) {
fprintf(stderr, "fatal: fake crash\n");
exit(3);
@@ -163,6 +182,7 @@ static int call(const char *tool, const char *k, const char *v,
cst_tool_ctx_t ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.workspace_root = g_tmp;
ctx.agent = g_agent;
memset(r, 0, sizeof(*r));
return cst_tool_run(&ctx, &c, r);
}
@@ -217,8 +237,8 @@ int main(int argc, char **argv) {
cst_mcp_info_t mi[CST_MCP_MAX];
int n = cst_mcp_info(mi, CST_MCP_MAX);
const cst_mcp_info_t *f = info_of("fake", mi, n);
CHECK(f && f->state == CST_MCP_READY && f->ntools == 6,
"fake ready with 6 tools over two pages");
CHECK(f && f->state == CST_MCP_READY && f->ntools == 7,
"fake ready with 7 tools over two pages");
const cst_mcp_info_t *rm = info_of("remote", mi, n);
CHECK(rm && rm->state == CST_MCP_FAILED && strstr(rm->error, "stdio"),
"url servers unsupported");
@@ -255,8 +275,40 @@ int main(int argc, char **argv) {
CHECK(r.exit_code == 0 && r.out && strstr(r.out, "first\n[image image/png") &&
strstr(r.out, "resource text"),
"content items mapped to text");
CHECK(r.nattach == 0, "no session: images stay text");
cst_tool_result_free(&r);
cst_agent_config_t acfg;
memset(&acfg, 0, sizeof(acfg));
snprintf(acfg.workspace_root, sizeof(acfg.workspace_root), "%s", g_tmp);
char sdir[1024];
snprintf(sdir, sizeof(sdir), "%.900s/cst-mcp-test-%d", g_tmp, (int)getpid());
os_mkdir_p(sdir, 0755);
snprintf(acfg.session_path, sizeof(acfg.session_path), "%s/s.jsonl", sdir);
g_agent = cst_agent_create(&acfg);
CHECK(g_agent != NULL, "agent for the session store");
call("mcp__fake__pic", NULL, NULL, &r);
CHECK(r.exit_code == 0 && r.nattach == 1 && r.attach &&
strcmp(r.attach[0].mime, "image/png") == 0 &&
strcmp(r.attach[0].origin, "mcp") == 0 &&
r.attach[0].width == 1 && r.attach[0].height == 1,
"image content stored and attached");
CHECK(r.out && strstr(r.out, "a chart\n[image ") &&
strstr(r.out, "shown to you as an image") &&
strstr(r.out, "not shown:"),
"one line per image, bad data falls back to text");
if (r.nattach == 1) {
char full[OS_PATH_MAX];
CHECK(cst_attach_full_path(acfg.session_path, &r.attach[0], full,
sizeof(full)) == 0 &&
access(full, R_OK) == 0,
"stored file exists");
}
cst_tool_result_free(&r);
cst_agent_destroy(g_agent);
g_agent = NULL;
test_rmrf(sdir);
volatile int cancel = 1;
os_set_cancel_flag(&cancel);
double t0 = os_now_ms();
+223
View File
@@ -0,0 +1,223 @@
/* Model registry: built-in data, longest/most specific match, the user file,
* the fetched cache and the env overrides. */
#define _POSIX_C_SOURCE 200809L
#include "cJSON.h"
#include "cst_agent.h"
#include "cst_models.h"
#include "cst_test_util.h"
#include "os_compat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int nfail;
#define EXPECT(cond, msg) \
do { \
if (!(cond)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \
nfail++; \
} \
} while (0)
static void test_builtin(void) {
cJSON *doc = cJSON_Parse(cst_models_builtin_json());
EXPECT(cJSON_IsObject(doc), "embedded built-in JSON parses");
cJSON_Delete(doc);
EXPECT(cst_models_load_error()[0] == 0, "no load error");
EXPECT(cst_provider_count() >= 4, "built-in providers");
const cst_provider_t *p = cst_provider_at(0);
EXPECT(p && strcmp(p->name, "minimax") == 0, "file order kept");
p = cst_provider_find("deepseek");
EXPECT(p && strcmp(p->model, "deepseek-flash") == 0 &&
strcmp(p->key_env, "DEEPSEEK_API_KEY") == 0 &&
strcmp(cst_provider_source(p), "built-in") == 0,
"deepseek preset");
cst_model_info_t mi;
/* most specific rule per field: price from deepseek-v4-pro, context from
* deepseek */
cst_model_info("deepseek", "deepseek-v4-pro", &mi);
EXPECT(mi.context == 1000000 && mi.vision == 0, "context inherited");
EXPECT(mi.has_price && mi.price.miss == 9.0 && mi.price.offpeak_half,
"longer match wins the price");
EXPECT(strcmp(mi.source, "built-in") == 0, "source built-in");
cst_model_info(NULL, "gpt-4o-mini-2024", &mi);
EXPECT(mi.price.miss == 0.15 && mi.context == 128000 && mi.vision == 1,
"gpt-4o-mini over gpt-4o");
cst_model_info(NULL, "Qwen/Qwen2.5-VL-72B-Instruct", &mi);
EXPECT(mi.vision == 1 && mi.context == 131072, "glob *-vl* + qwen prefix");
cst_model_info(NULL, "zhipu/glm-4.5v", &mi);
EXPECT(mi.vision == 1 && mi.context == 128000, "glm-*v*");
cst_model_info(NULL, "glm-4-plus", &mi);
EXPECT(mi.vision == -1, "glm without v: unknown");
cst_model_info(NULL, "anthropic/some-new-model", &mi);
EXPECT(mi.cache_style == CST_CACHE_ANTHROPIC, "vendor rule on the full id");
cst_model_info(NULL, "totally-unknown", &mi);
EXPECT(mi.context == 128000 && mi.vision == -1 && !mi.has_price &&
strcmp(mi.source, "default") == 0,
"defaults");
char tag[48];
cst_model_info(NULL, "gpt-4.1", &mi);
cst_model_info_label(&mi, tag, sizeof(tag));
EXPECT(strcmp(tag, "1M · vision") == 0, "label");
}
static const char *USER_JSON =
"{\"providers\": {"
" \"deepseek\": {\"api_base\": \"https://proxy.example/v1\"},"
" \"local\": {\"api_base\": \"http://127.0.0.1:8000/v1\","
" \"key_env\": \"LOCAL_KEY\", \"default_model\": \"qwen3-32b\","
" \"models\": [\"qwen3-32b\", \"my-vl\"]}"
"},"
"\"models\": ["
" {\"match\": \"qwen3\", \"context\": 32768},"
" {\"match\": \"my-vl\", \"vision\": true, \"price\": {\"miss\": 1,"
" \"out\": 2, \"currency\": \"¥\"}}"
"]}";
static void test_user(void) {
char err[256];
EXPECT(cst_models_load_json(cst_models_builtin_json(), USER_JSON, err,
sizeof(err)) == 0,
"load with user file");
const cst_provider_t *ds = cst_provider_find("deepseek");
EXPECT(ds && strcmp(ds->api_base, "https://proxy.example/v1") == 0 &&
strcmp(ds->model, "deepseek-flash") == 0 &&
strcmp(cst_provider_source(ds), "user") == 0,
"user overrides one field, keeps the rest");
const cst_provider_t *lp = cst_provider_find("local");
EXPECT(lp && strcmp(lp->model, "qwen3-32b") == 0 &&
strcmp(lp->key_env, "LOCAL_KEY") == 0,
"user adds a provider");
EXPECT(cst_provider_at(cst_provider_count() - 1) == lp, "appended last");
cst_model_info_t mi;
cst_model_info("local", "qwen3-32b", &mi);
EXPECT(mi.context == 32768 && strcmp(mi.source, "user") == 0,
"user rule beats built-in qwen");
cst_model_info("local", "my-vl", &mi);
EXPECT(mi.vision == 1 && mi.has_price && mi.price.hit == 1 &&
mi.price.write == 1 && strcmp(mi.price.currency, "¥") == 0,
"user price, hit/write default to miss");
/* passback follows the provider name even behind a proxy base */
cst_model_caps_t caps;
cst_model_caps("deepseek", "https://proxy.example/v1", "deepseek-flash",
&caps);
EXPECT(caps.reasoning_passback && caps.prefix_completion, "caps by name");
char names[32][128];
int n = cst_models_list(NULL, names, 32);
int seen = 0;
for (int i = 0; i < n; i++)
seen += strcmp(names[i], "my-vl") == 0;
EXPECT(seen == 1, "picker list includes user models");
EXPECT(cst_models_load_json(cst_models_builtin_json(), "{not json", err,
sizeof(err)) != 0 &&
err[0],
"bad user file reported");
}
static void test_cache(void) {
cst_models_load_json(cst_models_builtin_json(), NULL, NULL, 0);
cst_model_meta_t m[3];
memset(m, 0, sizeof(m));
snprintf(m[0].id, sizeof(m[0].id), "anthropic/claude-sonnet-4");
m[0].context = 1000000;
m[0].vision = -1;
snprintf(m[1].id, sizeof(m[1].id), "deepseek-flash");
m[1].vision = 1; /* API claims vision */
m[1].has_price = 1;
m[1].miss = 0.5;
m[1].hit = 0.1;
m[1].write = 0.5;
m[1].out = 1;
snprintf(m[2].id, sizeof(m[2].id), "brand-new");
m[2].vision = -1;
EXPECT(cst_models_cache_save("teamorouter", "https://x/v1", m, 3) == 0,
"cache saved");
cst_model_info_t mi;
cst_model_info("teamorouter", "anthropic/claude-sonnet-4", &mi);
EXPECT(mi.context == 1000000, "API context beats built-in");
EXPECT(mi.vision == 1 && mi.cache_style == CST_CACHE_ANTHROPIC,
"missing API fields fall through to built-in");
EXPECT(strcmp(mi.source, "api+built-in") == 0, "source lists both");
cst_model_info("teamorouter", "deepseek-flash", &mi);
EXPECT(mi.vision == 1 && mi.price.miss == 0.5 &&
strcmp(mi.price.currency, "$") == 0,
"API vision and USD price");
cst_model_info("deepseek", "deepseek-flash", &mi);
EXPECT(mi.vision == 0 && mi.price.miss == 2.0,
"another provider's cache is not used");
EXPECT(cst_model_context_tokens("anthropic/claude-sonnet-4") == 1000000,
"no provider: any cache, exact id");
cst_model_meta_t back[8];
long age = -1;
int n = cst_models_cache_load("teamorouter", back, 8, &age);
EXPECT(n == 3 && age >= 0 && age < 60 &&
strcmp(back[2].id, "brand-new") == 0,
"cache reads back");
EXPECT(cst_models_cache_load("openai", back, 8, &age) == -1, "no cache");
EXPECT(cst_models_cache_save("../evil", NULL, m, 1) != 0,
"provider name cannot leave the cache dir");
/* a fresh process (reload) sees the file */
cst_models_reload();
cst_model_info("teamorouter", "anthropic/claude-sonnet-4", &mi);
EXPECT(mi.context == 1000000, "cache survives a reload");
}
static void test_env(void) {
cst_models_load_json(cst_models_builtin_json(), NULL, NULL, 0);
setenv("CST_CTX_TOKENS", "4242", 1);
setenv("CST_PRICE", "1,0.5,3", 1);
cst_model_info_t mi;
cst_model_info(NULL, "deepseek-flash", &mi);
EXPECT(mi.context == 4242 && mi.price.miss == 1 && mi.price.write == 1 &&
strstr(mi.source, "env"),
"env overrides");
unsetenv("CST_CTX_TOKENS");
unsetenv("CST_PRICE");
setenv("CST_NO_CACHE_HINTS", "1", 1);
cst_model_caps_t caps;
cst_model_caps("openai", NULL, "gpt-4.1", &caps);
EXPECT(caps.cache_style == CST_CACHE_AUTO && caps.vision == 1,
"no cache hints, vision kept");
unsetenv("CST_NO_CACHE_HINTS");
setenv("CST_MODELS", "a, b:c", 1);
char names[8][128];
int n = cst_models_list("cur", names, 8);
EXPECT(n == 4 && strcmp(names[1], "b") == 0 && strcmp(names[3], "cur") == 0,
"CST_MODELS list");
unsetenv("CST_MODELS");
}
int main(void) {
char root[1024];
if (test_tmpdir(root, sizeof(root), "cst-models") != 0) {
fprintf(stderr, "cannot create a scratch dir\n");
return 1;
}
setenv("XDG_CONFIG_HOME", root, 1); /* no real models.json or caches */
unsetenv("CST_CTX_TOKENS");
unsetenv("CST_PRICE");
unsetenv("CST_MODELS");
unsetenv("CST_NO_CACHE_HINTS");
test_builtin();
test_user();
test_cache();
test_env();
test_rmrf(root);
if (nfail) {
fprintf(stderr, "test_model_registry: %d failed\n", nfail);
return 1;
}
printf("test_model_registry: OK\n");
return 0;
}
+70 -2
View File
@@ -3,6 +3,8 @@
#endif
#include "cst_agent.h"
#include "cst_models.h"
#include "os_compat.h"
#include <stdio.h>
#include <stdlib.h>
@@ -72,11 +74,51 @@ static int start_server(void) {
"Connection: close\r\n\r\n" body
int main(void) {
char cfgdir[1024];
CHECK(test_tmpdir(cfgdir, sizeof(cfgdir), "cst-models-cfg") == 0);
setenv("XDG_CONFIG_HOME", cfgdir, 1); /* ignore a real models-cache */
const cst_provider_t *tr = cst_provider_find("teamorouter");
CHECK(tr && strcmp(tr->api_base, "https://api.teamorouter.com/v1") == 0);
CHECK(tr->model[0] == 0); /* picked from GET /models */
CHECK(cst_model_context_tokens("anthropic/claude-sonnet-4") == 200000);
CHECK(cst_model_context_tokens("deepseek-flash") == 128000);
CHECK(cst_model_context_tokens("deepseek-flash") == 1000000);
CHECK(cst_model_context_tokens("deepseek/deepseek-v4-pro") == 1000000);
CHECK(cst_model_context_tokens("google/gemini-2.5-pro") == 1048576);
CHECK(cst_model_context_tokens("unknown-model") == 128000);
cst_model_caps_t caps;
cst_model_caps("deepseek", "https://api.deepseek.com/v1", "deepseek-flash",
&caps);
CHECK(caps.reasoning_passback && caps.prefix_completion);
CHECK(caps.cache_style == CST_CACHE_AUTO);
cst_model_caps("teamorouter", "https://api.teamorouter.com/v1",
"deepseek-flash", &caps);
CHECK(!caps.reasoning_passback && !caps.prefix_completion);
cst_model_caps("teamorouter", NULL, "anthropic/claude-sonnet-4", &caps);
CHECK(caps.cache_style == CST_CACHE_ANTHROPIC);
cst_model_caps("openai", NULL, "gpt-4.1", &caps);
CHECK(caps.cache_style == CST_CACHE_OPENAI);
cst_model_caps("openai", NULL, "o3-mini", &caps);
CHECK(caps.cache_style == CST_CACHE_OPENAI);
cst_model_caps("minimax", NULL, "MiniMax-M2", &caps);
CHECK(caps.cache_style == CST_CACHE_AUTO);
/* Beijing time: Thu 10:00 peak, Thu 13:00 and Sat 10:00 off-peak */
CHECK(cst_price_is_peak((time_t)1791424800));
CHECK(!cst_price_is_peak((time_t)1791435600));
CHECK(!cst_price_is_peak((time_t)1791597600));
cst_price_t pr;
CHECK(cst_model_price("deepseek-flash", &pr) == 0);
CHECK(pr.offpeak_half && pr.hit < pr.miss);
double peak = cst_price_cost(&pr, 1000000, 1000000, 0, 1000000,
(time_t)1791424800);
CHECK(peak > 9.99 && peak < 10.05); /* 2 + 0.04 + 8 */
double off = cst_price_cost(&pr, 1000000, 1000000, 0, 1000000,
(time_t)1791435600);
CHECK(off > peak / 2 - 0.001 && off < peak / 2 + 0.001);
CHECK(cst_model_price("anthropic/claude-sonnet-4", &pr) == 0);
CHECK(pr.write > pr.miss && strcmp(pr.currency, "$") == 0);
CHECK(cst_model_price("MiniMax-M2", &pr) != 0);
g_responses[0] = RESP("200 OK", "{\"object\":\"list\",\"data\":["
"{\"id\":\"deepseek-flash\"},"
@@ -87,7 +129,21 @@ int main(void) {
g_responses[3] = RESP("200 OK", "{\"models\":[{\"name\":\"m1\"},"
"{\"name\":\"m2\"}]}");
g_responses[4] = RESP("500 Internal Server Error", "{}");
g_nresp = 5;
/* OpenRouter-style metadata; prices are USD per token, as strings */
g_responses[5] = RESP(
"200 OK",
"{\"data\":[{\"id\":\"anthropic/claude-sonnet-4\","
"\"context_length\":1000000,"
"\"architecture\":{\"input_modalities\":[\"text\",\"image\"]},"
"\"pricing\":{\"prompt\":\"0.000003\",\"completion\":\"0.000015\","
"\"input_cache_read\":\"0.0000003\"}},"
"{\"id\":\"old/text-model\","
"\"architecture\":{\"modality\":\"text->text\"},"
"\"pricing\":{\"prompt\":\"-1\",\"completion\":\"-1\"}},"
"{\"id\":\"vllm-model\",\"max_model_len\":32768,"
"\"modalities\":{\"input\":[\"text\"]}},"
"{\"id\":\"plain\"}]}");
g_nresp = 6;
int port = start_server();
pthread_t th;
CHECK(pthread_create(&th, NULL, serve, NULL) == 0);
@@ -118,6 +174,17 @@ int main(void) {
n = cst_llm_list_models(base, "sk-test", ids, 8, err, sizeof(err));
CHECK(n == -1 && strstr(err, "500") != NULL);
cst_model_meta_t meta[8];
n = cst_llm_list_models_meta(base, "sk-test", meta, 8, err, sizeof(err));
CHECK(n == 4);
CHECK(meta[0].context == 1000000 && meta[0].vision == 1);
CHECK(meta[0].has_price && meta[0].miss > 2.99 && meta[0].miss < 3.01 &&
meta[0].out > 14.99 && meta[0].out < 15.01 && meta[0].hit < 0.31 &&
meta[0].write == meta[0].miss);
CHECK(meta[1].vision == 0 && !meta[1].has_price && meta[1].context == 0);
CHECK(meta[2].context == 32768 && meta[2].vision == 0);
CHECK(meta[3].vision == -1 && meta[3].context == 0 && !meta[3].has_price);
pthread_join(th, NULL);
test_sock_close(g_listen);
@@ -125,6 +192,7 @@ int main(void) {
n = cst_llm_list_models(base, "sk-test", ids, 8, err, sizeof(err));
CHECK(n == -1);
test_rmrf(cfgdir);
puts("test_models: ok");
return 0;
}
+392
View File
@@ -0,0 +1,392 @@
#define _POSIX_C_SOURCE 200809L
#include "cJSON.h"
#include "cst_agent.h"
#include "cst_util.h"
#include "os_compat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "cst_test_util.h"
/* Runs a few replayed turns (tool call, Plan mode on, approval change, a
* reply cut off by max tokens) and checks the recorded request bodies: every
* request must extend the previous one byte for byte, so the provider's
* prefix cache keeps hitting. */
static int nfail;
#define EXPECT(cond, msg) \
do { \
if (!(cond)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \
nfail++; \
} \
} while (0)
#define USAGE \
"data: {\"choices\":[],\"usage\":{\"prompt_tokens\":1000," \
"\"completion_tokens\":20,\"prompt_cache_hit_tokens\":900," \
"\"prompt_cache_miss_tokens\":100}}\n\n" \
"data: [DONE]\n\n"
#define TOOL(id, name, args) \
"data: {\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0," \
"\"id\":\"" id "\",\"type\":\"function\",\"function\":{\"name\":\"" name \
"\",\"arguments\":\"" args "\"}}]},\"finish_reason\":null}]}\n\n" \
"data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":" \
"\"tool_calls\"}]}\n\n" USAGE
#undef TEXT /* <windows.h> has one */
#define TEXT(s, finish) \
"data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"" s "\"}," \
"\"finish_reason\":null}]}\n\n" \
"data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"" finish \
"\"}]}\n\n" USAGE
#define THINK(s) \
"data: {\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"" s \
"\"},\"finish_reason\":null}]}\n\n"
static const char *FIXTURES[] = {
/* turn 1 */
THINK("look at the file") TOOL("c1", "read_file", "{\\\"path\\\":\\\"a.txt\\\"}"),
TEXT("done one", "stop"),
/* turn 2, Plan mode */
TOOL("c2", "write_file",
"{\\\"path\\\":\\\"b.txt\\\",\\\"content\\\":\\\"x\\\"}"),
TEXT("plan reply", "stop"),
/* turn 3, Build mode + yolo; cut off, then continued */
TEXT("part one", "length"),
TEXT(" part two", "stop"),
/* turn 4 */
TEXT("bye", "stop"),
};
#define NFIX ((int)(sizeof(FIXTURES) / sizeof(FIXTURES[0])))
static void write_file(const char *dir, const char *name, const char *body) {
char p[2048];
snprintf(p, sizeof(p), "%s/%s", dir, name);
FILE *fp = fopen(p, "wb");
EXPECT(fp != NULL, "write fixture");
if (fp) {
fputs(body, fp);
fclose(fp);
}
}
static cJSON *load_req(const char *dir, int seq) {
char p[2048];
snprintf(p, sizeof(p), "%s/%04d.req.json", dir, seq);
FILE *fp = fopen(p, "rb");
if (!fp)
return NULL;
cst_buf_t b = {0};
char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0)
cst_buf_append(&b, buf, n);
fclose(fp);
cJSON *j = b.buf ? cJSON_Parse(b.buf) : NULL;
cst_buf_free(&b);
return j;
}
static const cJSON *msgs_of(const cJSON *req) {
return cJSON_GetObjectItemCaseSensitive(req, "messages");
}
static int is_prefix_msg(const cJSON *m) {
return cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(m, "prefix"));
}
static const char *str_of(const cJSON *o, const char *key) {
const cJSON *v = cJSON_GetObjectItemCaseSensitive(o, key);
return cJSON_IsString(v) ? v->valuestring : NULL;
}
static char *print(const cJSON *j) { return j ? cJSON_PrintUnformatted(j) : NULL; }
/* messages of `a` (minus a request-only prefix message) start `b` */
static int extends(const cJSON *a, const cJSON *b) {
const cJSON *ma = msgs_of(a), *mb = msgs_of(b);
int na = cJSON_GetArraySize(ma), nb = cJSON_GetArraySize(mb);
if (na > 0 && is_prefix_msg(cJSON_GetArrayItem(ma, na - 1)))
na--;
if (na > nb)
return 0;
for (int i = 0; i < na; i++) {
char *x = print(cJSON_GetArrayItem(ma, i));
char *y = print(cJSON_GetArrayItem(mb, i));
int same = x && y && strcmp(x, y) == 0;
if (!same)
fprintf(stderr, " message %d differs:\n %.300s\n %.300s\n", i,
x ? x : "(null)", y ? y : "(null)");
free(x);
free(y);
if (!same)
return 0;
}
return 1;
}
static const cJSON *last_msg(const cJSON *req) {
const cJSON *m = msgs_of(req);
return cJSON_GetArrayItem(m, cJSON_GetArraySize(m) - 1);
}
static int any_msg_has(const cJSON *req, const char *key, const char *needle) {
const cJSON *m;
cJSON_ArrayForEach(m, msgs_of(req)) {
const char *s = str_of(m, key);
if (s && strstr(s, needle))
return 1;
}
return 0;
}
static int run(cst_agent_t *a, const char *text) {
char err[512];
int rc = cst_agent_run_turn(a, text, err, sizeof(err));
if (rc != 0)
fprintf(stderr, " turn \"%s\": %s\n", text, err);
return rc;
}
/* 8-byte PNG signature + IHDR header: enough for the attachment sniffer. */
static size_t tiny_png(unsigned char *b) {
static const unsigned char sig[8] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A,
'\n'};
memset(b, 0, 32);
memcpy(b, sig, 8);
b[11] = 13;
memcpy(b + 12, "IHDR", 4);
b[19] = 2; /* 2 x 2 */
b[23] = 2;
return 32;
}
static int tools_have(const cJSON *req, const char *name) {
char *t = print(cJSON_GetObjectItemCaseSensitive(req, "tools"));
int has = t && strstr(t, name) != NULL;
free(t);
return has;
}
static int has_image_url(const cJSON *req) {
char *t = print(msgs_of(req));
int has = t && strstr(t, "image_url") != NULL;
free(t);
return has;
}
/* A text-only model never sees images or view_image; a model of unknown
* vision that answers 400 to an image is retried with notes, and stays
* that way so later requests extend the retried one. */
static void test_vision(const char *root) {
char dir[1100], sse[1200], rec[1200];
snprintf(dir, sizeof(dir), "%s/vision", root);
snprintf(sse, sizeof(sse), "%s/sse", dir);
snprintf(rec, sizeof(rec), "%s/rec", dir);
os_mkdir_p(sse, 0755);
write_file(sse, "0000.sse", TEXT("blind reply", "stop"));
write_file(sse, "0001.sse", "{\"error\":{\"message\":\"image input is not "
"supported\"}}");
write_file(sse, "0001.status", "400\n");
write_file(sse, "0002.sse", TEXT("noted reply", "stop"));
write_file(sse, "0003.sse", TEXT("later reply", "stop"));
setenv("CST_REPLAY_SSE", sse, 1);
setenv("CST_RECORD_SSE", rec, 1);
cst_llm_trace_reset();
unsigned char png[32];
size_t pn = tiny_png(png);
char err[256];
cst_agent_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", root);
snprintf(cfg.session_path, sizeof(cfg.session_path), "%s/blind.jsonl", dir);
snprintf(cfg.api_base, sizeof(cfg.api_base), "http://replay/v1");
snprintf(cfg.model, sizeof(cfg.model), "deepseek-flash");
snprintf(cfg.provider, sizeof(cfg.provider), "deepseek");
cfg.protocol = CST_PROTO_NATIVE_EOS;
cfg.max_steps = 4;
cfg.headless = 1;
cfg.approval = CST_APPROVAL_AUTO;
cst_agent_t *a = cst_agent_create(&cfg);
EXPECT(a != NULL, "blind agent");
if (!a)
return;
EXPECT(cst_agent_sees_images(a) == 0, "deepseek-flash is text only");
EXPECT(cst_agent_attach_bytes(a, "shot.png", (const char *)png, pn, err,
sizeof(err)) == 0,
"attach to a blind model");
EXPECT(run(a, "what is in the picture?") == 0, "blind turn");
cst_agent_destroy(a);
snprintf(cfg.session_path, sizeof(cfg.session_path), "%s/unknown.jsonl",
dir);
snprintf(cfg.model, sizeof(cfg.model), "mystery-model-9");
snprintf(cfg.provider, sizeof(cfg.provider), "teamorouter");
a = cst_agent_create(&cfg);
EXPECT(a != NULL, "unknown-vision agent");
if (!a)
return;
EXPECT(cst_agent_sees_images(a) != 0, "unknown vision: images are tried");
EXPECT(cst_agent_attach_bytes(a, "shot.png", (const char *)png, pn, err,
sizeof(err)) == 0,
"attach to an unknown model");
EXPECT(run(a, "describe it") == 0, "400 retried without images");
EXPECT(run(a, "and now?") == 0, "next turn");
cst_agent_destroy(a);
cJSON *r[4];
for (int i = 0; i < 4; i++) {
r[i] = load_req(rec, i);
EXPECT(r[i] != NULL, "vision request recorded");
}
if (r[0] && r[1] && r[2] && r[3]) {
EXPECT(!tools_have(r[0], "view_image"), "blind: no view_image tool");
EXPECT(!has_image_url(r[0]), "blind: no image part");
const char *c = str_of(last_msg(r[0]), "content");
EXPECT(c && strstr(c, "what is in the picture?") &&
strstr(c, "not sent: this model does not take images"),
"blind: the image becomes a note");
EXPECT(tools_have(r[1], "view_image") && has_image_url(r[1]),
"unknown: first try sends the image");
EXPECT(!tools_have(r[2], "view_image") && !has_image_url(r[2]),
"retry: no image, no view_image");
c = str_of(last_msg(r[2]), "content");
EXPECT(c && strstr(c, "not sent"), "retry: note instead");
EXPECT(!has_image_url(r[3]) && extends(r[2], r[3]),
"images stay off; the next request extends the retry");
}
for (int i = 0; i < 4; i++)
cJSON_Delete(r[i]);
}
int main(void) {
char root[1024], sse[1100], rec[1100];
if (test_tmpdir(root, sizeof(root), "cst-prefix") != 0) {
fprintf(stderr, "cannot create a scratch dir\n");
return 1;
}
snprintf(sse, sizeof(sse), "%s/sse", root);
snprintf(rec, sizeof(rec), "%s/rec", root);
os_mkdir_p(sse, 0755);
for (int i = 0; i < NFIX; i++) {
char name[32];
snprintf(name, sizeof(name), "%04d.sse", i);
write_file(sse, name, FIXTURES[i]);
}
write_file(root, "a.txt", "hello\n");
setenv("CST_REPLAY_SSE", sse, 1);
setenv("CST_RECORD_SSE", rec, 1);
setenv("CST_MCP_WAIT_MS", "0", 1);
setenv("XDG_CONFIG_HOME", root, 1); /* no real models.json or caches */
unsetenv("CST_CTX_WINDOW");
unsetenv("CST_NO_CACHE_HINTS");
unsetenv("CST_PRICE");
cst_llm_trace_reset();
cst_agent_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", root);
snprintf(cfg.session_path, sizeof(cfg.session_path), "%s/s.jsonl", root);
snprintf(cfg.api_base, sizeof(cfg.api_base), "http://replay/v1");
snprintf(cfg.model, sizeof(cfg.model), "deepseek-flash");
snprintf(cfg.provider, sizeof(cfg.provider), "deepseek");
cfg.protocol = CST_PROTO_NATIVE_EOS;
cfg.max_steps = 8;
cfg.headless = 1;
cfg.approval = CST_APPROVAL_AUTO;
cst_agent_t *a = cst_agent_create(&cfg);
EXPECT(a != NULL, "agent created");
if (!a)
return 1;
EXPECT(run(a, "read a.txt") == 0, "turn 1");
cst_agent_config_mut(a)->plan_mode = 1;
EXPECT(run(a, "now write b.txt") == 0, "turn 2 (plan)");
cst_agent_config_mut(a)->plan_mode = 0;
cst_agent_config_mut(a)->approval = CST_APPROVAL_YOLO;
EXPECT(run(a, "a long answer please") == 0, "turn 3 (continued)");
EXPECT(run(a, "bye") == 0, "turn 4");
char bpath[1100];
snprintf(bpath, sizeof(bpath), "%s/b.txt", root);
EXPECT(access(bpath, F_OK) != 0, "Plan mode refused write_file");
cJSON *req[NFIX];
for (int i = 0; i < NFIX; i++) {
req[i] = load_req(rec, i);
EXPECT(req[i] != NULL, "request recorded");
}
if (req[NFIX - 1]) {
char *tools0 = print(cJSON_GetObjectItemCaseSensitive(req[0], "tools"));
char *sys0 = print(cJSON_GetArrayItem(msgs_of(req[0]), 0));
EXPECT(tools0 && strstr(tools0, "write_file"),
"write tools stay in the schema");
for (int i = 0; i < NFIX; i++) {
char *tools = print(cJSON_GetObjectItemCaseSensitive(req[i], "tools"));
char *sys = print(cJSON_GetArrayItem(msgs_of(req[i]), 0));
EXPECT(tools && tools0 && strcmp(tools, tools0) == 0,
"tools[] identical in every request");
EXPECT(sys && sys0 && strcmp(sys, sys0) == 0,
"system prompt identical in every request");
const char *tc = str_of(req[i], "tool_choice");
EXPECT(tc && strcmp(tc, "auto") == 0, "tool_choice stays auto");
free(tools);
free(sys);
if (i + 1 < NFIX && !extends(req[i], req[i + 1])) {
fprintf(stderr, " request %d does not extend request %d\n", i + 1, i);
nfail++;
}
}
free(tools0);
free(sys0);
EXPECT(any_msg_has(req[1], "reasoning_content", "look at the file"),
"DeepSeek reasoning passed back");
EXPECT(strstr(str_of(last_msg(req[2]), "content"), "Plan mode is now ON"),
"Plan mode announced by a note");
EXPECT(any_msg_has(req[3], "content", "Plan mode is on"),
"write_file denied with a Plan mode error");
const char *t3 = str_of(last_msg(req[4]), "content");
EXPECT(t3 && strstr(t3, "Plan mode is now OFF") &&
strstr(t3, "Approval mode is now yolo"),
"Build mode and approval change announced");
const cJSON *pm = last_msg(req[5]);
EXPECT(is_prefix_msg(pm) && strcmp(str_of(pm, "role"), "assistant") == 0 &&
strcmp(str_of(pm, "content"), "part one") == 0,
"cut-off reply continued through an assistant prefix");
EXPECT(any_msg_has(req[6], "content", "part one part two"),
"continued reply stored as one message");
}
for (int i = 0; i < NFIX; i++)
cJSON_Delete(req[i]);
cst_usage_t s, turn;
cst_agent_usage(a, &s, &turn);
EXPECT(s.requests == NFIX, "every response counted");
EXPECT(s.prompt == 1000L * NFIX && s.cached == 900L * NFIX &&
s.miss == 100L * NFIX,
"hit and miss tokens summed");
EXPECT(s.priced && s.cost > 0 && strcmp(s.currency, "¥") == 0,
"DeepSeek cost estimated");
EXPECT(turn.requests == 1, "last turn usage");
cst_agent_destroy(a);
test_vision(root);
test_rmrf(root);
if (nfail) {
fprintf(stderr, "test_prefix_stable: %d failed\n", nfail);
return 1;
}
printf("test_prefix_stable: OK\n");
return 0;
}