diff --git a/Cargo.lock b/Cargo.lock index f263072..5ac156f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,7 +226,7 @@ checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "cassady" -version = "0.3.3" +version = "0.3.4" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 3b90bd2..aa7bad1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cassady" -version = "0.3.3" +version = "0.3.4" edition = "2021" description = "Cassady/Cass minimal terminal coding agent" license = "MIT" diff --git a/src/providers/chatgpt_codex.rs b/src/providers/chatgpt_codex.rs index cf6c6bd..a9f058d 100644 --- a/src/providers/chatgpt_codex.rs +++ b/src/providers/chatgpt_codex.rs @@ -197,6 +197,8 @@ fn responses_body( body["reasoning"] = json!({"effort": "minimal", "summary": "auto"}); } else if let Some(effort) = reasoning_effort.request_value() { body["reasoning"] = json!({"effort": effort, "summary": "auto"}); + } else if reasoning_effort == ReasoningEffort::Off { + body["reasoning"] = json!({"effort": "none", "summary": "auto"}); } body } @@ -504,6 +506,24 @@ mod tests { ); } + #[test] + fn responses_body_sends_none_effort_when_reasoning_is_off() { + let body = responses_body( + "gpt-test", + vec![ModelMessage::User { + content: "hello".into(), + }], + Vec::new(), + ReasoningEffort::Off, + false, + ); + + assert_eq!( + body["reasoning"], + json!({"effort": "none", "summary": "auto"}) + ); + } + #[test] fn stream_parser_collects_text_and_function_call() { let (tx, _rx) = mpsc::unbounded_channel(); diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 1f67008..b3b926f 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -28,11 +28,13 @@ impl ProviderClient { match config.active_provider.kind.as_str() { DEFAULT_PROVIDER_KIND => { let api_key = config.resolved_api_key()?; - let reasoning_request_format = config - .model_metadata - .as_ref() + let model_metadata = config.model_metadata.as_ref(); + let reasoning_request_format = model_metadata .map(|model| model.reasoning.request_format) .unwrap_or_default(); + let reasoning_supported = model_metadata + .map(|model| model.reasoning.supported) + .unwrap_or(false); Ok(Self::OpenAiCompatible(OpenAiCompatibleProvider::new( OpenAiCompatibleSettings { model: config.model.clone(), @@ -40,6 +42,7 @@ impl ProviderClient { api_key, reasoning_effort: options.reasoning_effort, reasoning_request_format, + reasoning_supported, }, ))) } diff --git a/src/providers/openai_compatible.rs b/src/providers/openai_compatible.rs index 9d8341f..3d0f714 100644 --- a/src/providers/openai_compatible.rs +++ b/src/providers/openai_compatible.rs @@ -18,6 +18,7 @@ pub struct OpenAiCompatibleProvider { api_key: String, reasoning_effort: ReasoningEffort, reasoning_request_format: ReasoningRequestFormat, + reasoning_supported: bool, } #[derive(Debug, Clone)] @@ -27,6 +28,7 @@ pub struct OpenAiCompatibleSettings { pub api_key: String, pub reasoning_effort: ReasoningEffort, pub reasoning_request_format: ReasoningRequestFormat, + pub reasoning_supported: bool, } #[derive(Debug, Default)] @@ -45,6 +47,7 @@ impl OpenAiCompatibleProvider { api_key: settings.api_key, reasoning_effort: settings.reasoning_effort, reasoning_request_format: settings.reasoning_request_format, + reasoning_supported: settings.reasoning_supported, } } @@ -65,6 +68,7 @@ impl OpenAiCompatibleProvider { &mut body, self.reasoning_effort, self.reasoning_request_format, + self.reasoning_supported, ); let resp = self .client @@ -219,9 +223,17 @@ fn apply_reasoning_request( body: &mut Value, effort: ReasoningEffort, format: ReasoningRequestFormat, + supported: bool, ) { - let Some(effort) = effort.request_value() else { + if !supported { return; + } + let effort_str = match effort { + ReasoningEffort::Off => "none", + _ => match effort.request_value() { + Some(value) => value, + None => return, + }, }; let Value::Object(obj) = body else { return; @@ -230,11 +242,11 @@ fn apply_reasoning_request( ReasoningRequestFormat::ReasoningEffort => { obj.insert( "reasoning_effort".to_string(), - Value::String(effort.to_string()), + Value::String(effort_str.to_string()), ); } ReasoningRequestFormat::ReasoningObject => { - obj.insert("reasoning".to_string(), json!({ "effort": effort })); + obj.insert("reasoning".to_string(), json!({ "effort": effort_str })); } } } @@ -316,3 +328,60 @@ fn chat_url(base: &str) -> String { format!("{}/chat/completions", base.trim_end_matches('/')) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reasoning_effort_format_sends_none_when_off_and_supported() { + let mut body = json!({"model": "test"}); + apply_reasoning_request( + &mut body, + ReasoningEffort::Off, + ReasoningRequestFormat::ReasoningEffort, + true, + ); + assert_eq!( + body["reasoning_effort"], + Value::String("none".to_string()) + ); + } + + #[test] + fn reasoning_object_format_sends_none_when_off_and_supported() { + let mut body = json!({"model": "test"}); + apply_reasoning_request( + &mut body, + ReasoningEffort::Off, + ReasoningRequestFormat::ReasoningObject, + true, + ); + assert_eq!(body["reasoning"], json!({ "effort": "none" })); + } + + #[test] + fn reasoning_sends_nothing_when_unsupported_even_if_off() { + let mut body = json!({"model": "test"}); + apply_reasoning_request( + &mut body, + ReasoningEffort::Off, + ReasoningRequestFormat::ReasoningEffort, + false, + ); + assert!(body.get("reasoning_effort").is_none()); + assert!(body.get("reasoning").is_none()); + } + + #[test] + fn reasoning_sends_nothing_when_unsupported_even_if_high() { + let mut body = json!({"model": "test"}); + apply_reasoning_request( + &mut body, + ReasoningEffort::High, + ReasoningRequestFormat::ReasoningObject, + false, + ); + assert!(body.get("reasoning").is_none()); + } +}