use axum::http::HeaderMap;
use serde_json::Value;
pub(super) const ROUTING_MISMATCH_CAPABILITY_CLASS: &str = "routing_mismatch_capability";
fn header_value_str(headers: &HeaderMap, name: &str) -> Option<String> {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
}
fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() {
return true;
}
if haystack.len() < needle.len() {
return false;
}
haystack.windows(needle.len()).any(|w| w == needle)
}
fn looks_like_cloudflare_challenge_html(headers: &HeaderMap, body: &[u8]) -> bool {
let ct = headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_ascii_lowercase();
if !ct.starts_with("text/html") {
return false;
}
contains_bytes(body, b"__CF$cv$params")
|| contains_bytes(body, b"/cdn-cgi/")
|| contains_bytes(body, b"challenge-platform")
|| contains_bytes(body, b"cf-chl-")
}
fn looks_like_json(headers: &HeaderMap) -> bool {
let ct = headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_ascii_lowercase();
ct.contains("application/json") || ct.contains("+json")
}
fn json_get_str<'a>(v: &'a Value, key: &str) -> Option<&'a str> {
v.get(key).and_then(|x| x.as_str())
}
fn extract_error_message(v: &Value) -> Option<String> {
if let Some(err) = v.get("error") {
if let Some(msg) = json_get_str(err, "message") {
return Some(msg.to_string());
}
if let Some(msg) = json_get_str(err, "error") {
return Some(msg.to_string());
}
}
json_get_str(v, "message").map(|s| s.to_string())
}
fn extract_error_type(v: &Value) -> Option<String> {
if let Some(err) = v.get("error") {
if let Some(t) = json_get_str(err, "type") {
return Some(t.to_string());
}
if let Some(t) = json_get_str(err, "code") {
return Some(t.to_string());
}
}
if let Some(t) = json_get_str(v, "type")
&& t == "error"
&& let Some(err) = v.get("error")
&& let Some(et) = json_get_str(err, "type")
{
return Some(et.to_string());
}
None
}
pub(super) fn class_is_health_neutral(class: Option<&str>) -> bool {
matches!(class, Some(ROUTING_MISMATCH_CAPABILITY_CLASS))
}
fn capability_message_indicates_mismatch(message: &str) -> bool {
let m = message.to_ascii_lowercase();
let model_mismatch = m.contains("model")
&& (m.contains("not supported")
|| m.contains("unsupported")
|| m.contains("does not support")
|| m.contains("not available")
|| m.contains("unavailable")
|| m.contains("does not exist")
|| m.contains("do not have access")
|| m.contains("no access"));
let service_tier_mismatch = (m.contains("service_tier") || m.contains("service tier"))
&& (m.contains("not supported")
|| m.contains("unsupported")
|| m.contains("does not support")
|| m.contains("not available")
|| m.contains("unavailable"));
let reasoning_mismatch = (m.contains("reasoning")
|| m.contains("reasoning_effort")
|| m.contains("reasoning.effort"))
&& (m.contains("not supported")
|| m.contains("unsupported")
|| m.contains("does not support")
|| m.contains("not available")
|| m.contains("unavailable"));
model_mismatch || service_tier_mismatch || reasoning_mismatch
}
fn capability_type_indicates_mismatch(error_type: &str) -> bool {
let t = error_type.to_ascii_lowercase();
t.contains("unsupported_model")
|| t.contains("model_not_found")
|| t.contains("unsupported_value")
|| t.contains("unsupported_parameter")
}
pub(super) fn classify_upstream_response(
status_code: u16,
headers: &HeaderMap,
body: &[u8],
) -> (Option<String>, Option<String>, Option<String>) {
let cf_ray = header_value_str(headers, "cf-ray");
let server = header_value_str(headers, "server")
.unwrap_or_default()
.to_ascii_lowercase();
let looks_cf = server.contains("cloudflare") || cf_ray.is_some();
if looks_cf && status_code == 524 {
return (
Some("cloudflare_timeout".to_string()),
Some(
"Cloudflare 524:通常表示源站在规定时间内未返回响应;建议检查上游服务耗时、首包是否及时输出(SSE),以及 Cloudflare/WAF 规则。"
.to_string(),
),
cf_ray,
);
}
if looks_like_cloudflare_challenge_html(headers, body) {
return (
Some("cloudflare_challenge".to_string()),
Some(
"检测到 Cloudflare/WAF 拦截页(text/html + cdn-cgi/challenge 标记);通常不是 API JSON 错误,请检查 WAF 规则、UA/头部、以及是否需要放行该路径。"
.to_string(),
),
cf_ray,
);
}
if matches!(status_code, 400 | 404 | 409 | 422) && !body.is_empty() {
if looks_like_json(headers)
&& let Ok(v) = serde_json::from_slice::<Value>(body)
{
let msg = extract_error_message(&v);
let err_type = extract_error_type(&v);
if msg
.as_deref()
.is_some_and(capability_message_indicates_mismatch)
|| err_type
.as_deref()
.is_some_and(capability_type_indicates_mismatch)
&& msg
.as_deref()
.is_some_and(capability_message_indicates_mismatch)
{
return (
Some(ROUTING_MISMATCH_CAPABILITY_CLASS.to_string()),
Some(
"检测到模型/fast/service-tier/reasoning 能力不匹配;这属于路由兼容性问题,不应计入上游健康惩罚。"
.to_string(),
),
cf_ray,
);
}
} else {
let text = String::from_utf8_lossy(body);
if capability_message_indicates_mismatch(text.as_ref()) {
return (
Some(ROUTING_MISMATCH_CAPABILITY_CLASS.to_string()),
Some(
"检测到模型/fast/service-tier/reasoning 能力不匹配;这属于路由兼容性问题,不应计入上游健康惩罚。"
.to_string(),
),
cf_ray,
);
}
}
}
if matches!(status_code, 400 | 409 | 413 | 415 | 422)
&& looks_like_json(headers)
&& !body.is_empty()
&& let Ok(v) = serde_json::from_slice::<Value>(body)
{
if let Some(t) = extract_error_type(&v) {
let t = t.to_ascii_lowercase();
let non_retryable_type = t == "invalid_request_error"
|| t == "validation_error"
|| t == "bad_request"
|| t == "context_limit"
|| t == "context_length_exceeded"
|| t == "token_limit"
|| t == "content_filter";
if non_retryable_type {
return (
Some("client_error_non_retryable".to_string()),
Some(
"检测到更可能是请求参数/限制类错误(非瞬态);建议直接修正请求,而不是重试或切换 provider。"
.to_string(),
),
cf_ray,
);
}
}
if let Some(msg) = extract_error_message(&v) {
let m = msg.to_ascii_lowercase();
let non_retryable = (m.contains("tool_use") && m.contains("must be unique"))
|| m.contains("all messages must have non-empty content")
|| (m.contains("string should match pattern") && m.contains("srvtoolu_"))
|| (m.contains("unexpected") && m.contains("tool_use_id"))
|| (m.contains("json") && (m.contains("parse") || m.contains("invalid")))
|| (m.contains("schema") && m.contains("validation"));
if non_retryable {
return (
Some("client_error_non_retryable".to_string()),
Some(
"检测到更可能是请求格式/参数错误(非瞬态);建议直接修正请求,而不是重试或切换 provider。"
.to_string(),
),
cf_ray,
);
}
}
}
(None, None, cf_ray)
}