fix provider pool quota status handling

This commit is contained in:
fawney19
2026-05-07 11:28:44 +08:00
parent 01c9f9be49
commit fd44906bb7
37 changed files with 1844 additions and 500 deletions

View File

@@ -395,17 +395,38 @@ pub fn admin_pool_is_oauth_invalid(key: &StoredProviderCatalogKey, now_unix_secs
if key.auth_type.trim() != "oauth" {
return false;
}
if key
.oauth_invalid_reason
.as_deref()
.is_some_and(|value| !value.trim().is_empty())
{
return true;
if let Some(reason) = key.oauth_invalid_reason.as_deref().map(str::trim) {
let account_state = provider_status::resolve_pool_account_state(
None,
key.upstream_metadata.as_ref(),
Some(reason),
);
if account_state.blocked && !account_state.recoverable {
return true;
}
if admin_pool_reason_has_tag(reason, "[REFRESH_FAILED]") {
return key
.expires_at_unix_secs
.is_none_or(|value| value == 0 || value <= now_unix_secs);
}
if admin_pool_reason_has_tag(reason, "[REQUEST_FAILED]") {
return false;
}
if !reason.is_empty() {
return true;
}
}
key.expires_at_unix_secs
.is_some_and(|value| value > 0 && value <= now_unix_secs)
}
fn admin_pool_reason_has_tag(reason: &str, tag: &str) -> bool {
reason
.lines()
.map(str::trim)
.any(|line| line.starts_with(tag))
}
pub fn admin_pool_matches_quick_selector(
key: &StoredProviderCatalogKey,
selector: &str,

View File

@@ -396,7 +396,8 @@ fn codex_merge_invalid_reason(current: &str, candidate_reason: &str) -> String {
return current.to_string();
}
if current.starts_with(OAUTH_EXPIRED_PREFIX)
&& candidate_reason.starts_with(OAUTH_REQUEST_FAILED_PREFIX)
&& (candidate_reason.starts_with(OAUTH_REQUEST_FAILED_PREFIX)
|| candidate_reason.starts_with(OAUTH_REFRESH_FAILED_PREFIX))
{
return current.to_string();
}
@@ -872,9 +873,11 @@ pub fn parse_chatgpt_web_conversation_init_response(
#[cfg(test)]
mod tests {
use super::{
codex_runtime_invalid_reason, parse_chatgpt_web_conversation_init_response,
OAUTH_ACCOUNT_BLOCK_PREFIX, OAUTH_EXPIRED_PREFIX,
codex_build_invalid_state, codex_runtime_invalid_reason,
parse_chatgpt_web_conversation_init_response, OAUTH_ACCOUNT_BLOCK_PREFIX,
OAUTH_EXPIRED_PREFIX, OAUTH_REFRESH_FAILED_PREFIX, OAUTH_REQUEST_FAILED_PREFIX,
};
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
use serde_json::json;
#[test]
@@ -900,6 +903,73 @@ mod tests {
assert_eq!(codex_runtime_invalid_reason(403, Some("forbidden")), None);
}
#[test]
fn codex_invalid_state_keeps_oauth_expired_over_refresh_failure() {
let mut key = StoredProviderCatalogKey::new(
"key-1".to_string(),
"provider-1".to_string(),
"key-1".to_string(),
"oauth".to_string(),
None,
true,
)
.expect("key should build");
key.oauth_invalid_at_unix_secs = Some(100);
key.oauth_invalid_reason = Some(format!("{OAUTH_EXPIRED_PREFIX}session expired"));
assert_eq!(
codex_build_invalid_state(
&key,
format!("{OAUTH_REFRESH_FAILED_PREFIX}Token 续期失败"),
200,
),
(
Some(100),
Some(format!("{OAUTH_EXPIRED_PREFIX}session expired"))
)
);
assert_eq!(
codex_build_invalid_state(
&key,
format!("{OAUTH_REQUEST_FAILED_PREFIX}账号状态检查失败"),
200,
),
(
Some(100),
Some(format!("{OAUTH_EXPIRED_PREFIX}session expired"))
)
);
}
#[test]
fn codex_invalid_state_allows_account_block_to_override_oauth_expired() {
let mut key = StoredProviderCatalogKey::new(
"key-1".to_string(),
"provider-1".to_string(),
"key-1".to_string(),
"oauth".to_string(),
None,
true,
)
.expect("key should build");
key.oauth_invalid_at_unix_secs = Some(100);
key.oauth_invalid_reason = Some(format!("{OAUTH_EXPIRED_PREFIX}session expired"));
assert_eq!(
codex_build_invalid_state(
&key,
format!("{OAUTH_ACCOUNT_BLOCK_PREFIX}account has been deactivated"),
200,
),
(
Some(200),
Some(format!(
"{OAUTH_ACCOUNT_BLOCK_PREFIX}account has been deactivated"
))
)
);
}
#[test]
fn parses_chatgpt_web_image_quota_from_conversation_init() {
let parsed = parse_chatgpt_web_conversation_init_response(

View File

@@ -1,11 +1,6 @@
use serde_json::Value;
use std::collections::BTreeSet;
const OAUTH_ACCOUNT_BLOCK_PREFIX: &str = "[ACCOUNT_BLOCK] ";
const OAUTH_REFRESH_FAILED_PREFIX: &str = "[REFRESH_FAILED] ";
const OAUTH_EXPIRED_PREFIX: &str = "[OAUTH_EXPIRED] ";
const OAUTH_REQUEST_FAILED_PREFIX: &str = "[REQUEST_FAILED] ";
const ACCOUNT_BLOCK_REASON_KEYWORDS: &[&str] = &[
"suspended",
"banned",
@@ -326,55 +321,55 @@ fn resolve_from_metadata(
fn resolve_from_oauth_invalid_reason(reason: Option<&str>) -> Option<PoolAccountState> {
let text = clean_text(reason)?;
if text.starts_with(OAUTH_ACCOUNT_BLOCK_PREFIX) {
let cleaned = clean_text(Some(text.trim_start_matches(OAUTH_ACCOUNT_BLOCK_PREFIX)))
.unwrap_or_else(|| "账号异常".to_string());
let (code, label) = classify_block_reason(&cleaned);
if let Some(cleaned) = tagged_reason(&text, "ACCOUNT_BLOCK") {
let reason = if cleaned.is_empty() {
"账号异常".to_string()
} else {
cleaned
};
let (code, label) = classify_block_reason(&reason);
return Some(PoolAccountState {
blocked: true,
code: Some(code.to_string()),
label: Some(label.to_string()),
reason: Some(cleaned),
reason: Some(reason),
source: Some("oauth_invalid".to_string()),
recoverable: false,
});
}
if text.starts_with(OAUTH_EXPIRED_PREFIX) {
let cleaned = clean_text(Some(text.trim_start_matches(OAUTH_EXPIRED_PREFIX)))
.unwrap_or_else(|| "OAuth Token 已过期且无法续期".to_string());
if let Some(cleaned) = tagged_reason(&text, "OAUTH_EXPIRED") {
let reason = if cleaned.is_empty() {
"OAuth Token 已过期且无法续期".to_string()
} else {
cleaned
};
return Some(PoolAccountState {
blocked: true,
code: Some("oauth_token_invalid".to_string()),
label: Some("Token 失效".to_string()),
reason: Some(cleaned),
reason: Some(reason),
source: Some("oauth_invalid".to_string()),
recoverable: false,
});
}
if text.starts_with(OAUTH_REFRESH_FAILED_PREFIX) {
let cleaned = clean_text(Some(text.trim_start_matches(OAUTH_REFRESH_FAILED_PREFIX)))
.unwrap_or_else(|| "OAuth Token 续期失败".to_string());
return Some(PoolAccountState {
blocked: true,
code: Some("oauth_token_invalid".to_string()),
label: Some("Token 失效".to_string()),
reason: Some(cleaned),
source: Some("oauth_refresh".to_string()),
recoverable: false,
});
}
if text.starts_with(OAUTH_REQUEST_FAILED_PREFIX) {
let cleaned = clean_text(Some(text.trim_start_matches(OAUTH_REQUEST_FAILED_PREFIX)))
.unwrap_or_else(|| "账号状态检查失败".to_string());
if let Some(cleaned) = tagged_reason(&text, "REQUEST_FAILED") {
let reason = if cleaned.is_empty() {
"账号状态检查失败".to_string()
} else {
cleaned
};
return Some(PoolAccountState {
blocked: false,
code: Some("oauth_request_failed".to_string()),
label: Some("请求失败".to_string()),
reason: Some(cleaned),
reason: Some(reason),
source: Some("oauth_request".to_string()),
recoverable: true,
});
}
if tagged_reason(&text, "REFRESH_FAILED").is_some() {
return None;
}
if text.starts_with('[') {
return None;
}
@@ -462,20 +457,8 @@ pub fn resolve_account_status_snapshot(
};
}
if let Some(cleaned) = tagged_reason(&text, "REFRESH_FAILED") {
let reason = if cleaned.is_empty() {
"OAuth Token 续期失败".to_string()
} else {
cleaned
};
return AccountStatusSnapshot {
code: "oauth_token_invalid".to_string(),
label: Some("Token 失效".to_string()),
reason: Some(reason),
blocked: true,
source: Some("oauth_refresh".to_string()),
recoverable: false,
};
if tagged_reason(&text, "REFRESH_FAILED").is_some() {
return AccountStatusSnapshot::default();
}
if text.starts_with('[') {
@@ -578,31 +561,30 @@ mod tests {
}
#[test]
fn resolves_refresh_failed_as_token_invalid_pool_state() {
fn ignores_refresh_failed_as_pool_account_block() {
let state = resolve_pool_account_state(
Some("codex"),
None,
Some("[REFRESH_FAILED] Token 续期失败 (401): refresh_token 已失效"),
);
assert!(state.blocked);
assert!(!state.recoverable);
assert_eq!(state.code.as_deref(), Some("oauth_token_invalid"));
assert_eq!(state.label.as_deref(), Some("Token 失效"));
assert!(!state.blocked);
assert_eq!(state.code.as_deref(), None);
assert_eq!(state.label.as_deref(), None);
assert!(!should_auto_remove_account_state(&state));
}
#[test]
fn account_snapshot_marks_refresh_failed_as_token_invalid() {
fn account_snapshot_ignores_refresh_failed_as_account_block() {
let snapshot = resolve_account_status_snapshot(
Some("codex"),
None,
Some("[REFRESH_FAILED] Token 续期失败 (401): refresh_token 已失效"),
);
assert_eq!(snapshot.code, "oauth_token_invalid");
assert_eq!(snapshot.label.as_deref(), Some("Token 失效"));
assert!(snapshot.blocked);
assert_eq!(snapshot.code, "ok");
assert_eq!(snapshot.label.as_deref(), None);
assert!(!snapshot.blocked);
assert!(!snapshot.recoverable);
}