Skip to main content

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}