mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
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:
@@ -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>,
|
||||
|
||||
@@ -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"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user