fn is_model_unavailable_error(message: &str) -> bool {
let lower = message.to_lowercase();
lower.contains("model not found")
|| lower.contains("not supported model")
|| lower.contains("model_not_found")
|| lower.contains("unknown model")
|| lower.contains("invalid model")
|| lower.contains("no such model")
|| lower.contains("unsupported model")
|| (lower.contains("model") && lower.contains("is not supported"))
|| (lower.contains("model") && lower.contains("does not exist"))
|| (lower.contains("model") && lower.contains("is not available"))
|| (lower.contains("model") && lower.contains("has been deprecated"))
|| (lower.contains("model") && lower.contains("no longer available"))
}
pub fn extract_model_unavailable_json_error(raw: &str) -> Option<String> {
let trimmed = raw.trim();
extract_from_json_error(trimmed)
}
pub fn extract_model_unavailable_stderr_line(raw: &str) -> Option<String> {
let trimmed = raw.trim();
if let Some(message) = extract_from_json_error(trimmed) {
return Some(message);
}
if is_explicit_plain_error(trimmed) && is_model_unavailable_error(trimmed) {
return Some(trimmed.to_string());
}
None
}
fn is_explicit_plain_error(message: &str) -> bool {
let lower = message.to_lowercase();
lower.starts_with("error:")
|| lower.starts_with("api error:")
|| lower.starts_with("model not found")
|| lower.starts_with("not supported model")
|| lower.starts_with("unknown model")
|| lower.starts_with("unsupported model")
|| (lower.starts_with("the '") && lower.contains(" model "))
}
fn extract_from_json_error(json_str: &str) -> Option<String> {
let value: serde_json::Value = serde_json::from_str(json_str).ok()?;
let explicit_error = value.get("error").is_some()
|| value.get("is_error").and_then(serde_json::Value::as_bool) == Some(true)
|| value.get("type").and_then(serde_json::Value::as_str) == Some("error");
if !explicit_error {
return None;
}
for candidate in [
value.get("message"),
value.get("error").and_then(|e| e.get("message")),
value.get("error").and_then(|e| e.get("data")).and_then(|d| d.get("message")),
] {
if let Some(msg) = candidate.and_then(serde_json::Value::as_str) {
let msg = msg.trim();
if !msg.is_empty() && is_model_unavailable_error(msg) {
return Some(msg.to_string());
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_real_agent_failures() {
assert!(is_model_unavailable_error("Model not found: glm-4.7/."));
assert!(is_model_unavailable_error("Not supported model mimo-v2.5-pro-ultraspeed: Param Incorrect"));
assert!(is_model_unavailable_error(
"The 'gpt-4.1-nano' model is not supported when using Codex with a ChatGPT account."
));
}
#[test]
fn ignores_unrelated_and_transient_errors() {
assert!(!is_model_unavailable_error("rate limit exceeded"));
assert!(!is_model_unavailable_error("Insufficient balance. Manage your billing"));
assert!(!is_model_unavailable_error("Authentication failed. Please log in"));
assert!(!is_model_unavailable_error("connection reset by peer"));
}
#[test]
fn extracts_from_json_error_events() {
let opencode = r#"{"type":"error","error":{"name":"UnknownError","data":{"message":"Model not found: glm-4.7/."}}}"#;
assert_eq!(
extract_model_unavailable_json_error(opencode).as_deref(),
Some("Model not found: glm-4.7/.")
);
let codex = r#"{"type":"error","status":400,"error":{"type":"invalid_request_error","message":"The 'gpt-4.1-nano' model is not supported when using Codex with a ChatGPT account."}}"#;
assert!(extract_model_unavailable_json_error(codex).is_some());
}
#[test]
fn ignores_json_rate_limit_events() {
let rl = r#"{"type":"error","error":{"message":"429 rate limit exceeded"}}"#;
assert!(extract_model_unavailable_json_error(rl).is_none());
}
#[test]
fn extracts_from_explicit_stderr_line_without_a_length_rule() {
let detail = "x".repeat(600);
let line = format!("Error: unknown model 'foo': {detail}");
assert_eq!(
extract_model_unavailable_stderr_line(&line).as_deref(),
Some(line.as_str())
);
}
#[test]
fn ignores_plain_prose_and_non_error_json() {
assert!(extract_model_unavailable_stderr_line(
"The report says an unknown model would be rejected"
)
.is_none());
let message = r#"{"type":"agent_message","message":"Error: unknown model 'foo'"}"#;
assert!(extract_model_unavailable_json_error(message).is_none());
}
}