mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
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:
@@ -0,0 +1,170 @@
|
||||
use super::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn get_ldap_module_config(
|
||||
&self,
|
||||
) -> Result<Option<aether_data::repository::auth_modules::StoredLdapModuleConfig>, GatewayError>
|
||||
{
|
||||
self.app.get_ldap_module_config().await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_oauth_provider_configs(
|
||||
&self,
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::oauth_providers::StoredOAuthProviderConfig>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.list_oauth_provider_configs().await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_oauth_provider_config(
|
||||
&self,
|
||||
provider_type: &str,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::oauth_providers::StoredOAuthProviderConfig>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.get_oauth_provider_config(provider_type).await
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_oauth_provider_config(
|
||||
&self,
|
||||
record: &aether_data::repository::oauth_providers::UpsertOAuthProviderConfigRecord,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::oauth_providers::StoredOAuthProviderConfig>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.upsert_oauth_provider_config(record).await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_oauth_provider_config(
|
||||
&self,
|
||||
provider_type: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app.delete_oauth_provider_config(provider_type).await
|
||||
}
|
||||
|
||||
pub(crate) async fn count_locked_users_if_oauth_provider_disabled(
|
||||
&self,
|
||||
provider_type: &str,
|
||||
ldap_exclusive: bool,
|
||||
) -> Result<usize, GatewayError> {
|
||||
self.app
|
||||
.count_locked_users_if_oauth_provider_disabled(provider_type, ldap_exclusive)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_management_token_with_user(
|
||||
&self,
|
||||
token_id: &str,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::management_tokens::StoredManagementTokenWithUser>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.get_management_token_with_user(token_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_management_token(
|
||||
&self,
|
||||
token_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app.delete_management_token(token_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_management_tokens(
|
||||
&self,
|
||||
query: &aether_data::repository::management_tokens::ManagementTokenListQuery,
|
||||
) -> Result<
|
||||
aether_data::repository::management_tokens::StoredManagementTokenListPage,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.list_management_tokens(query).await
|
||||
}
|
||||
|
||||
pub(crate) async fn set_management_token_active(
|
||||
&self,
|
||||
token_id: &str,
|
||||
is_active: bool,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::management_tokens::StoredManagementToken>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.set_management_token_active(token_id, is_active)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_admin_security_blacklist(
|
||||
&self,
|
||||
ip_address: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app.remove_admin_security_blacklist(ip_address).await
|
||||
}
|
||||
|
||||
pub(crate) async fn add_admin_security_blacklist(
|
||||
&self,
|
||||
ip_address: &str,
|
||||
reason: &str,
|
||||
ttl_seconds: Option<u64>,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app
|
||||
.add_admin_security_blacklist(ip_address, reason, ttl_seconds)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_security_blacklist_stats(
|
||||
&self,
|
||||
) -> Result<(bool, usize, Option<String>), GatewayError> {
|
||||
self.app.admin_security_blacklist_stats().await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_security_blacklist(
|
||||
&self,
|
||||
) -> Result<Vec<crate::state::AdminSecurityBlacklistEntry>, GatewayError> {
|
||||
self.app.list_admin_security_blacklist().await
|
||||
}
|
||||
|
||||
pub(crate) async fn add_admin_security_whitelist(
|
||||
&self,
|
||||
ip_address: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app.add_admin_security_whitelist(ip_address).await
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_admin_security_whitelist(
|
||||
&self,
|
||||
ip_address: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app.remove_admin_security_whitelist(ip_address).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_security_whitelist(&self) -> Result<Vec<String>, GatewayError> {
|
||||
self.app.list_admin_security_whitelist().await
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_ldap_module_config(
|
||||
&self,
|
||||
config: &aether_data::repository::auth_modules::StoredLdapModuleConfig,
|
||||
) -> Result<Option<aether_data::repository::auth_modules::StoredLdapModuleConfig>, GatewayError>
|
||||
{
|
||||
self.app.upsert_ldap_module_config(config).await
|
||||
}
|
||||
|
||||
pub(crate) async fn count_active_local_admin_users_with_valid_password(
|
||||
&self,
|
||||
) -> Result<u64, GatewayError> {
|
||||
self.app
|
||||
.count_active_local_admin_users_with_valid_password()
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_enabled_oauth_module_providers(
|
||||
&self,
|
||||
) -> Result<
|
||||
Vec<aether_data::repository::auth_modules::StoredOAuthProviderModuleConfig>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.list_enabled_oauth_module_providers().await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
use super::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn list_admin_billing_collectors(
|
||||
&self,
|
||||
api_format: Option<&str>,
|
||||
task_type: Option<&str>,
|
||||
dimension_name: Option<&str>,
|
||||
is_enabled: Option<bool>,
|
||||
page: u32,
|
||||
page_size: u32,
|
||||
) -> Result<Option<(Vec<crate::AdminBillingCollectorRecord>, u64)>, GatewayError> {
|
||||
self.app
|
||||
.list_admin_billing_collectors(
|
||||
api_format,
|
||||
task_type,
|
||||
dimension_name,
|
||||
is_enabled,
|
||||
page,
|
||||
page_size,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_billing_collector(
|
||||
&self,
|
||||
collector_id: &str,
|
||||
) -> Result<Option<crate::AdminBillingCollectorRecord>, GatewayError> {
|
||||
self.app.read_admin_billing_collector(collector_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_admin_billing_collector(
|
||||
&self,
|
||||
input: &crate::AdminBillingCollectorWriteInput,
|
||||
) -> Result<crate::LocalMutationOutcome<crate::AdminBillingCollectorRecord>, GatewayError> {
|
||||
self.app.create_admin_billing_collector(input).await
|
||||
}
|
||||
|
||||
pub(crate) async fn update_admin_billing_collector(
|
||||
&self,
|
||||
collector_id: &str,
|
||||
input: &crate::AdminBillingCollectorWriteInput,
|
||||
) -> Result<crate::LocalMutationOutcome<crate::AdminBillingCollectorRecord>, GatewayError> {
|
||||
self.app
|
||||
.update_admin_billing_collector(collector_id, input)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_billing_enabled_default_value_exists(
|
||||
&self,
|
||||
api_format: &str,
|
||||
task_type: &str,
|
||||
dimension_name: &str,
|
||||
existing_id: Option<&str>,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app
|
||||
.admin_billing_enabled_default_value_exists(
|
||||
api_format,
|
||||
task_type,
|
||||
dimension_name,
|
||||
existing_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_admin_billing_preset(
|
||||
&self,
|
||||
preset: &str,
|
||||
mode: &str,
|
||||
collectors: &[crate::AdminBillingCollectorWriteInput],
|
||||
) -> Result<
|
||||
crate::LocalMutationOutcome<crate::state::AdminBillingPresetApplyResult>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.apply_admin_billing_preset(preset, mode, collectors)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_billing_rules(
|
||||
&self,
|
||||
task_type: Option<&str>,
|
||||
is_enabled: Option<bool>,
|
||||
page: u32,
|
||||
page_size: u32,
|
||||
) -> Result<Option<(Vec<crate::AdminBillingRuleRecord>, u64)>, GatewayError> {
|
||||
self.app
|
||||
.list_admin_billing_rules(task_type, is_enabled, page, page_size)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_billing_rule(
|
||||
&self,
|
||||
rule_id: &str,
|
||||
) -> Result<Option<crate::AdminBillingRuleRecord>, GatewayError> {
|
||||
self.app.read_admin_billing_rule(rule_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_admin_billing_rule(
|
||||
&self,
|
||||
input: &crate::AdminBillingRuleWriteInput,
|
||||
) -> Result<crate::LocalMutationOutcome<crate::AdminBillingRuleRecord>, GatewayError> {
|
||||
self.app.create_admin_billing_rule(input).await
|
||||
}
|
||||
|
||||
pub(crate) async fn update_admin_billing_rule(
|
||||
&self,
|
||||
rule_id: &str,
|
||||
input: &crate::AdminBillingRuleWriteInput,
|
||||
) -> Result<crate::LocalMutationOutcome<crate::AdminBillingRuleRecord>, GatewayError> {
|
||||
self.app.update_admin_billing_rule(rule_id, input).await
|
||||
}
|
||||
|
||||
pub(crate) async fn find_wallet(
|
||||
&self,
|
||||
lookup: aether_data::repository::wallet::WalletLookupKey<'_>,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
self.app.find_wallet(lookup).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_wallets(
|
||||
&self,
|
||||
status: Option<&str>,
|
||||
owner_type: Option<&str>,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<aether_data::repository::wallet::StoredAdminWalletListItem>,
|
||||
u64,
|
||||
),
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.list_admin_wallets(status, owner_type, limit, offset)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_wallet_ledger(
|
||||
&self,
|
||||
category: Option<&str>,
|
||||
reason_code: Option<&str>,
|
||||
owner_type: Option<&str>,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<aether_data::repository::wallet::StoredAdminWalletLedgerItem>,
|
||||
u64,
|
||||
),
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.list_admin_wallet_ledger(category, reason_code, owner_type, limit, offset)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_wallet_refund_requests(
|
||||
&self,
|
||||
status: Option<&str>,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<aether_data::repository::wallet::StoredAdminWalletRefundRequestItem>,
|
||||
u64,
|
||||
),
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.list_admin_wallet_refund_requests(status, limit, offset)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_wallet_transactions(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<aether_data::repository::wallet::StoredAdminWalletTransaction>,
|
||||
u64,
|
||||
),
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.list_admin_wallet_transactions(wallet_id, limit, offset)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_wallet_refunds(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<(Vec<crate::AdminWalletRefundRecord>, u64), GatewayError> {
|
||||
self.app
|
||||
.list_admin_wallet_refunds(wallet_id, limit, offset)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_payment_orders(
|
||||
&self,
|
||||
status: Option<&str>,
|
||||
payment_method: Option<&str>,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<Option<(Vec<crate::AdminWalletPaymentOrderRecord>, u64)>, GatewayError> {
|
||||
self.app
|
||||
.list_admin_payment_orders(status, payment_method, limit, offset)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_payment_callbacks(
|
||||
&self,
|
||||
payment_method: Option<&str>,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<Option<(Vec<crate::GatewayAdminPaymentCallbackView>, u64)>, GatewayError> {
|
||||
self.app
|
||||
.list_admin_payment_callbacks(payment_method, limit, offset)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_payment_order(
|
||||
&self,
|
||||
order_id: &str,
|
||||
) -> Result<crate::AdminWalletMutationOutcome<crate::AdminWalletPaymentOrderRecord>, GatewayError>
|
||||
{
|
||||
self.app.read_admin_payment_order(order_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_expire_payment_order(
|
||||
&self,
|
||||
order_id: &str,
|
||||
) -> Result<
|
||||
crate::AdminWalletMutationOutcome<(crate::AdminWalletPaymentOrderRecord, bool)>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.admin_expire_payment_order(order_id).await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn admin_credit_payment_order(
|
||||
&self,
|
||||
order_id: &str,
|
||||
gateway_order_id: Option<&str>,
|
||||
pay_amount: Option<f64>,
|
||||
pay_currency: Option<&str>,
|
||||
exchange_rate: Option<f64>,
|
||||
gateway_response_patch: Option<serde_json::Value>,
|
||||
operator_id: Option<&str>,
|
||||
) -> Result<
|
||||
crate::AdminWalletMutationOutcome<(crate::AdminWalletPaymentOrderRecord, bool)>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.admin_credit_payment_order(
|
||||
order_id,
|
||||
gateway_order_id,
|
||||
pay_amount,
|
||||
pay_currency,
|
||||
exchange_rate,
|
||||
gateway_response_patch,
|
||||
operator_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_fail_payment_order(
|
||||
&self,
|
||||
order_id: &str,
|
||||
) -> Result<crate::AdminWalletMutationOutcome<crate::AdminWalletPaymentOrderRecord>, GatewayError>
|
||||
{
|
||||
self.app.admin_fail_payment_order(order_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_adjust_wallet_balance(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
amount_usd: f64,
|
||||
balance_type: &str,
|
||||
operator_id: Option<&str>,
|
||||
description: Option<&str>,
|
||||
) -> Result<
|
||||
Option<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
crate::AdminWalletTransactionRecord,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.admin_adjust_wallet_balance(
|
||||
wallet_id,
|
||||
amount_usd,
|
||||
balance_type,
|
||||
operator_id,
|
||||
description,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_create_manual_wallet_recharge(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
amount_usd: f64,
|
||||
payment_method: &str,
|
||||
operator_id: Option<&str>,
|
||||
description: Option<&str>,
|
||||
) -> Result<
|
||||
Option<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
crate::AdminWalletPaymentOrderRecord,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.admin_create_manual_wallet_recharge(
|
||||
wallet_id,
|
||||
amount_usd,
|
||||
payment_method,
|
||||
operator_id,
|
||||
description,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_process_wallet_refund(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
refund_id: &str,
|
||||
operator_id: Option<&str>,
|
||||
) -> Result<
|
||||
crate::AdminWalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
crate::AdminWalletRefundRecord,
|
||||
crate::AdminWalletTransactionRecord,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.admin_process_wallet_refund(wallet_id, refund_id, operator_id)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_complete_wallet_refund(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
refund_id: &str,
|
||||
gateway_refund_id: Option<&str>,
|
||||
payout_reference: Option<&str>,
|
||||
payout_proof: Option<serde_json::Value>,
|
||||
) -> Result<crate::AdminWalletMutationOutcome<crate::AdminWalletRefundRecord>, GatewayError>
|
||||
{
|
||||
self.app
|
||||
.admin_complete_wallet_refund(
|
||||
wallet_id,
|
||||
refund_id,
|
||||
gateway_refund_id,
|
||||
payout_reference,
|
||||
payout_proof,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_fail_wallet_refund(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
refund_id: &str,
|
||||
reason: &str,
|
||||
operator_id: Option<&str>,
|
||||
) -> Result<
|
||||
crate::AdminWalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
crate::AdminWalletRefundRecord,
|
||||
Option<crate::AdminWalletTransactionRecord>,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.admin_fail_wallet_refund(wallet_id, refund_id, reason, operator_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
use super::AdminAppState;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) fn http_client(&self) -> &reqwest::Client {
|
||||
&self.app.client
|
||||
}
|
||||
|
||||
pub(crate) fn provider_oauth_token_url(
|
||||
&self,
|
||||
provider_type: &str,
|
||||
default_url: &str,
|
||||
) -> String {
|
||||
self.app
|
||||
.provider_oauth_token_url(provider_type, default_url)
|
||||
}
|
||||
|
||||
pub(crate) fn encryption_key(&self) -> Option<&str> {
|
||||
self.app.encryption_key()
|
||||
}
|
||||
|
||||
pub(crate) fn encrypt_catalog_secret_with_fallbacks(&self, secret: &str) -> Option<String> {
|
||||
crate::handlers::admin::shared::encrypt_catalog_secret_with_fallbacks(self.app, secret)
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt_catalog_secret_with_fallbacks(&self, ciphertext: &str) -> Option<String> {
|
||||
crate::handlers::admin::shared::decrypt_catalog_secret_with_fallbacks(
|
||||
self.app.encryption_key(),
|
||||
ciphertext,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_catalog_auth_config_json(
|
||||
&self,
|
||||
key: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey,
|
||||
) -> Option<serde_json::Map<String, serde_json::Value>> {
|
||||
crate::handlers::admin::shared::parse_catalog_auth_config_json(self.app, key)
|
||||
}
|
||||
|
||||
pub(crate) fn has_provider_catalog_data_reader(&self) -> bool {
|
||||
self.app.has_provider_catalog_data_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn has_provider_catalog_data_writer(&self) -> bool {
|
||||
self.app.has_provider_catalog_data_writer()
|
||||
}
|
||||
|
||||
pub(crate) fn has_request_candidate_data_reader(&self) -> bool {
|
||||
self.app.has_request_candidate_data_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn has_management_token_reader(&self) -> bool {
|
||||
self.app.has_management_token_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn has_management_token_writer(&self) -> bool {
|
||||
self.app.has_management_token_writer()
|
||||
}
|
||||
|
||||
pub(crate) fn has_global_model_data_reader(&self) -> bool {
|
||||
self.app.has_global_model_data_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn has_global_model_data_writer(&self) -> bool {
|
||||
self.app.has_global_model_data_writer()
|
||||
}
|
||||
|
||||
pub(crate) fn has_usage_data_reader(&self) -> bool {
|
||||
self.app.has_usage_data_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn has_gemini_file_mapping_data_reader(&self) -> bool {
|
||||
self.app.has_gemini_file_mapping_data_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn has_gemini_file_mapping_data_writer(&self) -> bool {
|
||||
self.app.has_gemini_file_mapping_data_writer()
|
||||
}
|
||||
|
||||
pub(crate) fn has_auth_api_key_data_reader(&self) -> bool {
|
||||
self.app.has_auth_api_key_data_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn has_user_data_reader(&self) -> bool {
|
||||
self.app.has_user_data_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn has_proxy_node_reader(&self) -> bool {
|
||||
self.app.has_proxy_node_reader()
|
||||
}
|
||||
|
||||
pub(crate) fn has_auth_api_key_writer(&self) -> bool {
|
||||
self.app.data.has_auth_api_key_writer()
|
||||
}
|
||||
|
||||
pub(crate) fn has_auth_module_writer(&self) -> bool {
|
||||
self.app.has_auth_module_writer()
|
||||
}
|
||||
|
||||
pub(crate) fn has_auth_user_write_capability(&self) -> bool {
|
||||
self.app.has_auth_user_write_capability()
|
||||
}
|
||||
|
||||
pub(crate) fn has_auth_wallet_write_capability(&self) -> bool {
|
||||
self.app.has_auth_wallet_write_capability()
|
||||
}
|
||||
|
||||
pub(crate) fn mark_provider_key_rpm_reset(&self, key_id: &str, now_unix_secs: u64) {
|
||||
self.app.mark_provider_key_rpm_reset(key_id, now_unix_secs)
|
||||
}
|
||||
|
||||
pub(crate) fn redis_kv_runner(&self) -> Option<aether_data::redis::RedisKvRunner> {
|
||||
self.app.redis_kv_runner()
|
||||
}
|
||||
|
||||
pub(crate) fn provider_key_rpm_reset_at(
|
||||
&self,
|
||||
key_id: &str,
|
||||
now_unix_secs: u64,
|
||||
) -> Option<u64> {
|
||||
self.app.provider_key_rpm_reset_at(key_id, now_unix_secs)
|
||||
}
|
||||
|
||||
pub(crate) fn has_postgres_pool(&self) -> bool {
|
||||
self.app.postgres_pool().is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn mark_admin_monitoring_error_stats_reset(&self, now_unix_secs: u64) {
|
||||
self.app
|
||||
.mark_admin_monitoring_error_stats_reset(now_unix_secs);
|
||||
}
|
||||
|
||||
pub(crate) fn admin_monitoring_error_stats_reset_at(&self) -> Option<u64> {
|
||||
self.app.admin_monitoring_error_stats_reset_at()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use crate::control::{GatewayControlDecision, GatewayPublicRequestContext};
|
||||
use axum::http::Method;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct AdminRequestContext<'a> {
|
||||
context: &'a GatewayPublicRequestContext,
|
||||
}
|
||||
|
||||
impl<'a> AdminRequestContext<'a> {
|
||||
pub(crate) fn new(context: &'a GatewayPublicRequestContext) -> Self {
|
||||
Self { context }
|
||||
}
|
||||
|
||||
pub(crate) fn decision(&self) -> Option<&GatewayControlDecision> {
|
||||
self.context.control_decision.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn route_family(&self) -> Option<&str> {
|
||||
self.decision()
|
||||
.and_then(|decision| decision.route_family.as_deref())
|
||||
}
|
||||
|
||||
pub(crate) fn route_kind(&self) -> Option<&str> {
|
||||
self.decision()
|
||||
.and_then(|decision| decision.route_kind.as_deref())
|
||||
}
|
||||
|
||||
pub(crate) fn method(&self) -> &Method {
|
||||
&self.context.request_method
|
||||
}
|
||||
|
||||
pub(crate) fn path(&self) -> &str {
|
||||
&self.context.request_path
|
||||
}
|
||||
|
||||
pub(crate) fn query_string(&self) -> Option<&str> {
|
||||
self.context.request_query_string.as_deref()
|
||||
}
|
||||
|
||||
pub(crate) fn content_type(&self) -> Option<&str> {
|
||||
self.context.request_content_type.as_deref()
|
||||
}
|
||||
|
||||
pub(crate) fn trace_id(&self) -> &str {
|
||||
self.context.trace_id.as_str()
|
||||
}
|
||||
|
||||
pub(crate) fn public(&self) -> &GatewayPublicRequestContext {
|
||||
self.context
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for AdminRequestContext<'a> {
|
||||
type Target = GatewayPublicRequestContext;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use super::AdminAppState;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn build_admin_health_summary_payload(&self) -> Option<serde_json::Value> {
|
||||
crate::handlers::admin::endpoint::build_admin_health_summary_payload(self).await
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_key_health_payload(
|
||||
&self,
|
||||
key_id: &str,
|
||||
api_format: Option<&str>,
|
||||
) -> Option<serde_json::Value> {
|
||||
crate::handlers::admin::endpoint::build_admin_key_health_payload(self, key_id, api_format)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn recover_admin_key_health(
|
||||
&self,
|
||||
key_id: &str,
|
||||
api_format: Option<&str>,
|
||||
) -> Option<serde_json::Value> {
|
||||
crate::handlers::admin::endpoint::recover_admin_key_health(self, key_id, api_format).await
|
||||
}
|
||||
|
||||
pub(crate) async fn recover_all_admin_key_health(&self) -> Option<serde_json::Value> {
|
||||
crate::handlers::admin::endpoint::recover_all_admin_key_health(self).await
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_endpoint_health_status_payload(
|
||||
&self,
|
||||
lookback_hours: u64,
|
||||
) -> Option<serde_json::Value> {
|
||||
crate::handlers::admin::endpoint::build_admin_endpoint_health_status_payload(
|
||||
self,
|
||||
lookback_hours,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_key_rpm_payload(
|
||||
&self,
|
||||
key_id: &str,
|
||||
) -> Option<serde_json::Value> {
|
||||
crate::handlers::admin::endpoint::build_admin_key_rpm_payload(self, key_id).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
use super::{AdminAppState, AdminCancelVideoTaskError};
|
||||
use crate::GatewayError;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn list_gemini_file_mappings(
|
||||
&self,
|
||||
query: &aether_data::repository::gemini_file_mappings::GeminiFileMappingListQuery,
|
||||
) -> Result<
|
||||
aether_data::repository::gemini_file_mappings::StoredGeminiFileMappingListPage,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.list_gemini_file_mappings(query).await
|
||||
}
|
||||
|
||||
pub(crate) async fn summarize_gemini_file_mappings(
|
||||
&self,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<aether_data::repository::gemini_file_mappings::GeminiFileMappingStats, GatewayError>
|
||||
{
|
||||
self.app.summarize_gemini_file_mappings(now_unix_secs).await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_gemini_file_mapping_by_id(
|
||||
&self,
|
||||
mapping_id: &str,
|
||||
) -> Result<
|
||||
Option<aether_data::repository::gemini_file_mappings::StoredGeminiFileMapping>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.delete_gemini_file_mapping_by_id(mapping_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_expired_gemini_file_mappings(
|
||||
&self,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<usize, GatewayError> {
|
||||
self.app
|
||||
.delete_expired_gemini_file_mappings(now_unix_secs)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn count_distinct_video_task_users(
|
||||
&self,
|
||||
filter: &aether_data_contracts::repository::video_tasks::VideoTaskQueryFilter,
|
||||
) -> Result<u64, GatewayError> {
|
||||
self.app.count_distinct_video_task_users(filter).await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_video_task_page(
|
||||
&self,
|
||||
filter: &aether_data_contracts::repository::video_tasks::VideoTaskQueryFilter,
|
||||
page: usize,
|
||||
page_size: usize,
|
||||
) -> Result<crate::async_task::VideoTaskPageResponse, GatewayError> {
|
||||
crate::async_task::read_video_task_page(self.app, filter, page, page_size).await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_video_task_stats(
|
||||
&self,
|
||||
filter: &aether_data_contracts::repository::video_tasks::VideoTaskQueryFilter,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<crate::async_task::VideoTaskStatsResponse, GatewayError> {
|
||||
crate::async_task::read_video_task_stats(self.app, filter, now_unix_secs).await
|
||||
}
|
||||
|
||||
pub(crate) async fn cancel_video_task_record(
|
||||
&self,
|
||||
task_id: &str,
|
||||
) -> Result<
|
||||
aether_data_contracts::repository::video_tasks::StoredVideoTask,
|
||||
AdminCancelVideoTaskError,
|
||||
> {
|
||||
crate::async_task::cancel_video_task_record(self.app, task_id)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
crate::async_task::CancelVideoTaskError::NotFound => {
|
||||
AdminCancelVideoTaskError::NotFound
|
||||
}
|
||||
crate::async_task::CancelVideoTaskError::InvalidStatus(status) => {
|
||||
AdminCancelVideoTaskError::InvalidStatus(status)
|
||||
}
|
||||
crate::async_task::CancelVideoTaskError::Response(response) => {
|
||||
AdminCancelVideoTaskError::Response(response)
|
||||
}
|
||||
crate::async_task::CancelVideoTaskError::Gateway(err) => {
|
||||
AdminCancelVideoTaskError::Gateway(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn read_video_task_detail(
|
||||
&self,
|
||||
task_id: &str,
|
||||
) -> Result<Option<aether_data_contracts::repository::video_tasks::StoredVideoTask>, GatewayError>
|
||||
{
|
||||
crate::async_task::read_video_task_detail(self.app, task_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_video_task_video_source(
|
||||
&self,
|
||||
task_id: &str,
|
||||
) -> Result<Option<crate::async_task::VideoTaskVideoSource>, GatewayError> {
|
||||
crate::async_task::read_video_task_video_source(self.app, task_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn build_video_task_video_response(
|
||||
&self,
|
||||
task_id: &str,
|
||||
source: crate::async_task::VideoTaskVideoSource,
|
||||
) -> Result<axum::response::Response, GatewayError> {
|
||||
crate::async_task::build_video_task_video_response(self.app, task_id, source).await
|
||||
}
|
||||
|
||||
pub(crate) async fn store_local_gemini_file_mapping(
|
||||
&self,
|
||||
file_name: &str,
|
||||
key_id: &str,
|
||||
user_id: Option<&str>,
|
||||
display_name: Option<&str>,
|
||||
mime_type: Option<&str>,
|
||||
) -> Result<(), GatewayError> {
|
||||
crate::usage::reporting::store_local_gemini_file_mapping(
|
||||
self.app,
|
||||
file_name,
|
||||
key_id,
|
||||
user_id,
|
||||
display_name,
|
||||
mime_type,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
mod auth;
|
||||
mod billing;
|
||||
mod capabilities;
|
||||
mod context;
|
||||
mod endpoint;
|
||||
mod features;
|
||||
mod models;
|
||||
mod observability;
|
||||
mod provider;
|
||||
mod provider_oauth;
|
||||
mod route_request;
|
||||
mod state;
|
||||
mod system;
|
||||
mod users;
|
||||
pub(crate) use self::context::AdminRequestContext;
|
||||
pub(crate) use self::provider_oauth::{
|
||||
admin_provider_oauth_template, admin_provider_oauth_template_types,
|
||||
is_fixed_provider_type_for_admin_oauth, AdminGatewayProviderTransportSnapshot,
|
||||
AdminKiroAuthConfig, AdminKiroOAuthRefreshAdapter, AdminKiroRequestAuth,
|
||||
AdminLocalOAuthRefreshError, AdminProviderOAuthTemplate,
|
||||
};
|
||||
pub(crate) use self::route_request::{AdminCancelVideoTaskError, AdminRouteRequest};
|
||||
pub(crate) use self::state::{AdminAppState, AdminRouteResponse, AdminRouteResult};
|
||||
@@ -0,0 +1,571 @@
|
||||
use super::AdminAppState;
|
||||
use crate::handlers::admin::provider::shared::payloads::{
|
||||
AdminImportProviderModelsRequest, AdminProviderModelCreateRequest,
|
||||
AdminProviderModelUpdateRequest,
|
||||
};
|
||||
use crate::handlers::admin::shared::{normalize_json_array, normalize_json_object};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::provider::{
|
||||
models as admin_provider_models_pure, models_write as admin_provider_models_write_pure,
|
||||
};
|
||||
use aether_data_contracts::repository::global_models::{
|
||||
AdminProviderModelListQuery, StoredAdminProviderModel, UpsertAdminProviderModelRecord,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn admin_provider_model_name_exists(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
provider_model_name: &str,
|
||||
exclude_model_id: Option<&str>,
|
||||
) -> Result<bool, GatewayError> {
|
||||
let target = provider_model_name.trim();
|
||||
if target.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
let models = self
|
||||
.list_admin_provider_models(
|
||||
&aether_data_contracts::repository::global_models::AdminProviderModelListQuery {
|
||||
provider_id: provider_id.to_string(),
|
||||
is_active: None,
|
||||
offset: 0,
|
||||
limit: 10_000,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(models.into_iter().any(|model| {
|
||||
model.provider_model_name == target
|
||||
&& exclude_model_id.is_none_or(|exclude| model.id != exclude)
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_admin_global_model_by_id_or_err(
|
||||
&self,
|
||||
global_model_id: &str,
|
||||
) -> Result<aether_data_contracts::repository::global_models::StoredAdminGlobalModel, String>
|
||||
{
|
||||
self.get_admin_global_model_by_id(global_model_id)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?
|
||||
.ok_or_else(|| format!("GlobalModel {global_model_id} 不存在"))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_provider_available_source_models_payload(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
) -> Option<serde_json::Value> {
|
||||
if !self.has_global_model_data_reader() || !self.has_provider_catalog_data_reader() {
|
||||
return None;
|
||||
}
|
||||
let provider = self
|
||||
.read_provider_catalog_providers_by_ids(&[provider_id.to_string()])
|
||||
.await
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.next()?;
|
||||
let models = self
|
||||
.list_admin_provider_available_source_models(&provider.id)
|
||||
.await
|
||||
.ok()?;
|
||||
Some(
|
||||
admin_provider_models_pure::build_admin_provider_available_source_models_payload(
|
||||
models,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_provider_model_create_record(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
payload: AdminProviderModelCreateRequest,
|
||||
) -> Result<UpsertAdminProviderModelRecord, String> {
|
||||
let provider_model_name =
|
||||
admin_provider_models_write_pure::normalize_required_trimmed_string(
|
||||
&payload.provider_model_name,
|
||||
"provider_model_name",
|
||||
)?;
|
||||
if self
|
||||
.admin_provider_model_name_exists(provider_id, &provider_model_name, None)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?
|
||||
{
|
||||
return Err(format!("模型 '{provider_model_name}' 已存在"));
|
||||
}
|
||||
let global_model_id = admin_provider_models_write_pure::normalize_required_trimmed_string(
|
||||
&payload.global_model_id,
|
||||
"global_model_id",
|
||||
)?;
|
||||
self.resolve_admin_global_model_by_id_or_err(&global_model_id)
|
||||
.await?;
|
||||
let price_per_request = admin_provider_models_write_pure::normalize_optional_price(
|
||||
payload.price_per_request,
|
||||
"price_per_request",
|
||||
)?;
|
||||
let tiered_pricing = normalize_json_object(payload.tiered_pricing, "tiered_pricing")?;
|
||||
let provider_model_mappings =
|
||||
normalize_json_array(payload.provider_model_mappings, "provider_model_mappings")?;
|
||||
let config = normalize_json_object(payload.config, "config")?;
|
||||
admin_provider_models_write_pure::build_admin_provider_model_create_record(
|
||||
Uuid::new_v4().to_string(),
|
||||
provider_id.to_string(),
|
||||
global_model_id,
|
||||
provider_model_name,
|
||||
provider_model_mappings,
|
||||
price_per_request,
|
||||
tiered_pricing,
|
||||
payload.supports_vision,
|
||||
payload.supports_function_calling,
|
||||
payload.supports_streaming,
|
||||
payload.supports_extended_thinking,
|
||||
payload.is_active,
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_provider_model_update_record(
|
||||
&self,
|
||||
existing: &StoredAdminProviderModel,
|
||||
raw_payload: &serde_json::Map<String, serde_json::Value>,
|
||||
payload: AdminProviderModelUpdateRequest,
|
||||
) -> Result<UpsertAdminProviderModelRecord, String> {
|
||||
let provider_model_name = if let Some(value) = raw_payload.get("provider_model_name") {
|
||||
let Some(name) = payload.provider_model_name.as_deref() else {
|
||||
return Err(if value.is_null() {
|
||||
"provider_model_name 不能为空".to_string()
|
||||
} else {
|
||||
"provider_model_name 必须是字符串".to_string()
|
||||
});
|
||||
};
|
||||
let name = admin_provider_models_write_pure::normalize_required_trimmed_string(
|
||||
name,
|
||||
"provider_model_name",
|
||||
)?;
|
||||
if self
|
||||
.admin_provider_model_name_exists(&existing.provider_id, &name, Some(&existing.id))
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?
|
||||
{
|
||||
return Err(format!("模型 '{name}' 已存在"));
|
||||
}
|
||||
name
|
||||
} else {
|
||||
existing.provider_model_name.clone()
|
||||
};
|
||||
|
||||
let global_model_id = if let Some(value) = raw_payload.get("global_model_id") {
|
||||
let Some(global_model_id) = payload.global_model_id.as_deref() else {
|
||||
return Err(if value.is_null() {
|
||||
"global_model_id 不能为空".to_string()
|
||||
} else {
|
||||
"global_model_id 必须是字符串".to_string()
|
||||
});
|
||||
};
|
||||
let global_model_id =
|
||||
admin_provider_models_write_pure::normalize_required_trimmed_string(
|
||||
global_model_id,
|
||||
"global_model_id",
|
||||
)?;
|
||||
self.resolve_admin_global_model_by_id_or_err(&global_model_id)
|
||||
.await?;
|
||||
global_model_id
|
||||
} else {
|
||||
existing.global_model_id.clone()
|
||||
};
|
||||
|
||||
let price_per_request = if raw_payload.contains_key("price_per_request") {
|
||||
admin_provider_models_write_pure::normalize_optional_price(
|
||||
payload.price_per_request,
|
||||
"price_per_request",
|
||||
)?
|
||||
} else {
|
||||
existing.price_per_request
|
||||
};
|
||||
let tiered_pricing = if raw_payload.contains_key("tiered_pricing") {
|
||||
normalize_json_object(payload.tiered_pricing, "tiered_pricing")?
|
||||
} else {
|
||||
existing.tiered_pricing.clone()
|
||||
};
|
||||
let provider_model_mappings = if raw_payload.contains_key("provider_model_mappings") {
|
||||
normalize_json_array(payload.provider_model_mappings, "provider_model_mappings")?
|
||||
} else {
|
||||
existing.provider_model_mappings.clone()
|
||||
};
|
||||
let config = if raw_payload.contains_key("config") {
|
||||
normalize_json_object(payload.config, "config")?
|
||||
} else {
|
||||
existing.config.clone()
|
||||
};
|
||||
|
||||
admin_provider_models_write_pure::build_admin_provider_model_update_record(
|
||||
existing,
|
||||
global_model_id,
|
||||
provider_model_name,
|
||||
provider_model_mappings,
|
||||
price_per_request,
|
||||
tiered_pricing,
|
||||
if raw_payload.contains_key("supports_vision") {
|
||||
payload.supports_vision
|
||||
} else {
|
||||
existing.supports_vision
|
||||
},
|
||||
if raw_payload.contains_key("supports_function_calling") {
|
||||
payload.supports_function_calling
|
||||
} else {
|
||||
existing.supports_function_calling
|
||||
},
|
||||
if raw_payload.contains_key("supports_streaming") {
|
||||
payload.supports_streaming
|
||||
} else {
|
||||
existing.supports_streaming
|
||||
},
|
||||
if raw_payload.contains_key("supports_extended_thinking") {
|
||||
payload.supports_extended_thinking
|
||||
} else {
|
||||
existing.supports_extended_thinking
|
||||
},
|
||||
payload.is_active.unwrap_or(existing.is_active),
|
||||
payload.is_available.unwrap_or(existing.is_available),
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_import_provider_models_payload(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
payload: AdminImportProviderModelsRequest,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let tiered_pricing = normalize_json_object(payload.tiered_pricing, "tiered_pricing")?;
|
||||
|
||||
let existing_models = self
|
||||
.list_admin_provider_models(&AdminProviderModelListQuery {
|
||||
provider_id: provider_id.to_string(),
|
||||
is_active: None,
|
||||
offset: 0,
|
||||
limit: 10_000,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
let mut existing_by_name = existing_models
|
||||
.iter()
|
||||
.map(|model| (model.provider_model_name.clone(), model.clone()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let mut success = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for model_id in payload.model_ids {
|
||||
let trimmed = match admin_provider_models_write_pure::normalize_admin_import_model_id(
|
||||
&model_id,
|
||||
) {
|
||||
Ok(value) => value,
|
||||
Err(detail) => {
|
||||
let raw = model_id.trim();
|
||||
errors.push(json!({
|
||||
"model_id": if raw.is_empty() { "<empty>" } else { raw },
|
||||
"error": detail,
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(existing) = existing_by_name.get(trimmed.as_str()) {
|
||||
success.push(json!({
|
||||
"model_id": trimmed,
|
||||
"global_model_id": existing.global_model_id,
|
||||
"global_model_name": existing.global_model_name,
|
||||
"provider_model_id": existing.id,
|
||||
"created_global_model": false,
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut created_global_model = false;
|
||||
let global_model = if let Some(existing) = self
|
||||
.get_admin_global_model_by_name(&trimmed)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?
|
||||
{
|
||||
existing
|
||||
} else {
|
||||
let created = self
|
||||
.create_admin_global_model(
|
||||
&admin_provider_models_write_pure::build_admin_import_global_model_record(
|
||||
Uuid::new_v4().to_string(),
|
||||
trimmed.to_string(),
|
||||
payload.price_per_request,
|
||||
tiered_pricing.clone(),
|
||||
)
|
||||
.map_err(|err| err.to_string())?,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
let Some(created) = created else {
|
||||
errors.push(json!({"model_id": trimmed, "error": "Create GlobalModel failed"}));
|
||||
continue;
|
||||
};
|
||||
created_global_model = true;
|
||||
created
|
||||
};
|
||||
|
||||
let record =
|
||||
admin_provider_models_write_pure::build_admin_import_provider_model_record(
|
||||
Uuid::new_v4().to_string(),
|
||||
provider_id.to_string(),
|
||||
global_model.id.clone(),
|
||||
trimmed.to_string(),
|
||||
payload.price_per_request,
|
||||
tiered_pricing.clone(),
|
||||
)?;
|
||||
|
||||
match self.create_admin_provider_model(&record).await {
|
||||
Ok(Some(created)) => {
|
||||
existing_by_name.insert(trimmed.to_string(), created.clone());
|
||||
success.push(json!({
|
||||
"model_id": trimmed,
|
||||
"global_model_id": global_model.id,
|
||||
"global_model_name": global_model.name,
|
||||
"provider_model_id": created.id,
|
||||
"created_global_model": created_global_model,
|
||||
}));
|
||||
}
|
||||
Ok(None) => errors.push(json!({
|
||||
"model_id": trimmed,
|
||||
"error": "Create provider model failed",
|
||||
})),
|
||||
Err(err) => errors.push(json!({
|
||||
"model_id": trimmed,
|
||||
"error": format!("{err:?}"),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"success": success,
|
||||
"errors": errors,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_batch_assign_global_models_payload(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
global_model_ids: Vec<String>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let existing_models = self
|
||||
.list_admin_provider_models(&AdminProviderModelListQuery {
|
||||
provider_id: provider_id.to_string(),
|
||||
is_active: None,
|
||||
offset: 0,
|
||||
limit: 10_000,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
let existing_global_model_ids = existing_models
|
||||
.into_iter()
|
||||
.map(|model| model.global_model_id)
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
|
||||
let mut success = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
for global_model_id in global_model_ids {
|
||||
let global_model_id = global_model_id.trim().to_string();
|
||||
if global_model_id.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let global_model = match self
|
||||
.resolve_admin_global_model_by_id_or_err(&global_model_id)
|
||||
.await
|
||||
{
|
||||
Ok(model) => model,
|
||||
Err(detail) => {
|
||||
errors.push(json!({
|
||||
"global_model_id": global_model_id,
|
||||
"error": detail,
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if existing_global_model_ids.contains(&global_model.id) {
|
||||
errors.push(json!({
|
||||
"global_model_id": global_model.id,
|
||||
"error": "Model already exists",
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
let record =
|
||||
admin_provider_models_write_pure::build_admin_batch_assign_provider_model_record(
|
||||
Uuid::new_v4().to_string(),
|
||||
provider_id.to_string(),
|
||||
global_model.id.clone(),
|
||||
global_model.name.clone(),
|
||||
)?;
|
||||
match self.create_admin_provider_model(&record).await {
|
||||
Ok(Some(created)) => success.push(json!({
|
||||
"global_model_id": global_model.id,
|
||||
"global_model_name": global_model.name,
|
||||
"provider_model_id": created.id,
|
||||
})),
|
||||
Ok(None) => errors.push(json!({
|
||||
"global_model_id": global_model.id,
|
||||
"error": "Create provider model failed",
|
||||
})),
|
||||
Err(err) => errors.push(json!({
|
||||
"global_model_id": global_model.id,
|
||||
"error": format!("{err:?}"),
|
||||
})),
|
||||
}
|
||||
}
|
||||
Ok(json!({
|
||||
"success": success,
|
||||
"errors": errors,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_external_models_cache(
|
||||
&self,
|
||||
) -> Result<Option<serde_json::Value>, GatewayError> {
|
||||
crate::handlers::admin::model::read_admin_external_models_cache(self).await
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_admin_external_models_cache(
|
||||
&self,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
crate::handlers::admin::model::clear_admin_external_models_cache(self).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_provider_models(
|
||||
&self,
|
||||
query: &aether_data_contracts::repository::global_models::AdminProviderModelListQuery,
|
||||
) -> Result<
|
||||
Vec<aether_data_contracts::repository::global_models::StoredAdminProviderModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.list_admin_provider_models(query).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_provider_available_source_models(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
) -> Result<
|
||||
Vec<aether_data_contracts::repository::global_models::StoredAdminProviderModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.list_admin_provider_available_source_models(provider_id)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_admin_provider_model(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
model_id: &str,
|
||||
) -> Result<
|
||||
Option<aether_data_contracts::repository::global_models::StoredAdminProviderModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.get_admin_provider_model(provider_id, model_id)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_admin_global_model_by_id(
|
||||
&self,
|
||||
global_model_id: &str,
|
||||
) -> Result<
|
||||
Option<aether_data_contracts::repository::global_models::StoredAdminGlobalModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.get_admin_global_model_by_id(global_model_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_admin_global_model_by_name(
|
||||
&self,
|
||||
model_name: &str,
|
||||
) -> Result<
|
||||
Option<aether_data_contracts::repository::global_models::StoredAdminGlobalModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.get_admin_global_model_by_name(model_name).await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_admin_provider_model(
|
||||
&self,
|
||||
record: &aether_data_contracts::repository::global_models::UpsertAdminProviderModelRecord,
|
||||
) -> Result<
|
||||
Option<aether_data_contracts::repository::global_models::StoredAdminProviderModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.create_admin_provider_model(record).await
|
||||
}
|
||||
|
||||
pub(crate) async fn update_admin_provider_model(
|
||||
&self,
|
||||
record: &aether_data_contracts::repository::global_models::UpsertAdminProviderModelRecord,
|
||||
) -> Result<
|
||||
Option<aether_data_contracts::repository::global_models::StoredAdminProviderModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.update_admin_provider_model(record).await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_admin_provider_model(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
model_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app
|
||||
.delete_admin_provider_model(provider_id, model_id)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_admin_global_model(
|
||||
&self,
|
||||
record: &aether_data_contracts::repository::global_models::CreateAdminGlobalModelRecord,
|
||||
) -> Result<
|
||||
Option<aether_data_contracts::repository::global_models::StoredAdminGlobalModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.create_admin_global_model(record).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_global_models(
|
||||
&self,
|
||||
query: &aether_data_contracts::repository::global_models::AdminGlobalModelListQuery,
|
||||
) -> Result<
|
||||
aether_data_contracts::repository::global_models::StoredAdminGlobalModelPage,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.list_admin_global_models(query).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_provider_models_by_global_model_id(
|
||||
&self,
|
||||
global_model_id: &str,
|
||||
) -> Result<
|
||||
Vec<aether_data_contracts::repository::global_models::StoredAdminProviderModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.list_admin_provider_models_by_global_model_id(global_model_id)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn update_admin_global_model(
|
||||
&self,
|
||||
record: &aether_data_contracts::repository::global_models::UpdateAdminGlobalModelRecord,
|
||||
) -> Result<
|
||||
Option<aether_data_contracts::repository::global_models::StoredAdminGlobalModel>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.update_admin_global_model(record).await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_admin_global_model(
|
||||
&self,
|
||||
global_model_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app.delete_admin_global_model(global_model_id).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use super::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn aggregate_finalized_request_candidate_timeline_by_endpoint_ids_since(
|
||||
&self,
|
||||
endpoint_ids: &[String],
|
||||
since_unix_secs: u64,
|
||||
until_unix_secs: u64,
|
||||
segments: u32,
|
||||
) -> Result<
|
||||
Vec<aether_data_contracts::repository::candidates::PublicHealthTimelineBucket>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.aggregate_finalized_request_candidate_timeline_by_endpoint_ids_since(
|
||||
endpoint_ids,
|
||||
since_unix_secs,
|
||||
until_unix_secs,
|
||||
segments,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_recent_request_candidates(
|
||||
&self,
|
||||
limit: usize,
|
||||
) -> Result<
|
||||
Vec<aether_data_contracts::repository::candidates::StoredRequestCandidate>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app.read_recent_request_candidates(limit).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_usage_audits(
|
||||
&self,
|
||||
query: &aether_data_contracts::repository::usage::UsageAuditListQuery,
|
||||
) -> Result<Vec<aether_data_contracts::repository::usage::StoredRequestUsageAudit>, GatewayError>
|
||||
{
|
||||
self.app.list_usage_audits(query).await
|
||||
}
|
||||
|
||||
pub(crate) async fn find_request_usage_by_id(
|
||||
&self,
|
||||
usage_id: &str,
|
||||
) -> Result<
|
||||
Option<aether_data_contracts::repository::usage::StoredRequestUsageAudit>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.app
|
||||
.data
|
||||
.find_request_usage_by_id(usage_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_usage_for_range(
|
||||
&self,
|
||||
time_range: &crate::handlers::admin::observability::AdminStatsTimeRange,
|
||||
filters: &crate::handlers::admin::observability::AdminStatsUsageFilter,
|
||||
) -> Result<Vec<aether_data_contracts::repository::usage::StoredRequestUsageAudit>, GatewayError>
|
||||
{
|
||||
crate::handlers::admin::observability::list_usage_for_range(self, time_range, filters).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_admin_usage_for_optional_range(
|
||||
&self,
|
||||
time_range: Option<&crate::handlers::admin::observability::AdminStatsTimeRange>,
|
||||
filters: &crate::handlers::admin::observability::AdminStatsUsageFilter,
|
||||
) -> Result<Vec<aether_data_contracts::repository::usage::StoredRequestUsageAudit>, GatewayError>
|
||||
{
|
||||
crate::handlers::admin::observability::list_usage_for_optional_range(
|
||||
self, time_range, filters,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn build_api_format_health_monitor_payload(
|
||||
&self,
|
||||
lookback_hours: u64,
|
||||
per_format_limit: usize,
|
||||
options: crate::handlers::public::ApiFormatHealthMonitorOptions,
|
||||
) -> Option<serde_json::Value> {
|
||||
crate::handlers::public::build_api_format_health_monitor_payload(
|
||||
self.app,
|
||||
lookback_hours,
|
||||
per_format_limit,
|
||||
options,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn execute_execution_runtime_sync_plan(
|
||||
&self,
|
||||
trace_id: Option<&str>,
|
||||
plan: &aether_contracts::ExecutionPlan,
|
||||
) -> Result<aether_contracts::ExecutionResult, GatewayError> {
|
||||
crate::execution_runtime::execute_execution_runtime_sync_plan(self.app, trace_id, plan)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
pub(crate) type AdminGatewayProviderTransportSnapshot =
|
||||
crate::provider_transport::GatewayProviderTransportSnapshot;
|
||||
pub(crate) type AdminLocalOAuthRefreshError = crate::provider_transport::LocalOAuthRefreshError;
|
||||
pub(crate) type AdminKiroRequestAuth = crate::provider_transport::kiro::KiroRequestAuth;
|
||||
pub(crate) type AdminKiroAuthConfig = crate::provider_transport::kiro::KiroAuthConfig;
|
||||
pub(crate) type AdminKiroOAuthRefreshAdapter =
|
||||
crate::provider_transport::kiro::KiroOAuthRefreshAdapter;
|
||||
pub(crate) type AdminProviderOAuthTemplate =
|
||||
crate::provider_transport::provider_types::ProviderOAuthTemplate;
|
||||
|
||||
pub(crate) fn is_fixed_provider_type_for_admin_oauth(provider_type: &str) -> bool {
|
||||
crate::provider_transport::provider_types::provider_type_is_fixed_for_admin_oauth(provider_type)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_provider_oauth_template(
|
||||
provider_type: &str,
|
||||
) -> Option<AdminProviderOAuthTemplate> {
|
||||
crate::provider_transport::provider_types::provider_type_admin_oauth_template(provider_type)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_provider_oauth_template_types() -> impl Iterator<Item = &'static str> {
|
||||
crate::provider_transport::provider_types::ADMIN_PROVIDER_OAUTH_TEMPLATE_TYPES
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use super::{AdminAppState, AdminRequestContext};
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::http::Response;
|
||||
|
||||
pub(crate) enum AdminCancelVideoTaskError {
|
||||
NotFound,
|
||||
InvalidStatus(aether_data_contracts::repository::video_tasks::VideoTaskStatus),
|
||||
Response(Response<Body>),
|
||||
Gateway(GatewayError),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct AdminRouteRequest<'a> {
|
||||
state: AdminAppState<'a>,
|
||||
request_context: AdminRequestContext<'a>,
|
||||
request_body: Option<&'a Bytes>,
|
||||
}
|
||||
|
||||
impl<'a> AdminRouteRequest<'a> {
|
||||
pub(crate) fn new(
|
||||
state: &'a AppState,
|
||||
request_context: &'a crate::control::GatewayPublicRequestContext,
|
||||
request_body: Option<&'a Bytes>,
|
||||
) -> Self {
|
||||
Self {
|
||||
state: AdminAppState::new(state),
|
||||
request_context: AdminRequestContext::new(request_context),
|
||||
request_body,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn state(self) -> AdminAppState<'a> {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub(crate) fn request_context(self) -> AdminRequestContext<'a> {
|
||||
self.request_context
|
||||
}
|
||||
|
||||
pub(crate) fn request_body(self) -> Option<&'a Bytes> {
|
||||
self.request_body
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct AdminAppState<'a> {
|
||||
pub(super) app: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) fn new(app: &'a AppState) -> Self {
|
||||
Self { app }
|
||||
}
|
||||
|
||||
pub(crate) fn app(&self) -> &AppState {
|
||||
self.app
|
||||
}
|
||||
|
||||
pub(crate) fn cloned_app(&self) -> AppState {
|
||||
self.app.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsRef<AppState> for AdminAppState<'a> {
|
||||
fn as_ref(&self) -> &AppState {
|
||||
self.app
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type AdminRouteResponse = axum::http::Response<axum::body::Body>;
|
||||
pub(crate) type AdminRouteResult = Result<Option<AdminRouteResponse>, GatewayError>;
|
||||
@@ -0,0 +1,193 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::build_proxy_error_response;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system::{
|
||||
admin_adaptive_dispatcher_not_found_response, admin_adaptive_key_not_found_response,
|
||||
admin_adaptive_key_payload, build_admin_adaptive_reset_learning_payload,
|
||||
build_admin_adaptive_set_limit_payload, build_admin_adaptive_stats_payload,
|
||||
build_admin_adaptive_summary_payload, build_admin_adaptive_toggle_mode_payload,
|
||||
};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn build_admin_adaptive_keys_response(
|
||||
&self,
|
||||
provider_id: Option<&str>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let payload = self
|
||||
.load_admin_adaptive_candidate_keys(provider_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|key| key.rpm_limit.is_none())
|
||||
.map(|key| admin_adaptive_key_payload(&key))
|
||||
.collect::<Vec<_>>();
|
||||
Ok(Json(payload).into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_adaptive_summary_response(
|
||||
&self,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let keys = self.load_admin_adaptive_candidate_keys(None).await?;
|
||||
Ok(Json(build_admin_adaptive_summary_payload(&keys)).into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_adaptive_stats_response(
|
||||
&self,
|
||||
key_id: &str,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(key) = self.find_admin_adaptive_key(key_id).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
Ok(Json(build_admin_adaptive_stats_payload(&key)).into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn toggle_admin_adaptive_mode_response(
|
||||
&self,
|
||||
key_id: &str,
|
||||
request_body: &Bytes,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AdminAdaptiveToggleModeRequest {
|
||||
enabled: bool,
|
||||
#[serde(default)]
|
||||
fixed_limit: Option<u32>,
|
||||
}
|
||||
|
||||
let body = match serde_json::from_slice::<AdminAdaptiveToggleModeRequest>(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(build_proxy_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid_request",
|
||||
"请求数据验证失败",
|
||||
None,
|
||||
));
|
||||
}
|
||||
};
|
||||
let Some(mut key) = self.find_admin_adaptive_key(key_id).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
let message = if body.enabled {
|
||||
key.rpm_limit = None;
|
||||
"已切换为自适应模式,系统将自动学习并调整 RPM 限制".to_string()
|
||||
} else {
|
||||
let Some(fixed_limit) = body.fixed_limit else {
|
||||
return Ok((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"detail": "禁用自适应模式时必须提供 fixed_limit 参数",
|
||||
})),
|
||||
)
|
||||
.into_response());
|
||||
};
|
||||
if !(1..=100).contains(&fixed_limit) {
|
||||
return Ok((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"detail": "fixed_limit 超出范围(1-100)",
|
||||
})),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
key.rpm_limit = Some(fixed_limit);
|
||||
format!("已切换为固定限制模式,RPM 限制设为 {fixed_limit}")
|
||||
};
|
||||
let Some(updated) = self.update_provider_catalog_key(&key).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
Ok(Json(build_admin_adaptive_toggle_mode_payload(&updated, message)).into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_admin_adaptive_limit_response(
|
||||
&self,
|
||||
key_id: &str,
|
||||
limit: u32,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(mut key) = self.find_admin_adaptive_key(key_id).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
let was_adaptive = key.rpm_limit.is_none();
|
||||
key.rpm_limit = Some(limit);
|
||||
let Some(updated) = self.update_provider_catalog_key(&key).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
Ok(Json(build_admin_adaptive_set_limit_payload(
|
||||
&updated,
|
||||
was_adaptive,
|
||||
limit,
|
||||
))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn reset_admin_adaptive_learning_response(
|
||||
&self,
|
||||
key_id: &str,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(mut key) = self.find_admin_adaptive_key(key_id).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
key.learned_rpm_limit = None;
|
||||
key.concurrent_429_count = None;
|
||||
key.rpm_429_count = None;
|
||||
key.last_429_at_unix_secs = None;
|
||||
key.last_429_type = None;
|
||||
key.adjustment_history = None;
|
||||
let Some(updated) = self.update_provider_catalog_key(&key).await? else {
|
||||
return Ok(admin_adaptive_key_not_found_response(key_id));
|
||||
};
|
||||
Ok(Json(build_admin_adaptive_reset_learning_payload(&updated.id)).into_response())
|
||||
}
|
||||
|
||||
pub(crate) fn admin_adaptive_dispatcher_not_found_response(&self) -> Response<Body> {
|
||||
admin_adaptive_dispatcher_not_found_response()
|
||||
}
|
||||
|
||||
async fn find_admin_adaptive_key(
|
||||
&self,
|
||||
key_id: &str,
|
||||
) -> Result<
|
||||
Option<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey>,
|
||||
GatewayError,
|
||||
> {
|
||||
Ok(self
|
||||
.read_provider_catalog_keys_by_ids(std::slice::from_ref(&key_id.to_string()))
|
||||
.await?
|
||||
.into_iter()
|
||||
.next())
|
||||
}
|
||||
|
||||
async fn load_admin_adaptive_candidate_keys(
|
||||
&self,
|
||||
provider_id: Option<&str>,
|
||||
) -> Result<
|
||||
Vec<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey>,
|
||||
GatewayError,
|
||||
> {
|
||||
if let Some(provider_id) = provider_id.filter(|value| !value.trim().is_empty()) {
|
||||
return self
|
||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(
|
||||
&provider_id.to_string(),
|
||||
))
|
||||
.await;
|
||||
}
|
||||
|
||||
let provider_ids = self
|
||||
.list_provider_catalog_providers(false)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|provider| provider.id)
|
||||
.collect::<Vec<_>>();
|
||||
if provider_ids.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
self.list_provider_catalog_keys_by_provider_ids(&provider_ids)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::system::shared::configs::is_sensitive_admin_system_config_key;
|
||||
use crate::handlers::admin::system::shared::export::{
|
||||
build_admin_system_export_providers_payload, decrypt_admin_system_export_secret,
|
||||
ADMIN_SYSTEM_CONFIG_EXPORT_VERSION, ADMIN_SYSTEM_EXPORT_PAGE_LIMIT,
|
||||
};
|
||||
use crate::handlers::shared::{system_config_string, unix_secs_to_rfc3339};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system::serialize_admin_system_users_export_wallet;
|
||||
use aether_data_contracts::repository::global_models::AdminGlobalModelListQuery;
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn build_admin_system_config_export_payload(
|
||||
&self,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let global_models = self
|
||||
.list_admin_global_models(&AdminGlobalModelListQuery {
|
||||
offset: 0,
|
||||
limit: ADMIN_SYSTEM_EXPORT_PAGE_LIMIT,
|
||||
is_active: None,
|
||||
search: None,
|
||||
})
|
||||
.await?
|
||||
.items;
|
||||
let global_model_name_by_id = global_models
|
||||
.iter()
|
||||
.map(|model| (model.id.clone(), model.name.clone()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let global_models_data = global_models
|
||||
.iter()
|
||||
.map(|model| {
|
||||
json!({
|
||||
"name": model.name,
|
||||
"display_name": model.display_name,
|
||||
"default_price_per_request": model.default_price_per_request,
|
||||
"default_tiered_pricing": model.default_tiered_pricing,
|
||||
"supported_capabilities": model.supported_capabilities,
|
||||
"config": model.config,
|
||||
"is_active": model.is_active,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let providers_data =
|
||||
build_admin_system_export_providers_payload(self, &global_model_name_by_id).await?;
|
||||
|
||||
let ldap_data = self.get_ldap_module_config().await?.map(|config| {
|
||||
let bind_password = config
|
||||
.bind_password_encrypted
|
||||
.as_deref()
|
||||
.and_then(|ciphertext| decrypt_admin_system_export_secret(self, ciphertext))
|
||||
.unwrap_or_default();
|
||||
json!({
|
||||
"server_url": config.server_url,
|
||||
"bind_dn": config.bind_dn,
|
||||
"bind_password": bind_password,
|
||||
"base_dn": config.base_dn,
|
||||
"user_search_filter": config.user_search_filter,
|
||||
"username_attr": config.username_attr,
|
||||
"email_attr": config.email_attr,
|
||||
"display_name_attr": config.display_name_attr,
|
||||
"is_enabled": config.is_enabled,
|
||||
"is_exclusive": config.is_exclusive,
|
||||
"use_starttls": config.use_starttls,
|
||||
"connect_timeout": config.connect_timeout,
|
||||
})
|
||||
});
|
||||
|
||||
let system_configs = self.list_system_config_entries().await?;
|
||||
let system_configs_data = system_configs
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let value = if is_sensitive_admin_system_config_key(&entry.key) {
|
||||
entry
|
||||
.value
|
||||
.as_str()
|
||||
.and_then(|ciphertext| decrypt_admin_system_export_secret(self, ciphertext))
|
||||
.map(serde_json::Value::String)
|
||||
.unwrap_or_else(|| entry.value.clone())
|
||||
} else {
|
||||
entry.value.clone()
|
||||
};
|
||||
json!({
|
||||
"key": entry.key,
|
||||
"value": value,
|
||||
"description": entry.description,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let oauth_providers = self.list_oauth_provider_configs().await?;
|
||||
let oauth_data = oauth_providers
|
||||
.iter()
|
||||
.map(|provider| {
|
||||
let client_secret = provider
|
||||
.client_secret_encrypted
|
||||
.as_deref()
|
||||
.and_then(|ciphertext| decrypt_admin_system_export_secret(self, ciphertext))
|
||||
.unwrap_or_default();
|
||||
json!({
|
||||
"provider_type": provider.provider_type,
|
||||
"display_name": provider.display_name,
|
||||
"client_id": provider.client_id,
|
||||
"client_secret": client_secret,
|
||||
"authorization_url_override": provider.authorization_url_override,
|
||||
"token_url_override": provider.token_url_override,
|
||||
"userinfo_url_override": provider.userinfo_url_override,
|
||||
"scopes": provider.scopes,
|
||||
"redirect_uri": provider.redirect_uri,
|
||||
"frontend_callback_url": provider.frontend_callback_url,
|
||||
"attribute_mapping": provider.attribute_mapping,
|
||||
"extra_config": provider.extra_config,
|
||||
"is_enabled": provider.is_enabled,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let proxy_nodes = self.list_proxy_nodes().await?;
|
||||
let proxy_nodes_data = proxy_nodes
|
||||
.iter()
|
||||
.map(|node| {
|
||||
json!({
|
||||
"id": node.id,
|
||||
"name": node.name,
|
||||
"ip": node.ip,
|
||||
"port": node.port,
|
||||
"region": node.region,
|
||||
"is_manual": node.is_manual,
|
||||
"proxy_url": node.proxy_url,
|
||||
"proxy_username": node.proxy_username,
|
||||
"proxy_password": node.proxy_password,
|
||||
"tunnel_mode": node.tunnel_mode,
|
||||
"heartbeat_interval": node.heartbeat_interval,
|
||||
"remote_config": node.remote_config,
|
||||
"config_version": node.config_version,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(json!({
|
||||
"version": ADMIN_SYSTEM_CONFIG_EXPORT_VERSION,
|
||||
"exported_at": Utc::now().to_rfc3339(),
|
||||
"global_models": global_models_data,
|
||||
"providers": providers_data,
|
||||
"proxy_nodes": proxy_nodes_data,
|
||||
"ldap_config": ldap_data,
|
||||
"oauth_providers": oauth_data,
|
||||
"system_configs": system_configs_data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_system_users_export_payload(
|
||||
&self,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
const ADMIN_SYSTEM_USERS_EXPORT_VERSION: &str = "1.3";
|
||||
|
||||
let users = self.list_non_admin_export_users().await?;
|
||||
let user_ids = users.iter().map(|user| user.id.clone()).collect::<Vec<_>>();
|
||||
let user_wallets = self.list_wallet_snapshots_by_user_ids(&user_ids).await?;
|
||||
let user_api_keys = self
|
||||
.list_auth_api_key_export_records_by_user_ids(&user_ids)
|
||||
.await?;
|
||||
let standalone_api_keys = self.list_auth_api_key_export_standalone_records().await?;
|
||||
let standalone_api_key_ids = standalone_api_keys
|
||||
.iter()
|
||||
.map(|key| key.api_key_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let standalone_wallets = self
|
||||
.list_wallet_snapshots_by_api_key_ids(&standalone_api_key_ids)
|
||||
.await?;
|
||||
|
||||
let wallets_by_user_id = user_wallets
|
||||
.into_iter()
|
||||
.filter_map(|wallet| wallet.user_id.clone().map(|user_id| (user_id, wallet)))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let wallets_by_api_key_id = standalone_wallets
|
||||
.into_iter()
|
||||
.filter_map(|wallet| {
|
||||
wallet
|
||||
.api_key_id
|
||||
.clone()
|
||||
.map(|api_key_id| (api_key_id, wallet))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let mut api_keys_by_user_id = BTreeMap::<
|
||||
String,
|
||||
Vec<aether_data::repository::auth::StoredAuthApiKeyExportRecord>,
|
||||
>::new();
|
||||
for key in user_api_keys.into_iter().filter(|key| !key.is_standalone) {
|
||||
api_keys_by_user_id
|
||||
.entry(key.user_id.clone())
|
||||
.or_default()
|
||||
.push(key);
|
||||
}
|
||||
|
||||
let users_data = users
|
||||
.iter()
|
||||
.map(|user| {
|
||||
let wallet = wallets_by_user_id.get(&user.id);
|
||||
let wallet_payload = serialize_admin_system_users_export_wallet(wallet);
|
||||
let api_keys = api_keys_by_user_id.remove(&user.id).unwrap_or_default();
|
||||
let api_keys_payload = api_keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
self.build_admin_system_users_export_api_key_payload(key, None, true)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
json!({
|
||||
"email": user.email.clone(),
|
||||
"email_verified": user.email_verified,
|
||||
"username": user.username.clone(),
|
||||
"password_hash": user.password_hash.clone(),
|
||||
"role": user.role.clone(),
|
||||
"allowed_providers": user.allowed_providers.clone(),
|
||||
"allowed_api_formats": user.allowed_api_formats.clone(),
|
||||
"allowed_models": user.allowed_models.clone(),
|
||||
"rate_limit": user.rate_limit,
|
||||
"model_capability_settings": user.model_capability_settings.clone(),
|
||||
"unlimited": wallet
|
||||
.map(|entry| entry.limit_mode.eq_ignore_ascii_case("unlimited"))
|
||||
.unwrap_or(false),
|
||||
"wallet": wallet_payload,
|
||||
"is_active": user.is_active,
|
||||
"api_keys": api_keys_payload,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let standalone_keys_data = standalone_api_keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
self.build_admin_system_users_export_api_key_payload(
|
||||
key,
|
||||
wallets_by_api_key_id.get(&key.api_key_id),
|
||||
false,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(json!({
|
||||
"version": ADMIN_SYSTEM_USERS_EXPORT_VERSION,
|
||||
"exported_at": Utc::now().to_rfc3339(),
|
||||
"users": users_data,
|
||||
"standalone_keys": standalone_keys_data,
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_admin_system_users_export_api_key_payload(
|
||||
&self,
|
||||
key: &aether_data::repository::auth::StoredAuthApiKeyExportRecord,
|
||||
wallet: Option<&aether_data::repository::wallet::StoredWalletSnapshot>,
|
||||
include_is_standalone: bool,
|
||||
) -> serde_json::Value {
|
||||
let mut payload = serde_json::Map::from_iter([
|
||||
("key_hash".to_string(), json!(key.key_hash.clone())),
|
||||
("name".to_string(), json!(key.name.clone())),
|
||||
(
|
||||
"allowed_providers".to_string(),
|
||||
json!(key.allowed_providers.clone()),
|
||||
),
|
||||
(
|
||||
"allowed_api_formats".to_string(),
|
||||
json!(key.allowed_api_formats.clone()),
|
||||
),
|
||||
(
|
||||
"allowed_models".to_string(),
|
||||
json!(key.allowed_models.clone()),
|
||||
),
|
||||
("rate_limit".to_string(), json!(key.rate_limit)),
|
||||
("concurrent_limit".to_string(), json!(key.concurrent_limit)),
|
||||
(
|
||||
"force_capabilities".to_string(),
|
||||
json!(key.force_capabilities.clone()),
|
||||
),
|
||||
("is_active".to_string(), json!(key.is_active)),
|
||||
(
|
||||
"expires_at".to_string(),
|
||||
json!(key.expires_at_unix_secs.and_then(unix_secs_to_rfc3339)),
|
||||
),
|
||||
(
|
||||
"auto_delete_on_expiry".to_string(),
|
||||
json!(key.auto_delete_on_expiry),
|
||||
),
|
||||
("total_requests".to_string(), json!(key.total_requests)),
|
||||
("total_cost_usd".to_string(), json!(key.total_cost_usd)),
|
||||
(
|
||||
"wallet".to_string(),
|
||||
serialize_admin_system_users_export_wallet(wallet)
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
),
|
||||
]);
|
||||
|
||||
if let Some(ciphertext) = key.key_encrypted.as_deref() {
|
||||
if let Some(plaintext) = decrypt_admin_system_export_secret(self, ciphertext) {
|
||||
payload.insert("key".to_string(), serde_json::Value::String(plaintext));
|
||||
} else {
|
||||
payload.insert(
|
||||
"key_encrypted".to_string(),
|
||||
serde_json::Value::String(ciphertext.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if include_is_standalone {
|
||||
payload.insert("is_standalone".to_string(), json!(key.is_standalone));
|
||||
}
|
||||
|
||||
serde_json::Value::Object(payload)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use super::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
|
||||
mod adaptive;
|
||||
mod export;
|
||||
mod modules;
|
||||
mod proxy_nodes;
|
||||
mod templates;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn upsert_system_config_json_value(
|
||||
&self,
|
||||
key: &str,
|
||||
value: &serde_json::Value,
|
||||
description: Option<&str>,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
self.app
|
||||
.upsert_system_config_json_value(key, value, description)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_system_config_json_value(
|
||||
&self,
|
||||
key: &str,
|
||||
) -> Result<Option<serde_json::Value>, GatewayError> {
|
||||
self.app.read_system_config_json_value(key).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_system_config_entries(
|
||||
&self,
|
||||
) -> Result<Vec<crate::data::state::StoredSystemConfigEntry>, GatewayError> {
|
||||
self.app.list_system_config_entries().await
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_system_config_entry(
|
||||
&self,
|
||||
key: &str,
|
||||
value: &serde_json::Value,
|
||||
description: Option<&str>,
|
||||
) -> Result<crate::data::state::StoredSystemConfigEntry, GatewayError> {
|
||||
self.app
|
||||
.upsert_system_config_entry(key, value, description)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_system_config_value(&self, key: &str) -> Result<bool, GatewayError> {
|
||||
self.app.delete_system_config_value(key).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_proxy_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<aether_data::repository::proxy_nodes::StoredProxyNode>, GatewayError> {
|
||||
self.app.list_proxy_nodes().await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_system_stats(
|
||||
&self,
|
||||
) -> Result<aether_data::repository::system::AdminSystemStats, GatewayError> {
|
||||
self.app.read_admin_system_stats().await
|
||||
}
|
||||
|
||||
pub(crate) async fn find_proxy_node(
|
||||
&self,
|
||||
node_id: &str,
|
||||
) -> Result<Option<aether_data::repository::proxy_nodes::StoredProxyNode>, GatewayError> {
|
||||
self.app.find_proxy_node(node_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_proxy_node_events(
|
||||
&self,
|
||||
node_id: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<aether_data::repository::proxy_nodes::StoredProxyNodeEvent>, GatewayError> {
|
||||
self.app.list_proxy_node_events(node_id, limit).await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_email_template_payload(
|
||||
&self,
|
||||
template_type: &str,
|
||||
) -> Result<Option<serde_json::Value>, GatewayError> {
|
||||
crate::handlers::shared::read_admin_email_template_payload(self.app, template_type).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::system::shared::modules as admin_system_modules;
|
||||
use crate::handlers::shared::module_available_from_env;
|
||||
use crate::GatewayError;
|
||||
use axum::{body::Bytes, http};
|
||||
use serde_json::json;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn build_admin_modules_status_payload(
|
||||
&self,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
admin_system_modules::build_admin_modules_status_payload(self).await
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_module_status_payload(
|
||||
&self,
|
||||
module_name: &str,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError>
|
||||
{
|
||||
let Some(module) = admin_system_modules::admin_module_by_name(module_name) else {
|
||||
return Ok(Err((
|
||||
http::StatusCode::NOT_FOUND,
|
||||
json!({ "detail": format!("模块 '{module_name}' 不存在") }),
|
||||
)));
|
||||
};
|
||||
let runtime = admin_system_modules::build_admin_module_runtime_state(self).await?;
|
||||
Ok(Ok(admin_system_modules::build_admin_module_status_payload(
|
||||
self, module, &runtime,
|
||||
)
|
||||
.await?))
|
||||
}
|
||||
|
||||
pub(crate) async fn set_admin_module_enabled_payload(
|
||||
&self,
|
||||
module_name: &str,
|
||||
request_body: &Bytes,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError>
|
||||
{
|
||||
let Some(module) = admin_system_modules::admin_module_by_name(module_name) else {
|
||||
return Ok(Err((
|
||||
http::StatusCode::NOT_FOUND,
|
||||
json!({ "detail": format!("模块 '{module_name}' 不存在") }),
|
||||
)));
|
||||
};
|
||||
let available = module_available_from_env(module.env_key, module.default_available);
|
||||
if !available {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({
|
||||
"detail": format!(
|
||||
"模块 '{}' 不可用,无法启用。请检查环境变量 {} 和依赖库。",
|
||||
module.name, module.env_key
|
||||
)
|
||||
}),
|
||||
)));
|
||||
}
|
||||
let payload = match serde_json::from_slice::<
|
||||
admin_system_modules::AdminSetModuleEnabledRequest,
|
||||
>(request_body)
|
||||
{
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({ "detail": "请求体格式错误,需要 enabled 字段" }),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let runtime = admin_system_modules::build_admin_module_runtime_state(self).await?;
|
||||
if payload.enabled {
|
||||
let (config_validated, config_error) =
|
||||
admin_system_modules::build_admin_module_validation_result(module, &runtime);
|
||||
if !config_validated {
|
||||
return Ok(Err((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({
|
||||
"detail": format!(
|
||||
"模块配置未验证通过: {}",
|
||||
config_error.unwrap_or_else(|| "未知错误".to_string())
|
||||
)
|
||||
}),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self
|
||||
.upsert_system_config_json_value(
|
||||
&format!("module.{}.enabled", module.name),
|
||||
&json!(payload.enabled),
|
||||
Some(&format!("模块 [{}] 启用状态", module.display_name)),
|
||||
)
|
||||
.await?;
|
||||
let updated_runtime = admin_system_modules::build_admin_module_runtime_state(self).await?;
|
||||
Ok(Ok(admin_system_modules::build_admin_module_status_payload(
|
||||
self,
|
||||
module,
|
||||
&updated_runtime,
|
||||
)
|
||||
.await?))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system::{
|
||||
build_admin_proxy_node_event_payload, build_admin_proxy_node_events_payload_response,
|
||||
build_admin_proxy_node_payload, build_admin_proxy_nodes_data_unavailable_response,
|
||||
build_admin_proxy_nodes_invalid_status_response, build_admin_proxy_nodes_list_payload_response,
|
||||
build_admin_proxy_nodes_not_found_response,
|
||||
};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn build_admin_proxy_nodes_list_response(
|
||||
&self,
|
||||
skip: usize,
|
||||
limit: usize,
|
||||
status: Option<String>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if !self.has_proxy_node_reader() {
|
||||
return Ok(build_admin_proxy_nodes_data_unavailable_response());
|
||||
}
|
||||
if let Some(status) = status.as_deref() {
|
||||
if !matches!(status, "offline" | "online") {
|
||||
return Ok(build_admin_proxy_nodes_invalid_status_response());
|
||||
}
|
||||
}
|
||||
|
||||
let mut nodes = self.list_proxy_nodes().await?;
|
||||
nodes.sort_by(|left, right| left.name.cmp(&right.name));
|
||||
let filtered = nodes
|
||||
.into_iter()
|
||||
.filter(|node| {
|
||||
status
|
||||
.as_deref()
|
||||
.map(|value| node.status.eq_ignore_ascii_case(value))
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let total = filtered.len();
|
||||
let items = filtered
|
||||
.into_iter()
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.map(|node| build_admin_proxy_node_payload(&node))
|
||||
.collect::<Vec<_>>();
|
||||
Ok(build_admin_proxy_nodes_list_payload_response(
|
||||
items, total, skip, limit,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_proxy_node_events_response(
|
||||
&self,
|
||||
node_id: &str,
|
||||
limit: usize,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if !self.has_proxy_node_reader() {
|
||||
return Ok(build_admin_proxy_nodes_data_unavailable_response());
|
||||
}
|
||||
if self.find_proxy_node(node_id).await?.is_none() {
|
||||
return Ok(build_admin_proxy_nodes_not_found_response());
|
||||
}
|
||||
let items = self
|
||||
.list_proxy_node_events(node_id, limit)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|event| build_admin_proxy_node_event_payload(&event))
|
||||
.collect::<Vec<_>>();
|
||||
Ok(build_admin_proxy_node_events_payload_response(items))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::shared::{
|
||||
admin_email_template_definition, admin_email_template_html_key,
|
||||
admin_email_template_subject_key, render_admin_email_template_html, system_config_string,
|
||||
};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system::{
|
||||
admin_email_template_not_found_error, build_admin_email_template_preview_payload,
|
||||
build_admin_email_template_reset_payload, build_admin_email_template_saved_payload,
|
||||
build_admin_email_templates_payload as build_admin_email_templates_payload_value,
|
||||
parse_admin_email_template_preview_payload, parse_admin_email_template_update,
|
||||
};
|
||||
use axum::{body::Bytes, http};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn build_admin_email_templates_payload(
|
||||
&self,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let mut templates = Vec::new();
|
||||
for template_type in ["verification", "password_reset"] {
|
||||
if let Some(payload) = self
|
||||
.read_admin_email_template_payload(template_type)
|
||||
.await?
|
||||
{
|
||||
let mut payload = payload;
|
||||
if let Some(object) = payload.as_object_mut() {
|
||||
object.remove("default_subject");
|
||||
object.remove("default_html");
|
||||
}
|
||||
templates.push(payload);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(build_admin_email_templates_payload_value(templates))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_email_template_payload(
|
||||
&self,
|
||||
template_type: &str,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError>
|
||||
{
|
||||
let Some(payload) = self
|
||||
.read_admin_email_template_payload(template_type)
|
||||
.await?
|
||||
else {
|
||||
return Ok(Err(admin_email_template_not_found_error(template_type)));
|
||||
};
|
||||
Ok(Ok(payload))
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_admin_email_template_update(
|
||||
&self,
|
||||
template_type: &str,
|
||||
request_body: &Bytes,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError>
|
||||
{
|
||||
let Some(definition) = admin_email_template_definition(template_type) else {
|
||||
return Ok(Err(admin_email_template_not_found_error(template_type)));
|
||||
};
|
||||
let update = match parse_admin_email_template_update(request_body) {
|
||||
Ok(update) => update,
|
||||
Err(err) => return Ok(Err(err)),
|
||||
};
|
||||
|
||||
let subject_key = admin_email_template_subject_key(definition.template_type);
|
||||
let html_key = admin_email_template_html_key(definition.template_type);
|
||||
|
||||
if let Some(subject) = update.subject {
|
||||
if subject.is_empty() {
|
||||
let _ = self.delete_system_config_value(&subject_key).await?;
|
||||
} else {
|
||||
let _ = self
|
||||
.upsert_system_config_json_value(
|
||||
&subject_key,
|
||||
&serde_json::json!(subject),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(html) = update.html {
|
||||
if html.is_empty() {
|
||||
let _ = self.delete_system_config_value(&html_key).await?;
|
||||
} else {
|
||||
let _ = self
|
||||
.upsert_system_config_json_value(&html_key, &serde_json::json!(html), None)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Ok(build_admin_email_template_saved_payload()))
|
||||
}
|
||||
|
||||
pub(crate) async fn preview_admin_email_template(
|
||||
&self,
|
||||
template_type: &str,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError>
|
||||
{
|
||||
let Some(definition) = admin_email_template_definition(template_type) else {
|
||||
return Ok(Err(admin_email_template_not_found_error(template_type)));
|
||||
};
|
||||
|
||||
let payload = match parse_admin_email_template_preview_payload(
|
||||
request_body.map(|bytes| bytes.as_ref()),
|
||||
) {
|
||||
Ok(payload) => payload,
|
||||
Err(err) => return Ok(Err(err)),
|
||||
};
|
||||
|
||||
let resolved = self
|
||||
.read_admin_email_template_payload(definition.template_type)
|
||||
.await?
|
||||
.expect("validated template type should exist");
|
||||
let resolved_html = resolved["html"].as_str().unwrap_or(definition.default_html);
|
||||
let html = payload
|
||||
.get("html")
|
||||
.and_then(|value| value.as_str())
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(resolved_html);
|
||||
|
||||
let email_app_name = self.read_system_config_json_value("email_app_name").await?;
|
||||
let smtp_from_name = self.read_system_config_json_value("smtp_from_name").await?;
|
||||
let app_name = system_config_string(email_app_name.as_ref())
|
||||
.or_else(|| system_config_string(smtp_from_name.as_ref()))
|
||||
.unwrap_or_else(|| "Aether".to_string());
|
||||
|
||||
let mut defaults = BTreeMap::new();
|
||||
defaults.insert("app_name".to_string(), app_name);
|
||||
defaults.insert("code".to_string(), "123456".to_string());
|
||||
defaults.insert("expire_minutes".to_string(), "30".to_string());
|
||||
defaults.insert("email".to_string(), "[email protected]".to_string());
|
||||
defaults.insert(
|
||||
"reset_link".to_string(),
|
||||
"https://example.com/reset?token=abc123".to_string(),
|
||||
);
|
||||
|
||||
let preview_variables = definition
|
||||
.variables
|
||||
.iter()
|
||||
.map(|key| {
|
||||
let value = payload
|
||||
.get(*key)
|
||||
.map(|value| match value {
|
||||
serde_json::Value::String(value) => value.clone(),
|
||||
serde_json::Value::Null => "None".to_string(),
|
||||
_ => value.to_string(),
|
||||
})
|
||||
.or_else(|| defaults.get(*key).cloned())
|
||||
.unwrap_or_else(|| format!("{{{{{key}}}}}"));
|
||||
((*key).to_string(), value)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let rendered_html = render_admin_email_template_html(html, &preview_variables)?;
|
||||
|
||||
Ok(Ok(build_admin_email_template_preview_payload(
|
||||
rendered_html,
|
||||
preview_variables,
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn reset_admin_email_template(
|
||||
&self,
|
||||
template_type: &str,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError>
|
||||
{
|
||||
let Some(definition) = admin_email_template_definition(template_type) else {
|
||||
return Ok(Err(admin_email_template_not_found_error(template_type)));
|
||||
};
|
||||
|
||||
let _ = self
|
||||
.delete_system_config_value(&admin_email_template_subject_key(definition.template_type))
|
||||
.await?;
|
||||
let _ = self
|
||||
.delete_system_config_value(&admin_email_template_html_key(definition.template_type))
|
||||
.await?;
|
||||
|
||||
Ok(Ok(build_admin_email_template_reset_payload(
|
||||
definition.template_type,
|
||||
definition.name,
|
||||
definition.default_subject,
|
||||
definition.default_html,
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
use super::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn find_user_auth_by_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
self.app.find_user_auth_by_id(user_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_users_by_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::users::StoredUserSummary>, GatewayError> {
|
||||
self.app.list_users_by_ids(user_ids).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_export_users_page(
|
||||
&self,
|
||||
query: &aether_data::repository::users::UserExportListQuery,
|
||||
) -> Result<Vec<aether_data::repository::users::StoredUserExportRow>, GatewayError> {
|
||||
self.app.list_export_users_page(query).await
|
||||
}
|
||||
|
||||
pub(crate) async fn find_export_user_by_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserExportRow>, GatewayError> {
|
||||
self.app.find_export_user_by_id(user_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_user_auth_by_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
self.app.list_user_auth_by_ids(user_ids).await
|
||||
}
|
||||
|
||||
pub(crate) async fn find_user_auth_by_identifier(
|
||||
&self,
|
||||
identifier: &str,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
self.app.find_user_auth_by_identifier(identifier).await
|
||||
}
|
||||
|
||||
pub(crate) async fn is_other_user_auth_email_taken(
|
||||
&self,
|
||||
email: &str,
|
||||
user_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app
|
||||
.is_other_user_auth_email_taken(email, user_id)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn is_other_user_auth_username_taken(
|
||||
&self,
|
||||
username: &str,
|
||||
user_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app
|
||||
.is_other_user_auth_username_taken(username, user_id)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn create_local_auth_user_with_settings(
|
||||
&self,
|
||||
email: Option<String>,
|
||||
email_verified: bool,
|
||||
username: String,
|
||||
password_hash: String,
|
||||
role: String,
|
||||
allowed_providers: Option<Vec<String>>,
|
||||
allowed_api_formats: Option<Vec<String>>,
|
||||
allowed_models: Option<Vec<String>>,
|
||||
rate_limit: Option<i32>,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
self.app
|
||||
.create_local_auth_user_with_settings(
|
||||
email,
|
||||
email_verified,
|
||||
username,
|
||||
password_hash,
|
||||
role,
|
||||
allowed_providers,
|
||||
allowed_api_formats,
|
||||
allowed_models,
|
||||
rate_limit,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn initialize_auth_user_wallet(
|
||||
&self,
|
||||
user_id: &str,
|
||||
initial_gift_usd: f64,
|
||||
unlimited: bool,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
self.app
|
||||
.initialize_auth_user_wallet(user_id, initial_gift_usd, unlimited)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn update_local_auth_user_profile(
|
||||
&self,
|
||||
user_id: &str,
|
||||
email: Option<String>,
|
||||
username: Option<String>,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
self.app
|
||||
.update_local_auth_user_profile(user_id, email, username)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn update_local_auth_user_password_hash(
|
||||
&self,
|
||||
user_id: &str,
|
||||
password_hash: String,
|
||||
updated_at: chrono::DateTime<chrono::Utc>,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
self.app
|
||||
.update_local_auth_user_password_hash(user_id, password_hash, updated_at)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn update_local_auth_user_admin_fields(
|
||||
&self,
|
||||
user_id: &str,
|
||||
role: Option<String>,
|
||||
allowed_providers_present: bool,
|
||||
allowed_providers: Option<Vec<String>>,
|
||||
allowed_api_formats_present: bool,
|
||||
allowed_api_formats: Option<Vec<String>>,
|
||||
allowed_models_present: bool,
|
||||
allowed_models: Option<Vec<String>>,
|
||||
rate_limit: Option<i32>,
|
||||
is_active: Option<bool>,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
|
||||
self.app
|
||||
.update_local_auth_user_admin_fields(
|
||||
user_id,
|
||||
role,
|
||||
allowed_providers_present,
|
||||
allowed_providers,
|
||||
allowed_api_formats_present,
|
||||
allowed_api_formats,
|
||||
allowed_models_present,
|
||||
allowed_models,
|
||||
rate_limit,
|
||||
is_active,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn update_auth_user_wallet_limit_mode(
|
||||
&self,
|
||||
user_id: &str,
|
||||
limit_mode: &str,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
self.app
|
||||
.update_auth_user_wallet_limit_mode(user_id, limit_mode)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn count_active_admin_users(&self) -> Result<u64, GatewayError> {
|
||||
self.app.count_active_admin_users().await
|
||||
}
|
||||
|
||||
pub(crate) async fn count_user_pending_refunds(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<u64, GatewayError> {
|
||||
self.app.count_user_pending_refunds(user_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn count_user_pending_payment_orders(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<u64, GatewayError> {
|
||||
self.app.count_user_pending_payment_orders(user_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_local_auth_user(&self, user_id: &str) -> Result<bool, GatewayError> {
|
||||
self.app.delete_local_auth_user(user_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_user_sessions(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Vec<crate::GatewayUserSessionView>, GatewayError> {
|
||||
self.app.list_user_sessions(user_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn find_user_session(
|
||||
&self,
|
||||
user_id: &str,
|
||||
session_id: &str,
|
||||
) -> Result<Option<crate::GatewayUserSessionView>, GatewayError> {
|
||||
self.app.find_user_session(user_id, session_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn revoke_user_session(
|
||||
&self,
|
||||
user_id: &str,
|
||||
session_id: &str,
|
||||
revoked_at: chrono::DateTime<chrono::Utc>,
|
||||
reason: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app
|
||||
.revoke_user_session(user_id, session_id, revoked_at, reason)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn revoke_all_user_sessions(
|
||||
&self,
|
||||
user_id: &str,
|
||||
revoked_at: chrono::DateTime<chrono::Utc>,
|
||||
reason: &str,
|
||||
) -> Result<u64, GatewayError> {
|
||||
self.app
|
||||
.revoke_all_user_sessions(user_id, revoked_at, reason)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_auth_api_key_snapshots_by_ids(
|
||||
&self,
|
||||
api_key_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::auth::StoredAuthApiKeySnapshot>, GatewayError> {
|
||||
self.app
|
||||
.data
|
||||
.list_auth_api_key_snapshots_by_ids(api_key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_auth_api_key_export_records_by_user_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.app
|
||||
.data
|
||||
.list_auth_api_key_export_records_by_user_ids(user_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_auth_api_key_export_records_by_ids(
|
||||
&self,
|
||||
api_key_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.app
|
||||
.data
|
||||
.list_auth_api_key_export_records_by_ids(api_key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_auth_api_key_export_standalone_records_page(
|
||||
&self,
|
||||
query: &aether_data::repository::auth::StandaloneApiKeyExportListQuery,
|
||||
) -> Result<Vec<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.app
|
||||
.list_auth_api_key_export_standalone_records_page(query)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn count_auth_api_key_export_standalone_records(
|
||||
&self,
|
||||
is_active: Option<bool>,
|
||||
) -> Result<u64, GatewayError> {
|
||||
self.app
|
||||
.count_auth_api_key_export_standalone_records(is_active)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_auth_api_key_export_standalone_records(
|
||||
&self,
|
||||
) -> Result<Vec<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.app.list_auth_api_key_export_standalone_records().await
|
||||
}
|
||||
|
||||
pub(crate) async fn find_auth_api_key_export_standalone_record_by_id(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.app
|
||||
.find_auth_api_key_export_standalone_record_by_id(api_key_id)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_user_api_key(
|
||||
&self,
|
||||
record: aether_data::repository::auth::CreateUserApiKeyRecord,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.app.create_user_api_key(record).await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_standalone_api_key(
|
||||
&self,
|
||||
record: aether_data::repository::auth::CreateStandaloneApiKeyRecord,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.app.create_standalone_api_key(record).await
|
||||
}
|
||||
|
||||
pub(crate) async fn update_user_api_key_basic(
|
||||
&self,
|
||||
record: aether_data::repository::auth::UpdateUserApiKeyBasicRecord,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.app.update_user_api_key_basic(record).await
|
||||
}
|
||||
|
||||
pub(crate) async fn update_standalone_api_key_basic(
|
||||
&self,
|
||||
record: aether_data::repository::auth::UpdateStandaloneApiKeyBasicRecord,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.app.update_standalone_api_key_basic(record).await
|
||||
}
|
||||
|
||||
pub(crate) async fn set_standalone_api_key_active(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
is_active: bool,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.app
|
||||
.set_standalone_api_key_active(api_key_id, is_active)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn set_user_api_key_locked(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
is_locked: bool,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app
|
||||
.set_user_api_key_locked(user_id, api_key_id, is_locked)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn set_user_api_key_allowed_providers(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
allowed_providers: Option<Vec<String>>,
|
||||
) -> Result<Option<aether_data::repository::auth::StoredAuthApiKeyExportRecord>, GatewayError>
|
||||
{
|
||||
self.app
|
||||
.set_user_api_key_allowed_providers(user_id, api_key_id, allowed_providers)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_user_api_key(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app.delete_user_api_key(user_id, api_key_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_standalone_api_key(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
) -> Result<bool, GatewayError> {
|
||||
self.app.delete_standalone_api_key(api_key_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_wallet_snapshots_by_api_key_ids(
|
||||
&self,
|
||||
api_key_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
self.app
|
||||
.list_wallet_snapshots_by_api_key_ids(api_key_ids)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_wallet_snapshots_by_user_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
self.app.list_wallet_snapshots_by_user_ids(user_ids).await
|
||||
}
|
||||
|
||||
pub(crate) async fn summarize_usage_total_tokens_by_api_key_ids(
|
||||
&self,
|
||||
api_key_ids: &[String],
|
||||
) -> Result<std::collections::BTreeMap<String, u64>, GatewayError> {
|
||||
self.app
|
||||
.summarize_usage_total_tokens_by_api_key_ids(api_key_ids)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_non_admin_export_users(
|
||||
&self,
|
||||
) -> Result<Vec<aether_data::repository::users::StoredUserExportRow>, GatewayError> {
|
||||
self.app.list_non_admin_export_users().await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user