summaryrefslogtreecommitdiff
path: root/libpanto/src/anthropic_messages_json.zig
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto/src/anthropic_messages_json.zig')
-rw-r--r--libpanto/src/anthropic_messages_json.zig192
1 files changed, 192 insertions, 0 deletions
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;