From e4c38718824bda32c054664d1a01e591b489f635 Mon Sep 17 00:00:00 2001 From: Jobdori Date: Thu, 9 Apr 2026 04:02:59 +0900 Subject: [PATCH] feat(api): add reasoning_effort field to MessageRequest and OpenAI-compat path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users of OpenAI-compatible reasoning models (o4-mini, o3, deepseek-r1, etc.) had no way to control reasoning effort — the field was missing from MessageRequest and never emitted in the request body. Changes: - Add `reasoning_effort: Option` to `MessageRequest` in types.rs - Annotated with skip_serializing_if = "Option::is_none" for clean JSON - Accepted values: "low", "medium", "high" (passed through verbatim) - In `build_chat_completion_request`, emit `"reasoning_effort"` when set - Two unit tests: - `reasoning_effort_is_included_when_set`: o4-mini + "high" → field present - `reasoning_effort_omitted_when_not_set`: gpt-4o, no field → absent Existing callers use `..Default::default()` and are unaffected. One struct-literal test that listed all fields explicitly updated with `reasoning_effort: None`. The CLI flag to expose this to users is a follow-up (ROADMAP #34 partial). This commit lands the foundational API-layer plumbing needed for that. Partial ROADMAP #34. --- .../crates/api/src/providers/openai_compat.rs | 34 +++++++++++++++++++ rust/crates/api/src/types.rs | 5 +++ 2 files changed, 39 insertions(+) diff --git a/rust/crates/api/src/providers/openai_compat.rs b/rust/crates/api/src/providers/openai_compat.rs index 3344765..140d93e 100644 --- a/rust/crates/api/src/providers/openai_compat.rs +++ b/rust/crates/api/src/providers/openai_compat.rs @@ -801,6 +801,10 @@ fn build_chat_completion_request(request: &MessageRequest, config: OpenAiCompatC payload["stop"] = json!(stop); } } + // reasoning_effort for OpenAI-compatible reasoning models (o4-mini, o3, etc.) + if let Some(effort) = &request.reasoning_effort { + payload["reasoning_effort"] = json!(effort); + } payload } @@ -1216,6 +1220,35 @@ mod tests { ); } + #[test] + fn reasoning_effort_is_included_when_set() { + let payload = build_chat_completion_request( + &MessageRequest { + model: "o4-mini".to_string(), + max_tokens: 1024, + messages: vec![InputMessage::user_text("think hard")], + reasoning_effort: Some("high".to_string()), + ..Default::default() + }, + OpenAiCompatConfig::openai(), + ); + assert_eq!(payload["reasoning_effort"], json!("high")); + } + + #[test] + fn reasoning_effort_omitted_when_not_set() { + let payload = build_chat_completion_request( + &MessageRequest { + model: "gpt-4o".to_string(), + max_tokens: 64, + messages: vec![InputMessage::user_text("hello")], + ..Default::default() + }, + OpenAiCompatConfig::openai(), + ); + assert!(payload.get("reasoning_effort").is_none()); + } + #[test] fn openai_streaming_requests_include_usage_opt_in() { let payload = build_chat_completion_request( @@ -1333,6 +1366,7 @@ mod tests { frequency_penalty: Some(0.5), presence_penalty: Some(0.3), stop: Some(vec!["\n".to_string()]), + reasoning_effort: None, }; let payload = build_chat_completion_request(&request, OpenAiCompatConfig::openai()); assert_eq!(payload["temperature"], 0.7); diff --git a/rust/crates/api/src/types.rs b/rust/crates/api/src/types.rs index 830b3de..e136a76 100644 --- a/rust/crates/api/src/types.rs +++ b/rust/crates/api/src/types.rs @@ -26,6 +26,11 @@ pub struct MessageRequest { pub presence_penalty: Option, #[serde(skip_serializing_if = "Option::is_none")] pub stop: Option>, + /// Reasoning effort level for OpenAI-compatible reasoning models (e.g. `o4-mini`). + /// Accepted values: `"low"`, `"medium"`, `"high"`. Omitted when `None`. + /// Silently ignored by backends that do not support it. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, } impl MessageRequest {