File size: 10,481 Bytes
a21c316 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | //! Ultra Priority Tests for High-End Models (Opus 4.6/4.5)
//!
//! 这些测试验证高端模型(如 Claude Opus 4.6/4.5)优先使用 Ultra 账号的逻辑。
//!
//! ## 背景
//! 用户的账号池包含大量 Gemini Pro 账号和少量 Ultra 账号。当请求 Claude Opus 4.6 模型时,
//! 系统按配额优先的策略可能会选择 Pro 账号,但 Pro 账号无法访问 Opus 4.6,导致 API 返回错误。
//!
//! ## 解决方案
//! 当用户请求高端模型时,优先选择 Ultra 账号;只有 Ultra 账号都不可用时才降级到 Pro/Free 账号。
//!
//! ## 测试覆盖
//! - `test_is_ultra_required_model`: 验证模型识别逻辑
//! - `test_ultra_priority_for_high_end_models`: 验证 Ultra 优先于 Pro(即使 Pro 配额更高)
//! - `test_ultra_accounts_sorted_by_quota`: 验证同为 Ultra 时按配额排序
//! - `test_full_sorting_mixed_accounts`: 验证混合账号池的完整排序
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use crate::proxy::token_manager::ProxyToken;
/// 创建测试用的 ProxyToken
fn create_test_token(
email: &str,
tier: Option<&str>,
health_score: f32,
reset_time: Option<i64>,
remaining_quota: Option<i32>,
supported_models: Vec<&str>,
) -> ProxyToken {
let mut model_quotas = HashMap::new();
// 模拟配额:所有支持的模型都给予相同的剩余配额
for m in supported_models {
model_quotas.insert(m.to_string(), remaining_quota.unwrap_or(100));
}
ProxyToken {
account_id: email.to_string(),
access_token: "test_token".to_string(),
refresh_token: "test_refresh".to_string(),
expires_in: 3600,
timestamp: chrono::Utc::now().timestamp() + 3600,
email: email.to_string(),
account_path: PathBuf::from("/tmp/test"),
project_id: None,
subscription_tier: tier.map(|s| s.to_string()),
remaining_quota,
protected_models: HashSet::new(),
health_score,
reset_time,
validation_blocked: false,
validation_blocked_until: 0,
validation_url: None,
model_quotas,
model_limits: std::collections::HashMap::new(),
}
}
/// 需要 Ultra 账号的高端模型列表
const ULTRA_REQUIRED_MODELS: &[&str] = &[
"claude-opus-4-6",
"claude-opus-4-5",
"opus", // 通配匹配
];
/// 检查模型是否需要 Ultra 账号
fn is_ultra_required_model(model: &str) -> bool {
let lower = model.to_lowercase();
ULTRA_REQUIRED_MODELS.iter().any(|m| lower.contains(m))
}
/// 测试 is_ultra_required_model 辅助函数
#[test]
fn test_is_ultra_required_model() {
// 应该识别为高端模型
assert!(is_ultra_required_model("claude-opus-4-6"));
assert!(is_ultra_required_model("claude-opus-4-5"));
assert!(is_ultra_required_model("Claude-Opus-4-6")); // 大小写不敏感
assert!(is_ultra_required_model("CLAUDE-OPUS-4-5")); // 大小写不敏感
assert!(is_ultra_required_model("opus")); // 通配匹配
assert!(is_ultra_required_model("opus-4-6-latest"));
assert!(is_ultra_required_model("models/claude-opus-4-6"));
// 应该识别为普通模型
assert!(!is_ultra_required_model("claude-sonnet-4-6"));
assert!(!is_ultra_required_model("claude-sonnet"));
assert!(!is_ultra_required_model("gemini-1.5-flash"));
assert!(!is_ultra_required_model("gemini-2.0-pro"));
assert!(!is_ultra_required_model("claude-haiku"));
}
/// 模拟 token_manager.rs 中的排序逻辑 (更新后:始终 Tier 优先)
fn compare_tokens_for_model(a: &ProxyToken, b: &ProxyToken, _target_model: &str) -> Ordering {
let tier_priority = |tier: &Option<String>| {
let t = tier.as_deref().unwrap_or("").to_lowercase();
if t.contains("ultra") { 0 }
else if t.contains("pro") { 1 }
else if t.contains("free") { 2 }
else { 3 }
};
// Priority 0: 始终优先订阅等级 (Ultra > Pro > Free)
let tier_cmp = tier_priority(&a.subscription_tier)
.cmp(&tier_priority(&b.subscription_tier));
if tier_cmp != Ordering::Equal {
return tier_cmp;
}
// Priority 1: Quota (higher is better)
// 注意:这里简化了,直接取 remaining_quota,实际上生产代码取的是 model_quotas.get(target)
let quota_a = a.remaining_quota.unwrap_or(0);
let quota_b = b.remaining_quota.unwrap_or(0);
let quota_cmp = quota_b.cmp("a_a);
if quota_cmp != Ordering::Equal {
return quota_cmp;
}
// Priority 2: Health score
let health_cmp = b.health_score.partial_cmp(&a.health_score)
.unwrap_or(Ordering::Equal);
if health_cmp != Ordering::Equal {
return health_cmp;
}
Ordering::Equal
}
/// 模拟过滤逻辑
fn filter_tokens_by_capability(tokens: Vec<ProxyToken>, target_model: &str) -> Vec<ProxyToken> {
tokens.into_iter()
.filter(|t| t.model_quotas.contains_key(target_model))
.collect()
}
/// 测试高端模型排序:Ultra 账号优先于 Pro 账号(即使 Pro 配额更高)
#[test]
fn test_ultra_priority_for_high_end_models() {
// 创建测试账号:Ultra 低配额 vs Pro 高配额
// Ultra 账号支持 Opus 4.6
let ultra_low_quota = create_test_token("ultra@test.com", Some("ULTRA"), 1.0, None, Some(20), vec!["claude-opus-4-6", "claude-sonnet-4-6"]);
// Pro 账号不支持 Opus 4.6 (假设)
let pro_high_quota = create_test_token("pro@test.com", Some("PRO"), 1.0, None, Some(80), vec!["claude-sonnet-4-6"]);
// 1. 验证过滤逻辑
let tokens = vec![ultra_low_quota.clone(), pro_high_quota.clone()];
let filtered = filter_tokens_by_capability(tokens, "claude-opus-4-6");
assert_eq!(filtered.len(), 1, "Pro account should be filtered out for Opus 4.6");
assert_eq!(filtered[0].email, "ultra@test.com");
// 2. 验证排序逻辑 (针对 Sonnet,两者都支持)
// 即使 Pro 配额更高,由于新策略是 "Ultra First",Ultra 仍然排在前面
assert_eq!(
compare_tokens_for_model(&ultra_low_quota, &pro_high_quota, "claude-sonnet-4-6"),
Ordering::Less, // Ultra 排在前面
"Sonnet should now prefer Ultra account over Pro (Strict Tier Policy)"
);
}
#[test]
fn test_capability_filtering() {
// Ultra 账号:有 Opus 4.6
let ultra = create_test_token("ultra@test.com", Some("ULTRA"), 1.0, None, Some(100), vec!["claude-opus-4-6"]);
// Pro 账号:无 Opus 4.6
let pro = create_test_token("pro@test.com", Some("PRO"), 1.0, None, Some(100), vec!["claude-sonnet-3-5"]);
// Future Pro 账号:有 Opus 4.6 (模拟未来可能开放)
let future_pro = create_test_token("future_pro@test.com", Some("PRO"), 1.0, None, Some(50), vec!["claude-opus-4-6"]);
let pool = vec![ultra.clone(), pro.clone(), future_pro.clone()];
// 1. 请求 Opus 4.6
let filtered_opus = filter_tokens_by_capability(pool.clone(), "claude-opus-4-6");
assert_eq!(filtered_opus.len(), 2, "Should retain Ultra and Future Pro");
// 验证 Pro 被移除
assert!(!filtered_opus.iter().any(|t| t.email == "pro@test.com"));
// 2. 排序 filtered_opus: Ultra 应该排在 Future Pro 前面 (Tier Priority)
let mut sorted_opus = filtered_opus.clone();
sorted_opus.sort_by(|a, b| compare_tokens_for_model(a, b, "claude-opus-4-6"));
assert_eq!(sorted_opus[0].email, "ultra@test.com", "Ultra should be prioritized over Pro even if Pro has capability");
assert_eq!(sorted_opus[1].email, "future_pro@test.com");
}
/// 测试排序:同为 Ultra 时按配额排序
#[test]
fn test_ultra_accounts_sorted_by_quota() {
let ultra_high = create_test_token("ultra_high@test.com", Some("ULTRA"), 1.0, None, Some(80), vec!["claude-opus-4-6"]);
let ultra_low = create_test_token("ultra_low@test.com", Some("ULTRA"), 1.0, None, Some(20), vec!["claude-opus-4-6"]);
// Opus 4.6: 同为 Ultra,高配额优先
assert_eq!(
compare_tokens_for_model(&ultra_high, &ultra_low, "claude-opus-4-6"),
Ordering::Less, // ultra_high 排在前面
"Among Ultra accounts, higher quota should come first"
);
}
/// 测试完整排序场景:混合账号池
#[test]
fn test_full_sorting_mixed_accounts() {
fn sort_tokens_for_model(tokens: &mut Vec<ProxyToken>, target_model: &str) {
tokens.sort_by(|a, b| compare_tokens_for_model(a, b, target_model));
}
// 创建混合账号池 (全部支持所有模型,简化测试)
let supported = vec!["claude-opus-4-6", "claude-sonnet-4-6"];
let ultra_high = create_test_token("ultra_high@test.com", Some("ULTRA"), 1.0, None, Some(80), supported.clone());
let ultra_low = create_test_token("ultra_low@test.com", Some("ULTRA"), 1.0, None, Some(20), supported.clone());
let pro_high = create_test_token("pro_high@test.com", Some("PRO"), 1.0, None, Some(90), supported.clone());
let pro_low = create_test_token("pro_low@test.com", Some("PRO"), 1.0, None, Some(30), supported.clone());
let free = create_test_token("free@test.com", Some("FREE"), 1.0, None, Some(100), supported.clone());
// 高端模型 (Opus 4.6) 排序
let mut tokens_opus = vec![pro_high.clone(), free.clone(), ultra_low.clone(), pro_low.clone(), ultra_high.clone()];
sort_tokens_for_model(&mut tokens_opus, "claude-opus-4-6");
let emails_opus: Vec<&str> = tokens_opus.iter().map(|t| t.email.as_str()).collect();
// 期望顺序: Ultra(高配额) > Ultra(低配额) > Pro(高配额) > Pro(低配额) > Free
assert_eq!(
emails_opus,
vec!["ultra_high@test.com", "ultra_low@test.com", "pro_high@test.com", "pro_low@test.com", "free@test.com"],
"Opus 4.6 should sort Ultra first, then by quota within each tier"
);
// 普通模型 (Sonnet) 排序
let mut tokens_sonnet = vec![pro_high.clone(), free.clone(), ultra_low.clone(), pro_low.clone(), ultra_high.clone()];
sort_tokens_for_model(&mut tokens_sonnet, "claude-sonnet-4-6");
let emails_sonnet: Vec<&str> = tokens_sonnet.iter().map(|t| t.email.as_str()).collect();
// 期望顺序: Ultra > Pro > Free (严格层级)
// Ultra 内按 quota: high > low
// Pro 内按 quota: high > low
assert_eq!(
emails_sonnet,
vec!["ultra_high@test.com", "ultra_low@test.com", "pro_high@test.com", "pro_low@test.com", "free@test.com"],
"Sonnet should now sort Ultra first, then Pro, then Free"
);
}
|