aimux_core/options.rs
1//! `CallOptions` — the provider-facing options passed to `do_generate` / `do_stream`.
2//!
3//! Aligned with V4 `LanguageModelV4CallOptions`.
4
5use std::collections::HashMap;
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use ts_rs::TS;
10
11use crate::AbortSignal;
12use crate::language_model_message::LanguageModelPrompt;
13pub use crate::tool::{FunctionTool, ProviderTool, Tool, ToolChoice};
14use crate::types::ReasoningEffort;
15
16/// How the model should format its response.
17#[derive(Debug, Clone, Serialize, Deserialize, TS)]
18#[ts(export)]
19pub enum ResponseFormat {
20 /// Plain text (default).
21 Text,
22 /// JSON output, optionally constrained to a schema.
23 Json {
24 schema: Option<Value>,
25 name: Option<String>,
26 description: Option<String>,
27 },
28}
29
30/// Per-call timeout configuration.
31///
32/// Aligned with V4 `LanguageModelV4CallOptions.timeout`
33/// (`TimeoutConfiguration`). All values are milliseconds; `None` disables the
34/// corresponding limit. A `total` timeout also covers retry backoff and the
35/// whole streamed response.
36#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
37#[ts(export)]
38pub struct TimeoutConfiguration {
39 /// Overall timeout for the entire call (including retries and, for
40 /// streaming, the whole stream), in milliseconds.
41 // `number`, not the `bigint` ts-rs infers from u64: the JS bindings pass
42 // options through `JSON.stringify`, which throws on BigInt. Milliseconds
43 // cannot reach 2^53 (~285k years), so precision is never at stake.
44 #[ts(type = "number | null")]
45 pub total_ms: Option<u64>,
46 /// Timeout for one generation step, including that step's attempts and
47 /// retry backoff, in milliseconds. Aimux currently has one step.
48 #[ts(type = "number | null")]
49 pub step_ms: Option<u64>,
50 /// Timeout waiting for the first stream chunk (streaming only).
51 ///
52 /// Counted from operation start, so it also bounds stream establishment
53 /// and any retries before the first semantic output: it is the
54 /// user-perceived time-to-first-output budget, not a per-attempt timer.
55 #[ts(type = "number | null")]
56 pub first_chunk_ms: Option<u64>,
57 /// Maximum idle time between consecutive stream chunks (streaming only).
58 #[ts(type = "number | null")]
59 pub chunk_ms: Option<u64>,
60}
61
62/// Options passed to `LanguageModel::do_generate` / `do_stream`.
63///
64/// This is the **provider-facing** options struct. Users interact with
65/// `GenerateTextOptions` (user-facing) which is converted to `CallOptions`
66/// by the `generate_text` / `stream_text` functions.
67#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
68#[ts(export)]
69pub struct CallOptions {
70 /// The standardized prompt (message array). Required.
71 pub prompt: LanguageModelPrompt,
72
73 /// Maximum tokens to generate.
74 pub max_output_tokens: Option<u32>,
75
76 /// Sampling temperature.
77 pub temperature: Option<f64>,
78
79 /// Stop sequences.
80 pub stop_sequences: Option<Vec<String>>,
81
82 /// Nucleus sampling `top_p`.
83 pub top_p: Option<f64>,
84
85 /// Top-k sampling.
86 pub top_k: Option<f64>,
87
88 /// Presence penalty.
89 pub presence_penalty: Option<f64>,
90
91 /// Frequency penalty.
92 pub frequency_penalty: Option<f64>,
93
94 /// Response format (text or JSON).
95 pub response_format: Option<ResponseFormat>,
96
97 /// Seed for reproducibility.
98 #[ts(type = "number | null")]
99 pub seed: Option<u64>,
100
101 /// Tools available to the model (function tools and/or provider-defined tools).
102 pub tools: Option<Vec<Tool>>,
103
104 /// How the model should choose tools.
105 pub tool_choice: ToolChoice,
106
107 /// Extra HTTP headers.
108 pub headers: Option<HashMap<String, String>>,
109
110 /// Provider-specific options (keyed by provider name).
111 pub provider_options: Option<HashMap<String, Value>>,
112
113 /// Top-level reasoning effort. Maps to OpenAI `reasoning_effort` and
114 /// Anthropic `thinking` config.
115 pub reasoning: Option<ReasoningEffort>,
116
117 /// Per-call request body overrides. Deep-merged into the provider-built
118 /// request body (after any built-in vendor override) before sending.
119 /// `null` values delete the corresponding key. See RFC-0017.
120 pub body_overrides: Option<Value>,
121
122 /// Per-call retry count override. `None` uses the provider's configured
123 /// Core operation retry. `Some(0)` disables retries.
124 pub max_retries: Option<u32>,
125
126 /// Per-call timeout configuration (total / first-chunk / chunk idle).
127 /// `None` = no timeouts (provider defaults still apply at the HTTP layer).
128 pub timeout: Option<TimeoutConfiguration>,
129
130 /// Session identifier, for grouping consecutive calls into a session
131 /// (observability, see RFC-0024). Explicit values take precedence; when
132 /// `None` and the optional session inferer is enabled, one may be inferred.
133 /// Orthogonal to RFC-0019 session-affinity headers: this field is for
134 /// local grouping, while session headers in `headers` are for upstream
135 /// routing — both may share an id value but travel different paths.
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub session_id: Option<String>,
138
139 /// Abort signal for cancelling the call.
140 ///
141 /// Runtime handle — never crosses the JSON boundary (bindings that only
142 /// pass JSON cannot set it; Node bridges a JS `AbortSignal` natively).
143 #[serde(skip)]
144 #[ts(skip)]
145 pub abort_signal: Option<AbortSignal>,
146
147 /// RFC-0023 recording correlation id (internal; never serialized).
148 #[serde(skip)]
149 #[ts(skip)]
150 pub call_id: Option<String>,
151
152 /// RFC-0023 recording context (R7 快照绑定;internal;never serialized)。
153 /// 层 A 入口构造一次,provider 复制到 `HttpRequest`。None = 不录制。
154 #[serde(skip)]
155 #[ts(skip)]
156 pub recording_context: Option<crate::recording::RecordingContext>,
157
158 /// Emit raw provider stream chunks as `StreamPart::Raw` (debugging aid).
159 /// When `Some(true)`, streaming providers yield one `Raw` part per JSON
160 /// SSE event, carrying the **parsed JSON payload** of the event, emitted
161 /// before the parsed parts. Excludes the `[DONE]` sentinel; unparsable
162 /// chunks emit only `Error`. `null`/`Some(false)` = off.
163 /// Currently honored by the OpenAI-compatible family (openai / azure /
164 /// openai-compatible registry providers); other provider families ignore
165 /// it for now (RFC-0016 M2).
166 pub include_raw_chunks: Option<bool>,
167}
168
169impl CallOptions {
170 /// Clone these options for one composite child step (Router child, MoA
171 /// reference or aggregator): the recording context, when present, is
172 /// replaced by a child context labeled `step`, so the child's exchanges
173 /// and retry attempts are recorded as their own step instead of blending
174 /// into the parent operation's.
175 #[must_use]
176 pub fn for_step(&self, step: impl Into<String>) -> Self {
177 let mut child = self.clone();
178 child.recording_context = self
179 .recording_context
180 .as_ref()
181 .map(|context| context.child(step.into()));
182 child
183 }
184
185 /// Create a `CallOptions` with the given prompt and all other fields set
186 /// to their defaults (`None` / `ToolChoice::Auto`).
187 ///
188 /// This is the recommended construction baseline in tests — combined with
189 /// struct-update syntax (`CallOptions { tools: Some(..), ..CallOptions::new(prompt) }`)
190 /// it avoids spelling out every `None` field, so adding a new optional
191 /// field to `CallOptions` no longer requires batch-editing every test.
192 #[must_use]
193 pub fn new(prompt: LanguageModelPrompt) -> Self {
194 CallOptions {
195 prompt,
196 max_output_tokens: None,
197 temperature: None,
198 stop_sequences: None,
199 top_p: None,
200 top_k: None,
201 presence_penalty: None,
202 frequency_penalty: None,
203 response_format: None,
204 seed: None,
205 tools: None,
206 tool_choice: ToolChoice::default(),
207 headers: None,
208 provider_options: None,
209 reasoning: None,
210 body_overrides: None,
211 max_retries: None,
212 timeout: None,
213 session_id: None,
214 abort_signal: None,
215 call_id: None,
216 recording_context: None,
217 include_raw_chunks: None,
218 }
219 }
220}