feat(adaptive): 完善自适应 RPM 学习并将 Pool 调度状态与健康分解耦

- orchestration 新增 AdaptiveSuccess 效果,在成功回报路径上根据利用率窗口扩张 learned_rpm_limit
- 429 路径改用 429_observation/adjustment 记录以及基于历史的置信度评估,新增 last_rpm_peak 边界字段
- Pool 调度状态不再因 health_score 低或熔断而降级/拦截,前端同步移除相关按钮与文案兜底
- 新增前端 poolTrace 工具(附测试)承接原 HorizontalRequestTimeline 内的候选合并逻辑
This commit is contained in:
fawney19
2026-04-20 00:27:48 +08:00
parent 1f74e660de
commit c1b9d94c84
16 changed files with 1776 additions and 405 deletions

View File

@@ -761,6 +761,93 @@ async fn gateway_marks_account_blocked_pool_key_in_list_keys_response() {
);
}
#[tokio::test]
async fn gateway_ignores_health_signals_in_pool_scheduling_status() {
let mut provider = sample_provider("provider-openai", "openai", 10).with_transport_fields(
true,
false,
true,
None,
None,
None,
None,
None,
Some(json!({
"pool_advanced": {
"enabled": true
}
})),
);
provider.provider_type = "openai".to_string();
let mut circuit_key = sample_key(
"key-openai-circuit",
"provider-openai",
"openai:chat",
"sk-circuit",
);
circuit_key.name = "circuit-open".to_string();
circuit_key.circuit_breaker_by_format = Some(json!({
"openai:chat": {
"open": true
}
}));
let mut low_health_key = sample_key(
"key-openai-health",
"provider-openai",
"openai:chat",
"sk-health",
);
low_health_key.name = "low-health".to_string();
low_health_key.health_by_format = Some(json!({
"openai:chat": {
"health_score": 0.2
}
}));
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![provider],
Vec::new(),
vec![circuit_key, low_health_key],
));
let state = AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(GatewayDataState::with_provider_catalog_reader_for_tests(
provider_catalog_repository,
));
let response = local_admin_pool_response(
&state,
http::Method::GET,
"/api/admin/pool/provider-openai/keys?page=1&page_size=50&status=all",
None,
)
.await;
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = serde_json::from_slice(
&to_bytes(response.into_body(), usize::MAX)
.await
.expect("body should read"),
)
.expect("json body should parse");
let keys = payload["keys"].as_array().expect("keys should be array");
assert_eq!(keys.len(), 2);
assert_eq!(keys[0]["key_name"], json!("circuit-open"));
assert_eq!(keys[0]["circuit_breaker_open"], json!(true));
assert_eq!(keys[0]["scheduling_status"], json!("available"));
assert_eq!(keys[0]["scheduling_reason"], json!("available"));
assert_eq!(keys[0]["scheduling_label"], json!("可用"));
assert_eq!(keys[1]["key_name"], json!("low-health"));
assert_eq!(keys[1]["health_score"], json!(0.2));
assert_eq!(keys[1]["scheduling_status"], json!("available"));
assert_eq!(keys[1]["scheduling_reason"], json!("available"));
assert_eq!(keys[1]["scheduling_label"], json!("可用"));
}
#[tokio::test]
async fn gateway_handles_admin_pool_list_keys_with_quota_compatibility_fields() {
let upstream_hits = Arc::new(Mutex::new(0usize));