From d7e81af88adff5d63d937088a2cbbca4202271db Mon Sep 17 00:00:00 2001 From: Morgan Ewing Date: Tue, 22 Jul 2025 14:50:15 +1000 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20reasoning=20param?= =?UTF-8?q?eter=20support=20for=20OpenRouter=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for OpenRouter's reasoning tokens feature to ChatCompletionRequest. This allows models like Grok and Claude to use reasoning/thinking tokens for improved decision making. - Add ReasoningEffort enum (low/medium/high) - Add ReasoningMode enum for mutual exclusivity between effort and max_tokens - Add Reasoning struct with optional mode, exclude, and enabled fields - Update ChatCompletionRequest with optional reasoning field - Add builder method support for reasoning parameter - Include comprehensive unit tests for serialization/deserialization - Add example demonstrating usage with OpenRouter --- examples/openrouter_reasoning.rs | 64 ++++++++++++++++++ src/v1/chat_completion.rs | 112 ++++++++++++++++++++++++++++++- 2 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 examples/openrouter_reasoning.rs diff --git a/examples/openrouter_reasoning.rs b/examples/openrouter_reasoning.rs new file mode 100644 index 0000000..4f5e258 --- /dev/null +++ b/examples/openrouter_reasoning.rs @@ -0,0 +1,64 @@ +use openai_api_rs::v1::api::OpenAIClient; +use openai_api_rs::v1::chat_completion::{self, ChatCompletionRequest, Reasoning, ReasoningMode, ReasoningEffort}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let api_key = env::var("OPENROUTER_API_KEY").unwrap().to_string(); + let mut client = OpenAIClient::builder() + .with_endpoint("https://openrouter.ai/api/v1") + .with_api_key(api_key) + .build()?; + + // Example 1: Using reasoning with effort + let mut req = ChatCompletionRequest::new( + "x-ai/grok-2-1212".to_string(), // Grok model that supports reasoning + vec![chat_completion::ChatCompletionMessage { + role: chat_completion::MessageRole::user, + content: chat_completion::Content::Text(String::from("Explain quantum computing in simple terms.")), + name: None, + tool_calls: None, + tool_call_id: None, + }], + ); + + // Set reasoning with high effort + req.reasoning = Some(Reasoning { + mode: Some(ReasoningMode::Effort { + effort: ReasoningEffort::High, + }), + exclude: Some(false), // Include reasoning in response + enabled: None, + }); + + let result = client.chat_completion(req).await?; + println!("Content: {:?}", result.choices[0].message.content); + + // Example 2: Using reasoning with max_tokens + let mut req2 = ChatCompletionRequest::new( + "anthropic/claude-3.7-sonnet".to_string(), // Claude model that supports max_tokens + vec![chat_completion::ChatCompletionMessage { + role: chat_completion::MessageRole::user, + content: chat_completion::Content::Text(String::from("What's the most efficient sorting algorithm?")), + name: None, + tool_calls: None, + tool_call_id: None, + }], + ); + + // Set reasoning with max_tokens + req2.reasoning = Some(Reasoning { + mode: Some(ReasoningMode::MaxTokens { + max_tokens: 2000, + }), + exclude: None, + enabled: None, + }); + + let result2 = client.chat_completion(req2).await?; + println!("Content: {:?}", result2.choices[0].message.content); + + Ok(()) +} + +// OPENROUTER_API_KEY=xxxx cargo run --package openai-api-rs --example openrouter_reasoning \ No newline at end of file diff --git a/src/v1/chat_completion.rs b/src/v1/chat_completion.rs index 8b3deb1..ad8dbd5 100644 --- a/src/v1/chat_completion.rs +++ b/src/v1/chat_completion.rs @@ -15,6 +15,35 @@ pub enum ToolChoiceType { ToolChoice { tool: Tool }, } +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ReasoningEffort { + Low, + Medium, + High, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum ReasoningMode { + Effort { + effort: ReasoningEffort, + }, + MaxTokens { + max_tokens: i64, + }, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Reasoning { + #[serde(flatten)] + pub mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub exclude: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ChatCompletionRequest { pub model: String, @@ -50,6 +79,8 @@ pub struct ChatCompletionRequest { #[serde(skip_serializing_if = "Option::is_none")] #[serde(serialize_with = "serialize_tool_choice")] pub tool_choice: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning: Option, } impl ChatCompletionRequest { @@ -72,6 +103,7 @@ impl ChatCompletionRequest { tools: None, parallel_tool_calls: None, tool_choice: None, + reasoning: None, } } } @@ -92,7 +124,8 @@ impl_builder_methods!( seed: i64, tools: Vec, parallel_tool_calls: bool, - tool_choice: ToolChoiceType + tool_choice: ToolChoiceType, + reasoning: Reasoning ); #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] @@ -318,3 +351,80 @@ pub struct Tool { pub enum ToolType { Function, } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_reasoning_effort_serialization() { + let reasoning = Reasoning { + mode: Some(ReasoningMode::Effort { + effort: ReasoningEffort::High, + }), + exclude: Some(false), + enabled: None, + }; + + let serialized = serde_json::to_value(&reasoning).unwrap(); + let expected = json!({ + "effort": "high", + "exclude": false + }); + + assert_eq!(serialized, expected); + } + + #[test] + fn test_reasoning_max_tokens_serialization() { + let reasoning = Reasoning { + mode: Some(ReasoningMode::MaxTokens { + max_tokens: 2000, + }), + exclude: None, + enabled: Some(true), + }; + + let serialized = serde_json::to_value(&reasoning).unwrap(); + let expected = json!({ + "max_tokens": 2000, + "enabled": true + }); + + assert_eq!(serialized, expected); + } + + #[test] + fn test_reasoning_deserialization() { + let json_str = r#"{"effort": "medium", "exclude": true}"#; + let reasoning: Reasoning = serde_json::from_str(json_str).unwrap(); + + match reasoning.mode { + Some(ReasoningMode::Effort { effort }) => { + assert_eq!(effort, ReasoningEffort::Medium); + } + _ => panic!("Expected effort mode"), + } + assert_eq!(reasoning.exclude, Some(true)); + } + + #[test] + fn test_chat_completion_request_with_reasoning() { + let mut req = ChatCompletionRequest::new( + "gpt-4".to_string(), + vec![], + ); + + req.reasoning = Some(Reasoning { + mode: Some(ReasoningMode::Effort { + effort: ReasoningEffort::Low, + }), + exclude: None, + enabled: None, + }); + + let serialized = serde_json::to_value(&req).unwrap(); + assert_eq!(serialized["reasoning"]["effort"], "low"); + } +} From 53e6e3b18aa221feb65cbe73a91bfbd458c6e214 Mon Sep 17 00:00:00 2001 From: Morgan Ewing Date: Tue, 22 Jul 2025 14:53:01 +1000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20docs:=20update=20model=20nam?= =?UTF-8?q?es=20in=20reasoning=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update example to use current model naming conventions: - Change Grok model to grok-3-mini - Change Claude model to claude-4-sonnet --- examples/openrouter_reasoning.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/openrouter_reasoning.rs b/examples/openrouter_reasoning.rs index 4f5e258..ea38d5b 100644 --- a/examples/openrouter_reasoning.rs +++ b/examples/openrouter_reasoning.rs @@ -12,7 +12,7 @@ async fn main() -> Result<(), Box> { // Example 1: Using reasoning with effort let mut req = ChatCompletionRequest::new( - "x-ai/grok-2-1212".to_string(), // Grok model that supports reasoning + "x-ai/grok-3-mini".to_string(), // Grok model that supports reasoning vec![chat_completion::ChatCompletionMessage { role: chat_completion::MessageRole::user, content: chat_completion::Content::Text(String::from("Explain quantum computing in simple terms.")), @@ -36,7 +36,7 @@ async fn main() -> Result<(), Box> { // Example 2: Using reasoning with max_tokens let mut req2 = ChatCompletionRequest::new( - "anthropic/claude-3.7-sonnet".to_string(), // Claude model that supports max_tokens + "anthropic/claude-4-sonnet".to_string(), // Claude model that supports max_tokens vec![chat_completion::ChatCompletionMessage { role: chat_completion::MessageRole::user, content: chat_completion::Content::Text(String::from("What's the most efficient sorting algorithm?")),