commit c5f3a1cbb149227b342688a63cb3375c4be6c547 from: Isaac Meerleo date: Wed Jul 15 16:37:10 2026 UTC support provider-only request suffixes commit - c07b6527d8b68986949d7889496b04f6756cbba1 commit + c5f3a1cbb149227b342688a63cb3375c4be6c547 blob - f749f8833bcf6f021552da601c88d996ac62cf97 blob + 4c5a83cb6030162b0b0e91b0acf055b0acbfa2e6 --- regress/anthropic/anthropic_test.c +++ regress/anthropic/anthropic_test.c @@ -200,6 +200,72 @@ test_build_full(void) } static void +test_build_suffix(void) +{ + struct msglist conv; + struct msg *base, *suffix; + struct anthropic_req r; + struct buf out; + struct json j; + char *s; + size_t sl; + int messages, message, content, block; + + TAILQ_INIT(&conv); + base = msg_new(ROLE_USER); + msg_add_text(base, "base Projection", 15); + TAILQ_INSERT_TAIL(&conv, base, entry); + suffix = msg_new(ROLE_USER); + msg_add_text(suffix, "Write continuation notes to yourself.", 37); + + memset(&r, 0, sizeof(r)); + r.model = "m"; + r.max_tokens = 10; + r.conv = &conv; + r.suffix = suffix; + r.cache = 1; + r.cache_tail_msg = 0; + + buf_init(&out); + anthropic_build_request(&out, &r); + CHECK(json_parse(&j, out.data, out.len, 0) == 0); + messages = json_obj_get(&j, 0, "messages"); + CHECK(json_arr_len(&j, messages) == 2); + message = json_arr_get(&j, messages, 1); + s = json_get_str(&j, json_obj_get(&j, message, "role"), &sl); + CHECK(s != NULL && sl == 4 && memcmp(s, "user", 4) == 0); + free(s); + content = json_obj_get(&j, message, "content"); + CHECK(json_arr_len(&j, content) == 1); + block = json_arr_get(&j, content, 0); + s = json_get_str(&j, json_obj_get(&j, block, "text"), &sl); + CHECK(s != NULL && sl == 37 && + memcmp(s, "Write continuation notes to yourself.", 37) == 0); + free(s); + + /* The borrowed suffix is outside the base Projection cache index. */ + CHECK(count_cache(&out) == 1); + message = json_arr_get(&j, messages, 0); + content = json_obj_get(&j, message, "content"); + block = json_arr_get(&j, content, 0); + CHECK(json_obj_get(&j, block, "cache_control") != -1); + message = json_arr_get(&j, messages, 1); + content = json_obj_get(&j, message, "content"); + block = json_arr_get(&j, content, 0); + CHECK(json_obj_get(&j, block, "cache_control") == -1); + + /* The base Projection is unchanged and the borrowed suffix stays unlinked. */ + CHECK(TAILQ_FIRST(&conv) == base); + CHECK(TAILQ_LAST(&conv, msglist) == base); + CHECK(TAILQ_NEXT(base, entry) == NULL); + CHECK(TAILQ_NEXT(suffix, entry) == NULL); + json_done(&j); + buf_free(&out); + msg_free(suffix); + msglist_free(&conv); +} + +static void test_build_openai_none(void) { /* the openai builder emits no cache fields; asserted in that @@ -566,6 +632,7 @@ main(void) test_build_minimal(); test_build_full(); + test_build_suffix(); test_build_openai_none(); test_build_first_request(); test_stream_whole(); blob - f6f1ca66e9305300295bc088651d172eb3e22379 blob + 65b444907cd4cf2102e48e4976abf77c9d5f71e9 --- regress/openai/openai_test.c +++ regress/openai/openai_test.c @@ -161,6 +161,51 @@ test_build_tool_only(void) msglist_free(&conv); } +static void +test_build_suffix(void) +{ + struct msglist conv; + struct msg *base, *suffix; + struct openai_req req; + struct buf out; + struct json j; + int messages, message; + + TAILQ_INIT(&conv); + base = msg_new(ROLE_USER); + msg_add_text(base, "base Projection", 15); + TAILQ_INSERT_TAIL(&conv, base, entry); + suffix = msg_new(ROLE_USER); + msg_add_text(suffix, "Write continuation notes to yourself.", 37); + + memset(&req, 0, sizeof(req)); + req.model = "m"; + req.max_tokens = 10; + req.conv = &conv; + req.suffix = suffix; + + buf_init(&out); + openai_build_request(&out, &req); + CHECK(json_parse(&j, out.data, out.len, 0) == 0); + messages = json_obj_get(&j, 0, "messages"); + CHECK(json_arr_len(&j, messages) == 2); + message = json_arr_get(&j, messages, 1); + CHECK(str_is(&j, json_obj_get(&j, message, "role"), "user")); + CHECK(str_is(&j, json_obj_get(&j, message, "content"), + "Write continuation notes to yourself.")); + CHECK(!has(&out, "cache_control")); + + /* The codec borrows the suffix without changing the base Projection. */ + CHECK(TAILQ_FIRST(&conv) == base); + CHECK(TAILQ_LAST(&conv, msglist) == base); + CHECK(TAILQ_NEXT(base, entry) == NULL); + CHECK(TAILQ_NEXT(suffix, entry) == NULL); + json_done(&j); + buf_free(&out); + msg_free(suffix); + msglist_free(&conv); +} + struct call_capture { char id[64]; char name[64]; @@ -509,6 +554,7 @@ main(void) test_build_full(); test_build_tool_only(); + test_build_suffix(); test_stream_whole(); test_stream_bytewise(); test_stream_error(); blob - 7c775a25ed45a86ada7b15e566e20bb88970738e blob + e2298154da85b6f0253d974cee2c206bfb2fe2a3 --- src/common/anthropic.h +++ src/common/anthropic.h @@ -40,6 +40,9 @@ struct anthropic_req { const struct tool_def *tools; int ntools; const struct msglist *conv; + /* Optional borrowed message serialized after conv without linking it. + * cache_tail_msg indexes conv only; the suffix is never cache-marked. */ + const struct msg *suffix; int cache; /* emit cache_control */ /* * Index (0-based, into conv) of the newest message that was blob - 2f954b21ca2d56abbf5f7951d5b7a522182ccfaa blob + 47f9f35a570a4f5df2633c598e952a108c76eec7 --- src/common/anthropic_req.c +++ src/common/anthropic_req.c @@ -94,6 +94,23 @@ emit_block(struct json_out *jo, const struct block *b, } } +static void +emit_message(struct json_out *jo, const struct msg *m, int cache) +{ + struct block *b, *last; + + last = TAILQ_LAST(&m->blocks, blocklist); + json_obj_begin(jo); + json_key(jo, "role"); + json_cstr(jo, m->role == ROLE_USER ? "user" : "assistant"); + json_key(jo, "content"); + json_arr_begin(jo); + TAILQ_FOREACH(b, &m->blocks, entry) + emit_block(jo, b, cache && b == last); + json_arr_end(jo); + json_obj_end(jo); +} + void anthropic_build_request(struct buf *out, const struct anthropic_req *r) { @@ -150,25 +167,16 @@ anthropic_build_request(struct buf *out, const struct json_arr_begin(&jo); mi = 0; TAILQ_FOREACH(m, r->conv, entry) { - struct block *b, *last; int mark; /* the advancing tail breakpoint lands on this message's * last block (behavior.md section 8) */ mark = r->cache && mi == r->cache_tail_msg; - last = TAILQ_LAST(&m->blocks, blocklist); - - json_obj_begin(&jo); - json_key(&jo, "role"); - json_cstr(&jo, m->role == ROLE_USER ? "user" : "assistant"); - json_key(&jo, "content"); - json_arr_begin(&jo); - TAILQ_FOREACH(b, &m->blocks, entry) - emit_block(&jo, b, mark && b == last); - json_arr_end(&jo); - json_obj_end(&jo); + emit_message(&jo, m, mark); mi++; } + if (r->suffix != NULL) + emit_message(&jo, r->suffix, 0); json_arr_end(&jo); json_obj_end(&jo); blob - 4edb100bf069dda4e2490ed37a33893f62839270 blob + 668ad1f4bed9525859a8b9e5f17f69ceae7eb77c --- src/common/openai.h +++ src/common/openai.h @@ -34,6 +34,8 @@ struct openai_req { const struct tool_def *tools; int ntools; const struct msglist *conv; + /* Optional borrowed message serialized after conv without linking it. */ + const struct msg *suffix; }; void openai_build_request(struct buf *, const struct openai_req *); blob - 90060aaf7b0325096a2055d8f40560fab152a26a blob + e7271a034999b2a46d1c69ed74315833ce04260d --- src/common/openai_req.c +++ src/common/openai_req.c @@ -135,6 +135,15 @@ emit_assistant(struct json_out *jo, const struct msg * buf_free(&text); } +static void +emit_message(struct json_out *jo, const struct msg *m) +{ + if (m->role == ROLE_ASSISTANT) + emit_assistant(jo, m); + else + emit_user(jo, m); +} + void openai_build_request(struct buf *out, const struct openai_req *r) { @@ -183,12 +192,10 @@ openai_build_request(struct buf *out, const struct ope json_arr_begin(&jo); if (r->system != NULL) emit_text_message(&jo, "system", r->system, r->systemlen); - TAILQ_FOREACH(m, r->conv, entry) { - if (m->role == ROLE_ASSISTANT) - emit_assistant(&jo, m); - else - emit_user(&jo, m); - } + TAILQ_FOREACH(m, r->conv, entry) + emit_message(&jo, m); + if (r->suffix != NULL) + emit_message(&jo, r->suffix); json_arr_end(&jo); json_obj_end(&jo); }