summaryrefslogtreecommitdiff
path: root/libpanto/src/anthropic_messages_json.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-13 23:45:59 -0600
committert <t@tjp.lol>2026-06-15 15:08:32 -0600
commit02b4c7a35ac0b714bc045d54fb4bb45d1ce4e490 (patch)
tree134196a82dd6fdd55b735be4c0cf38428c85038d /libpanto/src/anthropic_messages_json.zig
parent71643a5d69ffc40882c9fcde3cc8a3bcf02d7396 (diff)
Add Codex Responses support and session debugging
Teach provider config and auth resolution about the Codex Responses dialect, including user-facing `style = "openai_responses"` with `dialect = "codex"`. Serialize and parse Responses traffic with provider-specific reasoning replay, assistant phase metadata, and robust function-call assembly keyed by `output_index` so streamed tool inputs survive proxy quirks and empty terminal payloads. Also persist thinking origins and message metadata across sessions, add the Anthropic interleaved-thinking header switch, write per-session debug logs, and improve the TUI and scripts for inspecting tool output and session costs.
Diffstat (limited to 'libpanto/src/anthropic_messages_json.zig')
-rw-r--r--libpanto/src/anthropic_messages_json.zig54
1 files changed, 49 insertions, 5 deletions
diff --git a/libpanto/src/anthropic_messages_json.zig b/libpanto/src/anthropic_messages_json.zig
index f7df80b..1b1cf6e 100644
--- a/libpanto/src/anthropic_messages_json.zig
+++ b/libpanto/src/anthropic_messages_json.zig
@@ -138,7 +138,7 @@ pub fn serializeRequest(
for (window, 0..) |msg, mi| {
if (msg.role == .system) continue;
const mark: ?usize = if (cache_at) |c| (if (c.message == mi) c.block else null) else null;
- try writeMessage(&s, msg, mark);
+ try writeMessage(&s, msg, mark, cfg);
}
try s.endArray();
@@ -251,6 +251,7 @@ fn writeMessage(
s: *std.json.Stringify,
msg: conversation.Message,
cache_block: ?usize,
+ cfg: *const config_mod.AnthropicMessagesConfig,
) !void {
try s.beginObject();
@@ -261,7 +262,7 @@ fn writeMessage(
try s.beginArray();
for (msg.content.items, 0..) |block, bi| {
const mark = if (cache_block) |cb| cb == bi else false;
- try writeBlock(s, block, mark);
+ try writeBlock(s, block, mark, cfg);
}
try s.endArray();
@@ -278,7 +279,12 @@ fn writeCacheControl(s: *std.json.Stringify) !void {
try s.endObject();
}
-fn writeBlock(s: *std.json.Stringify, block: conversation.ContentBlock, mark_cache: bool) !void {
+fn writeBlock(
+ s: *std.json.Stringify,
+ block: conversation.ContentBlock,
+ mark_cache: bool,
+ cfg: *const config_mod.AnthropicMessagesConfig,
+) !void {
switch (block) {
.Text => |tb| {
try s.beginObject();
@@ -292,9 +298,14 @@ fn writeBlock(s: *std.json.Stringify, block: conversation.ContentBlock, mark_cac
.Thinking => |tb| {
// Anthropic requires the signature field to round-trip a thinking
// block. If we don't have one (e.g. block was synthesized from a
- // non-Anthropic provider), skip the block: Anthropic will reject
- // unsigned thinking on incoming messages.
+ // non-Anthropic provider), or if the signature came from a
+ // different provider/model than the current request, skip the
+ // block: Anthropic will reject unsigned thinking on incoming
+ // messages, and opaque signatures are not portable across
+ // provider/model boundaries.
const sig = tb.signature orelse return;
+ const origin = tb.signature_origin orelse return;
+ if (!origin.matches(.anthropic_messages, cfg.base_url, cfg.model)) return;
try s.beginObject();
try s.objectField("type");
try s.write("thinking");
@@ -789,6 +800,7 @@ test "serializeRequest - cache breakpoint skips a trailing thinking block" {
.signature = sig,
} },
}, null);
+ try conversation.setThinkingOrigins(allocator, conv.messages.items[0].content.items, .anthropic_messages, "u", "claude-x");
var cfg = testConfig("claude-x");
var empty_tools = tool_registry_mod.ToolRegistry.init(allocator);
@@ -935,6 +947,7 @@ test "serializeRequest - signed assistant Thinking blocks round-trip" {
} },
.{ .Text = try conversation.textualBlockFromSlice(allocator, "the answer is 42") },
}, null);
+ try conversation.setThinkingOrigins(allocator, conv.messages.items[0].content.items, .anthropic_messages, "u", "claude-x");
const cfg = testConfig("claude-x");
var empty_tools = tool_registry_mod.ToolRegistry.init(allocator);
@@ -962,6 +975,37 @@ test "serializeRequest - signed assistant Thinking blocks round-trip" {
try testing.expectEqualStrings("the answer is 42", content[1].object.get("text").?.string);
}
+test "serializeRequest - mismatched thinking origin drops the block" {
+ const allocator = testing.allocator;
+
+ var conv = conversation.Conversation.init(allocator);
+ defer conv.deinit();
+
+ const sig = try allocator.dupe(u8, "EqQBCgIYAhIM1gbcDa9GJwZA");
+ try conv.addAssistantMessage(&.{
+ .{ .Thinking = .{
+ .text = try conversation.textualBlockFromSlice(allocator, "other provider thinking"),
+ .signature = sig,
+ } },
+ .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer") },
+ }, null);
+ try conversation.setThinkingOrigins(allocator, conv.messages.items[0].content.items, .openai_responses, "https://api.individual.githubcopilot.com", "gpt-5.4-mini");
+
+ const cfg = testConfig("claude-x");
+ var empty_tools = tool_registry_mod.ToolRegistry.init(allocator);
+ defer empty_tools.deinit();
+ const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools);
+ defer allocator.free(body);
+
+ var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{});
+ defer parsed.deinit();
+
+ const content = parsed.value.object.get("messages").?.array.items[0]
+ .object.get("content").?.array.items;
+ try testing.expectEqual(@as(usize, 1), content.len);
+ try testing.expectEqualStrings("text", content[0].object.get("type").?.string);
+}
+
test "serializeRequest - unsigned Thinking blocks are dropped" {
// Anthropic rejects thinking blocks without a valid signature on inbound
// messages. If a block lacks one (e.g. from a different provider or an