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,202 @@
use super::*;
#[tokio::test]
async fn gateway_handles_public_openai_models_without_proxying_python_upstream() {
let python_upstream_hits = Arc::new(Mutex::new(0usize));
let python_upstream_hits_clone = Arc::clone(&python_upstream_hits);
let python_upstream = Router::new().route(
"/{*path}",
any(move |_request: Request| {
let python_upstream_hits_inner = Arc::clone(&python_upstream_hits_clone);
async move {
*python_upstream_hits_inner
.lock()
.expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("proxied"))
}
}),
);
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
Some(hash_api_key("sk-openai-models")),
unrestricted_models_snapshot("key-1", "user-1"),
)]));
let candidate_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
sample_models_candidate_row("provider-openai", "openai", "openai:chat", "gpt-5", 10),
sample_models_candidate_row("provider-openai", "openai", "openai:chat", "gpt-4.1", 10),
]));
let (python_upstream_url, python_upstream_handle) = start_server(python_upstream).await;
let gateway = build_router_with_state(
AppState::new(python_upstream_url, None)
.expect("gateway should build")
.with_data_state_for_tests(
crate::gateway::data::GatewayDataState::with_minimal_candidate_selection_and_auth_for_tests(
candidate_repository,
auth_repository,
),
),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/v1/models"))
.header("authorization", "Bearer sk-openai-models")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["object"], "list");
assert_eq!(payload["data"][0]["id"], "gpt-4.1");
assert_eq!(payload["data"][1]["id"], "gpt-5");
assert_eq!(payload["data"][0]["owned_by"], "openai");
assert_eq!(*python_upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
python_upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_public_claude_models_without_proxying_python_upstream() {
let python_upstream_hits = Arc::new(Mutex::new(0usize));
let python_upstream_hits_clone = Arc::clone(&python_upstream_hits);
let python_upstream = Router::new().route(
"/{*path}",
any(move |_request: Request| {
let python_upstream_hits_inner = Arc::clone(&python_upstream_hits_clone);
async move {
*python_upstream_hits_inner
.lock()
.expect("mutex should lock") += 1;
(StatusCode::OK, Json(json!({"proxied": true}))).into_response()
}
}),
);
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
Some(hash_api_key("sk-claude-models")),
unrestricted_models_snapshot("key-claude", "user-claude"),
)]));
let candidate_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
sample_models_candidate_row(
"provider-claude",
"claude",
"claude:chat",
"claude-3-7-sonnet",
10,
),
sample_models_candidate_row(
"provider-claude",
"claude",
"claude:chat",
"claude-3-5-haiku",
10,
),
]));
let (python_upstream_url, python_upstream_handle) = start_server(python_upstream).await;
let gateway = build_router_with_state(
AppState::new(python_upstream_url, None)
.expect("gateway should build")
.with_data_state_for_tests(
crate::gateway::data::GatewayDataState::with_minimal_candidate_selection_and_auth_for_tests(
candidate_repository,
auth_repository,
),
),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/v1/models?limit=1"))
.header("x-api-key", "sk-claude-models")
.header("anthropic-version", "2023-06-01")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["data"][0]["id"], "claude-3-5-haiku");
assert_eq!(payload["first_id"], "claude-3-5-haiku");
assert_eq!(payload["last_id"], "claude-3-5-haiku");
assert_eq!(payload["has_more"], true);
assert_eq!(*python_upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
python_upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_public_gemini_models_without_proxying_python_upstream() {
let python_upstream_hits = Arc::new(Mutex::new(0usize));
let python_upstream_hits_clone = Arc::clone(&python_upstream_hits);
let python_upstream = Router::new().route(
"/{*path}",
any(move |_request: Request| {
let python_upstream_hits_inner = Arc::clone(&python_upstream_hits_clone);
async move {
*python_upstream_hits_inner
.lock()
.expect("mutex should lock") += 1;
(StatusCode::OK, Json(json!({"proxied": true}))).into_response()
}
}),
);
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
Some(hash_api_key("sk-gemini-models")),
unrestricted_models_snapshot("key-gemini", "user-gemini"),
)]));
let candidate_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
sample_models_candidate_row(
"provider-gemini",
"gemini",
"gemini:chat",
"gemini-2.5-flash",
10,
),
sample_models_candidate_row(
"provider-gemini",
"gemini",
"gemini:chat",
"gemini-2.5-pro",
10,
),
]));
let (python_upstream_url, python_upstream_handle) = start_server(python_upstream).await;
let gateway = build_router_with_state(
AppState::new(python_upstream_url, None)
.expect("gateway should build")
.with_data_state_for_tests(
crate::gateway::data::GatewayDataState::with_minimal_candidate_selection_and_auth_for_tests(
candidate_repository,
auth_repository,
),
),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!(
"{gateway_url}/v1beta/models?pageSize=1&key=sk-gemini-models"
))
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["models"][0]["name"], "models/gemini-2.5-flash");
assert_eq!(payload["nextPageToken"], "1");
assert_eq!(*python_upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
python_upstream_handle.abort();
}