diff options
Diffstat (limited to 'src/models_toml.zig')
| -rw-r--r-- | src/models_toml.zig | 176 |
1 files changed, 167 insertions, 9 deletions
diff --git a/src/models_toml.zig b/src/models_toml.zig index 2e2a920..9d64cb9 100644 --- a/src/models_toml.zig +++ b/src/models_toml.zig @@ -9,15 +9,27 @@ //! //! [<provider>.<alias>] //! model = <string> # wire model id; defaults to <alias> if omitted -//! reasoning = <string> # default | off | minimal | low | medium | high //! max_tokens = <int> # per-request output token cap -//! api_version = <string> # anthropic_messages only //! # pricing (all optional; USD per million tokens): //! input = <float> //! output = <float> //! cache_read = <float> //! cache_write = <float> //! +//! # openai_chat-only knobs: +//! reasoning = <string> # default | off | minimal | low | medium | high +//! +//! # anthropic_messages-only knobs: +//! api_version = <string> # Anthropic-Version header; default "2023-06-01" +//! thinking = <string> # disabled | enabled | adaptive (default: enabled) +//! effort = <string> # low | medium | high | xhigh | max (default: medium) +//! # only used when thinking = "adaptive" +//! thinking_budget_tokens = <int> # max reasoning tokens for thinking = "enabled" +//! # default: 32000; null falls back to max_tokens - 1 +//! # ignored when thinking = "adaptive" or "disabled" +//! thinking_interleaved = <bool> # send interleaved-thinking beta header (default: false) +//! # only honoured when thinking = "enabled" +//! //! Pricing semantics are unchanged from the original models.toml: any //! omitted price field is "unknown" (null), not zero. Write `= 0` //! explicitly to declare a known-zero rate. @@ -25,13 +37,19 @@ //! Example: //! //! [anthropic.sonnet] -//! model = "claude-sonnet-4-20250514" -//! reasoning = "high" -//! max_tokens = 8192 -//! input = 3.0 -//! output = 15.0 -//! cache_read = 0.3 -//! cache_write = 3.75 +//! model = "claude-sonnet-4-20250514" +//! max_tokens = 8192 +//! thinking = "enabled" +//! thinking_budget_tokens = 16000 +//! input = 3.0 +//! output = 15.0 +//! cache_read = 0.3 +//! cache_write = 3.75 +//! +//! [anthropic.opus] +//! model = "claude-opus-4-8" +//! thinking = "adaptive" +//! effort = "high" //! //! [openai.gpt] //! model = "gpt-4o" @@ -55,6 +73,9 @@ pub const ReasoningEffort = panto.ReasoningEffort; /// A resolved model definition: the wire model id plus per-model knobs. /// Strings are owned by the `ModelRegistry`. +pub const Thinking = panto.Thinking; +pub const Effort = panto.Effort; + pub const ModelDef = struct { /// `<provider_name>` — matches a `[providers.<name>]` in config.toml. provider: []const u8, @@ -68,6 +89,14 @@ pub const ModelDef = struct { max_tokens: ?u32, /// anthropic_messages only; null = use the library default. api_version: ?[]const u8, + /// anthropic_messages only. Extended-thinking mode. + thinking: Thinking, + /// anthropic_messages only. Effort level for adaptive thinking. + effort: Effort, + /// anthropic_messages only. Manual thinking token budget; null = max_tokens - 1. + thinking_budget_tokens: ?u32, + /// anthropic_messages only. Whether to send the interleaved-thinking beta header. + thinking_interleaved: bool, }; /// In-memory map of `(provider, alias) -> ModelDef`. Owns all strings. @@ -239,6 +268,40 @@ fn ingestModel( break :blk null; }; + const thinking: Thinking = blk: { + if (v.get("thinking")) |t| { + if (t.asString()) |s| { + break :blk std.meta.stringToEnum(Thinking, s) orelse .enabled; + } + } + break :blk .enabled; + }; + + const effort: Effort = blk: { + if (v.get("effort")) |e| { + if (e.asString()) |s| { + break :blk std.meta.stringToEnum(Effort, s) orelse .medium; + } + } + break :blk .medium; + }; + + const thinking_budget_tokens: ?u32 = blk: { + if (v.get("thinking_budget_tokens")) |bt| { + if (bt.asI64()) |i| { + if (i > 0) break :blk @intCast(i); + } + } + break :blk null; + }; + + const thinking_interleaved: bool = blk: { + if (v.get("thinking_interleaved")) |ti| { + if (ti.asBool()) |b| break :blk b; + } + break :blk false; + }; + // Own all the strings. const provider_copy = try alloc.dupe(u8, provider); errdefer alloc.free(provider_copy); @@ -256,6 +319,10 @@ fn ingestModel( .reasoning = reasoning, .max_tokens = max_tokens, .api_version = api_version_copy, + .thinking = thinking, + .effort = effort, + .thinking_budget_tokens = thinking_budget_tokens, + .thinking_interleaved = thinking_interleaved, }); // Pricing keys on the *wire* model id so usage records (which carry @@ -420,3 +487,94 @@ test "configPath: falls back to HOME/.config" { defer a.free(got); try testing.expectEqualStrings("/home/user/.config/panto/models.toml", got); } + +test "parseInto: anthropic thinking fields parse correctly" { + var models = emptyModels(testing.allocator); + defer models.deinit(); + + const src = + \\[anthropic.sonnet] + \\model = "claude-sonnet-4-6" + \\thinking = "enabled" + \\thinking_budget_tokens = 16000 + \\thinking_interleaved = true + ; + try parseInto(&models, src); + + const def = models.defs.get("anthropic", "sonnet").?; + try testing.expectEqual(Thinking.enabled, def.thinking); + try testing.expectEqual(Effort.medium, def.effort); // default + try testing.expectEqual(@as(?u32, 16000), def.thinking_budget_tokens); + try testing.expectEqual(true, def.thinking_interleaved); +} + +test "parseInto: anthropic adaptive thinking with effort" { + var models = emptyModels(testing.allocator); + defer models.deinit(); + + const src = + \\[anthropic.opus] + \\model = "claude-opus-4-8" + \\thinking = "adaptive" + \\effort = "high" + ; + try parseInto(&models, src); + + const def = models.defs.get("anthropic", "opus").?; + try testing.expectEqual(Thinking.adaptive, def.thinking); + try testing.expectEqual(Effort.high, def.effort); + try testing.expectEqual(@as(?u32, null), def.thinking_budget_tokens); // absent => null + try testing.expectEqual(false, def.thinking_interleaved); // default +} + +test "parseInto: anthropic thinking disabled" { + var models = emptyModels(testing.allocator); + defer models.deinit(); + + const src = + \\[anthropic.haiku] + \\model = "claude-haiku-4-5-20251001" + \\thinking = "disabled" + ; + try parseInto(&models, src); + + const def = models.defs.get("anthropic", "haiku").?; + try testing.expectEqual(Thinking.disabled, def.thinking); +} + +test "parseInto: anthropic thinking defaults when absent" { + var models = emptyModels(testing.allocator); + defer models.deinit(); + + const src = + \\[anthropic.base] + \\model = "claude-sonnet-4-20250514" + ; + try parseInto(&models, src); + + const def = models.defs.get("anthropic", "base").?; + try testing.expectEqual(Thinking.enabled, def.thinking); + try testing.expectEqual(Effort.medium, def.effort); + try testing.expectEqual(@as(?u32, null), def.thinking_budget_tokens); + try testing.expectEqual(false, def.thinking_interleaved); +} + +test "parseInto: effort xhigh and max parse" { + var models = emptyModels(testing.allocator); + defer models.deinit(); + + const src = + \\[anthropic.opus-xhigh] + \\model = "claude-opus-4-7" + \\thinking = "adaptive" + \\effort = "xhigh" + \\[anthropic.opus-max] + \\model = "claude-opus-4-6" + \\thinking = "adaptive" + \\effort = "max" + ; + try parseInto(&models, src); + + try testing.expectEqual(Effort.xhigh, models.defs.get("anthropic", "opus-xhigh").?.effort); + try testing.expectEqual(Effort.max, models.defs.get("anthropic", "opus-max").?.effort); +} |
