feat: 扩展 Rust gateway 全功能模块,新增 billing/crypto/wallet crate 及完整数据层

- 新增 aether-billing、aether-crypto、aether-wallet 独立 crate
- aether-data 扩展 repository 层:announcements、auth_modules、billing、
  candidate_selection、gemini_file_mappings、global_models、management_tokens、
  oauth_providers、proxy_nodes、quota、users、wallet 等模块
- aether-gateway 新增 api/auth/billing/control/middleware/scheduler/usage/
  video_tasks/hooks/maintenance/model_fetch/provider_transport 等功能模块
- 重构 executor decision 和 gateway state 为模块目录结构
- 新增 gateway router、frontdoor 路由层及对应测试
- Python 侧 API 路由重构,新增 compat/support 模块
- 前端 Logo 组件更新及 Provider 管理页面调整
This commit is contained in:
fawney19
2026-03-31 19:19:04 +08:00
parent b5a0070023
commit ddf18fed9a
690 changed files with 235085 additions and 16299 deletions
@@ -0,0 +1,113 @@
use std::collections::BTreeMap;
use std::sync::RwLock;
use async_trait::async_trait;
use super::types::{BillingReadRepository, StoredBillingModelContext};
use crate::DataLayerError;
type BillingContextKey = (String, String, Option<String>);
type BillingContextMap = BTreeMap<BillingContextKey, StoredBillingModelContext>;
#[derive(Debug, Default)]
pub struct InMemoryBillingReadRepository {
by_key: RwLock<BillingContextMap>,
}
impl InMemoryBillingReadRepository {
pub fn seed<I>(items: I) -> Self
where
I: IntoIterator<Item = StoredBillingModelContext>,
{
let mut by_key = BTreeMap::new();
for item in items {
by_key.insert(
(
item.provider_id.clone(),
item.global_model_name.clone(),
item.provider_api_key_id.clone(),
),
item,
);
}
Self {
by_key: RwLock::new(by_key),
}
}
}
#[async_trait]
impl BillingReadRepository for InMemoryBillingReadRepository {
async fn find_model_context(
&self,
provider_id: &str,
provider_api_key_id: Option<&str>,
global_model_name: &str,
) -> Result<Option<StoredBillingModelContext>, DataLayerError> {
let key = (
provider_id.to_string(),
global_model_name.to_string(),
provider_api_key_id.map(ToOwned::to_owned),
);
let by_key = self.by_key.read().expect("billing repository lock");
if let Some(value) = by_key.get(&key) {
return Ok(Some(value.clone()));
}
if let Some(value) = by_key
.get(&(provider_id.to_string(), global_model_name.to_string(), None))
.cloned()
{
return Ok(Some(value));
}
Ok(by_key
.iter()
.find(|((stored_provider_id, stored_model_name, _), _)| {
stored_provider_id == provider_id && stored_model_name == global_model_name
})
.map(|(_, value)| value.clone()))
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::InMemoryBillingReadRepository;
use crate::repository::billing::{BillingReadRepository, StoredBillingModelContext};
fn sample_context() -> StoredBillingModelContext {
StoredBillingModelContext::new(
"provider-1".to_string(),
Some("pay_as_you_go".to_string()),
Some("key-1".to_string()),
Some(json!({"openai:chat": 0.8})),
Some(60),
"global-model-1".to_string(),
"gpt-5".to_string(),
Some(json!({"streaming": true})),
Some(0.02),
Some(json!({"tiers":[{"up_to":null,"input_price_per_1m":3.0,"output_price_per_1m":15.0}]})),
Some("model-1".to_string()),
Some("gpt-5-upstream".to_string()),
None,
Some(0.01),
None,
)
.expect("billing context should build")
}
#[tokio::test]
async fn falls_back_to_provider_without_key_scope() {
let repository = InMemoryBillingReadRepository::seed(vec![sample_context()]);
let stored = repository
.find_model_context("provider-1", Some("key-2"), "gpt-5")
.await
.expect("lookup should succeed")
.expect("context should exist");
assert_eq!(stored.provider_id, "provider-1");
assert_eq!(stored.global_model_name, "gpt-5");
}
}