diff options
Diffstat (limited to 'libpanto/src/anthropic_messages_json.zig')
| -rw-r--r-- | libpanto/src/anthropic_messages_json.zig | 54 |
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 |
