mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 08:27:46 +08:00
fix(providers): preserve health in redacted key summaries
This commit is contained in:
@@ -533,12 +533,10 @@ impl AppState {
|
|||||||
&self,
|
&self,
|
||||||
provider_ids: &[String],
|
provider_ids: &[String],
|
||||||
) -> Result<Vec<provider_catalog::StoredProviderCatalogKey>, GatewayError> {
|
) -> Result<Vec<provider_catalog::StoredProviderCatalogKey>, GatewayError> {
|
||||||
let keys = self
|
self.data
|
||||||
.data
|
|
||||||
.list_provider_catalog_key_summaries_by_provider_ids(provider_ids)
|
.list_provider_catalog_key_summaries_by_provider_ids(provider_ids)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||||
self.open_provider_catalog_keys(keys).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn list_provider_catalog_key_maintenance_summaries_by_provider_ids(
|
pub(crate) async fn list_provider_catalog_key_maintenance_summaries_by_provider_ids(
|
||||||
|
|||||||
@@ -272,6 +272,50 @@ mod tests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn app_state_reads_redacted_key_summaries_without_opening_credentials() {
|
||||||
|
for (api_key, auth_config) in [
|
||||||
|
(Some("summary"), None),
|
||||||
|
(None, Some("{}")),
|
||||||
|
(Some("summary"), Some("{}")),
|
||||||
|
] {
|
||||||
|
let health = serde_json::json!({"openai:chat": {"health_score": 0.75}});
|
||||||
|
let key = sample_key(
|
||||||
|
"key-1",
|
||||||
|
"provider-1",
|
||||||
|
api_key.map(ToOwned::to_owned),
|
||||||
|
auth_config.map(ToOwned::to_owned),
|
||||||
|
)
|
||||||
|
.with_health_fields(Some(health.clone()), None);
|
||||||
|
let repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||||
|
vec![sample_provider("provider-1")],
|
||||||
|
Vec::new(),
|
||||||
|
vec![key],
|
||||||
|
));
|
||||||
|
let state = AppState::new()
|
||||||
|
.expect("test state should build")
|
||||||
|
.with_data_state_for_tests(
|
||||||
|
GatewayDataState::with_provider_catalog_reader_for_tests(repository)
|
||||||
|
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY),
|
||||||
|
);
|
||||||
|
let provider_ids = ["provider-1".to_string()];
|
||||||
|
|
||||||
|
let summaries = state
|
||||||
|
.list_provider_catalog_key_summaries_by_provider_ids(&provider_ids)
|
||||||
|
.await
|
||||||
|
.expect("redacted summaries should not require credential authentication");
|
||||||
|
|
||||||
|
assert_eq!(summaries.len(), 1);
|
||||||
|
assert_eq!(summaries[0].health_by_format.as_ref(), Some(&health));
|
||||||
|
assert_eq!(summaries[0].encrypted_api_key.as_deref(), api_key);
|
||||||
|
assert_eq!(summaries[0].encrypted_auth_config.as_deref(), auth_config);
|
||||||
|
assert!(state
|
||||||
|
.list_provider_catalog_keys_by_provider_ids(&provider_ids)
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn app_state_migrates_both_legacy_fields_with_one_exact_cas() {
|
async fn app_state_migrates_both_legacy_fields_with_one_exact_cas() {
|
||||||
let legacy_api =
|
let legacy_api =
|
||||||
|
|||||||
@@ -70,6 +70,68 @@ async fn provider_health_summary(
|
|||||||
payload["items"][0].clone()
|
payload["items"][0].clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn admin_provider_summary_health_preserves_redacted_key_summaries() {
|
||||||
|
let endpoint = sample_endpoint(
|
||||||
|
"endpoint-chat",
|
||||||
|
"provider-openai",
|
||||||
|
"openai:chat",
|
||||||
|
"https://api.openai.example",
|
||||||
|
);
|
||||||
|
let keys = [
|
||||||
|
("key-api", "api_key", None, 0.25),
|
||||||
|
("key-oauth", "oauth", Some("{}"), 0.75),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(key_id, auth_type, auth_config, score)| {
|
||||||
|
let mut key = sample_key(key_id, "provider-openai", "openai:chat", "test")
|
||||||
|
.with_health_fields(
|
||||||
|
Some(json!({"openai:chat": {"health_score": score}})),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
key.auth_type = auth_type.to_string();
|
||||||
|
key.encrypted_api_key = Some("summary".to_string());
|
||||||
|
key.encrypted_auth_config = auth_config.map(ToOwned::to_owned);
|
||||||
|
key
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||||
|
vec![sample_provider("provider-openai", "openai", 10)],
|
||||||
|
vec![endpoint],
|
||||||
|
keys,
|
||||||
|
));
|
||||||
|
let state = AppState::new()
|
||||||
|
.expect("gateway should build")
|
||||||
|
.with_data_state_for_tests(GatewayDataState::with_provider_catalog_reader_for_tests(
|
||||||
|
repository,
|
||||||
|
));
|
||||||
|
|
||||||
|
for uri in [
|
||||||
|
"/api/admin/providers/summary",
|
||||||
|
"/api/admin/providers/provider-openai/summary",
|
||||||
|
] {
|
||||||
|
let response = local_admin_providers_response(&state, http::Method::GET, uri, None).await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = axum::body::to_bytes(response.into_body(), 1024 * 1024)
|
||||||
|
.await
|
||||||
|
.expect("summary body should read");
|
||||||
|
let payload: serde_json::Value = serde_json::from_slice(&body).expect("summary should parse");
|
||||||
|
let summary = if uri == "/api/admin/providers/summary" {
|
||||||
|
&payload["items"][0]
|
||||||
|
} else {
|
||||||
|
&payload
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(summary["total_keys"], 2);
|
||||||
|
assert_eq!(summary["active_keys"], 2);
|
||||||
|
assert_eq!(summary["endpoint_health_details"][0]["total_keys"], 2);
|
||||||
|
assert_eq!(summary["endpoint_health_details"][0]["active_keys"], 2);
|
||||||
|
assert_eq!(summary["endpoint_health_details"][0]["health_score"], 0.5);
|
||||||
|
assert_eq!(summary["avg_health_score"], 0.5);
|
||||||
|
assert_eq!(summary["unhealthy_endpoints"], 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn admin_provider_summary_health_ignores_disabled_keys() {
|
async fn admin_provider_summary_health_ignores_disabled_keys() {
|
||||||
let endpoint = sample_endpoint(
|
let endpoint = sample_endpoint(
|
||||||
|
|||||||
@@ -17,6 +17,15 @@
|
|||||||
|
|
||||||
`total_keys` 和 `active_keys` 仍反映密钥配置数量,不因缺少健康数据而减少。
|
`total_keys` 和 `active_keys` 仍反映密钥配置数量,不因缺少健康数据而减少。
|
||||||
|
|
||||||
|
## 数据读取
|
||||||
|
|
||||||
|
摘要从密钥的轻量投影读取 API 格式、启用状态和 `health_by_format`。
|
||||||
|
PostgreSQL 投影中的凭据字段使用 `summary` / `{}` 等脱敏占位值,并非真实密文,
|
||||||
|
因此摘要读取不执行凭据解密、认证或迁移。完整密钥读取仍保留原有的凭据安全校验。
|
||||||
|
|
||||||
|
若将这些占位值送入凭据校验,读取会失败,并被摘要聚合当作空密钥列表,
|
||||||
|
导致已配置密钥的端点也被错误显示为灰色;不能通过给缺失分数默认填 `100%` 来修复。
|
||||||
|
|
||||||
## 页面展示
|
## 页面展示
|
||||||
|
|
||||||
桌面表格和手机卡片使用相同规则:
|
桌面表格和手机卡片使用相同规则:
|
||||||
|
|||||||
Reference in New Issue
Block a user