modelplease/config.rs
1// Copyright 2026 Thomas Santerre and Moderately AI Inc.
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Language model generation configuration.
6
7use enumset::EnumSetType;
8
9/// Reasoning effort hint for thinking models.
10///
11/// Six qualitative levels covering the union of every supported provider's
12/// effort vocabulary. Per-model `ReasoningCapability` (see
13/// [`crate::capabilities::ReasoningCapability`]) declares which subset a
14/// given model accepts; calls with an unsupported effort fail validation
15/// before reaching the wire.
16///
17/// `EnumSetType` auto-derives `Copy + Clone + PartialEq + Eq` plus the
18/// bitset bookkeeping the `enumset` crate needs to use the values inside
19/// an `EnumSet`. `Debug` and `Hash` are derived separately.
20#[derive(EnumSetType, Debug, Hash)]
21pub enum ReasoningEffort {
22 /// No reasoning tokens. Wire-meaningful on OpenAI (`reasoning_effort:
23 /// "none"`) and Ollama; on Anthropic the same semantics are expressed
24 /// by [`ReasoningConfig::Off`] (omit the `thinking` field entirely).
25 None,
26 /// Shallow reasoning — fast but less thorough.
27 Low,
28 /// Balanced reasoning — the common default.
29 Medium,
30 /// Deep reasoning — slowest and most thorough.
31 High,
32 /// Deeper than `High`. OpenAI gpt-5 family + Anthropic Opus 4.7 only.
33 XHigh,
34 /// Anthropic adaptive-thinking's open-budget level. Anthropic-family
35 /// models (native + Bedrock-Claude) only.
36 Max,
37}
38
39impl ReasoningEffort {
40 /// Wire value the provider API expects.
41 #[must_use]
42 pub const fn as_str(self) -> &'static str {
43 match self {
44 Self::None => "none",
45 Self::Low => "low",
46 Self::Medium => "medium",
47 Self::High => "high",
48 Self::XHigh => "xhigh",
49 Self::Max => "max",
50 }
51 }
52}
53
54impl std::fmt::Display for ReasoningEffort {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 f.write_str(self.as_str())
57 }
58}
59
60/// Error returned when parsing a `ReasoningEffort` from a string that
61/// doesn't match one of the six accepted names.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct InvalidReasoningEffort(pub String);
64
65impl std::fmt::Display for InvalidReasoningEffort {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(
68 f,
69 "invalid reasoning effort '{}'; expected one of none, low, medium, high, xhigh, max",
70 self.0
71 )
72 }
73}
74
75impl std::error::Error for InvalidReasoningEffort {}
76
77impl std::str::FromStr for ReasoningEffort {
78 type Err = InvalidReasoningEffort;
79
80 fn from_str(s: &str) -> Result<Self, Self::Err> {
81 match s.to_lowercase().as_str() {
82 "none" => Ok(Self::None),
83 "low" => Ok(Self::Low),
84 "medium" => Ok(Self::Medium),
85 "high" => Ok(Self::High),
86 "xhigh" => Ok(Self::XHigh),
87 "max" => Ok(Self::Max),
88 _ => Err(InvalidReasoningEffort(s.to_string())),
89 }
90 }
91}
92
93/// Resolved reasoning intent passed to providers via
94/// [`LanguageModelConfig::reasoning`].
95///
96/// Three variants — none, adaptive (qualitative effort), manual (explicit
97/// budget). The caller resolves the user-facing "auto" mode against
98/// the per-model `ReasoningCapability` before constructing this; providers
99/// never see "auto".
100///
101/// Adaptive on OpenAI / Ollama maps to the `reasoning_effort` string; on
102/// Anthropic-family it maps to `thinking: {type: "adaptive", effort: ...}`.
103/// Manual is Anthropic-family only — `thinking: {type: "enabled",
104/// budget_tokens: N}` natively, and `additionalModelRequestFields.thinking`
105/// on Bedrock.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107#[non_exhaustive]
108pub enum ReasoningConfig {
109 /// Reasoning disabled. Omit the wire field (Anthropic) or send
110 /// `reasoning_effort: "none"` (OpenAI/Ollama) depending on the
111 /// provider's idiom. Providers decide per-implementation.
112 Off,
113 /// Adaptive / qualitative-effort mode.
114 Adaptive { effort: ReasoningEffort },
115 /// Manual fixed-budget mode (Anthropic-family only). `budget_tokens`
116 /// must satisfy the per-model `manual_budget_range` from
117 /// [`crate::capabilities::ReasoningCapability`] (validated upstream).
118 Manual { budget_tokens: u32 },
119}
120
121/// Prompt-cache time-to-live for cache breakpoints.
122///
123/// `FiveMin` is the default ephemeral cache (no special handling on
124/// either provider). `OneHour` selects the extended TTL — on Bedrock
125/// via `CacheTtl::OneHour`, on Anthropic via `cache_control.ttl = "1h"`
126/// behind the `extended-cache-ttl-2025-04-11` beta header. The extended
127/// TTL costs ~2× on a cache write but the same ~0.1× on reads, so it
128/// wins whenever a byte-stable prefix is reused beyond the 5-minute
129/// window. Applies to every breakpoint in the request.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
131pub enum CacheTtl {
132 /// Default 5-minute ephemeral cache.
133 #[default]
134 FiveMin,
135 /// Extended 1-hour cache.
136 OneHour,
137}
138
139/// Latency tier for the generation call.
140///
141/// `Optimized` requests AWS Bedrock's latency-optimized ("accelerated")
142/// inference — faster decode for a short, region-specific set of models,
143/// available only through a cross-region inference profile. Only the
144/// Bedrock provider honors this; other providers ignore it. The caller
145/// layer gates `Optimized` against the per-model
146/// [`ModelCapabilities::latency_optimized_supported`](crate::capabilities::ModelCapabilities::latency_optimized_supported)
147/// flag and fails loud before the wire, so a model that can't accept it
148/// never reaches Bedrock with the field set.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
150pub enum LatencyMode {
151 /// Standard latency (default).
152 #[default]
153 Standard,
154 /// Latency-optimized inference (Bedrock only; capability-gated).
155 Optimized,
156}
157
158/// Prompt-caching mode for the generation call.
159///
160/// `Auto` (default) lets the provider engage caching whenever the model is
161/// caching-capable and a `CacheBreakpoint` is present. `Off` suppresses
162/// caching entirely — no `cachePoint` / `cache_control` is emitted even
163/// when a breakpoint marker is present. Useful for latency-critical calls
164/// with tiny prompts where caching adds a round-trip for near-zero benefit.
165///
166/// There is deliberately no "force" mode: forcing caching past the
167/// per-model capability gate is what produces the runtime rejections this
168/// mode exists to avoid.
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
170pub enum PromptCaching {
171 /// Capability-gated caching (default).
172 #[default]
173 Auto,
174 /// Never emit cache breakpoints for this request.
175 Off,
176}
177
178/// Configuration for a language model generation call.
179///
180/// All fields are optional — callers set only what they need. Use
181/// `LanguageModelConfig::default()` for provider defaults.
182#[derive(Debug, Clone, Default)]
183pub struct LanguageModelConfig {
184 /// Sampling temperature (0.0 = deterministic, higher = more random).
185 pub temperature: Option<f64>,
186 /// Maximum number of tokens to generate.
187 pub max_tokens: Option<u32>,
188 /// Nucleus sampling threshold.
189 pub top_p: Option<f64>,
190 /// Stop sequences — generation halts when any of these are produced.
191 pub stop: Vec<String>,
192 /// Reasoning / extended-thinking intent. `None` (default) means the
193 /// caller hasn't expressed an intent; providers send no reasoning
194 /// fields at all. `Some(_)` carries fully-resolved mode + effort,
195 /// already validated against the model's `ReasoningCapability` by
196 /// the caller.
197 pub reasoning: Option<ReasoningConfig>,
198 /// Constrain the output format (JSON, schema-conformant JSON, etc.).
199 ///
200 /// Defaults to [`ResponseFormat::Text`] (unconstrained).
201 pub response_format: ResponseFormat,
202 /// TTL applied to any prompt-cache breakpoints emitted for this
203 /// request. Defaults to [`CacheTtl::FiveMin`]; only meaningful when
204 /// caching is engaged (a caching-capable model + a `CacheBreakpoint`
205 /// in the messages).
206 pub cache_ttl: CacheTtl,
207 /// Latency tier. Defaults to [`LatencyMode::Standard`]. Only the
208 /// Bedrock provider honors [`LatencyMode::Optimized`], and only for
209 /// capability-flagged models reached via a cross-region inference
210 /// profile; other providers ignore it.
211 pub latency: LatencyMode,
212 /// Prompt-caching mode. Defaults to [`PromptCaching::Auto`]
213 /// (capability-gated). [`PromptCaching::Off`] suppresses cache
214 /// breakpoints entirely for this request.
215 pub prompt_caching: PromptCaching,
216}
217
218/// Constrains the output format of a language model response.
219///
220/// Providers that don't support a given format return
221/// [`LanguageModelError::Provider`](crate::LanguageModelError::Provider).
222///
223/// # Provider support
224///
225/// | Format | OpenAI | Anthropic | Ollama |
226/// |--------|--------|-----------|--------|
227/// | `Text` | All models | All models | All models |
228/// | `JsonObject` | Most models | Not supported | Supported |
229/// | `JsonSchema` | Newer models | Beta (sonnet-4-5+, opus-4-1+) | Supported |
230#[derive(Debug, Clone, Default)]
231#[non_exhaustive]
232pub enum ResponseFormat {
233 /// No constraint — free-form text (default).
234 #[default]
235 Text,
236 /// Must produce valid JSON (no schema constraint).
237 ///
238 /// `OpenAI` requires that you also instruct the model to produce JSON in
239 /// your system or user messages — setting this alone is not sufficient.
240 JsonObject,
241 /// Must produce JSON conforming to the given schema.
242 JsonSchema {
243 /// A name for this schema (used by `OpenAI`, max 64 chars, `[a-zA-Z0-9_-]`).
244 name: String,
245 /// The JSON Schema definition.
246 schema: serde_json::Value,
247 /// Whether to enforce strict schema adherence.
248 strict: bool,
249 },
250}