Skip to content

Commit 5c43264

Browse files
authored
Merge pull request #173 from rq3glr16/main
feat: add reasoning parameter support for OpenRouter API
2 parents 7a50c48 + 53e6e3b commit 5c43264

File tree

2 files changed

+175
-1
lines changed

2 files changed

+175
-1
lines changed

examples/openrouter_reasoning.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use openai_api_rs::v1::api::OpenAIClient;
2+
use openai_api_rs::v1::chat_completion::{self, ChatCompletionRequest, Reasoning, ReasoningMode, ReasoningEffort};
3+
use std::env;
4+
5+
#[tokio::main]
6+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
7+
let api_key = env::var("OPENROUTER_API_KEY").unwrap().to_string();
8+
let mut client = OpenAIClient::builder()
9+
.with_endpoint("https://openrouter.ai/api/v1")
10+
.with_api_key(api_key)
11+
.build()?;
12+
13+
// Example 1: Using reasoning with effort
14+
let mut req = ChatCompletionRequest::new(
15+
"x-ai/grok-3-mini".to_string(), // Grok model that supports reasoning
16+
vec![chat_completion::ChatCompletionMessage {
17+
role: chat_completion::MessageRole::user,
18+
content: chat_completion::Content::Text(String::from("Explain quantum computing in simple terms.")),
19+
name: None,
20+
tool_calls: None,
21+
tool_call_id: None,
22+
}],
23+
);
24+
25+
// Set reasoning with high effort
26+
req.reasoning = Some(Reasoning {
27+
mode: Some(ReasoningMode::Effort {
28+
effort: ReasoningEffort::High,
29+
}),
30+
exclude: Some(false), // Include reasoning in response
31+
enabled: None,
32+
});
33+
34+
let result = client.chat_completion(req).await?;
35+
println!("Content: {:?}", result.choices[0].message.content);
36+
37+
// Example 2: Using reasoning with max_tokens
38+
let mut req2 = ChatCompletionRequest::new(
39+
"anthropic/claude-4-sonnet".to_string(), // Claude model that supports max_tokens
40+
vec![chat_completion::ChatCompletionMessage {
41+
role: chat_completion::MessageRole::user,
42+
content: chat_completion::Content::Text(String::from("What's the most efficient sorting algorithm?")),
43+
name: None,
44+
tool_calls: None,
45+
tool_call_id: None,
46+
}],
47+
);
48+
49+
// Set reasoning with max_tokens
50+
req2.reasoning = Some(Reasoning {
51+
mode: Some(ReasoningMode::MaxTokens {
52+
max_tokens: 2000,
53+
}),
54+
exclude: None,
55+
enabled: None,
56+
});
57+
58+
let result2 = client.chat_completion(req2).await?;
59+
println!("Content: {:?}", result2.choices[0].message.content);
60+
61+
Ok(())
62+
}
63+
64+
// OPENROUTER_API_KEY=xxxx cargo run --package openai-api-rs --example openrouter_reasoning

src/v1/chat_completion.rs

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,35 @@ pub enum ToolChoiceType {
1515
ToolChoice { tool: Tool },
1616
}
1717

