v0.7.5: arrow keys, image request fix, deepseek-flash vision, musl CI
ci / linux (clang) (push) Successful in 4m42s
ci / linux (gcc) (push) Successful in 5m35s
ci / musl (push) Successful in 5m52s
ci / windows-cross (push) Successful in 12m51s

TUI keys: register both CSI and SS3 forms of the cursor/Home/End keys and
the xterm modifier sequences when terminfo lacks them, so an arrow sent in
the "other" form no longer turns into Esc + a typed letter. Up/Down move by
visual (soft-wrapped) row, and the draft survives a trip through history.
scripts/tui_keys drives the TUI in a pty to check all of this.

Images: text inside a multimodal content array is now a {"type":"text"}
part; DeepSeek rejected the bare string with HTTP 422. deepseek-flash
(and its deepseek-chat / deepseek-reasoner aliases) read images, so the
registry marks them vision; deepseek-v4-pro stays text only.

CI: models.c lost _DEFAULT_SOURCE in 0.7.0 and gmtime_r broke the musl
build; tests sleep with poll() instead of usleep for the same reason.
This commit is contained in:
WHC
2026-10-08 22:35:34 +08:00
parent 32b4afd772
commit c7b32c3fd6
13 changed files with 646 additions and 49 deletions
+2 -1
View File
@@ -19,6 +19,7 @@
#include <io.h>
#include <windows.h>
#else
#include <poll.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
@@ -177,7 +178,7 @@ static CST_TEST_UNUSED void test_sleep_ms(int ms) {
#ifdef _WIN32
Sleep((DWORD)ms);
#else
usleep((useconds_t)ms * 1000);
poll(NULL, 0, ms); /* usleep is hidden by -std=c11 on musl */
#endif
}
+82
View File
@@ -1,5 +1,7 @@
#include "cJSON.h"
#include "cst_agent.h"
#include "cst_attach.h"
#include "cst_test_util.h"
#include <stdio.h>
#include <stdlib.h>
@@ -325,8 +327,88 @@ static void test_images_off(void) {
cJSON_Delete(j);
}
/* Every element of every content array must be a typed part object. */
static int parts_typed(const cJSON *root) {
const cJSON *m;
cJSON_ArrayForEach(m, cJSON_GetObjectItemCaseSensitive(root, "messages")) {
const cJSON *c = cJSON_GetObjectItemCaseSensitive(m, "content");
if (!cJSON_IsArray(c))
continue;
const cJSON *p;
cJSON_ArrayForEach(p, c) {
const cJSON *t = cJSON_GetObjectItemCaseSensitive(p, "type");
if (!cJSON_IsObject(p) || !cJSON_IsString(t))
return 0;
}
}
return 1;
}
static void test_images_on(void) {
char dir[1024], sess[1100], err[256];
if (test_tmpdir(dir, sizeof(dir), "cst-body") != 0) {
EXPECT(0, "scratch dir");
return;
}
snprintf(sess, sizeof(sess), "%s/s.jsonl", dir);
static const unsigned char png[32] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A,
'\n', 0, 0, 0, 13, 'I', 'H', 'D', 'R',
0, 0, 0, 2, 0, 0, 0, 2};
cst_attachment_t *img = cst_attach_import_bytes(sess, "shot.png",
(const char *)png,
sizeof(png), err, sizeof(err));
EXPECT(img != NULL, "image stored");
if (!img)
return;
cst_attachment_t gone = *img;
snprintf(gone.rel, sizeof(gone.rel), "missing.png");
cst_message_t m[5];
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?");
m[4] = msg(CST_ROLE_USER, "");
m[4].attach = &gone;
m[4].nattach = 1;
cst_llm_request_t r;
memset(&r, 0, sizeof(r));
r.model = "deepseek-flash";
r.msgs = m;
r.nmsgs = 5;
r.session_path = sess;
cJSON *j = build_req(&r);
EXPECT(parts_typed(j), "content arrays hold only typed parts");
const cJSON *p0 = cJSON_GetObjectItemCaseSensitive(
cJSON_GetArrayItem(cJSON_GetObjectItemCaseSensitive(j, "messages"), 0),
"content");
const cJSON *t0 = cJSON_GetObjectItemCaseSensitive(
cJSON_GetArrayItem(p0, 0), "text");
const cJSON *u1 = cJSON_GetObjectItemCaseSensitive(
cJSON_GetObjectItemCaseSensitive(cJSON_GetArrayItem(p0, 1), "image_url"),
"url");
EXPECT(cJSON_IsString(t0) && strcmp(t0->valuestring, "what is this") == 0,
"user text is a text part");
EXPECT(cJSON_IsString(u1) &&
strncmp(u1->valuestring, "data:image/png;base64,", 22) == 0,
"image is a data URL part");
char *s = cJSON_PrintUnformatted(j);
EXPECT(s && strstr(s, "and now?") && strstr(s, "not sent"),
"follow-up text and the missing-image note are parts too");
free(s);
cJSON_Delete(j);
cst_attach_free(img);
}
int main(void) {
test_images_off();
test_images_on();
test_reasoning_and_prefix();
test_cache_hints();
test_tools_snapshot();
+5 -1
View File
@@ -44,6 +44,10 @@ static void test_builtin(void) {
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("deepseek", "deepseek-flash", &mi);
EXPECT(mi.context == 1000000 && mi.vision == 1, "deepseek-flash takes images");
cst_model_info("deepseek", "deepseek-chat", &mi);
EXPECT(mi.vision == 1, "deepseek-chat is deepseek-flash");
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");
@@ -152,7 +156,7 @@ static void test_cache(void) {
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,
EXPECT(mi.price.miss == 2.0 && strcmp(mi.price.currency, "¥") == 0,
"another provider's cache is not used");
EXPECT(cst_model_context_tokens("anthropic/claude-sonnet-4") == 1000000,
"no provider: any cache, exact id");
+2 -2
View File
@@ -210,7 +210,7 @@ static void test_vision(const char *root) {
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.model, sizeof(cfg.model), "deepseek-v4-pro");
snprintf(cfg.provider, sizeof(cfg.provider), "deepseek");
cfg.protocol = CST_PROTO_NATIVE_EOS;
cfg.max_steps = 4;
@@ -220,7 +220,7 @@ static void test_vision(const char *root) {
EXPECT(a != NULL, "blind agent");
if (!a)
return;
EXPECT(cst_agent_sees_images(a) == 0, "deepseek-flash is text only");
EXPECT(cst_agent_sees_images(a) == 0, "deepseek-v4-pro is text only");
EXPECT(cst_agent_attach_bytes(a, "shot.png", (const char *)png, pn, err,
sizeof(err)) == 0,
"attach to a blind model");