diff options
Diffstat (limited to 'libpanto/src/anthropic_messages_json.zig')
| -rw-r--r-- | libpanto/src/anthropic_messages_json.zig | 79 |
1 files changed, 76 insertions, 3 deletions
diff --git a/libpanto/src/anthropic_messages_json.zig b/libpanto/src/anthropic_messages_json.zig index 1b1cf6e..5f119f3 100644 --- a/libpanto/src/anthropic_messages_json.zig +++ b/libpanto/src/anthropic_messages_json.zig @@ -262,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, cfg); + try writeBlock(s, block, mark, cfg, msg.identity); } try s.endArray(); @@ -284,6 +284,7 @@ fn writeBlock( block: conversation.ContentBlock, mark_cache: bool, cfg: *const config_mod.AnthropicMessagesConfig, + msg_identity: ?config_mod.WireIdentity, ) !void { switch (block) { .Text => |tb| { @@ -304,8 +305,7 @@ fn writeBlock( // 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; + if (!conversation.thinkingSignatureMatches(tb, msg_identity, .anthropic_messages, cfg.base_url, cfg.model)) return; try s.beginObject(); try s.objectField("type"); try s.write("thinking"); @@ -819,6 +819,79 @@ test "serializeRequest - cache breakpoint skips a trailing thinking block" { try testing.expect(content[1].object.get("cache_control") == null); } +test "serializeRequest - thinking signature replays from message identity when block origin is absent" { + // A reloaded thinking block need not carry its own signature_origin: the + // enclosing message's identity is the per-message source of truth, so a + // host can rely on it alone (no per-block identity copy required). + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try conv.addAssistantMessage(&.{ + .{ + .Thinking = .{ + .text = try conversation.textualBlockFromSlice(allocator, "reasoned"), + .signature = try allocator.dupe(u8, "EqQBCgIYAhIM1gbcDa9GJwZA"), + // No signature_origin on the block. + }, + }, + .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer") }, + }, null); + // Per-message identity matching the request. + conv.messages.items[0].identity = try conversation.dupeWireIdentity(allocator, .{ + .api_style = .anthropic_messages, + .base_url = "u", + .model = "claude-x", + }); + + 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.expectEqualStrings("thinking", content[0].object.get("type").?.string); + try testing.expectEqualStrings("EqQBCgIYAhIM1gbcDa9GJwZA", content[0].object.get("signature").?.string); +} + +test "serializeRequest - thinking dropped when message identity targets a different model" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try conv.addAssistantMessage(&.{ + .{ .Thinking = .{ + .text = try conversation.textualBlockFromSlice(allocator, "reasoned"), + .signature = try allocator.dupe(u8, "EqQBCgIYAhIM1gbcDa9GJwZA"), + } }, + .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer") }, + }, null); + // Identity points at a *different* model than the request. + conv.messages.items[0].identity = try conversation.dupeWireIdentity(allocator, .{ + .api_style = .anthropic_messages, + .base_url = "u", + .model = "some-other-model", + }); + + 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; + // Only the text block survives; the unportable thinking block is dropped. + try testing.expectEqual(@as(usize, 1), content.len); + try testing.expectEqualStrings("text", content[0].object.get("type").?.string); +} + test "serializeRequest - prompt_cache disabled omits all cache_control" { const allocator = testing.allocator; |
