use serde_json::Value;
pub use switchyard_protocol::LlmClientError;
pub type Result<T> = std::result::Result<T, LlmClientError>;
pub(crate) fn is_overflow_body<F>(body: &str, structured_check: F, phrases: &[&str]) -> bool
where
F: Fn(&Value) -> bool,
{
if let Ok(value) = serde_json::from_str::<Value>(body) {
if structured_check(&value) {
return true;
}
if let Some(message) = value
.get("error")
.and_then(|err| err.get("message"))
.and_then(Value::as_str)
&& contains_any(message, phrases)
{
return true;
}
}
contains_any(body, phrases)
}
fn contains_any(message: &str, phrases: &[&str]) -> bool {
let lower = message.to_ascii_lowercase();
phrases.iter().any(|phrase| lower.contains(phrase))
}
#[cfg(test)]
mod tests {
use super::*;
const PHRASES: &[&str] = &["context window", "too long"];
fn never(_value: &Value) -> bool {
false
}
#[test]
fn structured_check_short_circuits() {
let body = r#"{"error":{"code":"context_length_exceeded","message":"unrelated"}}"#;
let matched = is_overflow_body(
body,
|value| {
value
.get("error")
.and_then(|err| err.get("code"))
.and_then(Value::as_str)
== Some("context_length_exceeded")
},
&[],
);
assert!(matched);
}
#[test]
fn falls_back_to_message_phrase_match() {
let body = r#"{"error":{"message":"prompt too long"}}"#;
assert!(is_overflow_body(body, never, PHRASES));
}
#[test]
fn matches_plain_text_body() {
assert!(is_overflow_body(
"plain text mentioning context window",
never,
PHRASES
));
}
#[test]
fn non_match_returns_false() {
let body = r#"{"error":{"message":"rate limit exceeded"}}"#;
assert!(!is_overflow_body(body, never, PHRASES));
}
}