Commit Diff


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);
 }