use std::sync::{Mutex, PoisonError};
use tokio_util::sync::CancellationToken;
use super::error::EngineError;
pub(crate) const NONE: &str = "none";
pub(crate) const LADDER: [&str; 6] = ["minimal", "low", "medium", "high", "xhigh", "max"];
pub(crate) fn listed(message: &str) -> Vec<&'static str> {
let lower = message.to_lowercase();
let Some(at) = ["supported values", "supported levels"]
.iter()
.filter_map(|marker| lower.rfind(marker).map(|at| at + marker.len()))
.max()
else {
return Vec::new();
};
let sentence = lower[at..].split('.').next().unwrap_or_default();
let words: Vec<&str> = sentence
.split(|c: char| !c.is_ascii_alphanumeric())
.collect();
std::iter::once(NONE)
.chain(LADDER)
.filter(|value| words.contains(value))
.collect()
}
pub(crate) fn nearest(wish: &str, listed: &[&'static str]) -> Option<&'static str> {
let depths = LADDER
.iter()
.enumerate()
.filter(|(_, d)| listed.contains(d));
if wish == NONE {
return depths.map(|(_, d)| *d).next();
}
let asked = LADDER.iter().position(|d| *d == wish)?;
depths
.min_by_key(|(at, _)| (at.abs_diff(asked), *at))
.map(|(_, d)| *d)
}
#[derive(Debug, Default)]
pub(crate) struct EffortMemo(Mutex<Vec<(&'static str, Option<&'static str>)>>);
impl EffortMemo {
pub(crate) fn instead_of(&self, wish: &str) -> Option<Option<&'static str>> {
self.0
.lock()
.unwrap_or_else(PoisonError::into_inner)
.iter()
.find(|(refused, _)| *refused == wish)
.map(|(_, instead)| *instead)
}
pub(crate) fn learn(&self, wish: &'static str, instead: Option<&'static str>) -> bool {
let mut known = self.0.lock().unwrap_or_else(PoisonError::into_inner);
let new = !known.iter().any(|(refused, _)| *refused == wish);
if new {
known.push((wish, instead));
}
new
}
}
pub(crate) trait CarriesEffort {
fn effort(&self) -> Option<&'static str>;
fn respell_effort(&mut self, instead: Option<&'static str>);
}
pub(crate) trait EffortWire: Sync {
type Body: CarriesEffort + Send + Sync;
fn model(&self) -> &str;
fn efforts(&self) -> &EffortMemo;
fn send(
&self,
body: &Self::Body,
cancel: &CancellationToken,
) -> impl Future<Output = Result<Option<reqwest::Response>, EngineError>> + Send;
fn answer(&self, wish: &'static str, refused: &EngineError) -> Option<Option<&'static str>>;
}
pub(crate) async fn send_asking_again<W: EffortWire>(
wire: &W,
body: &mut W::Body,
cancel: &CancellationToken,
) -> Result<Option<reqwest::Response>, EngineError> {
let memo = wire.efforts();
if let Some(instead) = body.effort().and_then(|wish| memo.instead_of(wish)) {
body.respell_effort(instead);
}
let sent = body.effort();
let first = wire.send(body, cancel).await;
let (Some(wish), Err(refused)) = (sent, &first) else {
return first;
};
let Some(instead) = wire
.answer(wish, refused)
.filter(|instead| *instead != Some(wish))
else {
return first;
};
tracing::info!(
model = wire.model(),
refused = wish,
?instead,
"the model does not take this reasoning effort; asking again with the nearest it has"
);
body.respell_effort(instead);
let second = wire.send(body, cancel).await;
if matches!(second, Ok(Some(_))) {
memo.learn(wish, instead);
}
second
}
#[cfg(test)]
mod tests {
use super::*;
const OPENAI_NONE: &str = "Unsupported value: 'none' is not supported with the 'gpt-6.1-sol' model. Supported values are: 'low', 'medium', 'high', 'xhigh', and 'max'.";
const OPENAI_MINIMAL: &str = "Unsupported value: 'minimal' is not supported with the 'gpt-5.5' model. Supported values are: 'none', 'low', 'medium', 'high', and 'xhigh'.";
const OPENAI_O4: &str = "Unsupported value: 'xhigh' is not supported with the 'o4-mini' model. Supported values are: 'low', 'medium', and 'high'.";
const ANTHROPIC_XHIGH: &str = "This model does not support effort level 'xhigh'. Supported levels: high, low, max, medium.";
#[test]
fn the_list_is_read_in_both_spellings_and_in_the_scales_order() {
assert_eq!(
listed(OPENAI_NONE),
["low", "medium", "high", "xhigh", "max"]
);
assert_eq!(
listed(OPENAI_MINIMAL),
["none", "low", "medium", "high", "xhigh"]
);
assert_eq!(listed(ANTHROPIC_XHIGH), ["low", "medium", "high", "max"]);
}
#[test]
fn the_refused_value_is_not_read_as_listed() {
assert!(!listed(OPENAI_MINIMAL).contains(&"minimal"));
assert!(!listed(ANTHROPIC_XHIGH).contains(&"xhigh"));
}
#[test]
fn the_envelope_around_the_message_adds_nothing() {
let body = format!(
r#"engine (OpenAI Responses) returned status 400 Bad Request: {{"error": {{"message": "{OPENAI_NONE}", "type": "invalid_request_error", "param": "reasoning.effort", "code": "unsupported_value"}}}}"#
);
assert_eq!(listed(&body), ["low", "medium", "high", "xhigh", "max"]);
}
#[test]
fn a_refusal_that_lists_nothing_reads_as_nothing() {
assert!(listed("Invalid reasoning effort.").is_empty());
assert!(listed("Unsupported value: 'none' with the 'gpt-x' model.").is_empty());
assert!(listed("the budget is too low").is_empty());
}
#[test]
fn a_depth_becomes_the_nearest_listed_the_lower_on_a_tie() {
assert_eq!(nearest("minimal", &listed(OPENAI_NONE)), Some("low"));
assert_eq!(nearest("minimal", &listed(OPENAI_MINIMAL)), Some("low"));
assert_eq!(nearest("max", &listed(OPENAI_MINIMAL)), Some("xhigh"));
assert_eq!(nearest("xhigh", &listed(OPENAI_O4)), Some("high"));
assert_eq!(nearest("xhigh", &listed(ANTHROPIC_XHIGH)), Some("high"));
}
#[test]
fn none_becomes_the_lowest_depth_listed() {
assert_eq!(nearest(NONE, &listed(OPENAI_NONE)), Some("low"));
assert_eq!(nearest(NONE, &["medium", "high"]), Some("medium"));
}
#[test]
fn nothing_listed_offers_nothing() {
assert_eq!(nearest(NONE, &[]), None);
assert_eq!(nearest("minimal", &[]), None);
assert_eq!(nearest("high", &[NONE]), None);
assert_eq!(nearest("ultra", &["low", "high"]), None);
}
#[test]
fn the_memo_keeps_the_first_answer_per_value() {
let memo = EffortMemo::default();
assert_eq!(memo.instead_of("minimal"), None);
assert!(memo.learn("minimal", Some("low")));
assert!(memo.learn(NONE, None));
assert!(
!memo.learn("minimal", Some("high")),
"a second answer is not recorded, and says so"
);
assert_eq!(memo.instead_of("minimal"), Some(Some("low")));
assert_eq!(memo.instead_of(NONE), Some(None));
assert_eq!(memo.instead_of("max"), None);
}
}