修复: gen_stream判重+重试分类+状态机最短路径+既有测试修复
This commit is contained in:
@@ -495,7 +495,7 @@ pub(crate) async fn stream_llm(
|
||||
if full_text.is_empty() && tool_calls_acc.is_empty() {
|
||||
flush_delta(app_handle, conv_id, &mut pending_delta);
|
||||
return StreamResult::InitFailed {
|
||||
retryable: classify_status_or_class(&status_or_class),
|
||||
retryable: classify_status_with_body(&status_or_class, &raw),
|
||||
error: fmt_diag(
|
||||
provider.name(),
|
||||
DiagKind::MidStream,
|
||||
@@ -631,6 +631,34 @@ fn classify_status_or_class(status_or_class: &str) -> bool {
|
||||
|| lower.contains("broken pipe")
|
||||
}
|
||||
|
||||
/// 429 额度/配额耗尽特征检测:区分「额度耗尽型 429」(确定性,重试必失败)与
|
||||
/// 「普通瞬时限流 429」(可重试)。实证:anthropic/GLM 周月额度耗尽返回
|
||||
/// `[1310] 您已达到每周/每月使用上限,限额将在 X 重置`(code 1310)。
|
||||
/// 特征命中 → Fatal 不重试;未命中 → 交 is_status_retryable 正常判定。
|
||||
/// 入参 combined:状态码串 + 错误体原文(如 "HTTP 429 [1310] 已达到上限...")。
|
||||
fn is_quota_exhaustion(combined: &str) -> bool {
|
||||
let lower = combined.to_lowercase();
|
||||
let quota_markers = [
|
||||
"额度", "限额", "达到上限", "使用上限", "重置",
|
||||
"quota", "limit exceeded", "usage limit", "quota exhausted",
|
||||
"insufficient_quota",
|
||||
// 注意:不含 rate_limit_error——它是标准 429 错误类型(普通瞬时限流也带),
|
||||
// 误配会把可恢复的限流当额度耗尽降 Fatal。额度语义用上面明确词。
|
||||
];
|
||||
// 仅当文本同时含「429」与额度特征,才判定额度耗尽(避免误伤普通限流)。
|
||||
lower.contains("429")
|
||||
&& quota_markers.iter().any(|m| lower.contains(m))
|
||||
}
|
||||
|
||||
/// 额度耗尽型 429 → Fatal(确定性,不重试);否则交 classify_status_or_class 正常判定。
|
||||
/// 入参 body:错误体原文(provider bail 文本 / error 帧 message)。
|
||||
fn classify_status_with_body(status_or_class: &str, body: &str) -> bool {
|
||||
if is_quota_exhaustion(&format!("{status_or_class} {body}")) {
|
||||
return false;
|
||||
}
|
||||
classify_status_or_class(status_or_class)
|
||||
}
|
||||
|
||||
/// 据 provider 流式 error 帧的 message 文本分类是否可重试(A2-B12 / G4.2)。
|
||||
///
|
||||
/// openai/anthropic helper 仅将 error 帧的 `message` 字段透传进 chunk.error
|
||||
@@ -641,8 +669,10 @@ fn classify_status_or_class(status_or_class: &str) -> bool {
|
||||
/// - 其余 → retryable=true(保守,防误判瞬态为 Fatal)
|
||||
fn classify_error_frame_retryable(msg: &str, status_or_class: &str) -> bool {
|
||||
// 已有状态码/传输类 → 走既有单一分类源(显式 4xx Fatal,5xx/429/timeout/connect 可重试)
|
||||
// 额度耗尽型 429(如 [1310] 已达周/月上限)确定性失败,经 body 感知识别降 Fatal,
|
||||
// 不再被普通 429 路径误判可重试空耗。
|
||||
if status_or_class != "unknown" {
|
||||
return classify_status_or_class(status_or_class);
|
||||
return classify_status_with_body(status_or_class, msg);
|
||||
}
|
||||
// 无状态码:按错误类型关键词明确非重试签名降级 Fatal
|
||||
let lower = msg.to_lowercase();
|
||||
@@ -779,6 +809,53 @@ mod tests {
|
||||
assert!(classify_error_frame_retryable("stream error", &s2));
|
||||
}
|
||||
|
||||
// ---- is_quota_exhaustion / classify_status_with_body:额度耗尽型 429 不重试 ----
|
||||
|
||||
/// 额度耗尽型 429(anthropic/GLM 周月上限,含 [1310] 与「使用上限/重置」特征)→ 判定额度耗尽
|
||||
#[test]
|
||||
fn quota_exhaustion_429_detected() {
|
||||
let combined = "HTTP 429 [1310] 您已达到每周/每月使用上限,限额将在 2026-08-14 重置";
|
||||
assert!(is_quota_exhaustion(combined));
|
||||
assert!(!classify_status_with_body("HTTP 429", combined));
|
||||
}
|
||||
|
||||
/// 普通瞬时限流 429(无额度特征)→ 不判定额度耗尽,仍可重试
|
||||
#[test]
|
||||
fn transient_429_not_quota() {
|
||||
let combined = "HTTP 429 请求过于频繁,请稍后重试";
|
||||
assert!(!is_quota_exhaustion(combined));
|
||||
assert!(classify_status_with_body("HTTP 429", combined));
|
||||
}
|
||||
|
||||
/// 额度关键词但无 429 状态码 → 不判定额度耗尽(防误伤普通 5xx)
|
||||
#[test]
|
||||
fn quota_marker_without_429_not_detected() {
|
||||
assert!(!is_quota_exhaustion("HTTP 500 达到上限"));
|
||||
}
|
||||
|
||||
/// classify_status_with_body 非 429 路径行为不变(5xx 仍可重试)
|
||||
#[test]
|
||||
fn classify_status_with_body_5xx_retryable() {
|
||||
assert!(classify_status_with_body("HTTP 500", "Internal Server Error"));
|
||||
}
|
||||
|
||||
/// 普通瞬时限流 429 的标准 type=rate_limit_error 不应误判额度耗尽(仍可重试,防误伤)
|
||||
#[test]
|
||||
fn standard_rate_limit_error_not_quota() {
|
||||
let combined = r#"HTTP 429 {"type":"rate_limit_error","message":"请求过于频繁,请稍后重试"}"#;
|
||||
assert!(!is_quota_exhaustion(combined));
|
||||
assert!(classify_status_with_body("HTTP 429", combined));
|
||||
}
|
||||
|
||||
/// error 帧 message 含额度耗尽型 429 → 经 classify_error_frame_retryable 降 Fatal 不重试
|
||||
#[test]
|
||||
fn error_frame_quota_429_fatal() {
|
||||
let msg = "HTTP 429 [1310] 您已达到每周/每月使用上限,限额将在 2026-08-14 重置";
|
||||
let (s, _) = extract_error_diag_from_str(msg);
|
||||
assert_eq!(s, "HTTP 429");
|
||||
assert!(!classify_error_frame_retryable(msg, &s));
|
||||
}
|
||||
|
||||
// ---- extract_error_diag:业务层 bail(provider 串已含状态码)----
|
||||
|
||||
/// provider 在 non-2xx bail 的典型串:抠出 401(鉴权失败/Key 错)
|
||||
|
||||
Reference in New Issue
Block a user