mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat(rust): 支持 image 同步转流式 SSE、free_team_first 调度预设及账户错误自动重试
- openai:image 同步响应桥接为流式 SSE(image_generation.completed / image_edit.completed 事件) - image 请求解析与前置校验移除模型白名单,支持任意自定义模型名 - 调度器新增 free_team_first 预设(mode: both / free_only / team_only) - pool config 解析重构:新增 POOL_ALLOWED_SCHEDULING_PRESETS 白名单,规范化 mode 字段 - 错误分类器新增账户/账单错误模式集,升级为 RetryUpstreamFailure 而非 StopSemanticClientError - 修复 stream execution 中上游 headers 与输出 headers 混用导致 content-type 判断错误的问题 - codex image 工具始终写入 action 字段(generate/edit),仅 generate 操作填充默认 size/quality - 前端:pool 节点组只展示实际执行过的候选节点,全部 skipped 时折叠为最后一个节点 - Redis 测试就绪检测从 TCP 连接改为 PING/PONG 协议验证
This commit is contained in:
@@ -82,6 +82,30 @@ const RETRYABLE_RATE_LIMIT_PATTERNS: &[&str] = &[
|
||||
"quota hit",
|
||||
];
|
||||
|
||||
const RETRYABLE_ACCOUNT_OR_BILLING_PATTERNS: &[&str] = &[
|
||||
"organization has been disabled",
|
||||
"organization_disabled",
|
||||
"account has been disabled",
|
||||
"account_disabled",
|
||||
"account has been deactivated",
|
||||
"account_deactivated",
|
||||
"account deactivated",
|
||||
"account suspended",
|
||||
"account banned",
|
||||
"subscription inactive",
|
||||
"payment_required",
|
||||
"payment required",
|
||||
"insufficient_quota",
|
||||
"insufficient quota",
|
||||
"quota exhausted",
|
||||
"credits exhausted",
|
||||
"credit balance",
|
||||
"credit limit",
|
||||
"verify your account",
|
||||
"account verification",
|
||||
"verification required",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
struct ParsedLocalErrorResponse {
|
||||
type_name: Option<String>,
|
||||
@@ -169,6 +193,10 @@ pub(crate) fn classify_local_failover(
|
||||
return LocalFailoverClassification::RetrySemanticRateLimit;
|
||||
}
|
||||
|
||||
if is_semantic_account_or_billing_error(input.status_code, &parsed_error) {
|
||||
return LocalFailoverClassification::RetryUpstreamFailure;
|
||||
}
|
||||
|
||||
if is_semantic_client_error(input.status_code, &parsed_error) {
|
||||
return LocalFailoverClassification::StopSemanticClientError;
|
||||
}
|
||||
@@ -379,6 +407,21 @@ fn is_semantic_rate_limit_error(status_code: u16, parsed: &ParsedLocalErrorRespo
|
||||
.any(|pattern| search_text.contains(&pattern.to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
fn is_semantic_account_or_billing_error(
|
||||
status_code: u16,
|
||||
parsed: &ParsedLocalErrorResponse,
|
||||
) -> bool {
|
||||
if status_code < 400 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let search_text = semantic_search_text(parsed);
|
||||
!search_text.is_empty()
|
||||
&& RETRYABLE_ACCOUNT_OR_BILLING_PATTERNS
|
||||
.iter()
|
||||
.any(|pattern| search_text.contains(&pattern.to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
fn local_failover_regex_rule_matches(
|
||||
rule: &LocalFailoverRegexRule,
|
||||
response_text: &str,
|
||||
@@ -539,6 +582,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifier_retries_account_and_billing_errors_before_client_error_stop() {
|
||||
assert_eq!(
|
||||
classify_local_failover(
|
||||
&LocalFailoverPolicy::default(),
|
||||
LocalFailoverInput::new(
|
||||
403,
|
||||
Some(
|
||||
"{\"error\":{\"type\":\"invalid_request_error\",\"message\":\"verify your account before continuing\"}}"
|
||||
)
|
||||
)
|
||||
),
|
||||
LocalFailoverClassification::RetryUpstreamFailure
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
classify_local_failover(
|
||||
&LocalFailoverPolicy::default(),
|
||||
LocalFailoverInput::new(
|
||||
402,
|
||||
Some(
|
||||
"{\"error\":{\"type\":\"invalid_request_error\",\"message\":\"payment required: credit balance exhausted\"}}"
|
||||
)
|
||||
)
|
||||
),
|
||||
LocalFailoverClassification::RetryUpstreamFailure
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifier_keeps_embedded_rate_limit_error_in_success_response_on_default_path() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user