Skip to main content

openrouter/types/
request.rs

1//! Request payloads.
2
3use serde::{Deserialize, Serialize};
4
5use super::{Message, Plugin, Provider, ReasoningConfig, ResponseFormat, Tool, ToolChoice};
6
7/// Chat-completions request payload.
8#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
9pub struct ChatCompletionRequest {
10    /// Model id (e.g. `google/gemini-3.1-flash-lite`).
11    pub model: String,
12    /// Conversation messages.
13    pub messages: Vec<Message>,
14
15    /// Sampling temperature (0.0 = deterministic, ~1.0 = creative).
16    #[serde(skip_serializing_if = "Option::is_none", default)]
17    pub temperature: Option<f64>,
18    /// Nucleus sampling probability.
19    #[serde(skip_serializing_if = "Option::is_none", default)]
20    pub top_p: Option<f64>,
21    /// Cap on tokens considered each step.
22    #[serde(skip_serializing_if = "Option::is_none", default)]
23    pub top_k: Option<u32>,
24    /// Max generated tokens.
25    #[serde(skip_serializing_if = "Option::is_none", default)]
26    pub max_tokens: Option<u32>,
27    /// Stream-mode flag. The SDK forces `Some(true)` for streaming
28    /// methods and `Some(false)` for blocking methods.
29    #[serde(skip_serializing_if = "Option::is_none", default)]
30    pub stream: Option<bool>,
31    /// Stop sequence(s) — string or array of strings.
32    #[serde(skip_serializing_if = "Option::is_none", default)]
33    pub stop: Option<serde_json::Value>,
34    /// Sampling seed for reproducibility.
35    #[serde(skip_serializing_if = "Option::is_none", default)]
36    pub seed: Option<i64>,
37    /// Frequency penalty.
38    #[serde(skip_serializing_if = "Option::is_none", default)]
39    pub frequency_penalty: Option<f64>,
40    /// Presence penalty.
41    #[serde(skip_serializing_if = "Option::is_none", default)]
42    pub presence_penalty: Option<f64>,
43    /// Repetition penalty.
44    #[serde(skip_serializing_if = "Option::is_none", default)]
45    pub repetition_penalty: Option<f64>,
46    /// Per-token bias map (provider-specific shape).
47    #[serde(skip_serializing_if = "Option::is_none", default)]
48    pub logit_bias: Option<serde_json::Value>,
49    /// Request log-probabilities for the chosen tokens.
50    #[serde(skip_serializing_if = "Option::is_none", default)]
51    pub logprobs: Option<bool>,
52    /// Number of alternate tokens to report logprobs for.
53    #[serde(skip_serializing_if = "Option::is_none", default)]
54    pub top_logprobs: Option<u32>,
55    /// Min-p sampling threshold.
56    #[serde(skip_serializing_if = "Option::is_none", default)]
57    pub min_p: Option<f64>,
58    /// Top-a sampling threshold.
59    #[serde(skip_serializing_if = "Option::is_none", default)]
60    pub top_a: Option<f64>,
61
62    /// Tools the model may call.
63    #[serde(skip_serializing_if = "Option::is_none", default)]
64    pub tools: Option<Vec<Tool>>,
65    /// Tool-selection strategy.
66    #[serde(skip_serializing_if = "Option::is_none", default)]
67    pub tool_choice: Option<ToolChoice>,
68    /// Output format constraint (JSON schema / JSON mode).
69    #[serde(skip_serializing_if = "Option::is_none", default)]
70    pub response_format: Option<ResponseFormat>,
71    /// Provider-routing parameters.
72    #[serde(skip_serializing_if = "Option::is_none", default)]
73    pub provider: Option<Provider>,
74    /// Reasoning-token configuration.
75    #[serde(skip_serializing_if = "Option::is_none", default)]
76    pub reasoning: Option<ReasoningConfig>,
77    /// Message-transform pipeline (e.g. `["middle-out"]`).
78    #[serde(skip_serializing_if = "Option::is_none", default)]
79    pub transforms: Option<Vec<String>>,
80    /// Plugin configuration (web search, file parsing, ...).
81    #[serde(skip_serializing_if = "Option::is_none", default)]
82    pub plugins: Option<Vec<Plugin>>,
83    /// Provider-specific usage reporting hint.
84    #[serde(skip_serializing_if = "Option::is_none", default)]
85    pub usage: Option<serde_json::Value>,
86    /// Opaque end-user identifier for abuse monitoring.
87    #[serde(skip_serializing_if = "Option::is_none", default)]
88    pub user: Option<String>,
89}
90
91impl ChatCompletionRequest {
92    /// Construct a new request with just the required fields.
93    pub fn new(model: impl Into<String>, messages: Vec<Message>) -> Self {
94        Self {
95            model: model.into(),
96            messages,
97            ..Default::default()
98        }
99    }
100
101    /// Attach the list of tools the model may call.
102    pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
103        self.tools = Some(tools);
104        self
105    }
106
107    /// Set the tool-selection strategy.
108    pub fn with_tool_choice(mut self, choice: ToolChoice) -> Self {
109        self.tool_choice = Some(choice);
110        self
111    }
112
113    /// Set the response format directly.
114    pub fn with_response_format(mut self, format: ResponseFormat) -> Self {
115        self.response_format = Some(format);
116        self
117    }
118
119    /// Constrain responses to a named JSON schema. Sugar over
120    /// `with_response_format(ResponseFormat::json_schema(...))`.
121    pub fn with_json_schema(
122        self,
123        name: impl Into<String>,
124        strict: bool,
125        schema: serde_json::Value,
126    ) -> Self {
127        self.with_response_format(ResponseFormat::json_schema(name, strict, schema))
128    }
129
130    /// Ask the model to emit a JSON object (no schema). Sugar over
131    /// `with_response_format(ResponseFormat::json_object())`.
132    pub fn with_json_mode(self) -> Self {
133        self.with_response_format(ResponseFormat::json_object())
134    }
135
136    /// Set the message-transform pipeline. Passing an empty slice
137    /// explicitly disables OpenRouter's default transforms.
138    pub fn with_transforms<S, I>(mut self, transforms: I) -> Self
139    where
140        S: Into<String>,
141        I: IntoIterator<Item = S>,
142    {
143        self.transforms = Some(transforms.into_iter().map(Into::into).collect());
144        self
145    }
146
147    /// Replace the provider-routing config with `provider`.
148    pub fn with_provider(mut self, provider: Provider) -> Self {
149        self.provider = Some(provider);
150        self
151    }
152
153    fn provider_mut(&mut self) -> &mut Provider {
154        self.provider.get_or_insert_with(Provider::default)
155    }
156
157    /// Preferred provider order.
158    pub fn with_provider_order<S, I>(mut self, order: I) -> Self
159    where
160        S: Into<String>,
161        I: IntoIterator<Item = S>,
162    {
163        self.provider_mut().order = Some(order.into_iter().map(Into::into).collect());
164        self
165    }
166
167    /// Sort strategy: `"throughput"`, `"price"`, or `"latency"`.
168    pub fn with_provider_sort(mut self, sort: impl Into<String>) -> Self {
169        self.provider_mut().sort = Some(sort.into());
170        self
171    }
172
173    /// Restrict to this set of providers.
174    pub fn with_only_providers<S, I>(mut self, only: I) -> Self
175    where
176        S: Into<String>,
177        I: IntoIterator<Item = S>,
178    {
179        self.provider_mut().only = Some(only.into_iter().map(Into::into).collect());
180        self
181    }
182
183    /// Exclude these providers.
184    pub fn with_ignore_providers<S, I>(mut self, ignore: I) -> Self
185    where
186        S: Into<String>,
187        I: IntoIterator<Item = S>,
188    {
189        self.provider_mut().ignore = Some(ignore.into_iter().map(Into::into).collect());
190        self
191    }
192
193    /// Permitted quantization tiers.
194    pub fn with_quantizations<S, I>(mut self, q: I) -> Self
195    where
196        S: Into<String>,
197        I: IntoIterator<Item = S>,
198    {
199        self.provider_mut().quantizations = Some(q.into_iter().map(Into::into).collect());
200        self
201    }
202
203    /// Per-token max-price filter.
204    pub fn with_max_price(mut self, price: serde_json::Value) -> Self {
205        self.provider_mut().max_price = Some(price);
206        self
207    }
208
209    /// Data-collection policy: `"allow"` or `"deny"`.
210    pub fn with_data_collection(mut self, policy: impl Into<String>) -> Self {
211        self.provider_mut().data_collection = Some(policy.into());
212        self
213    }
214
215    /// Require providers to accept all sampling parameters.
216    pub fn with_require_parameters(mut self, required: bool) -> Self {
217        self.provider_mut().require_parameters = Some(required);
218        self
219    }
220
221    /// Allow / disallow OpenRouter to use other providers as fallbacks.
222    pub fn with_allow_fallbacks(mut self, allow: bool) -> Self {
223        self.provider_mut().allow_fallbacks = Some(allow);
224        self
225    }
226
227    /// Per-request Zero-Data-Retention enforcement.
228    pub fn with_zdr(mut self, zdr: bool) -> Self {
229        self.provider_mut().zdr = Some(zdr);
230        self
231    }
232
233    /// Shorthand for `with_provider_sort("throughput")` — equivalent to a
234    /// `:nitro` model suffix.
235    pub fn with_nitro(self) -> Self {
236        self.with_provider_sort("throughput")
237    }
238
239    /// Shorthand for `with_provider_sort("price")` — equivalent to a
240    /// `:floor` model suffix.
241    pub fn with_floor(self) -> Self {
242        self.with_provider_sort("price")
243    }
244
245    /// Replace the full reasoning config.
246    pub fn with_reasoning(mut self, reasoning: ReasoningConfig) -> Self {
247        self.reasoning = Some(reasoning);
248        self
249    }
250
251    fn reasoning_mut(&mut self) -> &mut ReasoningConfig {
252        self.reasoning.get_or_insert_with(ReasoningConfig::default)
253    }
254
255    /// Set the reasoning effort (`"low"`, `"medium"`, `"high"`).
256    pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
257        self.reasoning_mut().effort = Some(effort.into());
258        self
259    }
260
261    /// Cap the reasoning-token budget for this request.
262    pub fn with_reasoning_max_tokens(mut self, max_tokens: u32) -> Self {
263        self.reasoning_mut().max_tokens = Some(max_tokens);
264        self
265    }
266
267    /// Ask the provider to omit reasoning content from the response.
268    pub fn with_reasoning_exclude(mut self, exclude: bool) -> Self {
269        self.reasoning_mut().exclude = Some(exclude);
270        self
271    }
272
273    /// Replace the plugin list.
274    pub fn with_plugins(mut self, plugins: Vec<Plugin>) -> Self {
275        self.plugins = Some(plugins);
276        self
277    }
278
279    /// Enable the default web-search plugin. Equivalent to
280    /// `with_plugins(vec![Plugin::web()])`, but preserves any plugins
281    /// already configured (web is pushed onto the existing list).
282    pub fn with_web_search(mut self) -> Self {
283        self.plugins
284            .get_or_insert_with(Vec::new)
285            .push(Plugin::web());
286        self
287    }
288}
289
290/// Legacy text-completions request payload.
291#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
292pub struct CompletionRequest {
293    /// Model id.
294    pub model: String,
295    /// Prompt to complete.
296    pub prompt: String,
297
298    /// Sampling temperature.
299    #[serde(skip_serializing_if = "Option::is_none", default)]
300    pub temperature: Option<f64>,
301    /// Nucleus sampling probability.
302    #[serde(skip_serializing_if = "Option::is_none", default)]
303    pub top_p: Option<f64>,
304    /// Max generated tokens.
305    #[serde(skip_serializing_if = "Option::is_none", default)]
306    pub max_tokens: Option<u32>,
307    /// Stream-mode flag.
308    #[serde(skip_serializing_if = "Option::is_none", default)]
309    pub stream: Option<bool>,
310    /// Stop sequence(s).
311    #[serde(skip_serializing_if = "Option::is_none", default)]
312    pub stop: Option<serde_json::Value>,
313    /// Sampling seed for reproducibility.
314    #[serde(skip_serializing_if = "Option::is_none", default)]
315    pub seed: Option<i64>,
316    /// Frequency penalty.
317    #[serde(skip_serializing_if = "Option::is_none", default)]
318    pub frequency_penalty: Option<f64>,
319    /// Presence penalty.
320    #[serde(skip_serializing_if = "Option::is_none", default)]
321    pub presence_penalty: Option<f64>,
322    /// Provider-routing parameters.
323    #[serde(skip_serializing_if = "Option::is_none", default)]
324    pub provider: Option<Provider>,
325    /// Message-transform pipeline.
326    #[serde(skip_serializing_if = "Option::is_none", default)]
327    pub transforms: Option<Vec<String>>,
328    /// Plugin configuration.
329    #[serde(skip_serializing_if = "Option::is_none", default)]
330    pub plugins: Option<Vec<Plugin>>,
331    /// Opaque end-user identifier for abuse monitoring.
332    #[serde(skip_serializing_if = "Option::is_none", default)]
333    pub user: Option<String>,
334}
335
336impl CompletionRequest {
337    /// Construct a new legacy completion request.
338    pub fn new(model: impl Into<String>, prompt: impl Into<String>) -> Self {
339        Self {
340            model: model.into(),
341            prompt: prompt.into(),
342            ..Default::default()
343        }
344    }
345
346    /// Set the message-transform pipeline. Passing an empty slice
347    /// explicitly disables OpenRouter's default transforms.
348    pub fn with_transforms<S, I>(mut self, transforms: I) -> Self
349    where
350        S: Into<String>,
351        I: IntoIterator<Item = S>,
352    {
353        self.transforms = Some(transforms.into_iter().map(Into::into).collect());
354        self
355    }
356
357    /// Replace the provider-routing config.
358    pub fn with_provider(mut self, provider: Provider) -> Self {
359        self.provider = Some(provider);
360        self
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use crate::types::{FunctionDef, Tool, ToolChoice};
368    use pretty_assertions::assert_eq;
369    use serde_json::json;
370
371    #[test]
372    fn with_tools_serializes_only_set_fields() {
373        let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")])
374            .with_tools(vec![Tool::function(FunctionDef::new(
375                "f",
376                json!({"type":"object"}),
377            ))])
378            .with_tool_choice(ToolChoice::required());
379        let v = serde_json::to_value(&req).unwrap();
380        assert_eq!(
381            v,
382            json!({
383                "model": "x/y",
384                "messages": [{"role":"user","content":"hi"}],
385                "tools": [{
386                    "type": "function",
387                    "function": {"name":"f","parameters":{"type":"object"}}
388                }],
389                "tool_choice": "required"
390            })
391        );
392    }
393
394    #[test]
395    fn tool_choice_function_serializes() {
396        let v = serde_json::to_value(ToolChoice::function("get_weather")).unwrap();
397        assert_eq!(
398            v,
399            json!({"type":"function","function":{"name":"get_weather"}})
400        );
401    }
402
403    #[test]
404    fn with_json_mode_serializes() {
405        let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")]).with_json_mode();
406        let v = serde_json::to_value(&req).unwrap();
407        assert_eq!(v["response_format"], json!({"type":"json_object"}));
408    }
409
410    #[test]
411    fn with_transforms_serializes_array() {
412        let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")])
413            .with_transforms(["middle-out"]);
414        let v = serde_json::to_value(&req).unwrap();
415        assert_eq!(v["transforms"], json!(["middle-out"]));
416    }
417
418    #[test]
419    fn with_transforms_empty_disables_defaults() {
420        let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")])
421            .with_transforms(Vec::<String>::new());
422        let v = serde_json::to_value(&req).unwrap();
423        assert_eq!(v["transforms"], json!([]));
424    }
425
426    #[test]
427    fn with_provider_helpers_compose_into_one_object() {
428        let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")])
429            .with_provider_order(["openai", "anthropic"])
430            .with_only_providers(["openai"])
431            .with_zdr(true)
432            .with_nitro();
433        let v = serde_json::to_value(&req).unwrap();
434        assert_eq!(
435            v["provider"],
436            json!({
437                "order": ["openai", "anthropic"],
438                "only": ["openai"],
439                "zdr": true,
440                "sort": "throughput"
441            })
442        );
443    }
444
445    #[test]
446    fn with_web_search_serializes_default_plugin() {
447        let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")]).with_web_search();
448        let v = serde_json::to_value(&req).unwrap();
449        assert_eq!(v["plugins"], json!([{"id":"web"}]));
450    }
451
452    #[test]
453    fn with_plugins_custom_config_serializes() {
454        use crate::types::{Plugin, WebPluginConfig};
455        let plugin = Plugin::web_with(
456            WebPluginConfig::new()
457                .with_max_results(3)
458                .with_search_prompt("Cite sources.")
459                .with_engine("native"),
460        );
461        let req =
462            ChatCompletionRequest::new("x/y", vec![Message::user("hi")]).with_plugins(vec![plugin]);
463        let v = serde_json::to_value(&req).unwrap();
464        assert_eq!(
465            v["plugins"],
466            json!([{
467                "id":"web",
468                "max_results":3,
469                "search_prompt":"Cite sources.",
470                "engine":"native"
471            }])
472        );
473    }
474
475    #[test]
476    fn with_reasoning_helpers_compose() {
477        let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")])
478            .with_reasoning_effort("high")
479            .with_reasoning_max_tokens(512)
480            .with_reasoning_exclude(false);
481        let v = serde_json::to_value(&req).unwrap();
482        assert_eq!(
483            v["reasoning"],
484            json!({"effort":"high","max_tokens":512,"exclude":false})
485        );
486    }
487
488    #[test]
489    fn completion_with_transforms_serializes_array() {
490        let req = CompletionRequest::new("x/y", "hello").with_transforms(["middle-out"]);
491        let v = serde_json::to_value(&req).unwrap();
492        assert_eq!(v["transforms"], json!(["middle-out"]));
493    }
494
495    #[test]
496    fn with_json_schema_serializes_strict_named_schema() {
497        let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")]).with_json_schema(
498            "answer",
499            true,
500            json!({"type":"object","properties":{"x":{"type":"number"}}}),
501        );
502        let v = serde_json::to_value(&req).unwrap();
503        assert_eq!(
504            v["response_format"],
505            json!({
506                "type": "json_schema",
507                "json_schema": {
508                    "name": "answer",
509                    "schema": {"type":"object","properties":{"x":{"type":"number"}}},
510                    "strict": true
511                }
512            })
513        );
514    }
515}