use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
pub const THINKING_MODE_ADAPTIVE: &str = "adaptive"; pub const THINKING_MODE_ENABLED: &str = "enabled"; pub const THINKING_MODE_NONE: &str = "none";
pub const THINKING_MODE_ORDER: &[&str] = &[
THINKING_MODE_ADAPTIVE,
THINKING_MODE_ENABLED,
THINKING_MODE_NONE,
];
pub const DEFAULT_BUDGET_TOKENS: u64 = 30000;
pub fn thinking_kw_for_mode(mode: &str) -> Option<serde_json::Value> {
match mode {
THINKING_MODE_ADAPTIVE => Some(serde_json::json!({"type": "adaptive"})),
THINKING_MODE_ENABLED => Some(serde_json::json!({
"type": "enabled",
"budget_tokens": DEFAULT_BUDGET_TOKENS,
})),
_ => None,
}
}
pub fn next_mode(current_mode: &str) -> Option<&'static str> {
match current_mode {
THINKING_MODE_ADAPTIVE => Some(THINKING_MODE_ENABLED),
THINKING_MODE_ENABLED => Some(THINKING_MODE_NONE),
_ => None,
}
}
pub fn is_thinking_mode_rejected(error_msg: &str, mode: &str) -> bool {
let msg = error_msg.to_lowercase();
let needle = format!("\"thinking.type.{}\"", mode);
if msg.contains(&needle) {
return true;
}
for &other_mode in THINKING_MODE_ORDER {
if other_mode == mode {
continue;
}
let other_needle = format!("\"thinking.type.{}\"", other_mode);
if msg.contains(&other_needle) {
return false;
}
}
if msg.contains("unknown") && msg.contains("thinking") {
return true;
}
if msg.contains("not supported") && msg.contains("thinking") {
return true;
}
false
}
static THINKING_CACHE: OnceLock<Mutex<HashMap<String, &'static str>>> = OnceLock::new();
fn cache() -> &'static Mutex<HashMap<String, &'static str>> {
THINKING_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn get_cached_mode(model_name: &str) -> &'static str {
let m = cache().lock().unwrap_or_else(|e| e.into_inner());
m.get(model_name).copied().unwrap_or(THINKING_MODE_ADAPTIVE)
}
pub fn cache_mode(model_name: &str, mode: &'static str) {
let mut m = cache().lock().unwrap_or_else(|e| e.into_inner());
m.insert(model_name.to_string(), mode);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kw_adaptive_has_no_budget() {
let kw = thinking_kw_for_mode(THINKING_MODE_ADAPTIVE).unwrap();
assert_eq!(kw["type"], "adaptive");
assert!(kw.get("budget_tokens").is_none());
}
#[test]
fn kw_enabled_has_budget() {
let kw = thinking_kw_for_mode(THINKING_MODE_ENABLED).unwrap();
assert_eq!(kw["type"], "enabled");
assert_eq!(kw["budget_tokens"], DEFAULT_BUDGET_TOKENS);
assert!(kw["budget_tokens"].as_u64().unwrap() >= 1024);
}
#[test]
fn kw_none_is_none() {
assert!(thinking_kw_for_mode(THINKING_MODE_NONE).is_none());
}
#[test]
fn kw_unknown_is_none() {
assert!(thinking_kw_for_mode("garbage").is_none());
assert!(thinking_kw_for_mode("").is_none());
}
#[test]
fn budget_tokens_default_is_30000() {
assert_eq!(DEFAULT_BUDGET_TOKENS, 30000);
assert!(DEFAULT_BUDGET_TOKENS < 32000); assert!(DEFAULT_BUDGET_TOKENS < 96000); }
#[test]
fn next_mode_adaptive_to_enabled() {
assert_eq!(
next_mode(THINKING_MODE_ADAPTIVE),
Some(THINKING_MODE_ENABLED)
);
}
#[test]
fn next_mode_enabled_to_none() {
assert_eq!(next_mode(THINKING_MODE_ENABLED), Some(THINKING_MODE_NONE));
}
#[test]
fn next_mode_none_is_terminal() {
assert_eq!(next_mode(THINKING_MODE_NONE), None);
}
#[test]
fn next_mode_unknown_is_none() {
assert_eq!(next_mode("foo"), None);
assert_eq!(next_mode(""), None);
}
#[test]
fn next_mode_full_progression() {
let mut mode = THINKING_MODE_ADAPTIVE;
let mut steps = vec![mode];
loop {
match next_mode(mode) {
Some(n) => {
steps.push(n);
mode = n;
}
None => break,
}
}
assert_eq!(
steps,
vec![
THINKING_MODE_ADAPTIVE,
THINKING_MODE_ENABLED,
THINKING_MODE_NONE
]
);
}
#[test]
fn next_mode_no_infinite_loop() {
let mut mode = THINKING_MODE_ADAPTIVE;
for _ in 0..100 {
match next_mode(mode) {
Some(n) => mode = n,
None => break,
}
}
assert_eq!(mode, THINKING_MODE_NONE);
}
#[test]
fn reject_official_adaptive() {
let err = r#"Error code: 400 - {"type":"error","error":{"type":"invalid_request_error","message":"\"thinking.type.adaptive\" is not supported"}}"#;
assert!(is_thinking_mode_rejected(err, THINKING_MODE_ADAPTIVE));
}
#[test]
fn reject_official_enabled() {
let err = r#"400 "thinking.type.enabled" is not supported"#;
assert!(is_thinking_mode_rejected(err, THINKING_MODE_ENABLED));
}
#[test]
fn reject_third_party_unknown_field() {
let err = "400 Bad Request: unknown field: thinking";
assert!(is_thinking_mode_rejected(err, THINKING_MODE_ADAPTIVE));
assert!(is_thinking_mode_rejected(err, THINKING_MODE_ENABLED));
}
#[test]
fn reject_generic_not_supported() {
let err = "thinking is not supported on this model";
assert!(is_thinking_mode_rejected(err, THINKING_MODE_ADAPTIVE));
}
#[test]
fn reject_case_insensitive() {
let err = "THINKING.TYPE.ENABLED IS NOT SUPPORTED";
assert!(is_thinking_mode_rejected(err, THINKING_MODE_ENABLED));
}
#[test]
fn reject_specific_mode_must_match() {
let err = r#"400 "thinking.type.adaptive" is not supported"#;
assert!(!is_thinking_mode_rejected(err, THINKING_MODE_ENABLED));
let err = r#"400 "thinking.type.enabled" is not supported"#;
assert!(!is_thinking_mode_rejected(err, THINKING_MODE_ADAPTIVE));
}
#[test]
fn not_reject_auth_error() {
let err = "401 authentication_error: invalid api key";
assert!(!is_thinking_mode_rejected(err, THINKING_MODE_ADAPTIVE));
}
#[test]
fn not_reject_model_not_found() {
let err = "404 model_not_found: unknown model";
assert!(!is_thinking_mode_rejected(err, THINKING_MODE_ADAPTIVE));
}
#[test]
fn not_reject_rate_limit() {
let err = "429 rate_limit_exceeded: too many requests";
assert!(!is_thinking_mode_rejected(err, THINKING_MODE_ADAPTIVE));
}
#[test]
fn not_reject_quota() {
let err = "insufficient_quota: please check your plan";
assert!(!is_thinking_mode_rejected(err, THINKING_MODE_ADAPTIVE));
}
#[test]
fn not_reject_generic_400() {
let err = "400 Bad Request: invalid parameter: temperature";
assert!(!is_thinking_mode_rejected(err, THINKING_MODE_ADAPTIVE));
}
#[test]
fn not_reject_network_error() {
let err = "ConnectionError: connection reset";
assert!(!is_thinking_mode_rejected(err, THINKING_MODE_ADAPTIVE));
}
#[test]
fn not_reject_empty_message() {
let err = "";
assert!(!is_thinking_mode_rejected(err, THINKING_MODE_ADAPTIVE));
}
#[test]
fn not_reject_thinking_mentioned_but_supported() {
let err = "thinking stream interrupted";
assert!(!is_thinking_mode_rejected(err, THINKING_MODE_ADAPTIVE));
}
#[test]
fn not_reject_unrelated_thinking_word() {
let err = "I was thinking about the problem and got 500";
assert!(!is_thinking_mode_rejected(err, THINKING_MODE_ADAPTIVE));
}
#[test]
fn cache_default_adaptive() {
assert_eq!(get_cached_mode("never-seen-model"), THINKING_MODE_ADAPTIVE);
}
#[test]
fn cache_round_trip() {
cache_mode("test-model-cache-rt", THINKING_MODE_ENABLED);
assert_eq!(
get_cached_mode("test-model-cache-rt"),
THINKING_MODE_ENABLED
);
cache_mode("test-model-cache-rt", THINKING_MODE_NONE);
assert_eq!(get_cached_mode("test-model-cache-rt"), THINKING_MODE_NONE);
}
#[test]
fn mode_order_is_correct() {
assert_eq!(
THINKING_MODE_ORDER,
&[
THINKING_MODE_ADAPTIVE,
THINKING_MODE_ENABLED,
THINKING_MODE_NONE
]
);
}
#[test]
fn mode_constants_are_unique() {
use std::collections::HashSet;
let set: HashSet<&str> = THINKING_MODE_ORDER.iter().copied().collect();
assert_eq!(set.len(), 3);
}
}