18+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
19+
#[serde(rename_all = "lowercase")]
20+
pub enum ReasoningEffort {
21+
Low,
22+
Medium,
23+
High,
24+
}
25+
26+
#[derive(Debug, Serialize, Deserialize, Clone)]
27+
#[serde(untagged)]
28+
pub enum ReasoningMode {
29+
Effort {
30+
effort: ReasoningEffort,
31+
},
32+
MaxTokens {
33+
max_tokens: i64,
34+
},
35+
}
36+
37+
#[derive(Debug, Serialize, Deserialize, Clone)]
38+
pub struct Reasoning {
39+
#[serde(flatten)]
40+
pub mode: Option<ReasoningMode>,
41+
#[serde(skip_serializing_if = "Option::is_none")]
42+
pub exclude: Option<bool>,
43+
#[serde(skip_serializing_if = "Option::is_none")]
44+
pub enabled: Option<bool>,
45+
}
46+
1847
#[derive(Debug, Serialize, Deserialize, Clone)]
1948
pub struct ChatCompletionRequest {
2049
pub model: String,
@@ -50,6 +79,8 @@ pub struct ChatCompletionRequest {
5079
#[serde(skip_serializing_if = "Option::is_none")]
5180
#[serde(serialize_with = "serialize_tool_choice")]
5281
pub tool_choice: Option<ToolChoiceType>,
82+
#[serde(skip_serializing_if = "Option::is_none")]
83+
pub reasoning: Option<Reasoning>,
5384
}
5485

5586
impl ChatCompletionRequest {
@@ -72,6 +103,7 @@ impl ChatCompletionRequest {
72103
tools: None,
73104
parallel_tool_calls: None,
74105
tool_choice: None,
106+
reasoning: None,
75107
}
76108
}
77109
}
@@ -92,7 +124,8 @@ impl_builder_methods!(
92124
seed: i64,
93125
tools: Vec<Tool>,
94126
parallel_tool_calls: bool,
95-
tool_choice: ToolChoiceType
127+
tool_choice: ToolChoiceType,
128+
reasoning: Reasoning
96129
);
97130

98131
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
@@ -318,3 +351,80 @@ pub struct Tool {
318351
pub enum ToolType {
319352
Function,
320353
}
354+
355+
#[cfg(test)]
356+
mod tests {
357+
use super::*;
358+
use serde_json::json;
359+
360+
#[test]
361+
fn test_reasoning_effort_serialization() {
362+
let reasoning = Reasoning {
363+
mode: Some(ReasoningMode::Effort {
364+
effort: ReasoningEffort::High,
365+
}),
366+
exclude: Some(false),
367+
enabled: None,
368+
};
369+
370+
let serialized = serde_json::to_value(&reasoning).unwrap();
371+
let expected = json!({
372+
"effort": "high",
373+
"exclude": false
374+
});
375+
376+
assert_eq!(serialized, expected);
377+
}
378+
379+
#[test]
380+
fn test_reasoning_max_tokens_serialization() {
381+
let reasoning = Reasoning {
382+
mode: Some(ReasoningMode::MaxTokens {
383+
max_tokens: 2000,
384+
}),
385+
exclude: None,
386+
enabled: Some(true),
387+
};
388+
389+
let serialized = serde_json::to_value(&reasoning).unwrap();
390+
let expected = json!({
391+
"max_tokens": 2000,
392+
"enabled": true
393+
});
394+
395+
assert_eq!(serialized, expected);
396+
}
397+
398+
#[test]
399+
fn test_reasoning_deserialization() {
400+
let json_str = r#"{"effort": "medium", "exclude": true}"#;
401+
let reasoning: Reasoning = serde_json::from_str(json_str).unwrap();
402+
403+
match reasoning.mode {
404+
Some(ReasoningMode::Effort { effort }) => {
405+
assert_eq!(effort, ReasoningEffort::Medium);
406+
}
407+
_ => panic!("Expected effort mode"),
408+
}
409+
assert_eq!(reasoning.exclude, Some(true));
410+
}
411+
412+
#[test]
413+
fn test_chat_completion_request_with_reasoning() {
414+
let mut req = ChatCompletionRequest::new(
415+
"gpt-4".to_string(),
416+
vec![],
417+
);
418+
419+
req.reasoning = Some(Reasoning {
420+
mode: Some(ReasoningMode::Effort {
421+
effort: ReasoningEffort::Low,
422+
}),
423+
exclude: None,
424+
enabled: None,
425+
});
426+
427+
let serialized = serde_json::to_value(&req).unwrap();
428+
assert_eq!(serialized["reasoning"]["effort"], "low");
429+
}
430+
}

0 commit comments

Comments
 (0)