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 <53283266+Entropy-Xu@users.noreply.github.com>
This commit is contained in:
fawney19
2026-04-23 14:42:51 +08:00
parent f55f22d2e8
commit fa328e18a1
128 changed files with 5583 additions and 690 deletions

View File

@@ -7,7 +7,7 @@ use aether_runtime::{ConcurrencyGate, DistributedConcurrencyGate};
use super::super::async_task::{VideoTaskPollerConfig, VideoTaskService};
use super::super::cache::{
AuthApiKeyLastUsedCache, AuthContextCache, DashboardResponseCache, DirectPlanBypassCache,
SchedulerAffinityCache,
SchedulerAffinityCache, SystemConfigCache,
};
use super::super::data::GatewayDataState;
use super::super::fallback_metrics;
@@ -59,6 +59,7 @@ pub struct AppState {
pub(crate) direct_plan_bypass_cache: Arc<DirectPlanBypassCache>,
pub(crate) scheduler_affinity_cache: Arc<SchedulerAffinityCache>,
pub(crate) dashboard_response_cache: Arc<DashboardResponseCache>,
pub(crate) system_config_cache: Arc<SystemConfigCache>,
pub(crate) fallback_metrics: Arc<fallback_metrics::GatewayFallbackMetrics>,
pub(crate) frontdoor_cors: Option<Arc<FrontdoorCorsConfig>>,
pub(crate) frontdoor_user_rpm: Arc<FrontdoorUserRpmLimiter>,

View File

@@ -24,6 +24,7 @@ use super::super::async_task::{
use super::super::cache::{
AuthApiKeyLastUsedCache, AuthContextCache, DashboardResponseCache, DirectPlanBypassCache,
SchedulerAffinityCache, SchedulerAffinitySnapshotEntry, SchedulerAffinityTarget,
SystemConfigCache,
};
use super::super::data::{GatewayDataConfig, GatewayDataState};
use super::super::fallback_metrics;
@@ -48,6 +49,8 @@ use crate::maintenance::spawn_stats_hourly_aggregation_worker;
use crate::maintenance::spawn_usage_cleanup_worker;
use crate::maintenance::spawn_wallet_daily_usage_aggregation_worker;
const SYSTEM_CONFIG_CACHE_TTL: Duration = Duration::from_secs(3);
impl AppState {
fn spawn_scheduler_affinity_redis_write(
&self,
@@ -90,6 +93,7 @@ impl AppState {
pub(crate) fn replace_data_state(&mut self, data: Arc<GatewayDataState>) {
self.clear_provider_transport_snapshot_cache();
self.system_config_cache.clear();
self.tunnel = crate::tunnel::EmbeddedTunnelState::with_data(Arc::clone(&data));
self.data = data;
}
@@ -147,6 +151,7 @@ impl AppState {
direct_plan_bypass_cache: Arc::new(DirectPlanBypassCache::default()),
scheduler_affinity_cache: Arc::new(SchedulerAffinityCache::default()),
dashboard_response_cache: Arc::new(DashboardResponseCache::default()),
system_config_cache: Arc::new(SystemConfigCache::default()),
fallback_metrics: Arc::new(fallback_metrics::GatewayFallbackMetrics::default()),
frontdoor_cors: None,
frontdoor_user_rpm: Arc::new(FrontdoorUserRpmLimiter::new(
@@ -396,10 +401,18 @@ impl AppState {
&self,
key: &str,
) -> Result<Option<serde_json::Value>, GatewayError> {
self.data
if let Some(value) = self.system_config_cache.get(key, SYSTEM_CONFIG_CACHE_TTL) {
return Ok(value);
}
let value = self
.data
.find_system_config_value(key)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
.map_err(|err| GatewayError::Internal(err.to_string()))?;
self.system_config_cache
.insert(key.to_string(), value.clone(), SYSTEM_CONFIG_CACHE_TTL);
Ok(value)
}
pub(crate) async fn upsert_system_config_json_value(
@@ -408,10 +421,17 @@ impl AppState {
value: &serde_json::Value,
description: Option<&str>,
) -> Result<serde_json::Value, GatewayError> {
self.data
let value = self
.data
.upsert_system_config_value(key, value, description)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
.map_err(|err| GatewayError::Internal(err.to_string()))?;
self.system_config_cache.insert(
key.to_string(),
Some(value.clone()),
SYSTEM_CONFIG_CACHE_TTL,
);
Ok(value)
}
pub(crate) async fn list_system_config_entries(
@@ -436,10 +456,14 @@ impl AppState {
}
pub(crate) async fn delete_system_config_value(&self, key: &str) -> Result<bool, GatewayError> {
self.data
let deleted = self
.data
.delete_system_config_value(key)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
.map_err(|err| GatewayError::Internal(err.to_string()))?;
self.system_config_cache
.insert(key.to_string(), None, SYSTEM_CONFIG_CACHE_TTL);
Ok(deleted)
}
pub(crate) async fn read_admin_system_stats(
@@ -842,3 +866,89 @@ fn runtime_miss_diagnostic_has_candidate_signal(
|| diagnostic.skipped_candidate_count.unwrap_or(0) > 0
|| !diagnostic.skip_reasons.is_empty()
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use serde_json::json;
use super::AppState;
use crate::data::GatewayDataState;
#[tokio::test]
async fn system_config_reads_use_short_lived_cache_until_app_invalidation() {
let state = AppState::new()
.expect("app state should build")
.with_data_state_for_tests(
GatewayDataState::disabled()
.with_system_config_values_for_tests([("site_name".to_string(), json!("old"))]),
);
assert_eq!(
state
.read_system_config_json_value("site_name")
.await
.expect("system config read should succeed"),
Some(json!("old"))
);
state
.data
.upsert_system_config_value("site_name", &json!("bypassed"), None)
.await
.expect("direct data write should succeed");
assert_eq!(
state
.read_system_config_json_value("site_name")
.await
.expect("cached system config read should succeed"),
Some(json!("old"))
);
state
.upsert_system_config_json_value("site_name", &json!("fresh"), None)
.await
.expect("app system config write should succeed");
assert_eq!(
state
.read_system_config_json_value("site_name")
.await
.expect("refreshed system config read should succeed"),
Some(json!("fresh"))
);
}
#[tokio::test]
async fn replacing_data_state_clears_system_config_cache() {
let mut state = AppState::new()
.expect("app state should build")
.with_data_state_for_tests(
GatewayDataState::disabled()
.with_system_config_values_for_tests([("site_name".to_string(), json!("old"))]),
);
assert_eq!(
state
.read_system_config_json_value("site_name")
.await
.expect("system config read should succeed"),
Some(json!("old"))
);
state.replace_data_state(Arc::new(
GatewayDataState::disabled()
.with_system_config_values_for_tests([("site_name".to_string(), json!("new"))]),
));
assert_eq!(
state
.read_system_config_json_value("site_name")
.await
.expect("system config read should reflect replaced data"),
Some(json!("new"))
);
}
}

View File

@@ -33,6 +33,16 @@ impl AppState {
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn read_provider_quota_snapshots(
&self,
provider_ids: &[String],
) -> Result<Vec<quota::StoredProviderQuotaSnapshot>, GatewayError> {
self.data
.find_provider_quotas_by_provider_ids(provider_ids)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn read_recent_request_candidates(
&self,
limit: usize,