Send reasoning effort 'none' when reasoning is off

When a reasoning-capable model has reasoning effort set to off, send
'none' rather than omitting the field or sending 'off'. This applies
to both the ChatGPT Codex Responses API and OpenAI-compatible chat
completions (both reasoning_effort and reasoning object formats).
Non-reasoning models continue to send no reasoning field at all.

Bumps version to 0.3.4.
This commit is contained in:
2026-06-25 20:08:15 -05:00
parent d05bb87a23
commit d1e10a184b
5 changed files with 100 additions and 8 deletions
Generated
+1 -1
View File
@@ -226,7 +226,7 @@ checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
[[package]]
name = "cassady"
version = "0.3.3"
version = "0.3.4"
dependencies = [
"anyhow",
"async-trait",
+1 -1
View File
@@ -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"
+20
View File
@@ -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();
+6 -3
View File
@@ -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,
},
)))
}
+72 -3
View File
@@ -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());
}
}