use std::sync::atomic::{AtomicU64, Ordering};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::core::config::Effort;
fn openai_value(effort: Effort) -> &'static str {
match effort {
Effort::Minimal => "minimal",
Effort::Low => "low",
Effort::Medium => "medium",
Effort::High => "high",
}
}
fn anthropic_value(effort: Effort) -> &'static str {
match effort {
Effort::Minimal | Effort::Low => "low",
Effort::Medium => "medium",
Effort::High => "high",
}
}
#[must_use]
pub fn openai_supports_effort(model: &str) -> bool {
let bare = model
.rsplit('/')
.next()
.unwrap_or(model)
.trim()
.to_ascii_lowercase();
if bare.is_empty() || bare.contains("chat") {
return false;
}
bare.starts_with("o1")
|| bare.starts_with("o3")
|| bare.starts_with("o4")
|| bare.starts_with("gpt-5")
|| bare.starts_with("gpt-6")
|| bare.contains("codex")
}
pub fn apply_openai_chat(doc: &mut Value, effort: Effort) -> bool {
let Some(obj) = doc.as_object_mut() else {
return false;
};
if obj.contains_key("reasoning_effort") {
return false; }
let model = obj.get("model").and_then(Value::as_str).unwrap_or_default();
if !openai_supports_effort(model) {
return false;
}
obj.insert(
"reasoning_effort".to_string(),
Value::String(openai_value(effort).to_string()),
);
record(Provider::OpenAi);
true
}
pub fn apply_openai_responses(doc: &mut Value, effort: Effort) -> bool {
let Some(obj) = doc.as_object_mut() else {
return false;
};
let model = obj.get("model").and_then(Value::as_str).unwrap_or_default();
if !openai_supports_effort(model) {
return false;
}
if obj.get("reasoning").and_then(|r| r.get("effort")).is_some() {
return false; }
let reasoning = obj
.entry("reasoning")
.or_insert_with(|| Value::Object(serde_json::Map::new()));
let Some(map) = reasoning.as_object_mut() else {
return false; };
map.insert(
"effort".to_string(),
Value::String(openai_value(effort).to_string()),
);
record(Provider::OpenAi);
true
}
pub fn apply_anthropic(doc: &mut Value, effort: Effort) -> bool {
let Some(obj) = doc.as_object_mut() else {
return false;
};
let is_adaptive = obj
.get("thinking")
.and_then(|t| t.get("type"))
.and_then(Value::as_str)
== Some("adaptive");
if !is_adaptive {
return false;
}
if obj
.get("output_config")
.and_then(|o| o.get("effort"))
.is_some()
{
return false; }
let output_config = obj
.entry("output_config")
.or_insert_with(|| Value::Object(serde_json::Map::new()));
let Some(map) = output_config.as_object_mut() else {
return false;
};
map.insert(
"effort".to_string(),
Value::String(anthropic_value(effort).to_string()),
);
record(Provider::Anthropic);
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GeminiStyle {
Level,
Budget,
}
fn gemini_style(model: &str) -> Option<GeminiStyle> {
let bare = model
.rsplit('/')
.next()
.unwrap_or(model)
.trim()
.to_ascii_lowercase();
let rest = bare.strip_prefix("gemini-")?;
let major: u32 = rest
.chars()
.take_while(char::is_ascii_digit)
.collect::<String>()
.parse()
.ok()?;
if major >= 3 {
Some(GeminiStyle::Level)
} else if rest.starts_with("2.5") && !rest.contains("flash-lite") {
Some(GeminiStyle::Budget)
} else {
None
}
}
fn google_level(effort: Effort) -> &'static str {
match effort {
Effort::Minimal => "minimal",
Effort::Low => "low",
Effort::Medium => "medium",
Effort::High => "high",
}
}
fn google_budget(effort: Effort) -> i64 {
match effort {
Effort::Minimal => 512,
Effort::Low => 4096,
Effort::Medium => 8192,
Effort::High => 24576,
}
}
pub fn apply_google(doc: &mut Value, effort: Effort, model: Option<&str>) -> bool {
let Some(style) = model.and_then(gemini_style) else {
return false;
};
let Some(obj) = doc.as_object_mut() else {
return false;
};
let gen_cfg = obj
.entry("generationConfig")
.or_insert_with(|| Value::Object(serde_json::Map::new()));
let Some(gen_cfg) = gen_cfg.as_object_mut() else {
return false; };
let tc = gen_cfg
.entry("thinkingConfig")
.or_insert_with(|| Value::Object(serde_json::Map::new()));
let Some(tc) = tc.as_object_mut() else {
return false;
};
if tc.contains_key("thinkingLevel") || tc.contains_key("thinkingBudget") {
return false; }
match style {
GeminiStyle::Level => {
tc.insert(
"thinkingLevel".to_string(),
Value::String(google_level(effort).to_string()),
);
}
GeminiStyle::Budget => {
tc.insert(
"thinkingBudget".to_string(),
Value::Number(google_budget(effort).into()),
);
}
}
record(Provider::Google);
true
}
#[derive(Debug, Clone, Copy)]
enum Provider {
OpenAi,
Anthropic,
Google,
}
static OPENAI_STEERED: AtomicU64 = AtomicU64::new(0);
static ANTHROPIC_STEERED: AtomicU64 = AtomicU64::new(0);
static GOOGLE_STEERED: AtomicU64 = AtomicU64::new(0);
fn record(provider: Provider) {
match provider {
Provider::OpenAi => &OPENAI_STEERED,
Provider::Anthropic => &ANTHROPIC_STEERED,
Provider::Google => &GOOGLE_STEERED,
}
.fetch_add(1, Ordering::Relaxed);
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct EffortStats {
pub mode: String,
pub openai_steered: u64,
pub anthropic_steered: u64,
pub google_steered: u64,
}
#[must_use]
pub fn snapshot(active: Option<Effort>) -> EffortStats {
EffortStats {
mode: active.map_or("off", Effort::label).to_string(),
openai_steered: OPENAI_STEERED.load(Ordering::Relaxed),
anthropic_steered: ANTHROPIC_STEERED.load(Ordering::Relaxed),
google_steered: GOOGLE_STEERED.load(Ordering::Relaxed),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn openai_support_detection() {
for m in [
"gpt-5",
"gpt-5.5",
"gpt-5.4",
"gpt-5-codex",
"gpt-5.1-codex-max",
"o1",
"o1-mini",
"o3",
"o3-mini",
"o4-mini",
"openai/gpt-5.5", "openrouter/openai/o3", ] {
assert!(
openai_supports_effort(m),
"{m} must support reasoning effort"
);
}
for m in [
"gpt-4o",
"gpt-4.1",
"gpt-4-turbo",
"gpt-3.5-turbo",
"gpt-5-chat-latest", "gpt-5.1-chat-latest",
"",
" ",
] {
assert!(
!openai_supports_effort(m),
"{m:?} must NOT support reasoning effort"
);
}
}
#[test]
fn openai_chat_sets_effort_on_reasoning_model() {
let mut doc = serde_json::json!({"model": "gpt-5.5", "messages": []});
assert!(apply_openai_chat(&mut doc, Effort::Low));
assert_eq!(doc["reasoning_effort"], "low");
}
#[test]
fn openai_chat_respects_client_value() {
let mut doc = serde_json::json!({
"model": "gpt-5.5", "reasoning_effort": "high", "messages": []
});
assert!(
!apply_openai_chat(&mut doc, Effort::Low),
"a client-set reasoning_effort must never be overridden"
);
assert_eq!(doc["reasoning_effort"], "high");
}
#[test]
fn openai_chat_skips_non_reasoning_model() {
let mut doc = serde_json::json!({"model": "gpt-4o", "messages": []});
assert!(
!apply_openai_chat(&mut doc, Effort::Low),
"a non-reasoning model must be skipped (would 400)"
);
assert!(doc.get("reasoning_effort").is_none());
}
#[test]
fn openai_responses_sets_nested_effort_and_preserves_siblings() {
let mut doc = serde_json::json!({
"model": "gpt-5.5",
"reasoning": {"summary": "auto"},
"input": []
});
assert!(apply_openai_responses(&mut doc, Effort::Medium));
assert_eq!(doc["reasoning"]["effort"], "medium");
assert_eq!(
doc["reasoning"]["summary"], "auto",
"existing reasoning.* fields must be preserved"
);
}
#[test]
fn openai_responses_creates_reasoning_object_when_absent() {
let mut doc = serde_json::json!({"model": "o3", "input": []});
assert!(apply_openai_responses(&mut doc, Effort::Minimal));
assert_eq!(doc["reasoning"]["effort"], "minimal");
}
#[test]
fn openai_responses_respects_client_value() {
let mut doc = serde_json::json!({
"model": "gpt-5.5", "reasoning": {"effort": "high"}, "input": []
});
assert!(!apply_openai_responses(&mut doc, Effort::Low));
assert_eq!(doc["reasoning"]["effort"], "high");
}
#[test]
fn anthropic_dials_existing_adaptive_request() {
let mut doc = serde_json::json!({
"model": "claude-opus-4-8",
"thinking": {"type": "adaptive"},
"messages": []
});
assert!(apply_anthropic(&mut doc, Effort::Low));
assert_eq!(doc["output_config"]["effort"], "low");
}
#[test]
fn anthropic_minimal_collapses_to_low() {
let mut doc = serde_json::json!({
"thinking": {"type": "adaptive"}, "messages": []
});
assert!(apply_anthropic(&mut doc, Effort::Minimal));
assert_eq!(
doc["output_config"]["effort"], "low",
"Anthropic has no `minimal`; it must collapse onto `low`"
);
}
#[test]
fn anthropic_skips_when_thinking_absent() {
let mut doc = serde_json::json!({"model": "claude-opus-4-8", "messages": []});
assert!(!apply_anthropic(&mut doc, Effort::Low));
assert!(doc.get("output_config").is_none());
}
#[test]
fn anthropic_skips_non_adaptive_thinking() {
let mut doc = serde_json::json!({
"thinking": {"type": "enabled", "budget_tokens": 4096}, "messages": []
});
assert!(!apply_anthropic(&mut doc, Effort::Low));
assert!(doc.get("output_config").is_none());
}
#[test]
fn anthropic_respects_client_value() {
let mut doc = serde_json::json!({
"thinking": {"type": "adaptive"},
"output_config": {"effort": "high"},
"messages": []
});
assert!(!apply_anthropic(&mut doc, Effort::Low));
assert_eq!(doc["output_config"]["effort"], "high");
}
#[test]
fn gemini_style_detection() {
use GeminiStyle::{Budget, Level};
for m in [
"gemini-3-pro",
"gemini-3.5-flash",
"google/gemini-3-pro",
"gemini-4-pro",
] {
assert_eq!(gemini_style(m), Some(Level), "{m} → Level");
}
for m in [
"gemini-2.5-pro",
"gemini-2.5-flash",
"openrouter/google/gemini-2.5-flash",
] {
assert_eq!(gemini_style(m), Some(Budget), "{m} → Budget");
}
for m in [
"gemini-2.5-flash-lite",
"gemini-2.0-flash",
"gemini-1.5-pro",
"gpt-5",
"",
] {
assert_eq!(gemini_style(m), None, "{m} → None");
}
}
#[test]
fn google_3x_sets_thinking_level() {
let mut doc = serde_json::json!({"contents": []});
assert!(apply_google(&mut doc, Effort::Medium, Some("gemini-3-pro")));
assert_eq!(
doc["generationConfig"]["thinkingConfig"]["thinkingLevel"],
"medium"
);
}
#[test]
fn google_3x_minimal_maps_directly() {
let mut doc = serde_json::json!({"contents": []});
assert!(apply_google(
&mut doc,
Effort::Minimal,
Some("gemini-3.5-flash")
));
assert_eq!(
doc["generationConfig"]["thinkingConfig"]["thinkingLevel"],
"minimal"
);
}
#[test]
fn google_25_sets_thinking_budget_in_range() {
let mut doc = serde_json::json!({"contents": []});
assert!(apply_google(&mut doc, Effort::Low, Some("gemini-2.5-pro")));
assert_eq!(
doc["generationConfig"]["thinkingConfig"]["thinkingBudget"],
4096
);
}
#[test]
fn google_preserves_existing_generation_config() {
let mut doc = serde_json::json!({
"contents": [],
"generationConfig": {"temperature": 0.2}
});
assert!(apply_google(&mut doc, Effort::High, Some("gemini-3-pro")));
assert_eq!(doc["generationConfig"]["temperature"], 0.2);
assert_eq!(
doc["generationConfig"]["thinkingConfig"]["thinkingLevel"],
"high"
);
}
#[test]
fn google_skips_flash_lite_to_avoid_enabling_thinking() {
let mut doc = serde_json::json!({"contents": []});
assert!(!apply_google(
&mut doc,
Effort::Low,
Some("gemini-2.5-flash-lite")
));
assert!(doc.get("generationConfig").is_none());
}
#[test]
fn google_skips_unknown_model_and_missing_model() {
let mut a = serde_json::json!({"contents": []});
assert!(!apply_google(&mut a, Effort::Low, Some("gemini-2.0-flash")));
assert!(a.get("generationConfig").is_none());
let mut b = serde_json::json!({"contents": []});
assert!(!apply_google(&mut b, Effort::Low, None));
assert!(b.get("generationConfig").is_none());
}
#[test]
fn google_respects_client_thinking_level() {
let mut doc = serde_json::json!({
"contents": [],
"generationConfig": {"thinkingConfig": {"thinkingLevel": "high"}}
});
assert!(!apply_google(&mut doc, Effort::Low, Some("gemini-3-pro")));
assert_eq!(
doc["generationConfig"]["thinkingConfig"]["thinkingLevel"],
"high"
);
}
#[test]
fn google_never_sends_both_fields() {
let mut doc = serde_json::json!({
"contents": [],
"generationConfig": {"thinkingConfig": {"thinkingBudget": 1024}}
});
assert!(!apply_google(&mut doc, Effort::Low, Some("gemini-3-pro")));
assert!(
doc["generationConfig"]["thinkingConfig"]
.get("thinkingLevel")
.is_none()
);
}
#[test]
fn google_is_deterministic_across_turns() {
let mk = || serde_json::json!({"contents": []});
let (mut a, mut b) = (mk(), mk());
apply_google(&mut a, Effort::Medium, Some("gemini-3-pro"));
apply_google(&mut b, Effort::Medium, Some("gemini-3-pro"));
assert_eq!(
serde_json::to_vec(&a).unwrap(),
serde_json::to_vec(&b).unwrap()
);
}
#[test]
fn snapshot_reports_active_mode() {
assert_eq!(snapshot(None).mode, "off");
assert_eq!(snapshot(Some(Effort::Minimal)).mode, "minimal");
assert_eq!(snapshot(Some(Effort::High)).mode, "high");
}
#[test]
fn appliers_are_deterministic_across_turns() {
let mk_chat = || serde_json::json!({"model": "gpt-5.5", "messages": []});
let (mut a, mut b) = (mk_chat(), mk_chat());
apply_openai_chat(&mut a, Effort::Low);
apply_openai_chat(&mut b, Effort::Low);
assert_eq!(
serde_json::to_vec(&a).unwrap(),
serde_json::to_vec(&b).unwrap()
);
let mk_anthropic = || serde_json::json!({"thinking": {"type": "adaptive"}, "messages": []});
let (mut c, mut d) = (mk_anthropic(), mk_anthropic());
apply_anthropic(&mut c, Effort::Medium);
apply_anthropic(&mut d, Effort::Medium);
assert_eq!(
serde_json::to_vec(&c).unwrap(),
serde_json::to_vec(&d).unwrap()
);
}
}