Skip to main content

openrouter/types/
common.rs

1//! Shared sub-types: tools, response format, provider routing, reasoning.
2
3use serde::{Deserialize, Serialize};
4
5/// A function-call invocation requested by the model.
6///
7/// Both `id` and `kind` are optional because OpenRouter streams tool calls
8/// as fragments: the first chunk for an `index` typically carries `id` +
9/// `type` + `function.name`, and continuation chunks for the same `index`
10/// carry only additional `function.arguments` bytes.
11#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
12pub struct ToolCall {
13    /// Stable call identifier.
14    #[serde(default)]
15    pub id: String,
16    /// Wire `type` discriminator (currently always `"function"`).
17    #[serde(rename = "type", default)]
18    pub kind: String,
19    /// Function invocation payload.
20    pub function: FunctionCall,
21    /// Streaming index for matching subsequent argument fragments.
22    #[serde(skip_serializing_if = "Option::is_none", default)]
23    pub index: Option<u32>,
24}
25
26/// Function-call payload (name + serialized JSON arguments).
27#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
28pub struct FunctionCall {
29    /// Function name. Streaming fragments may carry this only on the
30    /// first chunk for a given tool-call index.
31    #[serde(skip_serializing_if = "Option::is_none", default)]
32    pub name: Option<String>,
33    /// Serialized JSON arguments. Streaming sends this in fragments —
34    /// concatenate via [`crate::ToolCallAccumulator`].
35    #[serde(skip_serializing_if = "Option::is_none", default)]
36    pub arguments: Option<String>,
37}
38
39/// A tool the model is allowed to call.
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41#[serde(tag = "type", rename_all = "snake_case")]
42pub enum Tool {
43    /// A callable function tool.
44    Function {
45        /// Function definition.
46        function: FunctionDef,
47    },
48}
49
50/// Definition of a callable function tool.
51#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
52pub struct FunctionDef {
53    /// Function name as exposed to the model.
54    pub name: String,
55    /// Optional human-readable description shown to the model.
56    #[serde(skip_serializing_if = "Option::is_none", default)]
57    pub description: Option<String>,
58    /// JSON Schema describing the function's parameters.
59    #[serde(skip_serializing_if = "Option::is_none", default)]
60    pub parameters: Option<serde_json::Value>,
61    /// When true, require strict schema adherence.
62    #[serde(skip_serializing_if = "Option::is_none", default)]
63    pub strict: Option<bool>,
64}
65
66/// Tool-selection strategy.
67#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
68#[serde(untagged)]
69pub enum ToolChoice {
70    /// Named mode: `"auto"`, `"none"`, or `"required"`.
71    Mode(String),
72    /// Force a specific tool by name.
73    Specific {
74        /// Wire `type` discriminator (currently always `"function"`).
75        #[serde(rename = "type")]
76        kind: String,
77        /// Function reference.
78        function: FunctionRef,
79    },
80}
81
82/// Lightweight reference to a tool by function name.
83#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
84pub struct FunctionRef {
85    /// Function name to invoke.
86    pub name: String,
87}
88
89impl ToolChoice {
90    /// Let the model decide whether to call a tool.
91    pub fn auto() -> Self {
92        ToolChoice::Mode("auto".to_string())
93    }
94
95    /// Forbid tool calls.
96    pub fn none() -> Self {
97        ToolChoice::Mode("none".to_string())
98    }
99
100    /// Require the model to call some tool.
101    pub fn required() -> Self {
102        ToolChoice::Mode("required".to_string())
103    }
104
105    /// Force the model to call a specific function by name.
106    pub fn function(name: impl Into<String>) -> Self {
107        ToolChoice::Specific {
108            kind: "function".to_string(),
109            function: FunctionRef { name: name.into() },
110        }
111    }
112}
113
114impl Tool {
115    /// Build a function tool from a `FunctionDef`.
116    pub fn function(def: FunctionDef) -> Self {
117        Tool::Function { function: def }
118    }
119}
120
121impl FunctionDef {
122    /// Construct a new function definition with a JSON-Schema parameter object.
123    pub fn new(name: impl Into<String>, parameters: serde_json::Value) -> Self {
124        Self {
125            name: name.into(),
126            description: None,
127            parameters: Some(parameters),
128            strict: None,
129        }
130    }
131
132    /// Attach a human-readable description.
133    pub fn with_description(mut self, description: impl Into<String>) -> Self {
134        self.description = Some(description.into());
135        self
136    }
137
138    /// Toggle strict schema adherence.
139    pub fn with_strict(mut self, strict: bool) -> Self {
140        self.strict = Some(strict);
141        self
142    }
143}
144
145/// Response-format hint (JSON mode or JSON schema).
146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
147#[serde(tag = "type", rename_all = "snake_case")]
148pub enum ResponseFormat {
149    /// Plain text (default).
150    Text,
151    /// Generic JSON-object mode (no schema constraint).
152    JsonObject,
153    /// JSON constrained to a schema.
154    JsonSchema {
155        /// The schema definition.
156        json_schema: JsonSchema,
157    },
158}
159
160/// Structured-output JSON schema definition.
161#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
162pub struct JsonSchema {
163    /// Schema name (shown to the model).
164    pub name: String,
165    /// Optional schema description.
166    #[serde(skip_serializing_if = "Option::is_none", default)]
167    pub description: Option<String>,
168    /// JSON Schema document.
169    pub schema: serde_json::Value,
170    /// Strict mode flag.
171    #[serde(skip_serializing_if = "Option::is_none", default)]
172    pub strict: Option<bool>,
173}
174
175impl ResponseFormat {
176    /// Simple JSON-object mode: the model is asked to emit a valid JSON
177    /// object, but the shape is not constrained.
178    pub fn json_object() -> Self {
179        ResponseFormat::JsonObject
180    }
181
182    /// Constrain the response to a named JSON schema.
183    pub fn json_schema(name: impl Into<String>, strict: bool, schema: serde_json::Value) -> Self {
184        ResponseFormat::JsonSchema {
185            json_schema: JsonSchema {
186                name: name.into(),
187                description: None,
188                schema,
189                strict: Some(strict),
190            },
191        }
192    }
193}
194
195/// Provider routing controls. Fleshed out further in Phase 3.
196#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
197pub struct Provider {
198    /// Ordered preference list of provider slugs.
199    #[serde(skip_serializing_if = "Option::is_none", default)]
200    pub order: Option<Vec<String>>,
201    /// Whether OpenRouter may fall back to other providers on failure.
202    #[serde(skip_serializing_if = "Option::is_none", default)]
203    pub allow_fallbacks: Option<bool>,
204    /// Require providers to accept every supplied sampling parameter.
205    #[serde(skip_serializing_if = "Option::is_none", default)]
206    pub require_parameters: Option<bool>,
207    /// Data-collection policy (`"allow"` / `"deny"`).
208    #[serde(skip_serializing_if = "Option::is_none", default)]
209    pub data_collection: Option<String>,
210    /// Allowlist of provider slugs.
211    #[serde(skip_serializing_if = "Option::is_none", default)]
212    pub only: Option<Vec<String>>,
213    /// Denylist of provider slugs.
214    #[serde(skip_serializing_if = "Option::is_none", default)]
215    pub ignore: Option<Vec<String>>,
216    /// Allowed quantization tiers.
217    #[serde(skip_serializing_if = "Option::is_none", default)]
218    pub quantizations: Option<Vec<String>>,
219    /// Sort strategy: `"throughput"`, `"price"`, or `"latency"`.
220    #[serde(skip_serializing_if = "Option::is_none", default)]
221    pub sort: Option<String>,
222    /// Per-token max price filter (free-form JSON value).
223    #[serde(skip_serializing_if = "Option::is_none", default)]
224    pub max_price: Option<serde_json::Value>,
225    /// Require zero-data-retention endpoints.
226    #[serde(skip_serializing_if = "Option::is_none", default)]
227    pub zdr: Option<bool>,
228}
229
230impl Provider {
231    /// New, empty provider-routing config.
232    pub fn new() -> Self {
233        Self::default()
234    }
235
236    /// Ordered preference list of provider slugs.
237    pub fn with_order<S, I>(mut self, order: I) -> Self
238    where
239        S: Into<String>,
240        I: IntoIterator<Item = S>,
241    {
242        self.order = Some(order.into_iter().map(Into::into).collect());
243        self
244    }
245
246    /// Sort strategy: `"throughput"`, `"price"`, or `"latency"`.
247    pub fn with_sort(mut self, sort: impl Into<String>) -> Self {
248        self.sort = Some(sort.into());
249        self
250    }
251
252    /// Whether OpenRouter may fall back to other providers if the preferred ones fail.
253    pub fn with_allow_fallbacks(mut self, allow: bool) -> Self {
254        self.allow_fallbacks = Some(allow);
255        self
256    }
257
258    /// Restrict to this set of providers.
259    pub fn with_only<S, I>(mut self, only: I) -> Self
260    where
261        S: Into<String>,
262        I: IntoIterator<Item = S>,
263    {
264        self.only = Some(only.into_iter().map(Into::into).collect());
265        self
266    }
267
268    /// Exclude these providers from consideration.
269    pub fn with_ignore<S, I>(mut self, ignore: I) -> Self
270    where
271        S: Into<String>,
272        I: IntoIterator<Item = S>,
273    {
274        self.ignore = Some(ignore.into_iter().map(Into::into).collect());
275        self
276    }
277
278    /// Permitted quantization tiers (e.g. `"fp8"`, `"int4"`).
279    pub fn with_quantizations<S, I>(mut self, q: I) -> Self
280    where
281        S: Into<String>,
282        I: IntoIterator<Item = S>,
283    {
284        self.quantizations = Some(q.into_iter().map(Into::into).collect());
285        self
286    }
287
288    /// Per-token max price filter (free-form value, see OpenRouter docs).
289    pub fn with_max_price(mut self, price: serde_json::Value) -> Self {
290        self.max_price = Some(price);
291        self
292    }
293
294    /// Data-collection policy: `"allow"` or `"deny"`.
295    pub fn with_data_collection(mut self, policy: impl Into<String>) -> Self {
296        self.data_collection = Some(policy.into());
297        self
298    }
299
300    /// Require that providers accept all supplied sampling parameters.
301    pub fn with_require_parameters(mut self, required: bool) -> Self {
302        self.require_parameters = Some(required);
303        self
304    }
305
306    /// Per-request Zero-Data-Retention enforcement.
307    pub fn with_zdr(mut self, zdr: bool) -> Self {
308        self.zdr = Some(zdr);
309        self
310    }
311}
312
313/// Reasoning-tokens configuration.
314///
315/// `effort` and `max_tokens` are mutually exclusive on OpenRouter's side —
316/// setting both yields a 400. Pick the one that matches the constraint you
317/// care about (qualitative effort budget vs. hard token cap).
318#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
319pub struct ReasoningConfig {
320    /// Qualitative effort budget (`"low"`, `"medium"`, `"high"`).
321    #[serde(skip_serializing_if = "Option::is_none", default)]
322    pub effort: Option<String>,
323    /// Hard cap on reasoning tokens. Mutually exclusive with `effort`.
324    #[serde(skip_serializing_if = "Option::is_none", default)]
325    pub max_tokens: Option<u32>,
326    /// Ask the provider to omit reasoning content from the response.
327    #[serde(skip_serializing_if = "Option::is_none", default)]
328    pub exclude: Option<bool>,
329}
330
331/// A request-time plugin. Variants serialize with a tagged `id` field
332/// (`web`, `file-parser`); new variants can be added without breaking
333/// existing callers.
334#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
335#[serde(tag = "id", rename_all = "kebab-case")]
336pub enum Plugin {
337    /// Real-time web search plugin.
338    Web(WebPluginConfig),
339    /// PDF / file-parser plugin.
340    #[serde(rename = "file-parser")]
341    File(FilePluginConfig),
342}
343
344impl Plugin {
345    /// Default web-search plugin (server-side defaults for engine and prompt).
346    pub fn web() -> Self {
347        Plugin::Web(WebPluginConfig::default())
348    }
349
350    /// Web-search plugin with explicit overrides.
351    pub fn web_with(config: WebPluginConfig) -> Self {
352        Plugin::Web(config)
353    }
354
355    /// File-parser plugin with the given PDF parsing engine. Pass `None` to
356    /// let OpenRouter pick a default.
357    pub fn file_parser(pdf_engine: Option<&str>) -> Self {
358        let pdf = pdf_engine.map(|e| FilePdfConfig {
359            engine: Some(e.to_string()),
360        });
361        Plugin::File(FilePluginConfig { pdf })
362    }
363}
364
365/// Configuration for the `file-parser` plugin.
366#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
367pub struct FilePluginConfig {
368    /// PDF-specific configuration.
369    #[serde(skip_serializing_if = "Option::is_none", default)]
370    pub pdf: Option<FilePdfConfig>,
371}
372
373/// PDF-specific options for the `file-parser` plugin.
374#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
375pub struct FilePdfConfig {
376    /// PDF parsing engine (see [`crate::FileParserEngine`]).
377    #[serde(skip_serializing_if = "Option::is_none", default)]
378    pub engine: Option<String>,
379}
380
381/// Configuration for the `web` plugin.
382#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
383pub struct WebPluginConfig {
384    /// Maximum search-results to feed the model.
385    #[serde(skip_serializing_if = "Option::is_none", default)]
386    pub max_results: Option<u32>,
387    /// Override the internal search prompt.
388    #[serde(skip_serializing_if = "Option::is_none", default)]
389    pub search_prompt: Option<String>,
390    /// Search engine slug.
391    #[serde(skip_serializing_if = "Option::is_none", default)]
392    pub engine: Option<String>,
393}
394
395impl WebPluginConfig {
396    /// New, empty web-plugin configuration.
397    pub fn new() -> Self {
398        Self::default()
399    }
400    /// Builder: set [`Self::max_results`].
401    pub fn with_max_results(mut self, n: u32) -> Self {
402        self.max_results = Some(n);
403        self
404    }
405    /// Builder: set [`Self::search_prompt`].
406    pub fn with_search_prompt(mut self, prompt: impl Into<String>) -> Self {
407        self.search_prompt = Some(prompt.into());
408        self
409    }
410    /// Builder: set [`Self::engine`].
411    pub fn with_engine(mut self, engine: impl Into<String>) -> Self {
412        self.engine = Some(engine.into());
413        self
414    }
415}
416
417/// A typed annotation attached to an assistant message. OpenRouter emits
418/// `url_citation` for the web-search plugin and `file` for the file-parser
419/// plugin (so previously-parsed PDFs can be replayed without re-parsing).
420#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
421#[serde(tag = "type", rename_all = "snake_case")]
422pub enum Annotation {
423    /// A URL citation produced by the web-search plugin.
424    UrlCitation {
425        /// The citation payload.
426        url_citation: UrlCitation,
427    },
428    /// A parsed-file annotation reusable across turns.
429    File {
430        /// The file annotation payload.
431        file: FileAnnotation,
432    },
433}
434
435/// Parsed-file annotation: feed it back into a follow-up request to reuse
436/// the prior parse result instead of re-running the file-parser.
437#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
438pub struct FileAnnotation {
439    /// Display filename of the parsed file.
440    pub filename: String,
441    /// Opaque parsed-content payload to feed back into a follow-up request.
442    pub file_data: String,
443}
444
445/// A URL citation produced by the web-search plugin.
446#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
447pub struct UrlCitation {
448    /// Citation URL.
449    pub url: String,
450    /// Page title.
451    #[serde(skip_serializing_if = "Option::is_none", default)]
452    pub title: Option<String>,
453    /// Extracted snippet from the citation.
454    #[serde(skip_serializing_if = "Option::is_none", default)]
455    pub content: Option<String>,
456    /// Start character offset within the assistant message.
457    #[serde(skip_serializing_if = "Option::is_none", default)]
458    pub start_index: Option<u32>,
459    /// End character offset within the assistant message.
460    #[serde(skip_serializing_if = "Option::is_none", default)]
461    pub end_index: Option<u32>,
462}
463
464impl ReasoningConfig {
465    /// New, empty reasoning config.
466    pub fn new() -> Self {
467        Self::default()
468    }
469
470    /// Set the reasoning effort (`"low"`, `"medium"`, `"high"`).
471    pub fn with_effort(mut self, effort: impl Into<String>) -> Self {
472        self.effort = Some(effort.into());
473        self
474    }
475
476    /// Cap the number of reasoning tokens.
477    pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
478        self.max_tokens = Some(max_tokens);
479        self
480    }
481
482    /// Ask the provider to omit reasoning tokens from the response (counts
483    /// still appear in usage when supported).
484    pub fn with_exclude(mut self, exclude: bool) -> Self {
485        self.exclude = Some(exclude);
486        self
487    }
488}