diff options
| author | t <t@tjp.lol> | 2026-06-10 16:46:30 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-10 16:46:48 -0600 |
| commit | b435cf06b78853bec87bf238f526bc60679d7712 (patch) | |
| tree | 0c4191f24730f9a0336accb701d627450483c9cf /libpanto/src | |
| parent | cee08918509689adbdbd26b6384d5ef3a47e91a2 (diff) | |
fix anthropic thinking effort json placement
Diffstat (limited to 'libpanto/src')
| -rw-r--r-- | libpanto/src/anthropic_messages_json.zig | 12 | ||||
| -rw-r--r-- | libpanto/src/config.zig | 12 | ||||
| -rw-r--r-- | libpanto/src/provider_anthropic_messages.zig | 6 | ||||
| -rw-r--r-- | libpanto/src/provider_openai_chat.zig | 6 |
4 files changed, 28 insertions, 8 deletions
diff --git a/libpanto/src/anthropic_messages_json.zig b/libpanto/src/anthropic_messages_json.zig index 3ad6069..99d89eb 100644 --- a/libpanto/src/anthropic_messages_json.zig +++ b/libpanto/src/anthropic_messages_json.zig @@ -55,7 +55,7 @@ pub fn serializeRequest( // Extended thinking configuration. // `.disabled` — omit the field entirely. // `.enabled` — manual budget: { type, budget_tokens, display }. - // `.adaptive` — adaptive mode: { type, display } + top-level effort. + // `.adaptive` — adaptive mode: { type, display } + output_config.effort. switch (cfg.thinking) { .disabled => {}, .enabled => { @@ -82,8 +82,11 @@ pub fn serializeRequest( try s.objectField("display"); try s.write("summarized"); try s.endObject(); + try s.objectField("output_config"); + try s.beginObject(); try s.objectField("effort"); try s.write(@tagName(cfg.effort)); + try s.endObject(); }, } @@ -1285,7 +1288,9 @@ test "serializeRequest - thinking adaptive default effort" { 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); + try testing.expect(root.get("effort") == null); + const oc = root.get("output_config").?.object; + try testing.expectEqualStrings("medium", oc.get("effort").?.string); } test "serializeRequest - thinking adaptive explicit effort levels" { @@ -1312,7 +1317,8 @@ test "serializeRequest - thinking adaptive explicit effort levels" { 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); + const oc = parsed.value.object.get("output_config").?.object; + try testing.expectEqualStrings(entry.name, oc.get("effort").?.string); } } diff --git a/libpanto/src/config.zig b/libpanto/src/config.zig index 587f00a..4aae81f 100644 --- a/libpanto/src/config.zig +++ b/libpanto/src/config.zig @@ -46,7 +46,8 @@ pub const Thinking = enum { /// Do not request extended thinking. disabled, /// Manual extended thinking with an explicit token budget (`thinking_budget_tokens`). - /// Supported on all current models including Haiku 4.5. Default. + /// Supported on Haiku 4.5 and older models, but NOT on Opus 4.8+ (which + /// only accepts adaptive). Not safe as a cross-model default. enabled, /// Adaptive thinking: Claude decides when and how much to think. /// Requires Opus 4.6 / Sonnet 4.6 or newer; automatic on Opus 4.7+. @@ -82,7 +83,12 @@ pub const AnthropicMessagesConfig = struct { /// Extended-thinking mode. `.enabled` sends a manual thinking block with /// `thinking_budget_tokens`; `.adaptive` lets Claude decide and uses /// `effort` instead. `.disabled` omits thinking entirely. - thinking: Thinking = .enabled, + /// + /// Defaults to `.disabled`: it is the only mode accepted by every current + /// model. `.enabled` fails on Opus 4.8+ (adaptive-only) and `.adaptive` + /// fails on Haiku 4.5 (no adaptive support), so neither is a safe default + /// without parsing model strings. + thinking: Thinking = .disabled, /// Effort level for adaptive thinking. Only emitted on the wire when /// `thinking == .adaptive`; ignored otherwise. effort: Effort = .medium, @@ -274,7 +280,7 @@ test "ProviderConfig - anthropic_messages variant" { try t.expectEqual(APIStyle.anthropic_messages, cfg.style()); try t.expectEqualStrings("2023-06-01", cfg.anthropic_messages.api_version); try t.expectEqual(@as(u32, 64_000), cfg.anthropic_messages.max_tokens); - try t.expectEqual(Thinking.enabled, cfg.anthropic_messages.thinking); + try t.expectEqual(Thinking.disabled, cfg.anthropic_messages.thinking); try t.expectEqual(Effort.medium, cfg.anthropic_messages.effort); try t.expectEqual(@as(?u32, 32_000), cfg.anthropic_messages.thinking_budget_tokens); try t.expectEqual(false, cfg.anthropic_messages.thinking_interleaved); diff --git a/libpanto/src/provider_anthropic_messages.zig b/libpanto/src/provider_anthropic_messages.zig index 37ce818..9740c0e 100644 --- a/libpanto/src/provider_anthropic_messages.zig +++ b/libpanto/src/provider_anthropic_messages.zig @@ -124,6 +124,10 @@ pub const AnthropicMessagesRequest = struct { rr.response = try rr.req.receiveHead(&redirect_buf); if (@intFromEnum(rr.response.head.status) >= 400) { + // `head.bytes` (which `iterateHeaders` walks) points into the + // connection read buffer and is invalidated the moment the body + // stream is initialized below. Capture Retry-After first. + const retry_after_ms = provider_mod.retryAfterFromHead(rr.response.head); const body_reader = rr.response.reader(&rr.transfer_buf); var err_buf: std.ArrayList(u8) = .empty; defer err_buf.deinit(self.allocator); @@ -143,7 +147,7 @@ pub const AnthropicMessagesRequest = struct { const classified = provider_mod.classifyHttpStatus(status, err_buf.items); if (self.diag) |d| { d.status_code = status; - d.retry_after_ms = provider_mod.retryAfterFromHead(rr.response.head); + d.retry_after_ms = retry_after_ms; } return classified; } diff --git a/libpanto/src/provider_openai_chat.zig b/libpanto/src/provider_openai_chat.zig index 8319d9d..5152d3e 100644 --- a/libpanto/src/provider_openai_chat.zig +++ b/libpanto/src/provider_openai_chat.zig @@ -140,6 +140,10 @@ pub const OpenAIChatRequest = struct { rr.response = try rr.req.receiveHead(&redirect_buf); if (@intFromEnum(rr.response.head.status) >= 400) { + // `head.bytes` (which `iterateHeaders` walks) points into the + // connection read buffer and is invalidated the moment the body + // stream is initialized below. Capture Retry-After first. + const retry_after_ms = provider_mod.retryAfterFromHead(rr.response.head); // Drain body for diagnostics. const body_reader = rr.response.reader(&rr.transfer_buf); var err_buf: std.ArrayList(u8) = .empty; @@ -159,7 +163,7 @@ pub const OpenAIChatRequest = struct { const classified = provider_mod.classifyHttpStatus(status, err_buf.items); if (self.diag) |d| { d.status_code = status; - d.retry_after_ms = provider_mod.retryAfterFromHead(rr.response.head); + d.retry_after_ms = retry_after_ms; } return classified; } |
