use std::env;
use std::sync::{LazyLock, RwLock};
use super::super::auth::env_value;
use super::super::opencode_models;
pub fn default_reasoning_effort(model_spec: &str) -> Option<String> {
let parsed = crate::llm::ParsedModelSpec::parse(model_spec);
if let Some(effort) = parsed.reasoning_effort.map(str::to_string) {
return Some(effort);
}
reasoning_effort_option(model_spec)
}
pub fn reasoning_effort_option(model_spec: &str) -> Option<String> {
if THINKING_OVERRIDE
.read()
.expect("thinking override lock poisoned")
.is_some()
|| env::var("OY_THINKING").is_ok()
|| env::var("OY_REASONING_EFFORT").is_ok()
{
return configured_reasoning_effort();
}
let parsed = crate::llm::ParsedModelSpec::parse(model_spec);
if parsed.reasoning_effort.is_some() {
return None;
}
let base_model = parsed.base_model;
let provider = parsed.provider_or_openai();
if is_moonshot_kimi_model(model_spec) {
return Some("none".to_string());
}
if let Some(effort) = opencode_reasoning_effort(provider, base_model) {
return Some(effort);
}
reasoning_capable_fallback(base_model).map(|s| s.to_string())
}
static THINKING_OVERRIDE: LazyLock<RwLock<Option<String>>> = LazyLock::new(|| RwLock::new(None));
pub fn set_thinking_override(value: Option<&str>) {
let mut guard = THINKING_OVERRIDE
.write()
.expect("thinking override lock poisoned");
match value {
Some("auto") | Some("") | None => *guard = None,
Some(v) => *guard = Some(v.to_string()),
}
}
pub fn get_thinking_effort() -> Option<String> {
THINKING_OVERRIDE
.read()
.expect("thinking override lock poisoned")
.clone()
.or_else(|| env_value("OY_THINKING"))
.or_else(|| env_value("OY_REASONING_EFFORT"))
}
fn configured_reasoning_effort() -> Option<String> {
get_thinking_effort().and_then(normalize_effort_value)
}
fn normalize_effort_value(value: String) -> Option<String> {
match value.trim().to_ascii_lowercase().as_str() {
"" | "auto" => None,
"off" | "false" | "0" | "none" => Some("none".to_string()),
"minimal" => Some("minimal".to_string()),
"low" => Some("low".to_string()),
"medium" => Some("medium".to_string()),
"high" | "true" | "1" | "on" => Some("high".to_string()),
_ => None,
}
}
pub fn reasoning_efforts_for(model_spec: &str) -> Vec<String> {
let parsed = crate::llm::ParsedModelSpec::parse(model_spec);
let provider = parsed.provider_or_openai();
let model_name = parsed.base_model;
if let Some(info) = opencode_models::find(provider, model_name) {
let efforts = info.reasoning_efforts();
if !efforts.is_empty() {
return efforts.iter().map(|s| s.to_string()).collect();
}
if info.supports_reasoning() {
return vec!["high".to_string()];
}
}
vec![
"minimal".to_string(),
"low".to_string(),
"medium".to_string(),
"high".to_string(),
]
}
fn is_moonshot_kimi_model(model_spec: &str) -> bool {
let lower = model_spec.to_ascii_lowercase();
lower.contains("moonshot") || lower.contains("kimi")
}
fn opencode_reasoning_effort(provider: &str, model_name: &str) -> Option<String> {
opencode_models::find(provider, model_name)
.and_then(|info| info.default_reasoning_effort().map(|s| s.to_string()))
}
pub(crate) fn reasoning_capable_fallback(model: &str) -> Option<&'static str> {
let model = model.to_ascii_lowercase();
let capable = model.starts_with("gpt-5")
|| model.contains("codex")
|| model.starts_with("o1")
|| model.starts_with("o3")
|| model.starts_with("o4")
|| model.starts_with("claude-3-7")
|| model.starts_with("claude-4")
|| model.starts_with("claude-sonnet-4")
|| model.starts_with("claude-opus-4")
|| model.starts_with("gemini-3");
capable.then_some("high")
}