feat: provider api_formats 可空继承、OpenAI 图片 edit/variation 与用量配额多项补强

- 鉴权: provider_api_keys.api_formats 改为可空,OAuth 托管 key 自动继承 provider endpoints 激活格式,相关 handler/测试同步更新
- 图片 planner: OpenAI 图片路由新增 edit/variation 操作并完善参数校验、响应合并与流式处理
- 用量: user me usage 返回区分 client_requested_stream/upstream_is_stream,前端 usage 列表筛选与展示增强
- 统计: stats_daily_model 新增 cache_creation_ephemeral_5m/1h tokens 字段与回填链路
- 配额/observability: quota repository 新增内存与 SQL 扩展,admin observability usage 字段扩充
- 其它: OAuth 导入/轮询收敛、provider 汇总与 pool admin 读写链路小修、新增 system_config 缓存与 provider template handler

Closes #318

Co-authored-by: Entropy.Xu <[email protected]>
This commit is contained in:
fawney19
2026-04-23 14:42:51 +08:00
co-authored by Entropy.Xu
parent f55f22d2e8
commit fa328e18a1
128 changed files with 5583 additions and 690 deletions
@@ -1,5 +1,5 @@
use crate::handlers::admin::request::AdminAppState;
use crate::handlers::public::provider_key_api_formats;
use crate::provider_key_auth::provider_key_effective_api_formats;
use aether_scheduler_core::count_recent_rpm_requests_for_provider_key_since;
use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};
@@ -18,6 +18,16 @@ pub(crate) async fn build_admin_key_health_payload(
.await
.ok()
.and_then(|mut keys| keys.drain(..).next())?;
let provider = state
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&key.provider_id))
.await
.ok()
.and_then(|mut providers| providers.drain(..).next())?;
let endpoints = state
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&key.provider_id))
.await
.ok()
.unwrap_or_default();
let request_count = key.request_count.unwrap_or(0);
let success_count = key.success_count.unwrap_or(0);
@@ -97,7 +107,9 @@ pub(crate) async fn build_admin_key_health_payload(
.unwrap_or(0));
} else {
let mut formats_payload = serde_json::Map::new();
for format_name in provider_key_api_formats(&key) {
for format_name in
provider_key_effective_api_formats(&key, &provider.provider_type, &endpoints)
{
let health_data = health_by_format.and_then(|formats| formats.get(&format_name));
let circuit_data = circuit_by_format.and_then(|formats| formats.get(&format_name));
let is_open = circuit_data
@@ -363,11 +375,27 @@ pub(crate) async fn recover_all_admin_key_health(
if !updated {
continue;
}
let provider = state
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&key.provider_id))
.await
.ok()
.and_then(|mut providers| providers.drain(..).next());
let endpoints = state
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&key.provider_id))
.await
.ok()
.unwrap_or_default();
let api_formats = provider
.as_ref()
.map(|provider| {
provider_key_effective_api_formats(&key, &provider.provider_type, &endpoints)
})
.unwrap_or_default();
payload_items.push(json!({
"key_id": key.id,
"key_name": key.name,
"provider_id": key.provider_id,
"api_formats": key.api_formats.unwrap_or_else(|| json!([])),
"api_formats": api_formats,
}));
}
@@ -1,9 +1,8 @@
use crate::handlers::admin::request::AdminAppState;
use crate::handlers::admin::shared::unix_secs_to_rfc3339;
use crate::handlers::public::{
api_format_display_name, build_public_health_timeline, provider_key_api_formats,
};
use crate::handlers::public::{api_format_display_name, build_public_health_timeline};
use crate::handlers::shared::unix_ms_to_rfc3339;
use crate::provider_key_auth::provider_key_effective_api_formats;
use aether_data_contracts::repository::candidates::PublicHealthTimelineBucket;
use aether_scheduler_core::{is_provider_key_circuit_open, provider_key_health_score};
use serde_json::json;
@@ -50,17 +49,26 @@ pub(crate) async fn build_admin_endpoint_health_status_payload(
let mut endpoint_to_format = BTreeMap::<String, String>::new();
let mut provider_ids_by_format = BTreeMap::<String, BTreeSet<String>>::new();
let mut active_provider_formats = BTreeSet::<(String, String)>::new();
let provider_type_by_id = providers
.iter()
.map(|provider| (provider.id.clone(), provider.provider_type.clone()))
.collect::<BTreeMap<_, _>>();
let mut active_endpoints_by_provider = BTreeMap::<String, Vec<_>>::new();
for endpoint in active_endpoints {
endpoint_to_format.insert(endpoint.id.clone(), endpoint.api_format.clone());
endpoint_ids_by_format
.entry(endpoint.api_format.clone())
.or_default()
.push(endpoint.id);
.push(endpoint.id.clone());
provider_ids_by_format
.entry(endpoint.api_format.clone())
.or_default()
.insert(endpoint.provider_id.clone());
active_provider_formats.insert((endpoint.provider_id, endpoint.api_format));
active_provider_formats.insert((endpoint.provider_id.clone(), endpoint.api_format.clone()));
active_endpoints_by_provider
.entry(endpoint.provider_id.clone())
.or_default()
.push(endpoint);
}
let all_endpoint_ids = endpoint_to_format.keys().cloned().collect::<Vec<_>>();
@@ -74,7 +82,15 @@ pub(crate) async fn build_admin_endpoint_health_status_payload(
.ok()
.unwrap_or_default();
for key in keys {
for api_format in provider_key_api_formats(&key) {
let provider_type = provider_type_by_id
.get(&key.provider_id)
.map(String::as_str)
.unwrap_or("");
let endpoints = active_endpoints_by_provider
.get(&key.provider_id)
.map(Vec::as_slice)
.unwrap_or(&[]);
for api_format in provider_key_effective_api_formats(&key, provider_type, endpoints) {
if !active_provider_formats.contains(&(key.provider_id.clone(), api_format.clone()))
{
continue;