From a7fe265365ba5e8bfbd0e65c827d38c038be7b0f Mon Sep 17 00:00:00 2001 From: t Date: Tue, 9 Jun 2026 11:56:15 -0600 Subject: anthropic thinking --- libpanto/src/anthropic_messages_json.zig | 192 +++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) (limited to 'libpanto/src/anthropic_messages_json.zig') diff --git a/libpanto/src/anthropic_messages_json.zig b/libpanto/src/anthropic_messages_json.zig index bccf692..78f4b5f 100644 --- a/libpanto/src/anthropic_messages_json.zig +++ b/libpanto/src/anthropic_messages_json.zig @@ -52,6 +52,41 @@ pub fn serializeRequest( try s.objectField("stream"); try s.write(true); + // Extended thinking configuration. + // `.disabled` — omit the field entirely. + // `.enabled` — manual budget: { type, budget_tokens, display }. + // `.adaptive` — adaptive mode: { type, display } + top-level effort. + switch (cfg.thinking) { + .disabled => {}, + .enabled => { + // Resolve budget: explicit value or fall back to max_tokens - 1. + // Clamp so budget is always strictly less than max_tokens (required + // by Anthropic when not using interleaved thinking). + const raw_budget: u32 = cfg.thinking_budget_tokens orelse (cfg.max_tokens -| 1); + const budget: u32 = if (raw_budget >= cfg.max_tokens) cfg.max_tokens -| 1 else raw_budget; + try s.objectField("thinking"); + try s.beginObject(); + try s.objectField("type"); + try s.write("enabled"); + try s.objectField("budget_tokens"); + try s.write(budget); + try s.objectField("display"); + try s.write("summarized"); + try s.endObject(); + }, + .adaptive => { + try s.objectField("thinking"); + try s.beginObject(); + try s.objectField("type"); + try s.write("adaptive"); + try s.objectField("display"); + try s.write("summarized"); + try s.endObject(); + try s.objectField("effort"); + try s.write(@tagName(cfg.effort)); + }, + } + // Build and emit the concatenated system prompt, if any. var system_buf: std.ArrayList(u8) = .empty; defer system_buf.deinit(allocator); @@ -1134,6 +1169,163 @@ test "serializeRequest - tool result with image part emits image block" { try testing.expectEqualStrings("iVBOR==", src.get("data").?.string); } +test "serializeRequest - thinking disabled omits thinking and effort fields" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try conv.addUserMessage("hi"); + + var cfg = testConfig("claude-x"); + cfg.thinking = .disabled; + 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 root = parsed.value.object; + try testing.expect(root.get("thinking") == null); + try testing.expect(root.get("effort") == null); +} + +test "serializeRequest - thinking enabled explicit budget" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try conv.addUserMessage("hi"); + + var cfg = testConfig("claude-x"); // max_tokens = 1024 + cfg.thinking = .enabled; + cfg.thinking_budget_tokens = 500; // within max_tokens + 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 root = parsed.value.object; + const th = root.get("thinking").?.object; + try testing.expectEqualStrings("enabled", th.get("type").?.string); + try testing.expectEqual(@as(i64, 500), th.get("budget_tokens").?.integer); + try testing.expectEqualStrings("summarized", th.get("display").?.string); + try testing.expect(root.get("effort") == null); +} + +test "serializeRequest - thinking enabled null budget falls back to max_tokens - 1" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try conv.addUserMessage("hi"); + + var cfg = testConfig("claude-x"); // max_tokens = 1024 + cfg.thinking = .enabled; + cfg.thinking_budget_tokens = null; + 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 th = parsed.value.object.get("thinking").?.object; + try testing.expectEqual(@as(i64, 1023), th.get("budget_tokens").?.integer); +} + +test "serializeRequest - thinking enabled budget clamped when >= max_tokens" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try conv.addUserMessage("hi"); + + var cfg = testConfig("claude-x"); // max_tokens = 1024 + cfg.thinking = .enabled; + cfg.thinking_budget_tokens = 2_000; // > max_tokens + 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 th = parsed.value.object.get("thinking").?.object; + // Clamped to max_tokens - 1 = 1023 + try testing.expectEqual(@as(i64, 1023), th.get("budget_tokens").?.integer); +} + +test "serializeRequest - thinking adaptive default effort" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try conv.addUserMessage("hi"); + + var cfg = testConfig("claude-x"); + cfg.thinking = .adaptive; + // effort defaults to .medium + 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 root = parsed.value.object; + const th = root.get("thinking").?.object; + try testing.expectEqualStrings("adaptive", th.get("type").?.string); + try testing.expectEqualStrings("summarized", th.get("display").?.string); + try testing.expect(th.get("budget_tokens") == null); + try testing.expectEqualStrings("medium", root.get("effort").?.string); +} + +test "serializeRequest - thinking adaptive explicit effort levels" { + const allocator = testing.allocator; + const efforts = [_]struct { e: config_mod.Effort, name: []const u8 }{ + .{ .e = .low, .name = "low" }, + .{ .e = .medium, .name = "medium" }, + .{ .e = .high, .name = "high" }, + .{ .e = .xhigh, .name = "xhigh" }, + .{ .e = .max, .name = "max" }, + }; + for (efforts) |entry| { + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try conv.addUserMessage("hi"); + + var cfg = testConfig("claude-x"); + cfg.thinking = .adaptive; + cfg.effort = entry.e; + 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(); + try testing.expectEqualStrings(entry.name, parsed.value.object.get("effort").?.string); + } +} + +test "serializeRequest - thinking adaptive ignores budget_tokens" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try conv.addUserMessage("hi"); + + var cfg = testConfig("claude-x"); + cfg.thinking = .adaptive; + cfg.thinking_budget_tokens = 9_999; // should be ignored + 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 th = parsed.value.object.get("thinking").?.object; + try testing.expect(th.get("budget_tokens") == null); +} + test "serializeRequest - empty ToolUse input becomes {} not invalid JSON" { const allocator = testing.allocator; -- cgit v1.3