diff options
| author | t <t@tjp.lol> | 2026-06-09 11:56:15 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-09 11:56:35 -0600 |
| commit | a7fe265365ba5e8bfbd0e65c827d38c038be7b0f (patch) | |
| tree | 6c7bc4a5ef2f1ba6167b498aa03d17c5b7afb609 /libpanto/src/config.zig | |
| parent | 1cef19b12d59147ec32e4c0cd6a4828384b16417 (diff) | |
anthropic thinking
Diffstat (limited to 'libpanto/src/config.zig')
| -rw-r--r-- | libpanto/src/config.zig | 84 |
1 files changed, 81 insertions, 3 deletions
diff --git a/libpanto/src/config.zig b/libpanto/src/config.zig index 5e01bb1..587f00a 100644 --- a/libpanto/src/config.zig +++ b/libpanto/src/config.zig @@ -41,6 +41,28 @@ pub const ReasoningEffort = enum { high, }; +/// Anthropic extended-thinking mode. +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. + 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+. + adaptive, +}; + +/// Effort level sent with Anthropic adaptive thinking (`thinking = .adaptive`). +/// Ignored when `thinking` is `.enabled` or `.disabled`. +pub const Effort = enum { + low, + medium, + high, + xhigh, + max, +}; + pub const OpenAIChatConfig = struct { api_key: []const u8, base_url: []const u8, @@ -57,6 +79,22 @@ pub const AnthropicMessagesConfig = struct { api_version: []const u8 = "2023-06-01", /// Required by Anthropic's Messages API. max_tokens: u32 = 64_000, + /// 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, + /// Effort level for adaptive thinking. Only emitted on the wire when + /// `thinking == .adaptive`; ignored otherwise. + effort: Effort = .medium, + /// Maximum tokens Claude may spend on internal reasoning when + /// `thinking == .enabled`. `null` falls back to `max_tokens - 1`. + /// Ignored when `thinking == .adaptive` or `.disabled`. + thinking_budget_tokens: ?u32 = 32_000, + /// When true and `thinking == .enabled`, sends the + /// `interleaved-thinking-2025-05-14` beta header so Claude can think + /// between tool calls. Ignored when `thinking == .adaptive` (interleaving + /// is automatic there) or `.disabled`. + thinking_interleaved: bool = false, }; /// Per-provider transport/auth/model configuration. Tagged by `APIStyle`. @@ -70,8 +108,8 @@ pub const ProviderConfig = union(APIStyle) { /// The wire-format provider identity for this config: the ground-truth /// `{api_style, base_url, model, reasoning}` that a turn is sent with. - /// Anthropic has no reasoning-effort knob, so it reports `.default`. - /// Borrowed slices; valid as long as the config is. + /// Anthropic carries thinking/effort/budget/interleaved instead of + /// reasoning. Borrowed slices; valid as long as the config is. pub fn wireIdentity(self: ProviderConfig) WireIdentity { return switch (self) { .openai_chat => |c| .{ @@ -84,7 +122,10 @@ pub const ProviderConfig = union(APIStyle) { .api_style = .anthropic_messages, .base_url = c.base_url, .model = c.model, - .reasoning = .default, + .thinking = c.thinking, + .effort = c.effort, + .thinking_budget_tokens = c.thinking_budget_tokens, + .thinking_interleaved = c.thinking_interleaved, }, }; } @@ -93,11 +134,24 @@ pub const ProviderConfig = union(APIStyle) { /// Wire-format provider identity (see `ProviderConfig.wireIdentity`). This /// is the same shape as `session_store.WireIdentity`; defined here to avoid /// a module cycle (config must not import session_store). +/// +/// OpenAI uses `reasoning`; Anthropic uses `thinking`/`effort`/ +/// `thinking_budget_tokens`/`thinking_interleaved`. Fields unused by the +/// active provider carry their zero/default values. pub const WireIdentity = struct { api_style: APIStyle, base_url: []const u8, model: []const u8, + /// OpenAI only. reasoning: ReasoningEffort = .default, + /// Anthropic only. + thinking: Thinking = .enabled, + /// Anthropic only; only meaningful when `thinking == .adaptive`. + effort: Effort = .medium, + /// Anthropic only; only meaningful when `thinking == .enabled`. + thinking_budget_tokens: ?u32 = 32_000, + /// Anthropic only; only meaningful when `thinking == .enabled`. + thinking_interleaved: bool = false, }; /// Compaction settings the agent consults when summarizing old history. @@ -220,6 +274,30 @@ 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(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); +} + +test "ProviderConfig - anthropic_messages wireIdentity carries thinking fields" { + const cfg: ProviderConfig = .{ .anthropic_messages = .{ + .api_key = "k", + .base_url = "https://api.anthropic.com", + .model = "claude-opus-4-8", + .thinking = .adaptive, + .effort = .high, + .thinking_budget_tokens = null, + .thinking_interleaved = false, + } }; + const id = cfg.wireIdentity(); + try t.expectEqual(APIStyle.anthropic_messages, id.api_style); + try t.expectEqual(Thinking.adaptive, id.thinking); + try t.expectEqual(Effort.high, id.effort); + try t.expectEqual(@as(?u32, null), id.thinking_budget_tokens); + try t.expectEqual(false, id.thinking_interleaved); + // reasoning field carries its zero default; not meaningful for Anthropic + try t.expectEqual(ReasoningEffort.default, id.reasoning); } test "ProviderConfig - openai_chat reasoning defaults to .default" { |
