refactor: 大规模模块拆分与重组,新增 aether-admin crate

- 新建独立 aether-admin crate 承载 admin 相关共享契约与纯辅助函数
- 拆分 ai_pipeline 下 kiro/private_envelope/conversion/planner 等大文件为子模块目录
- 重组 admin handlers 各业务域(billing/oauth/provider/system/users 等)为目录结构,移除 shared.rs/builders.rs 等反模式
- 移除 ai_pipeline runtime adapters 旧实现(claude/openai/gemini/kiro/vertex/antigravity 等),改由 provider transport 统一承载
- 移除 control_facade/execution_facade/auth_snapshot_facade 等冗余 facade 层
- 拆分 query/billing 与 query/monitoring 模块、state/runtime/payments 与 security 模块
- 扩展架构测试覆盖 admin_billing/admin_model/admin_users 等新模块
- 删除 docs/architecture/refactor-execution-plan.md 已完成的执行计划文档
This commit is contained in:
fawney19
2026-04-09 00:10:38 +08:00
parent 4fb9882b54
commit 4fc95adfb9
663 changed files with 48471 additions and 40232 deletions
@@ -0,0 +1,362 @@
use super::*;
impl<'a> AdminAppState<'a> {
pub(crate) async fn build_admin_keys_grouped_by_format_payload(
&self,
) -> Option<serde_json::Value> {
crate::handlers::public::build_admin_keys_grouped_by_format_payload(self.app).await
}
pub(crate) async fn build_admin_create_provider_key_record(
&self,
provider: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
payload: crate::handlers::admin::provider::shared::payloads::AdminProviderKeyCreateRequest,
) -> Result<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey, String>
{
crate::handlers::admin::provider::write::keys::build_admin_create_provider_key_record(
self, provider, payload,
)
.await
}
pub(crate) async fn build_admin_update_provider_key_record(
&self,
provider: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
existing: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey,
raw_payload: &serde_json::Map<String, serde_json::Value>,
payload: crate::handlers::admin::provider::shared::payloads::AdminProviderKeyUpdateRequest,
) -> Result<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey, String>
{
crate::handlers::admin::provider::write::keys::build_admin_update_provider_key_record(
self,
provider,
existing,
raw_payload,
payload,
)
.await
}
pub(crate) fn build_admin_provider_key_response(
&self,
key: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey,
now_unix_secs: u64,
) -> serde_json::Value {
crate::handlers::admin::shared::build_admin_provider_key_response(
self.app,
key,
now_unix_secs,
)
}
pub(crate) fn masked_catalog_api_key(
&self,
key: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey,
) -> String {
crate::handlers::admin::shared::masked_catalog_api_key(self.app, key)
}
pub(crate) async fn build_admin_provider_keys_payload(
&self,
provider_id: &str,
skip: usize,
limit: usize,
) -> Option<serde_json::Value> {
crate::handlers::admin::provider::write::keys::build_admin_provider_keys_payload(
self,
provider_id,
skip,
limit,
)
.await
}
pub(crate) fn build_admin_reveal_key_payload(
&self,
key: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey,
) -> Result<serde_json::Value, String> {
crate::handlers::admin::provider::write::reveal::build_admin_reveal_key_payload(self, key)
}
pub(crate) async fn build_admin_export_key_payload(
&self,
key: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey,
) -> Result<serde_json::Value, String> {
crate::handlers::admin::provider::write::reveal::build_admin_export_key_payload(self, key)
.await
}
pub(crate) async fn build_admin_providers_payload(
&self,
skip: usize,
limit: usize,
is_active: Option<bool>,
) -> Option<serde_json::Value> {
crate::handlers::admin::provider::summary::build_admin_providers_payload(
self, skip, limit, is_active,
)
.await
}
pub(crate) async fn build_admin_provider_summary_payload(
&self,
provider_id: &str,
) -> Option<serde_json::Value> {
crate::handlers::admin::provider::summary::build_admin_provider_summary_payload(
self,
provider_id,
)
.await
}
pub(crate) async fn build_admin_create_provider_record(
&self,
payload: crate::handlers::admin::provider::shared::payloads::AdminProviderCreateRequest,
) -> Result<
(
aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
Option<i32>,
),
String,
> {
crate::handlers::admin::provider::write::provider::build_admin_create_provider_record(
self, payload,
)
.await
}
pub(crate) async fn build_admin_update_provider_record(
&self,
existing: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
raw_payload: &serde_json::Map<String, serde_json::Value>,
payload: crate::handlers::admin::provider::shared::payloads::AdminProviderUpdateRequest,
) -> Result<
aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
String,
> {
crate::handlers::admin::provider::write::provider::build_admin_update_provider_record(
self,
existing,
raw_payload,
payload,
)
.await
}
pub(crate) async fn build_admin_providers_summary_payload(
&self,
page: usize,
page_size: usize,
search: &str,
status: &str,
api_format: &str,
model_id: &str,
) -> Option<serde_json::Value> {
crate::handlers::admin::provider::summary::build_admin_providers_summary_payload(
self, page, page_size, search, status, api_format, model_id,
)
.await
}
pub(crate) async fn build_admin_provider_health_monitor_payload(
&self,
provider_id: &str,
lookback_hours: u64,
per_endpoint_limit: usize,
) -> Option<serde_json::Value> {
crate::handlers::admin::provider::summary::build_admin_provider_health_monitor_payload(
self,
provider_id,
lookback_hours,
per_endpoint_limit,
)
.await
}
pub(crate) async fn build_admin_provider_mapping_preview_payload(
&self,
provider_id: &str,
) -> Option<serde_json::Value> {
crate::handlers::admin::provider::delete_task::build_admin_provider_mapping_preview_payload(
self,
provider_id,
)
.await
}
pub(crate) async fn build_admin_provider_pool_status_payload(
&self,
provider_id: &str,
) -> Option<serde_json::Value> {
crate::handlers::admin::provider::pool::runtime::build_admin_provider_pool_status_payload(
self,
provider_id,
)
.await
}
pub(crate) async fn build_admin_create_provider_endpoint_record(
&self,
provider: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
payload: crate::handlers::admin::provider::endpoints_admin::payloads::AdminProviderEndpointCreateRequest,
) -> Result<
aether_data_contracts::repository::provider_catalog::StoredProviderCatalogEndpoint,
String,
> {
use crate::api::ai::{
admin_default_body_rules_for_signature, admin_endpoint_signature_parts,
};
use crate::handlers::public::normalize_admin_base_url;
use aether_admin::provider::endpoints as admin_provider_endpoints_pure;
if payload.provider_id.trim() != provider.id {
return Err("provider_id 不匹配".to_string());
}
if self.provider_type_is_fixed(&provider.provider_type) {
return Err("固定类型 Provider 不允许手动新增 Endpoint".to_string());
}
if !(0..=999).contains(&payload.max_retries) {
return Err("max_retries 必须在 0 到 999 之间".to_string());
}
let (normalized_api_format, api_family, endpoint_kind) =
admin_endpoint_signature_parts(&payload.api_format)
.ok_or_else(|| format!("无效的 api_format: {}", payload.api_format))?;
let base_url = normalize_admin_base_url(&payload.base_url)?;
let existing_endpoints = self
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
.await
.map_err(|err| format!("{err:?}"))?;
if existing_endpoints
.iter()
.any(|endpoint| endpoint.api_format == normalized_api_format)
{
return Err(format!(
"Provider {} 已存在 {} 格式的 Endpoint",
provider.name, normalized_api_format
));
}
let body_rules = match payload.body_rules {
Some(value) => Some(value),
None => admin_default_body_rules_for_signature(
normalized_api_format,
Some(provider.provider_type.as_str()),
)
.and_then(|(_, rules)| (!rules.is_empty()).then_some(serde_json::Value::Array(rules))),
};
let now_unix_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs())
.unwrap_or(0);
admin_provider_endpoints_pure::build_admin_provider_endpoint_record(
uuid::Uuid::new_v4().to_string(),
provider.id.clone(),
normalized_api_format.to_string(),
api_family.to_string(),
endpoint_kind.to_string(),
base_url,
payload.custom_path,
payload.header_rules,
body_rules,
payload.max_retries,
payload.config,
payload.proxy,
payload.format_acceptance_config,
now_unix_secs,
)
}
pub(crate) async fn build_admin_update_provider_endpoint_record(
&self,
provider: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
existing_endpoint: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogEndpoint,
raw_payload: &serde_json::Map<String, serde_json::Value>,
payload: crate::handlers::admin::provider::endpoints_admin::payloads::AdminProviderEndpointUpdateRequest,
) -> Result<
aether_data_contracts::repository::provider_catalog::StoredProviderCatalogEndpoint,
String,
> {
use crate::api::ai::admin_endpoint_signature_parts;
use crate::handlers::public::{admin_requested_force_stream, normalize_admin_base_url};
use aether_admin::provider::endpoints as admin_provider_endpoints_pure;
if self.provider_type_is_fixed(&provider.provider_type)
&& (raw_payload.contains_key("base_url") || raw_payload.contains_key("custom_path"))
{
return Err(
"固定类型 Provider 的 Endpoint 不允许修改 base_url/custom_path".to_string(),
);
}
let mut update_fields = admin_provider_endpoints_pure::AdminProviderEndpointUpdateFields {
base_url: payload.base_url,
custom_path: payload.custom_path,
header_rules: payload.header_rules,
body_rules: payload.body_rules,
max_retries: payload.max_retries,
is_active: payload.is_active,
config: payload.config,
proxy: payload.proxy,
format_acceptance_config: payload.format_acceptance_config,
};
if let Some(base_url) = update_fields.base_url.as_deref() {
update_fields.base_url = Some(normalize_admin_base_url(base_url)?);
}
let mut updated =
admin_provider_endpoints_pure::apply_admin_provider_endpoint_update_fields(
existing_endpoint,
raw_payload,
&update_fields,
)?;
let provider_type = provider.provider_type.trim().to_ascii_lowercase();
if provider_type == "codex" && existing_endpoint.api_format == "openai:cli" {
let has_config_in_payload = raw_payload.contains_key("config");
let config_payload = if has_config_in_payload {
updated
.config
.clone()
.unwrap_or_else(|| serde_json::json!({}))
} else {
existing_endpoint
.config
.clone()
.unwrap_or_else(|| serde_json::json!({}))
};
let mut config = config_payload.as_object().cloned().unwrap_or_default();
let requested = config
.get("upstream_stream_policy")
.or_else(|| config.get("upstreamStreamPolicy"))
.or_else(|| config.get("upstream_stream"));
if has_config_in_payload
&& requested.is_some()
&& !admin_requested_force_stream(requested.expect("checked above"))
{
return Err("Codex OpenAI CLI 端点固定为强制流式,不允许修改".to_string());
}
config.remove("upstreamStreamPolicy");
config.remove("upstream_stream");
config.insert(
"upstream_stream_policy".to_string(),
serde_json::json!("force_stream"),
);
updated.config = Some(serde_json::Value::Object(config));
}
let (_, api_family, endpoint_kind) = admin_endpoint_signature_parts(&updated.api_format)
.ok_or_else(|| format!("无效的 api_format: {}", updated.api_format))?;
updated.api_family = Some(api_family.to_string());
updated.endpoint_kind = Some(endpoint_kind.to_string());
updated.updated_at_unix_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs());
Ok(updated)
}
}
@@ -0,0 +1,208 @@
use super::*;
impl<'a> AdminAppState<'a> {
pub(crate) async fn read_provider_catalog_keys_by_ids(
&self,
key_ids: &[String],
) -> Result<
Vec<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey>,
GatewayError,
> {
self.app.read_provider_catalog_keys_by_ids(key_ids).await
}
pub(crate) async fn read_provider_catalog_providers_by_ids(
&self,
provider_ids: &[String],
) -> Result<
Vec<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider>,
GatewayError,
> {
self.app
.read_provider_catalog_providers_by_ids(provider_ids)
.await
}
pub(crate) async fn read_provider_catalog_endpoints_by_ids(
&self,
endpoint_ids: &[String],
) -> Result<
Vec<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogEndpoint>,
GatewayError,
> {
self.app
.read_provider_catalog_endpoints_by_ids(endpoint_ids)
.await
}
pub(crate) async fn list_provider_catalog_providers(
&self,
active_only: bool,
) -> Result<
Vec<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider>,
GatewayError,
> {
self.app.list_provider_catalog_providers(active_only).await
}
pub(crate) async fn list_provider_catalog_endpoints_by_provider_ids(
&self,
provider_ids: &[String],
) -> Result<
Vec<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogEndpoint>,
GatewayError,
> {
self.app
.list_provider_catalog_endpoints_by_provider_ids(provider_ids)
.await
}
pub(crate) async fn list_provider_catalog_keys_by_provider_ids(
&self,
provider_ids: &[String],
) -> Result<
Vec<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey>,
GatewayError,
> {
self.app
.list_provider_catalog_keys_by_provider_ids(provider_ids)
.await
}
pub(crate) async fn list_provider_catalog_keys_by_ids(
&self,
key_ids: &[String],
) -> Result<
Vec<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey>,
GatewayError,
> {
self.app.list_provider_catalog_keys_by_ids(key_ids).await
}
pub(crate) async fn list_provider_catalog_key_page(
&self,
query: &aether_data_contracts::repository::provider_catalog::ProviderCatalogKeyListQuery,
) -> Result<
aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKeyPage,
GatewayError,
> {
self.app.list_provider_catalog_key_page(query).await
}
pub(crate) async fn list_provider_catalog_key_stats_by_provider_ids(
&self,
provider_ids: &[String],
) -> Result<
Vec<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKeyStats>,
GatewayError,
> {
self.app
.list_provider_catalog_key_stats_by_provider_ids(provider_ids)
.await
}
pub(crate) async fn update_provider_catalog_key_health_state(
&self,
key_id: &str,
is_active: bool,
health_by_format: Option<&serde_json::Value>,
circuit_breaker_by_format: Option<&serde_json::Value>,
) -> Result<bool, GatewayError> {
self.app
.update_provider_catalog_key_health_state(
key_id,
is_active,
health_by_format,
circuit_breaker_by_format,
)
.await
}
pub(crate) async fn create_provider_catalog_endpoint(
&self,
endpoint: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogEndpoint,
) -> Result<
Option<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogEndpoint>,
GatewayError,
> {
self.app.create_provider_catalog_endpoint(endpoint).await
}
pub(crate) async fn update_provider_catalog_endpoint(
&self,
endpoint: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogEndpoint,
) -> Result<
Option<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogEndpoint>,
GatewayError,
> {
self.app.update_provider_catalog_endpoint(endpoint).await
}
pub(crate) async fn delete_provider_catalog_endpoint(
&self,
endpoint_id: &str,
) -> Result<bool, GatewayError> {
self.app.delete_provider_catalog_endpoint(endpoint_id).await
}
pub(crate) async fn update_provider_catalog_key(
&self,
key: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey,
) -> Result<
Option<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey>,
GatewayError,
> {
self.app.update_provider_catalog_key(key).await
}
pub(crate) async fn create_provider_catalog_key(
&self,
key: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey,
) -> Result<
Option<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey>,
GatewayError,
> {
self.app.create_provider_catalog_key(key).await
}
pub(crate) async fn delete_provider_catalog_key(
&self,
key_id: &str,
) -> Result<bool, GatewayError> {
self.app.delete_provider_catalog_key(key_id).await
}
pub(crate) async fn create_provider_catalog_provider(
&self,
provider: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
shift_existing_priorities_from: Option<i32>,
) -> Result<
Option<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider>,
GatewayError,
> {
self.app
.create_provider_catalog_provider(provider, shift_existing_priorities_from)
.await
}
pub(crate) async fn update_provider_catalog_provider(
&self,
provider: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
) -> Result<
Option<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider>,
GatewayError,
> {
self.app.update_provider_catalog_provider(provider).await
}
pub(crate) async fn cleanup_deleted_provider_catalog_refs(
&self,
provider_id: &str,
endpoint_ids: &[String],
key_ids: &[String],
) -> Result<(), GatewayError> {
self.app
.cleanup_deleted_provider_catalog_refs(provider_id, endpoint_ids, key_ids)
.await
}
}
@@ -0,0 +1,15 @@
use super::{
AdminAppState, AdminGatewayProviderTransportSnapshot, AdminKiroRequestAuth,
AdminLocalOAuthRefreshError, AdminProviderOAuthTemplate,
};
use crate::GatewayError;
use axum::body::Body;
use axum::http::Response;
use std::collections::BTreeMap;
mod builders;
mod catalog;
mod oauth;
mod routes;
mod tasks;
mod transport;
@@ -0,0 +1,520 @@
use super::*;
use crate::handlers::admin::provider::oauth::errors::build_internal_control_error_response;
use aether_data::repository::provider_oauth::{
build_provider_oauth_batch_task_status_payload, provider_oauth_batch_task_storage_key,
provider_oauth_device_session_storage_key, provider_oauth_state_storage_key,
StoredAdminProviderOAuthDeviceSession, StoredAdminProviderOAuthState,
PROVIDER_OAUTH_BATCH_TASK_TTL_SECS, PROVIDER_OAUTH_STATE_TTL_SECS,
};
use axum::http;
use serde_json::json;
use url::Url;
const KIRO_IDC_AMZ_USER_AGENT: &str =
"aws-sdk-js/3.738.0 ua/2.1 os/other lang/js md/browser#unknown_unknown api/sso-oidc#3.738.0 m/E KiroIDE";
impl<'a> AdminAppState<'a> {
pub(crate) async fn update_provider_catalog_key_oauth_credentials(
&self,
key_id: &str,
encrypted_api_key: &str,
encrypted_auth_config: Option<&str>,
expires_at_unix_secs: Option<u64>,
) -> Result<bool, GatewayError> {
self.app
.update_provider_catalog_key_oauth_credentials(
key_id,
encrypted_api_key,
encrypted_auth_config,
expires_at_unix_secs,
)
.await
}
pub(crate) async fn clear_provider_catalog_key_oauth_invalid_marker(
&self,
key_id: &str,
) -> Result<bool, GatewayError> {
self.app
.clear_provider_catalog_key_oauth_invalid_marker(key_id)
.await
}
pub(crate) async fn force_local_oauth_refresh_entry(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
) -> Result<Option<crate::provider_transport::CachedOAuthEntry>, AdminLocalOAuthRefreshError>
{
self.app.force_local_oauth_refresh_entry(transport).await
}
pub(crate) async fn save_provider_oauth_state(
&self,
key_id: &str,
provider_id: &str,
provider_type: &str,
pkce_verifier: Option<&str>,
) -> Result<String, GatewayError> {
let nonce = aether_admin::provider::state::generate_provider_oauth_nonce();
let payload = json!({
"nonce": nonce,
"key_id": key_id,
"provider_id": provider_id,
"provider_type": provider_type,
"pkce_verifier": pkce_verifier,
"created_at": aether_admin::provider::state::current_unix_secs(),
});
let key = provider_oauth_state_storage_key(&nonce);
let value = payload.to_string();
if let Some(runner) = self.redis_kv_runner() {
runner
.setex(&key, &value, Some(PROVIDER_OAUTH_STATE_TTL_SECS))
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
return Ok(nonce);
}
if self
.as_ref()
.save_provider_oauth_state_for_tests(&key, &value)
{
return Ok(nonce);
}
Err(GatewayError::Internal(
"provider oauth redis unavailable".to_string(),
))
}
pub(crate) async fn consume_provider_oauth_state(
&self,
nonce: &str,
) -> Result<Option<StoredAdminProviderOAuthState>, GatewayError> {
let key = provider_oauth_state_storage_key(nonce);
let raw = if let Some(runner) = self.redis_kv_runner() {
let mut connection = runner
.client()
.get_multiplexed_async_connection()
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
let namespaced_key = runner.keyspace().key(&key);
redis::cmd("GETDEL")
.arg(&namespaced_key)
.query_async::<Option<String>>(&mut connection)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?
} else {
self.as_ref().take_provider_oauth_state_for_tests(&key)
};
raw.map(|value| {
serde_json::from_str::<StoredAdminProviderOAuthState>(&value)
.map_err(|err| GatewayError::Internal(err.to_string()))
})
.transpose()
}
pub(crate) async fn exchange_admin_provider_oauth_code(
&self,
template: AdminProviderOAuthTemplate,
code: &str,
state_nonce: &str,
pkce_verifier: Option<&str>,
) -> Result<serde_json::Value, Response<Body>> {
crate::handlers::admin::provider::oauth::state::exchange_admin_provider_oauth_code(
self,
template,
code,
state_nonce,
pkce_verifier,
)
.await
}
pub(crate) async fn exchange_admin_provider_oauth_refresh_token(
&self,
template: AdminProviderOAuthTemplate,
refresh_token: &str,
) -> Result<serde_json::Value, Response<Body>> {
crate::handlers::admin::provider::oauth::state::exchange_admin_provider_oauth_refresh_token(
self,
template,
refresh_token,
)
.await
}
pub(crate) async fn save_provider_oauth_batch_task_payload(
&self,
task_id: &str,
task_state: &serde_json::Value,
) -> Result<(), GatewayError> {
let key = provider_oauth_batch_task_storage_key(task_id);
let serialized = serde_json::to_string(task_state)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
if let Some(runner) = self.redis_kv_runner() {
let Ok(mut connection) = runner.client().get_multiplexed_async_connection().await
else {
return Err(GatewayError::Internal(
"provider oauth batch task redis unavailable".to_string(),
));
};
let redis_key = runner.keyspace().key(&key);
redis::cmd("SET")
.arg(redis_key)
.arg(&serialized)
.arg("EX")
.arg(PROVIDER_OAUTH_BATCH_TASK_TTL_SECS)
.query_async::<()>(&mut connection)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
return Ok(());
}
if self
.as_ref()
.save_provider_oauth_batch_task_for_tests(&key, &serialized)
{
return Ok(());
}
Err(GatewayError::Internal(
"provider oauth batch task redis unavailable".to_string(),
))
}
pub(crate) async fn read_provider_oauth_batch_task_payload(
&self,
provider_id: &str,
task_id: &str,
) -> Result<Option<serde_json::Value>, GatewayError> {
let key = provider_oauth_batch_task_storage_key(task_id);
let raw = if let Some(runner) = self.redis_kv_runner() {
let Ok(mut connection) = runner.client().get_multiplexed_async_connection().await
else {
return Err(GatewayError::Internal(
"provider oauth batch task redis unavailable".to_string(),
));
};
let redis_key = runner.keyspace().key(&key);
redis::cmd("GET")
.arg(redis_key)
.query_async(&mut connection)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?
} else {
self.as_ref().load_provider_oauth_batch_task_for_tests(&key)
};
let Some(raw) = raw else {
return Ok(None);
};
let parsed = match serde_json::from_str::<serde_json::Value>(&raw) {
Ok(value) => value,
Err(_) => return Ok(None),
};
let Some(state) = parsed.as_object() else {
return Ok(None);
};
if state
.get("provider_id")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
!= provider_id
{
return Ok(None);
}
Ok(Some(build_provider_oauth_batch_task_status_payload(
provider_id,
state,
)))
}
pub(crate) async fn save_provider_oauth_device_session(
&self,
session_id: &str,
session: &StoredAdminProviderOAuthDeviceSession,
ttl_seconds: u64,
) -> Result<(), Response<Body>> {
let key = provider_oauth_device_session_storage_key(session_id);
let value = serde_json::to_string(session).map_err(|_| {
build_internal_control_error_response(
http::StatusCode::SERVICE_UNAVAILABLE,
"provider oauth redis unavailable",
)
})?;
if let Some(runner) = self.redis_kv_runner() {
runner
.setex(&key, &value, Some(ttl_seconds))
.await
.map_err(|_| {
build_internal_control_error_response(
http::StatusCode::SERVICE_UNAVAILABLE,
"provider oauth redis unavailable",
)
})?;
return Ok(());
}
if self
.as_ref()
.save_provider_oauth_device_session_for_tests(&key, &value)
{
return Ok(());
}
Err(build_internal_control_error_response(
http::StatusCode::SERVICE_UNAVAILABLE,
"provider oauth redis unavailable",
))
}
pub(crate) async fn read_provider_oauth_device_session(
&self,
session_id: &str,
) -> Result<Option<StoredAdminProviderOAuthDeviceSession>, GatewayError> {
let key = provider_oauth_device_session_storage_key(session_id);
let raw = if let Some(runner) = self.redis_kv_runner() {
let mut connection = runner
.client()
.get_multiplexed_async_connection()
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
let namespaced_key = runner.keyspace().key(&key);
redis::cmd("GET")
.arg(&namespaced_key)
.query_async::<Option<String>>(&mut connection)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?
} else {
self.as_ref()
.load_provider_oauth_device_session_for_tests(&key)
};
raw.map(|value| {
serde_json::from_str::<StoredAdminProviderOAuthDeviceSession>(&value)
.map_err(|err| GatewayError::Internal(err.to_string()))
})
.transpose()
}
pub(crate) async fn register_admin_kiro_device_oidc_client(
&self,
region: &str,
start_url: &str,
) -> Result<serde_json::Value, Response<Body>> {
let payload = post_kiro_device_oidc_json(
self,
"kiro_device_register",
format!("https://oidc.{region}.amazonaws.com/client/register"),
json!({
"clientName": "Aether Gateway",
"clientType": "public",
"scopes": [
"codewhisperer:completions",
"codewhisperer:analysis",
"codewhisperer:conversations",
"codewhisperer:transformations",
"codewhisperer:taskassist"
],
"grantTypes": [
"urn:ietf:params:oauth:grant-type:device_code",
"refresh_token"
],
"issuerUrl": start_url,
}),
)
.await?;
if payload
.get("_error")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
{
let error_desc = aether_admin::provider::state::json_non_empty_string(
payload.get("error_description"),
)
.or_else(|| aether_admin::provider::state::json_non_empty_string(payload.get("error")))
.unwrap_or_else(|| "unknown".to_string());
return Err(build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
format!("注册 OIDC 客户端失败: {error_desc}"),
));
}
Ok(payload)
}
pub(crate) async fn start_admin_kiro_device_authorization(
&self,
region: &str,
client_id: &str,
client_secret: &str,
start_url: &str,
) -> Result<serde_json::Value, Response<Body>> {
let payload = post_kiro_device_oidc_json(
self,
"kiro_device_authorize",
format!("https://oidc.{region}.amazonaws.com/device_authorization"),
json!({
"clientId": client_id,
"clientSecret": client_secret,
"startUrl": start_url,
}),
)
.await?;
if payload
.get("_error")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
{
let error_desc = aether_admin::provider::state::json_non_empty_string(
payload.get("error_description"),
)
.or_else(|| aether_admin::provider::state::json_non_empty_string(payload.get("error")))
.unwrap_or_else(|| "unknown".to_string());
return Err(build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
format!("发起设备授权失败: {error_desc}"),
));
}
Ok(payload)
}
pub(crate) async fn poll_admin_kiro_device_token(
&self,
region: &str,
client_id: &str,
client_secret: &str,
device_code: &str,
) -> Result<serde_json::Value, Response<Body>> {
post_kiro_device_oidc_json(
self,
"kiro_device_poll",
format!("https://oidc.{region}.amazonaws.com/token"),
json!({
"clientId": client_id,
"clientSecret": client_secret,
"grantType": "urn:ietf:params:oauth:grant-type:device_code",
"deviceCode": device_code,
}),
)
.await
}
pub(crate) async fn find_duplicate_provider_oauth_key(
&self,
provider_id: &str,
auth_config: &serde_json::Map<String, serde_json::Value>,
exclude_key_id: Option<&str>,
) -> Result<
Option<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey>,
String,
> {
crate::handlers::admin::provider::oauth::duplicates::find_duplicate_provider_oauth_key(
self,
provider_id,
auth_config,
exclude_key_id,
)
.await
}
pub(crate) async fn create_provider_oauth_catalog_key(
&self,
provider_id: &str,
name: &str,
access_token: &str,
auth_config: &serde_json::Map<String, serde_json::Value>,
api_formats: &[String],
proxy: Option<serde_json::Value>,
expires_at_unix_secs: Option<u64>,
) -> Result<
Option<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey>,
GatewayError,
> {
crate::handlers::admin::provider::oauth::provisioning::create_provider_oauth_catalog_key(
self,
provider_id,
name,
access_token,
auth_config,
api_formats,
proxy,
expires_at_unix_secs,
)
.await
}
pub(crate) async fn update_existing_provider_oauth_catalog_key(
&self,
existing_key: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey,
access_token: &str,
auth_config: &serde_json::Map<String, serde_json::Value>,
proxy: Option<serde_json::Value>,
expires_at_unix_secs: Option<u64>,
) -> Result<
Option<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey>,
GatewayError,
> {
crate::handlers::admin::provider::oauth::provisioning::update_existing_provider_oauth_catalog_key(
self,
existing_key,
access_token,
auth_config,
proxy,
expires_at_unix_secs,
)
.await
}
pub(crate) async fn refresh_provider_oauth_account_state_after_update(
&self,
provider: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
key_id: &str,
) -> Result<(bool, Option<String>), GatewayError> {
crate::handlers::admin::provider::oauth::runtime::refresh_provider_oauth_account_state_after_update(
self,
provider,
key_id,
)
.await
}
}
async fn post_kiro_device_oidc_json(
state: &AdminAppState<'_>,
endpoint_key: &str,
default_url: String,
body: serde_json::Value,
) -> Result<serde_json::Value, Response<Body>> {
let url = state.provider_oauth_token_url(endpoint_key, &default_url);
let host = Url::parse(&url)
.ok()
.and_then(|value| value.host_str().map(ToOwned::to_owned))
.unwrap_or_default();
let response = state
.http_client()
.post(url)
.header("Content-Type", "application/json")
.header("Accept", "*/*")
.header("User-Agent", "node")
.header("x-amz-user-agent", KIRO_IDC_AMZ_USER_AGENT)
.header("Host", host)
.json(&body)
.send()
.await
.map_err(|_| {
build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
"发起设备授权失败: unknown",
)
})?;
let status = response.status();
let body_text = response.text().await.map_err(|_| {
build_internal_control_error_response(
http::StatusCode::BAD_REQUEST,
"发起设备授权失败: unknown",
)
})?;
Ok(
serde_json::from_str::<serde_json::Value>(&body_text).unwrap_or_else(|_| {
json!({
"_error": !status.is_success(),
"error": body_text.trim(),
})
}),
)
}
@@ -0,0 +1,62 @@
use crate::handlers::admin::provider::crud::{delete_task, pool, reads, writes};
use crate::handlers::admin::request::AdminAppState;
use crate::handlers::admin::AdminRequestContext;
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
response::Response,
};
impl<'a> AdminAppState<'a> {
pub(crate) async fn maybe_build_admin_provider_crud_route_response(
&self,
request_context: &AdminRequestContext<'_>,
request_body: Option<&Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
let Some(decision) = request_context.decision() else {
return Ok(None);
};
if decision.route_family.as_deref() != Some("providers_manage") {
return Ok(None);
}
let route_kind = decision.route_kind.as_deref();
if let Some(response) = writes::maybe_build_local_admin_provider_writes_response(
self,
request_context,
request_body,
route_kind,
)
.await?
{
return Ok(Some(response));
}
if let Some(response) = reads::maybe_build_local_admin_provider_reads_response(
self,
request_context,
route_kind,
)
.await?
{
return Ok(Some(response));
}
if let Some(response) = delete_task::maybe_build_local_admin_provider_delete_task_response(
self,
request_context,
route_kind,
)
.await?
{
return Ok(Some(response));
}
if let Some(response) =
pool::maybe_build_local_admin_provider_pool_response(self, request_context, route_kind)
.await?
{
return Ok(Some(response));
}
Ok(None)
}
}
@@ -0,0 +1,3 @@
mod crud;
mod query;
mod strategy;
@@ -0,0 +1,148 @@
use crate::handlers::admin::provider::query::{
models::{
build_admin_provider_query_models_response,
build_admin_provider_query_test_model_failover_response,
build_admin_provider_query_test_model_response,
},
payload::{
parse_admin_provider_query_body, provider_query_extract_failover_models,
provider_query_extract_model, provider_query_extract_provider_id,
provider_query_extract_request_id, provider_query_payload_keys,
},
response::{
build_admin_provider_query_bad_request_response,
ADMIN_PROVIDER_QUERY_FAILOVER_MODELS_REQUIRED_DETAIL,
ADMIN_PROVIDER_QUERY_MODEL_REQUIRED_DETAIL,
ADMIN_PROVIDER_QUERY_PROVIDER_ID_REQUIRED_DETAIL,
},
};
use crate::handlers::admin::request::AdminAppState;
use crate::handlers::admin::AdminRequestContext;
use crate::log_ids::short_request_id;
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
http,
response::Response,
};
use tracing::warn;
impl<'a> AdminAppState<'a> {
pub(crate) async fn maybe_build_admin_provider_query_route_response(
&self,
request_context: &AdminRequestContext<'_>,
request_body: Option<&Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
let Some(decision) = request_context.decision() else {
return Ok(None);
};
if decision.route_family.as_deref() != Some("provider_query_manage") {
return Ok(None);
}
if request_context.method() != http::Method::POST {
return Ok(None);
}
let payload = match parse_admin_provider_query_body(request_body) {
Ok(value) => value,
Err(response) => return Ok(Some(response)),
};
let route_kind = decision.route_kind.as_deref().unwrap_or("query_models");
match route_kind {
"query_models" => Ok(Some(
build_admin_provider_query_models_response(self, &payload).await?,
)),
"test_model" => {
let Some(provider_id) = provider_query_extract_provider_id(&payload) else {
log_admin_provider_query_validation_failure(
request_context,
route_kind,
ADMIN_PROVIDER_QUERY_PROVIDER_ID_REQUIRED_DETAIL,
&payload,
);
return Ok(Some(build_admin_provider_query_bad_request_response(
ADMIN_PROVIDER_QUERY_PROVIDER_ID_REQUIRED_DETAIL,
)));
};
let Some(model) = provider_query_extract_model(&payload) else {
log_admin_provider_query_validation_failure(
request_context,
route_kind,
ADMIN_PROVIDER_QUERY_MODEL_REQUIRED_DETAIL,
&payload,
);
return Ok(Some(build_admin_provider_query_bad_request_response(
ADMIN_PROVIDER_QUERY_MODEL_REQUIRED_DETAIL,
)));
};
Ok(Some(build_admin_provider_query_test_model_response(
provider_id,
model,
)))
}
"test_model_failover" => {
let Some(provider_id) = provider_query_extract_provider_id(&payload) else {
log_admin_provider_query_validation_failure(
request_context,
route_kind,
ADMIN_PROVIDER_QUERY_PROVIDER_ID_REQUIRED_DETAIL,
&payload,
);
return Ok(Some(build_admin_provider_query_bad_request_response(
ADMIN_PROVIDER_QUERY_PROVIDER_ID_REQUIRED_DETAIL,
)));
};
let failover_models = provider_query_extract_failover_models(&payload);
if failover_models.is_empty() {
log_admin_provider_query_validation_failure(
request_context,
route_kind,
ADMIN_PROVIDER_QUERY_FAILOVER_MODELS_REQUIRED_DETAIL,
&payload,
);
return Ok(Some(build_admin_provider_query_bad_request_response(
ADMIN_PROVIDER_QUERY_FAILOVER_MODELS_REQUIRED_DETAIL,
)));
}
Ok(Some(
build_admin_provider_query_test_model_failover_response(
provider_id,
failover_models,
),
))
}
_ => Ok(Some(
build_admin_provider_query_models_response(self, &payload).await?,
)),
}
}
}
fn log_admin_provider_query_validation_failure(
request_context: &AdminRequestContext<'_>,
route_kind: &str,
detail: &'static str,
payload: &serde_json::Value,
) {
let provider_id =
provider_query_extract_provider_id(payload).unwrap_or_else(|| "-".to_string());
let model = provider_query_extract_model(payload).unwrap_or_else(|| "-".to_string());
let request_id = provider_query_extract_request_id(payload).unwrap_or_else(|| "-".to_string());
let request_id_for_log = short_request_id(request_id.as_str());
let payload_keys = provider_query_payload_keys(payload);
warn!(
event_name = "admin_provider_query_request_rejected",
log_type = "validation",
route_kind,
path = %request_context.path(),
request_id = %request_id_for_log,
provider_id = %provider_id,
model = %model,
payload_keys = ?payload_keys,
detail,
"admin provider query request rejected"
);
}
@@ -0,0 +1,160 @@
use crate::handlers::admin::provider::{
shared::paths::{
admin_provider_id_for_provider_strategy_billing,
admin_provider_id_for_provider_strategy_quota,
admin_provider_id_for_provider_strategy_stats, is_admin_provider_strategy_strategies_root,
},
strategy::{
builders::{
build_provider_strategy_list_response, build_provider_strategy_reset_quota_response,
build_provider_strategy_stats_response,
build_provider_strategy_update_billing_response, AdminProviderStrategyBillingRequest,
},
responses::{
admin_provider_strategy_data_unavailable_response,
ADMIN_PROVIDER_STRATEGY_DATA_UNAVAILABLE_DETAIL,
ADMIN_PROVIDER_STRATEGY_STATS_DATA_UNAVAILABLE_DETAIL,
},
},
};
use crate::handlers::admin::request::AdminAppState;
use crate::handlers::admin::shared::query_param_value;
use crate::handlers::admin::AdminRequestContext;
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
http,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
impl<'a> AdminAppState<'a> {
pub(crate) async fn maybe_build_admin_provider_strategy_route_response(
&self,
request_context: &AdminRequestContext<'_>,
request_body: Option<&Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
let Some(decision) = request_context.decision() else {
return Ok(None);
};
if decision.route_family.as_deref() != Some("provider_strategy_manage") {
return Ok(None);
}
if decision.route_kind.as_deref() == Some("list_strategies")
&& request_context.method() == http::Method::GET
&& is_admin_provider_strategy_strategies_root(request_context.path())
{
return Ok(Some(build_provider_strategy_list_response()));
}
if decision.route_kind.as_deref() == Some("update_provider_billing")
&& request_context.method() == http::Method::PUT
{
if !self.has_provider_catalog_data_reader() || !self.has_provider_catalog_data_writer()
{
return Ok(Some(admin_provider_strategy_data_unavailable_response(
ADMIN_PROVIDER_STRATEGY_DATA_UNAVAILABLE_DETAIL,
)));
}
let Some(provider_id) =
admin_provider_id_for_provider_strategy_billing(request_context.path())
else {
return Ok(Some(admin_provider_strategy_provider_not_found_response()));
};
let Some(request_body) = request_body else {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体不能为空" })),
)
.into_response(),
));
};
let payload =
match serde_json::from_slice::<AdminProviderStrategyBillingRequest>(request_body) {
Ok(payload) => payload,
Err(_) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求数据验证失败" })),
)
.into_response(),
))
}
};
return Ok(Some(
build_provider_strategy_update_billing_response(self, provider_id, payload).await?,
));
}
if decision.route_kind.as_deref() == Some("get_provider_stats")
&& request_context.method() == http::Method::GET
{
if !self.has_provider_catalog_data_reader() || !self.has_usage_data_reader() {
return Ok(Some(admin_provider_strategy_data_unavailable_response(
ADMIN_PROVIDER_STRATEGY_STATS_DATA_UNAVAILABLE_DETAIL,
)));
}
let Some(provider_id) =
admin_provider_id_for_provider_strategy_stats(request_context.path())
else {
return Ok(Some(admin_provider_strategy_provider_not_found_response()));
};
let hours = query_param_value(request_context.query_string(), "hours")
.and_then(|value| value.parse::<u64>().ok())
.filter(|value| *value > 0)
.unwrap_or(24);
return Ok(Some(
build_provider_strategy_stats_response(self, provider_id, hours).await?,
));
}
if decision.route_kind.as_deref() == Some("reset_provider_quota")
&& request_context.method() == http::Method::DELETE
{
if !self.has_provider_catalog_data_reader() || !self.has_provider_catalog_data_writer()
{
return Ok(Some(admin_provider_strategy_data_unavailable_response(
ADMIN_PROVIDER_STRATEGY_DATA_UNAVAILABLE_DETAIL,
)));
}
let Some(provider_id) =
admin_provider_id_for_provider_strategy_quota(request_context.path())
else {
return Ok(Some(admin_provider_strategy_provider_not_found_response()));
};
return Ok(Some(
build_provider_strategy_reset_quota_response(self, provider_id).await?,
));
}
Ok(Some(admin_provider_strategy_dispatcher_not_found_response()))
}
}
fn admin_provider_strategy_provider_not_found_response() -> Response<Body> {
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "Provider not found" })),
)
.into_response()
}
fn admin_provider_strategy_dispatcher_not_found_response() -> Response<Body> {
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "Provider strategy route not found" })),
)
.into_response()
}
@@ -0,0 +1,345 @@
use super::*;
use axum::{
body::Body,
http,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
impl<'a> AdminAppState<'a> {
pub(crate) async fn clear_admin_provider_pool_cooldown(&self, provider_id: &str, key_id: &str) {
crate::handlers::admin::provider::pool::runtime::clear_admin_provider_pool_cooldown(
self,
provider_id,
key_id,
)
.await
}
pub(crate) async fn reset_admin_provider_pool_cost(&self, provider_id: &str, key_id: &str) {
crate::handlers::admin::provider::pool::runtime::reset_admin_provider_pool_cost(
self,
provider_id,
key_id,
)
.await
}
pub(crate) fn put_provider_delete_task(&self, task: crate::LocalProviderDeleteTaskState) {
self.app.put_provider_delete_task(task)
}
pub(crate) async fn run_admin_provider_delete_task(
&self,
provider_id: &str,
task_id: &str,
) -> Result<crate::LocalProviderDeleteTaskState, GatewayError> {
crate::handlers::admin::provider::delete_task::run_admin_provider_delete_task(
self,
provider_id,
task_id,
)
.await
}
pub(crate) fn get_provider_delete_task(
&self,
task_id: &str,
) -> Option<crate::LocalProviderDeleteTaskState> {
self.app.get_provider_delete_task(task_id)
}
pub(crate) fn get_admin_pool_batch_delete_task_for_provider(
&self,
provider_id: &str,
task_id: &str,
) -> Result<crate::LocalProviderDeleteTaskState, Response<Body>> {
let Some(task) = self.get_provider_delete_task(task_id) else {
return Err((
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "批量删除任务不存在" })),
)
.into_response());
};
if task.provider_id != provider_id {
return Err((
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "批量删除任务不存在" })),
)
.into_response());
}
Ok(task)
}
pub(crate) async fn build_admin_pool_batch_import_response(
&self,
provider_id: &str,
payload: aether_admin::provider::pool::AdminPoolBatchImportRequest,
) -> Result<Response<Body>, GatewayError> {
use aether_admin::provider::pool as admin_provider_pool_pure;
let Some(provider) = self
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&provider_id.to_string()))
.await?
.into_iter()
.next()
else {
return Ok((
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": format!("Provider {provider_id} 不存在") })),
)
.into_response());
};
let endpoints = self
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
.await?;
let existing_keys = self
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
.await?;
let api_formats =
admin_provider_pool_pure::admin_pool_resolved_api_formats(&endpoints, &existing_keys);
if api_formats.is_empty() {
return Ok((
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "Provider 没有可用 endpoint 或现有 key,无法推断 api_formats" })),
)
.into_response());
}
let proxy =
admin_provider_pool_pure::admin_pool_key_proxy_value(payload.proxy_node_id.as_deref());
let mut imported = 0usize;
let skipped = 0usize;
let mut errors = Vec::new();
let now_unix_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs())
.unwrap_or(0);
for (index, item) in payload.keys.iter().enumerate() {
let api_key = item.api_key.trim();
if api_key.is_empty() {
errors.push(json!({
"index": index,
"reason": "api_key is empty",
}));
continue;
}
let Some(encrypted_api_key) = self.encrypt_catalog_secret_with_fallbacks(api_key)
else {
errors.push(json!({
"index": index,
"reason": "gateway 未配置 provider key 加密密钥",
}));
continue;
};
let auth_type = item.auth_type.trim().to_ascii_lowercase();
let auth_type = if auth_type.is_empty() {
"api_key".to_string()
} else {
auth_type
};
let name = item.name.trim();
let record = match admin_provider_pool_pure::build_admin_pool_batch_import_key_record(
uuid::Uuid::new_v4().to_string(),
provider.id.clone(),
if name.is_empty() {
format!("imported-{index}")
} else {
name.to_string()
},
auth_type,
api_formats.clone(),
encrypted_api_key,
proxy.clone(),
now_unix_secs,
) {
Ok(value) => value,
Err(err) => {
errors.push(json!({
"index": index,
"reason": err.to_string(),
}));
continue;
}
};
let Some(_) = self.create_provider_catalog_key(&record).await? else {
return Ok((
http::StatusCode::SERVICE_UNAVAILABLE,
Json(
json!({ "detail": "Admin pool cleanup requires provider catalog writer" }),
),
)
.into_response());
};
imported += 1;
}
Ok(Json(
admin_provider_pool_pure::build_admin_pool_batch_import_result_payload(
imported, skipped, errors,
),
)
.into_response())
}
pub(crate) async fn build_admin_pool_cleanup_banned_keys_response(
&self,
provider_id: &str,
) -> Result<Response<Body>, GatewayError> {
use aether_admin::provider::pool as admin_provider_pool_pure;
let Some(provider) = self
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&provider_id.to_string()))
.await?
.into_iter()
.next()
else {
return Ok((
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": format!("Provider {provider_id} 不存在") })),
)
.into_response());
};
let banned_keys = self
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
.await?
.into_iter()
.filter(admin_provider_pool_pure::admin_pool_key_is_known_banned)
.collect::<Vec<_>>();
if banned_keys.is_empty() {
return Ok(Json(
admin_provider_pool_pure::build_admin_pool_cleanup_empty_payload(
"未发现可清理的异常账号",
),
)
.into_response());
}
let deleted_key_ids = banned_keys
.iter()
.map(|key| key.id.clone())
.collect::<Vec<_>>();
for key in &banned_keys {
self.clear_admin_provider_pool_cooldown(&provider.id, &key.id)
.await;
self.reset_admin_provider_pool_cost(&provider.id, &key.id)
.await;
}
let mut affected = 0usize;
for key_id in &deleted_key_ids {
if self.delete_provider_catalog_key(key_id).await? {
affected += 1;
}
}
self.cleanup_deleted_provider_catalog_refs(&provider.id, &[], &deleted_key_ids)
.await?;
Ok(
Json(admin_provider_pool_pure::build_admin_pool_cleanup_result_payload(affected))
.into_response(),
)
}
pub(crate) async fn build_admin_pool_batch_action_response(
&self,
provider_id: &str,
payload: aether_admin::provider::pool::AdminPoolBatchActionRequest,
) -> Result<Response<Body>, GatewayError> {
use aether_admin::provider::pool::{
self as admin_provider_pool_pure, AdminPoolBatchActionKind,
};
let Some(provider) = self
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&provider_id.to_string()))
.await?
.into_iter()
.next()
else {
return Ok((
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": format!("Provider {provider_id} 不存在") })),
)
.into_response());
};
let plan = match admin_provider_pool_pure::build_admin_pool_batch_action_plan(payload) {
Ok(plan) => plan,
Err(detail) => {
return Ok((
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": detail })),
)
.into_response());
}
};
let keys = self
.read_provider_catalog_keys_by_ids(&plan.key_ids)
.await?
.into_iter()
.filter(|key| key.provider_id == provider.id)
.collect::<Vec<_>>();
if plan.action == AdminPoolBatchActionKind::Delete {
let deleted_key_ids = keys.iter().map(|key| key.id.clone()).collect::<Vec<_>>();
for key in &keys {
self.clear_admin_provider_pool_cooldown(&provider.id, &key.id)
.await;
self.reset_admin_provider_pool_cost(&provider.id, &key.id)
.await;
}
let mut affected = 0usize;
for key_id in &deleted_key_ids {
if self.delete_provider_catalog_key(key_id).await? {
affected = affected.saturating_add(1);
}
}
self.cleanup_deleted_provider_catalog_refs(&provider.id, &[], &deleted_key_ids)
.await?;
return Ok(Json(
admin_provider_pool_pure::build_admin_pool_batch_action_result_payload(
affected,
plan.action_label,
),
)
.into_response());
}
let mut affected = 0usize;
for mut key in keys {
match plan.action {
AdminPoolBatchActionKind::Enable => key.is_active = true,
AdminPoolBatchActionKind::Disable => key.is_active = false,
AdminPoolBatchActionKind::ClearProxy => key.proxy = None,
AdminPoolBatchActionKind::SetProxy => key.proxy = plan.proxy_payload.clone(),
AdminPoolBatchActionKind::RegenerateFingerprint => {
key.fingerprint =
Some(aether_provider_transport::claude_code::generate_random_fingerprint())
}
AdminPoolBatchActionKind::Delete => unreachable!(),
}
if self.update_provider_catalog_key(&key).await?.is_some() {
affected = affected.saturating_add(1);
}
}
Ok(Json(
admin_provider_pool_pure::build_admin_pool_batch_action_result_payload(
affected,
plan.action_label,
),
)
.into_response())
}
}
@@ -0,0 +1,224 @@
use super::*;
impl<'a> AdminAppState<'a> {
pub(crate) async fn read_provider_transport_snapshot(
&self,
provider_id: &str,
endpoint_id: &str,
key_id: &str,
) -> Result<Option<AdminGatewayProviderTransportSnapshot>, GatewayError> {
self.app
.read_provider_transport_snapshot(provider_id, endpoint_id, key_id)
.await
}
pub(crate) async fn resolve_local_oauth_request_auth(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
) -> Result<Option<crate::provider_transport::LocalResolvedOAuthRequestAuth>, GatewayError>
{
self.app.resolve_local_oauth_request_auth(transport).await
}
pub(crate) async fn resolve_local_oauth_header_auth(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
) -> Result<Option<(String, String)>, GatewayError> {
Ok(
match self.resolve_local_oauth_request_auth(transport).await? {
Some(crate::provider_transport::LocalResolvedOAuthRequestAuth::Header {
name,
value,
}) => Some((name, value)),
_ => None,
},
)
}
pub(crate) async fn resolve_local_oauth_kiro_request_auth(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
) -> Result<Option<AdminKiroRequestAuth>, GatewayError> {
Ok(
match self.resolve_local_oauth_request_auth(transport).await? {
Some(crate::provider_transport::LocalResolvedOAuthRequestAuth::Kiro(auth)) => {
Some(auth)
}
_ => None,
},
)
}
pub(crate) fn resolve_local_antigravity_identity_headers(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
) -> Option<(String, BTreeMap<String, String>)> {
match crate::provider_transport::antigravity::resolve_local_antigravity_request_auth(
transport,
) {
crate::provider_transport::antigravity::AntigravityRequestAuthSupport::Supported(
auth,
) => Some((
auth.project_id.clone(),
crate::provider_transport::antigravity::build_antigravity_static_identity_headers(
&auth,
),
)),
crate::provider_transport::antigravity::AntigravityRequestAuthSupport::Unsupported(
_,
) => None,
}
}
pub(crate) async fn resolve_transport_proxy_snapshot_with_tunnel_affinity(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
) -> Option<aether_contracts::ProxySnapshot> {
crate::provider_transport::resolve_transport_proxy_snapshot_with_tunnel_affinity(
self.app, transport,
)
.await
}
pub(crate) fn fixed_provider_template(
&self,
provider_type: &str,
) -> Option<(&'static str, &'static [&'static str])> {
crate::provider_transport::provider_types::fixed_provider_template(provider_type)
}
pub(crate) fn provider_type_is_fixed(&self, provider_type: &str) -> bool {
crate::provider_transport::provider_types::provider_type_is_fixed(provider_type)
}
pub(crate) fn provider_type_enables_format_conversion_by_default(
&self,
provider_type: &str,
) -> bool {
crate::provider_transport::provider_types::provider_type_enables_format_conversion_by_default(
provider_type,
)
}
pub(crate) fn supports_local_gemini_transport_with_network(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
api_format: &str,
) -> bool {
crate::provider_transport::policy::supports_local_gemini_transport_with_network(
transport, api_format,
)
}
pub(crate) fn resolve_local_gemini_auth(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
) -> Option<(String, String)> {
crate::provider_transport::auth::resolve_local_gemini_auth(transport)
}
pub(crate) fn build_passthrough_headers_with_auth(
&self,
headers: &axum::http::HeaderMap,
auth_header: &str,
auth_value: &str,
extra_headers: &BTreeMap<String, String>,
) -> BTreeMap<String, String> {
crate::provider_transport::auth::build_passthrough_headers_with_auth(
headers,
auth_header,
auth_value,
extra_headers,
)
}
pub(crate) fn apply_local_header_rules(
&self,
headers: &mut BTreeMap<String, String>,
rules: Option<&serde_json::Value>,
protected_keys: &[&str],
body: &serde_json::Value,
original_body: Option<&serde_json::Value>,
) -> bool {
crate::provider_transport::apply_local_header_rules(
headers,
rules,
protected_keys,
body,
original_body,
)
}
pub(crate) fn build_gemini_files_passthrough_url(
&self,
upstream_base_url: &str,
path: &str,
query: Option<&str>,
) -> Option<String> {
crate::provider_transport::url::build_gemini_files_passthrough_url(
upstream_base_url,
path,
query,
)
}
pub(crate) fn resolve_transport_tls_profile(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
) -> Option<String> {
crate::provider_transport::resolve_transport_tls_profile(transport)
}
pub(crate) fn resolve_transport_execution_timeouts(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
) -> Option<aether_contracts::ExecutionTimeouts> {
crate::provider_transport::resolve_transport_execution_timeouts(transport)
}
pub(crate) fn build_passthrough_path_url(
&self,
upstream_base_url: &str,
path: &str,
query: Option<&str>,
blocked_keys: &[&str],
) -> Option<String> {
crate::provider_transport::url::build_passthrough_path_url(
upstream_base_url,
path,
query,
blocked_keys,
)
}
pub(crate) fn build_claude_messages_url(
&self,
upstream_base_url: &str,
query: Option<&str>,
) -> String {
crate::provider_transport::url::build_claude_messages_url(upstream_base_url, query)
}
pub(crate) fn build_gemini_content_url(
&self,
upstream_base_url: &str,
model: &str,
stream: bool,
query: Option<&str>,
) -> Option<String> {
crate::provider_transport::url::build_gemini_content_url(
upstream_base_url,
model,
stream,
query,
)
}
pub(crate) fn build_openai_chat_url(
&self,
upstream_base_url: &str,
query: Option<&str>,
) -> String {
crate::provider_transport::url::build_openai_chat_url(upstream_base_url, query)
}
}