refactor: 拆分 gateway 单体为独立 crate,新增 systemd 部署方案

将 gateway 内部的 model-fetch、provider-transport、scheduler-core、
usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway
内部模块结构(state/router/cache/data/query 等);移除大量遗留模块
文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关
API 和组件。
This commit is contained in:
fawney19
2026-04-05 20:23:16 +08:00
parent cbc811f6ce
commit 763ff03a7b
777 changed files with 42659 additions and 21469 deletions

View File

@@ -1,4 +1,5 @@
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use axum::body::{Body, Bytes};
use axum::http::Response;

View File

@@ -5,8 +5,9 @@ use super::adaptive_shared::{
admin_adaptive_key_not_found_response, admin_adaptive_key_payload,
admin_adaptive_load_candidate_keys,
};
use crate::gateway::handlers::{query_param_value, unix_secs_to_rfc3339};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{query_param_value, unix_secs_to_rfc3339};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -1,5 +1,5 @@
use crate::gateway::handlers::json_string_list;
use crate::gateway::{AppState, GatewayError};
use crate::handlers::json_string_list;
use crate::{AppState, GatewayError};
use aether_data::repository::provider_catalog::StoredProviderCatalogKey;
use axum::{
body::Body,

View File

@@ -1,15 +1,16 @@
use crate::gateway::handlers::admin::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::{
default_admin_user_api_key_name, format_optional_unix_secs_iso8601,
generate_admin_user_api_key_plaintext, hash_admin_user_api_key, masked_user_api_key_display,
normalize_admin_optional_api_key_name, normalize_admin_user_api_formats,
normalize_admin_user_string_list,
};
use crate::gateway::handlers::public::serialize_admin_system_users_export_wallet;
use crate::gateway::handlers::{
use crate::handlers::public::serialize_admin_system_users_export_wallet;
use crate::handlers::{
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks, query_param_bool,
query_param_optional_bool, query_param_value,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::Body,
http,

View File

@@ -11,7 +11,9 @@ use super::{
hash_admin_user_api_key, masked_user_api_key_display, normalize_admin_optional_api_key_name,
normalize_admin_user_api_formats, normalize_admin_user_string_list,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::{AppState, GatewayError};
use axum::{
body::Body,
http,
@@ -111,22 +113,28 @@ pub(super) async fn build_admin_create_api_key_response(
return Ok(build_admin_api_keys_data_unavailable_response());
};
Ok(Json(json!({
"id": created.api_key_id,
"key": plaintext_key,
"name": created.name,
"key_display": masked_user_api_key_display(state, created.key_encrypted.as_deref()),
"is_standalone": true,
"is_active": created.is_active,
"rate_limit": created.rate_limit,
"allowed_providers": created.allowed_providers,
"allowed_api_formats": created.allowed_api_formats,
"allowed_models": created.allowed_models,
"expires_at": format_optional_unix_secs_iso8601(created.expires_at_unix_secs),
"wallet": serde_json::Value::Null,
"message": "独立余额Key创建成功请妥善保存完整密钥后续将无法查看",
}))
.into_response())
Ok(attach_admin_audit_response(
Json(json!({
"id": created.api_key_id,
"key": plaintext_key,
"name": created.name,
"key_display": masked_user_api_key_display(state, created.key_encrypted.as_deref()),
"is_standalone": true,
"is_active": created.is_active,
"rate_limit": created.rate_limit,
"allowed_providers": created.allowed_providers,
"allowed_api_formats": created.allowed_api_formats,
"allowed_models": created.allowed_models,
"expires_at": format_optional_unix_secs_iso8601(created.expires_at_unix_secs),
"wallet": serde_json::Value::Null,
"message": "独立余额Key创建成功请妥善保存完整密钥后续将无法查看",
}))
.into_response(),
"admin_standalone_api_key_created",
"create_standalone_api_key",
"api_key",
&created.api_key_id,
))
}
pub(super) async fn build_admin_update_api_key_response(
@@ -244,7 +252,13 @@ pub(super) async fn build_admin_update_api_key_response(
let mut payload =
build_admin_api_key_detail_payload(state, &updated, total_tokens, wallet.as_ref());
payload["message"] = json!("API密钥已更新");
Ok(Json(payload).into_response())
Ok(attach_admin_audit_response(
Json(payload).into_response(),
"admin_standalone_api_key_updated",
"update_standalone_api_key",
"api_key",
&api_key_id,
))
}
pub(super) async fn build_admin_toggle_api_key_response(
@@ -295,12 +309,18 @@ pub(super) async fn build_admin_toggle_api_key_response(
return Ok(build_admin_api_keys_not_found_response());
};
Ok(Json(json!({
"id": updated.api_key_id,
"is_active": updated.is_active,
"message": if updated.is_active { "API密钥已启用" } else { "API密钥已禁用" },
}))
.into_response())
Ok(attach_admin_audit_response(
Json(json!({
"id": updated.api_key_id,
"is_active": updated.is_active,
"message": if updated.is_active { "API密钥已启用" } else { "API密钥已禁用" },
}))
.into_response(),
"admin_standalone_api_key_toggled",
"toggle_standalone_api_key",
"api_key",
&api_key_id,
))
}
pub(super) async fn build_admin_delete_api_key_response(
@@ -316,7 +336,13 @@ pub(super) async fn build_admin_delete_api_key_response(
};
match state.delete_standalone_api_key(&api_key_id).await? {
true => Ok(Json(json!({ "message": "API密钥已删除" })).into_response()),
true => Ok(attach_admin_audit_response(
Json(json!({ "message": "API密钥已删除" })).into_response(),
"admin_standalone_api_key_deleted",
"delete_standalone_api_key",
"api_key",
&api_key_id,
)),
false => Ok(build_admin_api_keys_not_found_response()),
}
}

View File

@@ -5,7 +5,9 @@ use super::admin_api_keys_shared::{
build_admin_api_keys_data_unavailable_response, build_admin_api_keys_not_found_response,
};
use super::{decrypt_catalog_secret_with_fallbacks, query_param_bool, query_param_optional_bool};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::{AppState, GatewayError};
use axum::{
body::Body,
http,
@@ -119,7 +121,13 @@ pub(super) async fn build_admin_api_key_detail_response(
)
.into_response());
};
return Ok(Json(json!({ "key": key })).into_response());
return Ok(attach_admin_audit_response(
Json(json!({ "key": key })).into_response(),
"admin_standalone_api_key_revealed",
"reveal_standalone_api_key",
"api_key",
&api_key_id,
));
}
let wallet = state

View File

@@ -6,7 +6,8 @@ use super::admin_api_keys_read_routes::{
build_admin_api_key_detail_response, build_admin_list_api_keys_response,
};
use super::admin_api_keys_shared::build_admin_api_keys_data_unavailable_response;
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use axum::{body::Body, http, response::Response};
pub(super) async fn maybe_build_local_admin_api_keys_routes_response(

View File

@@ -3,7 +3,7 @@ use super::{
serialize_admin_system_users_export_wallet, AppState, Body, GatewayError,
GatewayPublicRequestContext, IntoResponse, Json, Response,
};
use crate::gateway::handlers::admin::api_keys::ADMIN_API_KEYS_DATA_UNAVAILABLE_DETAIL;
use crate::handlers::admin::api_keys::ADMIN_API_KEYS_DATA_UNAVAILABLE_DETAIL;
#[derive(Debug, Default, serde::Deserialize)]
pub(super) struct AdminStandaloneApiKeyCreateRequest {

View File

@@ -1,5 +1,6 @@
use crate::gateway::handlers::{query_param_value, unix_secs_to_rfc3339};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{query_param_value, unix_secs_to_rfc3339};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -1,13 +1,14 @@
use super::{
admin_billing_optional_bool_filter, admin_billing_optional_epoch_value,
admin_billing_optional_filter, admin_billing_pages, admin_billing_parse_page,
admin_billing_parse_page_size, admin_billing_validate_safe_expression,
build_admin_billing_bad_request_response, build_admin_billing_not_found_response,
build_admin_billing_read_only_response, default_admin_billing_true,
normalize_admin_billing_optional_text, normalize_admin_billing_required_text,
admin_billing_optional_bool_filter, admin_billing_optional_filter, admin_billing_pages,
admin_billing_parse_page, admin_billing_parse_page_size,
admin_billing_validate_safe_expression, build_admin_billing_bad_request_response,
build_admin_billing_not_found_response, build_admin_billing_read_only_response,
default_admin_billing_true, normalize_admin_billing_optional_text,
normalize_admin_billing_required_text,
};
use crate::gateway::handlers::unix_secs_to_rfc3339;
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::unix_secs_to_rfc3339;
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,
@@ -16,7 +17,6 @@ use axum::{
};
use serde::Deserialize;
use serde_json::json;
use sqlx::Row;
fn default_admin_billing_collector_value_type() -> String {
"float".to_string()
@@ -43,7 +43,7 @@ struct AdminBillingCollectorUpsertRequest {
}
fn build_admin_billing_collector_payload_from_record(
record: &crate::gateway::AdminBillingCollectorRecord,
record: &crate::AdminBillingCollectorRecord,
) -> serde_json::Value {
json!({
"id": record.id,
@@ -75,26 +75,6 @@ fn admin_billing_collector_id_from_path(request_path: &str) -> Option<String> {
}
}
fn admin_billing_collector_payload(
row: &sqlx::postgres::PgRow,
) -> Result<serde_json::Value, GatewayError> {
Ok(json!({
"id": row.try_get::<String, _>("id").map_err(|err| GatewayError::Internal(err.to_string()))?,
"api_format": row.try_get::<String, _>("api_format").map_err(|err| GatewayError::Internal(err.to_string()))?,
"task_type": row.try_get::<String, _>("task_type").map_err(|err| GatewayError::Internal(err.to_string()))?,
"dimension_name": row.try_get::<String, _>("dimension_name").map_err(|err| GatewayError::Internal(err.to_string()))?,
"source_type": row.try_get::<String, _>("source_type").map_err(|err| GatewayError::Internal(err.to_string()))?,
"source_path": row.try_get::<Option<String>, _>("source_path").map_err(|err| GatewayError::Internal(err.to_string()))?,
"value_type": row.try_get::<String, _>("value_type").map_err(|err| GatewayError::Internal(err.to_string()))?,
"transform_expression": row.try_get::<Option<String>, _>("transform_expression").map_err(|err| GatewayError::Internal(err.to_string()))?,
"default_value": row.try_get::<Option<String>, _>("default_value").map_err(|err| GatewayError::Internal(err.to_string()))?,
"priority": row.try_get::<i32, _>("priority").map_err(|err| GatewayError::Internal(err.to_string()))?,
"is_enabled": row.try_get::<bool, _>("is_enabled").map_err(|err| GatewayError::Internal(err.to_string()))?,
"created_at": admin_billing_optional_epoch_value(row, "created_at_unix_secs")?,
"updated_at": admin_billing_optional_epoch_value(row, "updated_at_unix_secs")?,
}))
}
async fn build_admin_list_dimension_collectors_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
@@ -116,7 +96,7 @@ async fn build_admin_list_dimension_collectors_response(
Err(detail) => return Ok(build_admin_billing_bad_request_response(detail)),
};
if let Some((items, total)) = state
let (items, total) = state
.list_admin_billing_collectors(
api_format.as_deref(),
task_type.as_deref(),
@@ -126,90 +106,13 @@ async fn build_admin_list_dimension_collectors_response(
page_size,
)
.await?
{
return Ok(Json(json!({
"items": items
.iter()
.map(build_admin_billing_collector_payload_from_record)
.collect::<Vec<_>>(),
"total": total,
"page": page,
"page_size": page_size,
"pages": admin_billing_pages(total, page_size),
}))
.into_response());
}
let mut total = 0_u64;
let mut items = Vec::new();
if let Some(pool) = state.postgres_pool() {
let count_row = sqlx::query(
r#"
SELECT COUNT(*) AS total
FROM dimension_collectors
WHERE ($1::TEXT IS NULL OR api_format = $1)
AND ($2::TEXT IS NULL OR task_type = $2)
AND ($3::TEXT IS NULL OR dimension_name = $3)
AND ($4::BOOL IS NULL OR is_enabled = $4)
"#,
)
.bind(api_format.as_deref())
.bind(task_type.as_deref())
.bind(dimension_name.as_deref())
.bind(is_enabled)
.fetch_one(&pool)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
total = count_row
.try_get::<i64, _>("total")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.max(0) as u64;
let offset = u64::from(page.saturating_sub(1) * page_size);
let rows = sqlx::query(
r#"
SELECT
id,
api_format,
task_type,
dimension_name,
source_type,
source_path,
value_type,
transform_expression,
default_value,
priority,
is_enabled,
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
FROM dimension_collectors
WHERE ($1::TEXT IS NULL OR api_format = $1)
AND ($2::TEXT IS NULL OR task_type = $2)
AND ($3::TEXT IS NULL OR dimension_name = $3)
AND ($4::BOOL IS NULL OR is_enabled = $4)
ORDER BY updated_at DESC, priority DESC, id ASC
OFFSET $5
LIMIT $6
"#,
)
.bind(api_format.as_deref())
.bind(task_type.as_deref())
.bind(dimension_name.as_deref())
.bind(is_enabled)
.bind(i64::try_from(offset).map_err(|err| GatewayError::Internal(err.to_string()))?)
.bind(i64::from(page_size))
.fetch_all(&pool)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
items = rows
.iter()
.map(admin_billing_collector_payload)
.collect::<Result<Vec<_>, GatewayError>>()?;
}
.unwrap_or_default();
Ok(Json(json!({
"items": items,
"items": items
.iter()
.map(build_admin_billing_collector_payload_from_record)
.collect::<Vec<_>>(),
"total": total,
"page": page,
"page_size": page_size,
@@ -229,45 +132,10 @@ async fn build_admin_get_dimension_collector_response(
));
};
if let Some(record) = state.read_admin_billing_collector(&collector_id).await? {
return Ok(
Json(build_admin_billing_collector_payload_from_record(&record)).into_response(),
);
}
let Some(pool) = state.postgres_pool() else {
return Ok(build_admin_billing_not_found_response(
"Dimension collector not found",
));
};
let row = sqlx::query(
r#"
SELECT
id,
api_format,
task_type,
dimension_name,
source_type,
source_path,
value_type,
transform_expression,
default_value,
priority,
is_enabled,
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
FROM dimension_collectors
WHERE id = $1
"#,
)
.bind(&collector_id)
.fetch_optional(&pool)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
match row {
Some(row) => Ok(Json(admin_billing_collector_payload(&row)?).into_response()),
match state.read_admin_billing_collector(&collector_id).await? {
Some(record) => {
Ok(Json(build_admin_billing_collector_payload_from_record(&record)).into_response())
}
None => Ok(build_admin_billing_not_found_response(
"Dimension collector not found",
)),
@@ -278,7 +146,7 @@ async fn parse_admin_billing_collector_request(
state: &AppState,
request_body: Option<&Bytes>,
existing_id: Option<&str>,
) -> Result<crate::gateway::AdminBillingCollectorWriteInput, Response<Body>> {
) -> Result<crate::AdminBillingCollectorWriteInput, Response<Body>> {
let Some(request_body) = request_body else {
return Err(build_admin_billing_bad_request_response("请求体不能为空"));
};
@@ -391,7 +259,7 @@ async fn parse_admin_billing_collector_request(
}
}
Ok(crate::gateway::AdminBillingCollectorWriteInput {
Ok(crate::AdminBillingCollectorWriteInput {
api_format,
task_type,
dimension_name,
@@ -414,16 +282,16 @@ async fn build_admin_create_dimension_collector_response(
Err(response) => return Ok(response),
};
match state.create_admin_billing_collector(&input).await? {
crate::gateway::LocalMutationOutcome::Applied(record) => {
crate::LocalMutationOutcome::Applied(record) => {
Ok(Json(build_admin_billing_collector_payload_from_record(&record)).into_response())
}
crate::gateway::LocalMutationOutcome::Invalid(detail) => {
crate::LocalMutationOutcome::Invalid(detail) => {
Ok(build_admin_billing_bad_request_response(detail))
}
crate::gateway::LocalMutationOutcome::NotFound => Ok(
crate::LocalMutationOutcome::NotFound => Ok(
build_admin_billing_not_found_response("Dimension collector not found"),
),
crate::gateway::LocalMutationOutcome::Unavailable => Ok(
crate::LocalMutationOutcome::Unavailable => Ok(
build_admin_billing_read_only_response("当前为只读模式,无法创建维度采集器"),
),
}
@@ -450,16 +318,16 @@ async fn build_admin_update_dimension_collector_response(
.update_admin_billing_collector(&collector_id, &input)
.await?
{
crate::gateway::LocalMutationOutcome::Applied(record) => {
crate::LocalMutationOutcome::Applied(record) => {
Ok(Json(build_admin_billing_collector_payload_from_record(&record)).into_response())
}
crate::gateway::LocalMutationOutcome::NotFound => Ok(
crate::LocalMutationOutcome::NotFound => Ok(
build_admin_billing_not_found_response("Dimension collector not found"),
),
crate::gateway::LocalMutationOutcome::Invalid(detail) => {
crate::LocalMutationOutcome::Invalid(detail) => {
Ok(build_admin_billing_bad_request_response(detail))
}
crate::gateway::LocalMutationOutcome::Unavailable => Ok(
crate::LocalMutationOutcome::Unavailable => Ok(
build_admin_billing_read_only_response("当前为只读模式,无法更新维度采集器"),
),
}

View File

@@ -2,7 +2,9 @@ use super::{
build_admin_billing_bad_request_response, build_admin_billing_not_found_response,
build_admin_billing_read_only_response, normalize_admin_billing_required_text,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
response::{IntoResponse, Response},
@@ -36,9 +38,9 @@ fn build_admin_billing_presets_payload() -> serde_json::Value {
}
fn build_admin_billing_aether_core_collectors(
) -> Vec<crate::gateway::AdminBillingCollectorWriteInput> {
) -> Vec<crate::AdminBillingCollectorWriteInput> {
vec![
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "OPENAI:CHAT".to_string(),
task_type: "chat".to_string(),
dimension_name: "input_tokens".to_string(),
@@ -50,7 +52,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 10,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "OPENAI:CHAT".to_string(),
task_type: "chat".to_string(),
dimension_name: "output_tokens".to_string(),
@@ -62,7 +64,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 10,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "CLAUDE:CHAT".to_string(),
task_type: "chat".to_string(),
dimension_name: "input_tokens".to_string(),
@@ -74,7 +76,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 10,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "CLAUDE:CHAT".to_string(),
task_type: "chat".to_string(),
dimension_name: "output_tokens".to_string(),
@@ -86,7 +88,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 10,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "GEMINI:CHAT".to_string(),
task_type: "chat".to_string(),
dimension_name: "input_tokens".to_string(),
@@ -98,7 +100,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 10,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "GEMINI:CHAT".to_string(),
task_type: "chat".to_string(),
dimension_name: "output_tokens".to_string(),
@@ -110,7 +112,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 10,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "OPENAI:CHAT".to_string(),
task_type: "video".to_string(),
dimension_name: "video_resolution_key".to_string(),
@@ -122,7 +124,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 10,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "OPENAI:CHAT".to_string(),
task_type: "video".to_string(),
dimension_name: "video_resolution_key".to_string(),
@@ -134,7 +136,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 0,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "OPENAI:CHAT".to_string(),
task_type: "video".to_string(),
dimension_name: "video_size_bytes".to_string(),
@@ -146,7 +148,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 0,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "OPENAI:CHAT".to_string(),
task_type: "video".to_string(),
dimension_name: "video_duration_seconds".to_string(),
@@ -158,7 +160,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 10,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "OPENAI:CHAT".to_string(),
task_type: "video".to_string(),
dimension_name: "video_duration_seconds".to_string(),
@@ -170,7 +172,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 0,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "GEMINI:CHAT".to_string(),
task_type: "video".to_string(),
dimension_name: "video_resolution_key".to_string(),
@@ -182,7 +184,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 10,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "GEMINI:CHAT".to_string(),
task_type: "video".to_string(),
dimension_name: "video_resolution_key".to_string(),
@@ -194,7 +196,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 0,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "GEMINI:CHAT".to_string(),
task_type: "video".to_string(),
dimension_name: "video_size_bytes".to_string(),
@@ -206,7 +208,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 0,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "GEMINI:CHAT".to_string(),
task_type: "video".to_string(),
dimension_name: "video_duration_seconds".to_string(),
@@ -218,7 +220,7 @@ fn build_admin_billing_aether_core_collectors(
priority: 10,
is_enabled: true,
},
crate::gateway::AdminBillingCollectorWriteInput {
crate::AdminBillingCollectorWriteInput {
api_format: "GEMINI:CHAT".to_string(),
task_type: "video".to_string(),
dimension_name: "video_duration_seconds".to_string(),
@@ -237,7 +239,7 @@ fn resolve_admin_billing_preset_collectors(
preset: &str,
) -> Option<(
&'static str,
Vec<crate::gateway::AdminBillingCollectorWriteInput>,
Vec<crate::AdminBillingCollectorWriteInput>,
)> {
let normalized = preset.trim().to_ascii_lowercase();
match normalized.as_str() {
@@ -277,6 +279,7 @@ fn parse_admin_billing_preset_apply_request(
async fn build_admin_apply_billing_preset_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
request_body: Option<&Bytes>,
) -> Result<Response<Body>, GatewayError> {
let (preset, mode) = match parse_admin_billing_preset_apply_request(request_body) {
@@ -301,23 +304,32 @@ async fn build_admin_apply_billing_preset_response(
.apply_admin_billing_preset(resolved_preset, &mode, &collectors)
.await?
{
crate::gateway::LocalMutationOutcome::Applied(result) => Ok(Json(json!({
"ok": result.errors.is_empty(),
"preset": result.preset,
"mode": result.mode,
"created": result.created,
"updated": result.updated,
"skipped": result.skipped,
"errors": result.errors,
}))
.into_response()),
crate::gateway::LocalMutationOutcome::Unavailable => Ok(
crate::LocalMutationOutcome::Applied(result) => {
let response = Json(json!({
"ok": result.errors.is_empty(),
"preset": result.preset,
"mode": result.mode,
"created": result.created,
"updated": result.updated,
"skipped": result.skipped,
"errors": result.errors,
}))
.into_response();
Ok(attach_admin_audit_response(
response,
"admin_billing_preset_applied",
"apply_billing_preset",
"billing_preset",
resolved_preset,
))
}
crate::LocalMutationOutcome::Unavailable => Ok(
build_admin_billing_read_only_response("当前为只读模式,无法应用计费预设"),
),
crate::gateway::LocalMutationOutcome::Invalid(detail) => {
crate::LocalMutationOutcome::Invalid(detail) => {
Ok(build_admin_billing_bad_request_response(detail))
}
crate::gateway::LocalMutationOutcome::NotFound => Ok(
crate::LocalMutationOutcome::NotFound => Ok(
build_admin_billing_not_found_response("Billing preset not found"),
),
}
@@ -353,7 +365,8 @@ pub(super) async fn maybe_build_local_admin_billing_presets_response(
) =>
{
Ok(Some(
build_admin_apply_billing_preset_response(state, request_body).await?,
build_admin_apply_billing_preset_response(state, request_context, request_body)
.await?,
))
}
_ => Ok(None),

View File

@@ -1,14 +1,14 @@
use super::{
admin_billing_optional_bool_filter, admin_billing_optional_epoch_value,
admin_billing_optional_filter, admin_billing_pages, admin_billing_parse_page,
admin_billing_parse_page_size, admin_billing_validate_safe_expression,
build_admin_billing_bad_request_response, build_admin_billing_not_found_response,
build_admin_billing_read_only_response, default_admin_billing_json_object,
default_admin_billing_true, normalize_admin_billing_optional_text,
normalize_admin_billing_required_text,
admin_billing_optional_bool_filter, admin_billing_optional_filter, admin_billing_pages,
admin_billing_parse_page, admin_billing_parse_page_size,
admin_billing_validate_safe_expression, build_admin_billing_bad_request_response,
build_admin_billing_not_found_response, build_admin_billing_read_only_response,
default_admin_billing_json_object, default_admin_billing_true,
normalize_admin_billing_optional_text, normalize_admin_billing_required_text,
};
use crate::gateway::handlers::unix_secs_to_rfc3339;
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::unix_secs_to_rfc3339;
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,
@@ -17,7 +17,6 @@ use axum::{
};
use serde::Deserialize;
use serde_json::json;
use sqlx::Row;
fn default_admin_billing_rule_task_type() -> String {
"chat".to_string()
@@ -42,7 +41,7 @@ struct AdminBillingRuleUpsertRequest {
}
fn build_admin_billing_rule_payload_from_record(
record: &crate::gateway::AdminBillingRuleRecord,
record: &crate::AdminBillingRuleRecord,
) -> serde_json::Value {
json!({
"id": record.id,
@@ -74,7 +73,7 @@ fn admin_billing_rule_id_from_path(request_path: &str) -> Option<String> {
fn parse_admin_billing_rule_request(
request_body: Option<&Bytes>,
) -> Result<crate::gateway::AdminBillingRuleWriteInput, Response<Body>> {
) -> Result<crate::AdminBillingRuleWriteInput, Response<Body>> {
let Some(request_body) = request_body else {
return Err(build_admin_billing_bad_request_response("请求体不能为空"));
};
@@ -158,7 +157,7 @@ fn parse_admin_billing_rule_request(
}
}
Ok(crate::gateway::AdminBillingRuleWriteInput {
Ok(crate::AdminBillingRuleWriteInput {
name,
task_type,
global_model_id,
@@ -170,24 +169,6 @@ fn parse_admin_billing_rule_request(
})
}
fn admin_billing_rule_payload(
row: &sqlx::postgres::PgRow,
) -> Result<serde_json::Value, GatewayError> {
Ok(json!({
"id": row.try_get::<String, _>("id").map_err(|err| GatewayError::Internal(err.to_string()))?,
"name": row.try_get::<String, _>("name").map_err(|err| GatewayError::Internal(err.to_string()))?,
"task_type": row.try_get::<String, _>("task_type").map_err(|err| GatewayError::Internal(err.to_string()))?,
"global_model_id": row.try_get::<Option<String>, _>("global_model_id").map_err(|err| GatewayError::Internal(err.to_string()))?,
"model_id": row.try_get::<Option<String>, _>("model_id").map_err(|err| GatewayError::Internal(err.to_string()))?,
"expression": row.try_get::<String, _>("expression").map_err(|err| GatewayError::Internal(err.to_string()))?,
"variables": row.try_get::<Option<serde_json::Value>, _>("variables").map_err(|err| GatewayError::Internal(err.to_string()))?.unwrap_or_else(|| json!({})),
"dimension_mappings": row.try_get::<Option<serde_json::Value>, _>("dimension_mappings").map_err(|err| GatewayError::Internal(err.to_string()))?.unwrap_or_else(|| json!({})),
"is_enabled": row.try_get::<bool, _>("is_enabled").map_err(|err| GatewayError::Internal(err.to_string()))?,
"created_at": admin_billing_optional_epoch_value(row, "created_at_unix_secs")?,
"updated_at": admin_billing_optional_epoch_value(row, "updated_at_unix_secs")?,
}))
}
async fn build_admin_list_billing_rules_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
@@ -207,72 +188,20 @@ async fn build_admin_list_billing_rules_response(
Err(detail) => return Ok(build_admin_billing_bad_request_response(detail)),
};
let mut total = 0_u64;
let mut items = Vec::new();
if let Some((records, record_total)) = state
let (items, total) = if let Some((records, record_total)) = state
.list_admin_billing_rules(task_type.as_deref(), is_enabled, page, page_size)
.await?
{
total = record_total;
items = records
.iter()
.map(build_admin_billing_rule_payload_from_record)
.collect::<Vec<_>>();
} else if let Some(pool) = state.postgres_pool() {
let count_row = sqlx::query(
r#"
SELECT COUNT(*) AS total
FROM billing_rules
WHERE ($1::TEXT IS NULL OR task_type = $1)
AND ($2::BOOL IS NULL OR is_enabled = $2)
"#,
(
records
.iter()
.map(build_admin_billing_rule_payload_from_record)
.collect::<Vec<_>>(),
record_total,
)
.bind(task_type.as_deref())
.bind(is_enabled)
.fetch_one(&pool)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
total = count_row
.try_get::<i64, _>("total")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.max(0) as u64;
let offset = u64::from(page.saturating_sub(1) * page_size);
let rows = sqlx::query(
r#"
SELECT
id,
name,
task_type,
global_model_id,
model_id,
expression,
variables,
dimension_mappings,
is_enabled,
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
FROM billing_rules
WHERE ($1::TEXT IS NULL OR task_type = $1)
AND ($2::BOOL IS NULL OR is_enabled = $2)
ORDER BY updated_at DESC
OFFSET $3
LIMIT $4
"#,
)
.bind(task_type.as_deref())
.bind(is_enabled)
.bind(i64::try_from(offset).map_err(|err| GatewayError::Internal(err.to_string()))?)
.bind(i64::from(page_size))
.fetch_all(&pool)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
items = rows
.iter()
.map(admin_billing_rule_payload)
.collect::<Result<Vec<_>, GatewayError>>()?;
}
} else {
(Vec::new(), 0)
};
Ok(Json(json!({
"items": items,
@@ -291,40 +220,10 @@ async fn build_admin_get_billing_rule_response(
let Some(rule_id) = admin_billing_rule_id_from_path(&request_context.request_path) else {
return Ok(build_admin_billing_bad_request_response("缺少 rule_id"));
};
if let Some(record) = state.read_admin_billing_rule(&rule_id).await? {
return Ok(Json(build_admin_billing_rule_payload_from_record(&record)).into_response());
}
let Some(pool) = state.postgres_pool() else {
return Ok(build_admin_billing_not_found_response(
"Billing rule not found",
));
};
let row = sqlx::query(
r#"
SELECT
id,
name,
task_type,
global_model_id,
model_id,
expression,
variables,
dimension_mappings,
is_enabled,
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
FROM billing_rules
WHERE id = $1
"#,
)
.bind(&rule_id)
.fetch_optional(&pool)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
match row {
Some(row) => Ok(Json(admin_billing_rule_payload(&row)?).into_response()),
match state.read_admin_billing_rule(&rule_id).await? {
Some(record) => {
Ok(Json(build_admin_billing_rule_payload_from_record(&record)).into_response())
}
None => Ok(build_admin_billing_not_found_response(
"Billing rule not found",
)),
@@ -340,16 +239,16 @@ async fn build_admin_create_billing_rule_response(
Err(response) => return Ok(response),
};
match state.create_admin_billing_rule(&input).await? {
crate::gateway::LocalMutationOutcome::Applied(record) => {
crate::LocalMutationOutcome::Applied(record) => {
Ok(Json(build_admin_billing_rule_payload_from_record(&record)).into_response())
}
crate::gateway::LocalMutationOutcome::Invalid(detail) => {
crate::LocalMutationOutcome::Invalid(detail) => {
Ok(build_admin_billing_bad_request_response(detail))
}
crate::gateway::LocalMutationOutcome::NotFound => Ok(
crate::LocalMutationOutcome::NotFound => Ok(
build_admin_billing_not_found_response("Billing rule not found"),
),
crate::gateway::LocalMutationOutcome::Unavailable => Ok(
crate::LocalMutationOutcome::Unavailable => Ok(
build_admin_billing_read_only_response("当前为只读模式,无法创建计费规则"),
),
}
@@ -368,16 +267,16 @@ async fn build_admin_update_billing_rule_response(
Err(response) => return Ok(response),
};
match state.update_admin_billing_rule(&rule_id, &input).await? {
crate::gateway::LocalMutationOutcome::Applied(record) => {
crate::LocalMutationOutcome::Applied(record) => {
Ok(Json(build_admin_billing_rule_payload_from_record(&record)).into_response())
}
crate::gateway::LocalMutationOutcome::NotFound => Ok(
crate::LocalMutationOutcome::NotFound => Ok(
build_admin_billing_not_found_response("Billing rule not found"),
),
crate::gateway::LocalMutationOutcome::Invalid(detail) => {
crate::LocalMutationOutcome::Invalid(detail) => {
Ok(build_admin_billing_bad_request_response(detail))
}
crate::gateway::LocalMutationOutcome::Unavailable => Ok(
crate::LocalMutationOutcome::Unavailable => Ok(
build_admin_billing_read_only_response("当前为只读模式,无法更新计费规则"),
),
}

View File

@@ -1,12 +1,12 @@
use super::{
normalize_auth_type, normalize_json_object, normalize_string_list, validate_vertex_api_formats,
};
use crate::gateway::handlers::{
use crate::handlers::{
build_admin_provider_key_response, decrypt_catalog_secret_with_fallbacks,
encrypt_catalog_secret_with_fallbacks, json_string_list, parse_catalog_auth_config_json,
AdminProviderKeyCreateRequest, AdminProviderKeyUpdateRequest,
};
use crate::gateway::AppState;
use crate::AppState;
use aether_data::repository::provider_catalog::{
StoredProviderCatalogKey, StoredProviderCatalogProvider,
};

View File

@@ -2,13 +2,13 @@ use super::{
normalize_json_object, normalize_provider_billing_type, normalize_provider_type_input,
parse_optional_rfc3339_unix_secs,
};
use crate::gateway::api::ai::{
use crate::api::ai::{
admin_default_body_rules_for_signature, admin_endpoint_signature_parts,
};
use crate::gateway::handlers::public::normalize_admin_base_url;
use crate::gateway::handlers::{AdminProviderCreateRequest, AdminProviderUpdateRequest};
use crate::gateway::provider_transport::provider_type_enables_format_conversion_by_default;
use crate::gateway::AppState;
use crate::handlers::public::normalize_admin_base_url;
use crate::handlers::{AdminProviderCreateRequest, AdminProviderUpdateRequest};
use crate::provider_transport::provider_types::provider_type_enables_format_conversion_by_default;
use crate::AppState;
use aether_data::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
};

View File

@@ -6,10 +6,10 @@ fn normalize_reveal_auth_type(value: &str) -> &str {
}
}
use crate::gateway::handlers::{
use crate::handlers::{
decrypt_catalog_secret_with_fallbacks, parse_catalog_auth_config_json,
};
use crate::gateway::AppState;
use crate::AppState;
use aether_data::repository::provider_catalog::StoredProviderCatalogKey;
use chrono::{SecondsFormat, Utc};
use serde_json::json;

View File

@@ -1,4 +1,5 @@
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -1,9 +1,10 @@
use crate::gateway::handlers::internal::build_management_token_payload;
use crate::gateway::handlers::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::internal::build_management_token_payload;
use crate::handlers::{
admin_management_token_id_from_path, admin_management_token_status_id_from_path,
is_admin_management_tokens_root, query_param_optional_bool, query_param_value,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use aether_data::repository::management_tokens::ManagementTokenListQuery;
use axum::{
body::Body,

View File

@@ -3,7 +3,8 @@ use super::super::{
read_admin_external_models_cache,
};
use super::build_admin_model_catalog_data_unavailable_response;
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use axum::{
body::Body,
http,

View File

@@ -1,10 +1,11 @@
use crate::gateway::handlers::public::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::public::{
admin_module_by_name, admin_module_name_from_enabled_path, admin_module_name_from_status_path,
build_admin_module_runtime_state, build_admin_module_status_payload,
build_admin_module_validation_result, build_admin_modules_status_payload,
module_available_from_env, AdminSetModuleEnabledRequest,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -2,11 +2,13 @@ use super::super::{
build_admin_oauth_provider_payload, build_admin_oauth_supported_types_payload,
build_admin_oauth_upsert_record, build_proxy_error_response,
};
use crate::gateway::handlers::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::handlers::{
admin_oauth_provider_type_from_path, admin_oauth_test_provider_type_from_path,
AdminOAuthProviderUpsertRequest,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,
@@ -44,7 +46,7 @@ pub(super) async fn maybe_build_local_admin_core_oauth_response(
)
{
let providers = state.list_oauth_provider_configs().await?;
return Ok(Some(
return Ok(Some(attach_admin_audit_response(
Json(
providers
.iter()
@@ -52,7 +54,11 @@ pub(super) async fn maybe_build_local_admin_core_oauth_response(
.collect::<Vec<_>>(),
)
.into_response(),
));
"admin_oauth_provider_configs_viewed",
"list_oauth_provider_configs",
"oauth_provider",
"all",
)));
}
if decision.route_kind.as_deref() == Some("get_provider")
@@ -71,9 +77,13 @@ pub(super) async fn maybe_build_local_admin_core_oauth_response(
};
return Ok(Some(
match state.get_oauth_provider_config(&provider_type).await? {
Some(provider) => {
Json(build_admin_oauth_provider_payload(&provider)).into_response()
}
Some(provider) => attach_admin_audit_response(
Json(build_admin_oauth_provider_payload(&provider)).into_response(),
"admin_oauth_provider_config_viewed",
"view_oauth_provider_config",
"oauth_provider",
&provider_type,
),
None => (
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "Provider 配置不存在" })),
@@ -299,7 +309,7 @@ pub(super) async fn maybe_build_local_admin_core_oauth_response(
} else {
"provider 未安装/不可用"
};
return Ok(Some(
return Ok(Some(attach_admin_audit_response(
Json(json!({
"authorization_url_reachable": false,
"token_url_reachable": false,
@@ -307,7 +317,11 @@ pub(super) async fn maybe_build_local_admin_core_oauth_response(
"details": details,
}))
.into_response(),
));
"admin_oauth_provider_tested",
"test_oauth_provider_config",
"oauth_provider",
&provider_type,
)));
}
Ok(None)

View File

@@ -1,6 +1,8 @@
use super::super::build_proxy_error_response;
use super::ADMIN_AWS_REGIONS;
use crate::gateway::handlers::public::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::handlers::public::{
apply_admin_email_template_update, apply_admin_system_config_update,
apply_admin_system_settings_update, build_admin_api_formats_payload,
build_admin_email_template_payload, build_admin_email_templates_payload,
@@ -10,12 +12,12 @@ use crate::gateway::handlers::public::{
build_admin_system_users_export_payload, current_aether_version, delete_admin_system_config,
preview_admin_email_template, reset_admin_email_template,
};
use crate::gateway::handlers::{
use crate::handlers::{
admin_system_config_key_from_path, admin_system_email_template_preview_type_from_path,
admin_system_email_template_reset_type_from_path, admin_system_email_template_type_from_path,
is_admin_system_configs_root, is_admin_system_email_templates_root,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,
@@ -85,18 +87,26 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
&& request_context.request_method == http::Method::GET
&& request_context.request_path == "/api/admin/system/config/export"
{
return Ok(Some(
return Ok(Some(attach_admin_audit_response(
Json(build_admin_system_config_export_payload(state).await?).into_response(),
));
"admin_system_config_exported",
"export_system_config",
"system_config_export",
"global",
)));
}
if decision.route_kind.as_deref() == Some("users_export")
&& request_context.request_method == http::Method::GET
&& request_context.request_path == "/api/admin/system/users/export"
{
return Ok(Some(
return Ok(Some(attach_admin_audit_response(
Json(build_admin_system_users_export_payload(state).await?).into_response(),
));
"admin_system_users_exported",
"export_system_users",
"user_export",
"all_users",
)));
}
if matches!(
@@ -139,7 +149,13 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
};
return Ok(Some(
match apply_admin_system_settings_update(state, request_body).await? {
Ok(payload) => Json(payload).into_response(),
Ok(payload) => attach_admin_audit_response(
Json(payload).into_response(),
"admin_system_settings_updated",
"update_system_settings",
"system_settings",
"global",
),
Err((status, payload)) => (status, Json(payload)).into_response(),
},
));
@@ -197,7 +213,13 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
};
return Ok(Some(
match apply_admin_system_config_update(state, &config_key, request_body).await? {
Ok(payload) => Json(payload).into_response(),
Ok(payload) => attach_admin_audit_response(
Json(payload).into_response(),
"admin_system_config_updated",
"update_system_config",
"system_config",
&config_key,
),
Err((status, payload)) => (status, Json(payload)).into_response(),
},
));
@@ -217,7 +239,13 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
};
return Ok(Some(
match delete_admin_system_config(state, &config_key).await? {
Ok(payload) => Json(payload).into_response(),
Ok(payload) => attach_admin_audit_response(
Json(payload).into_response(),
"admin_system_config_deleted",
"delete_system_config",
"system_config",
&config_key,
),
Err((status, payload)) => (status, Json(payload)).into_response(),
},
));

View File

@@ -1,4 +1,5 @@
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use axum::body::{Body, Bytes};
use axum::http::Response;

View File

@@ -3,11 +3,12 @@ use super::super::{
build_admin_health_summary_payload, build_admin_key_health_payload, recover_admin_key_health,
recover_all_admin_key_health,
};
use crate::gateway::handlers::public::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::public::{
build_api_format_health_monitor_payload, ApiFormatHealthMonitorOptions,
};
use crate::gateway::handlers::query_param_value;
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::handlers::query_param_value;
use crate::{AppState, GatewayError};
use axum::{
body::Body,
http,

View File

@@ -7,13 +7,15 @@ use super::super::{
refresh_antigravity_provider_quota_locally, refresh_codex_provider_quota_locally,
refresh_kiro_provider_quota_locally,
};
use crate::gateway::handlers::public::build_admin_keys_grouped_by_format_payload;
use crate::gateway::handlers::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::handlers::public::build_admin_keys_grouped_by_format_payload;
use crate::handlers::{
admin_provider_id_for_keys, query_param_value, AdminProviderKeyBatchDeleteRequest,
AdminProviderKeyCreateRequest, AdminProviderKeyUpdateRequest, AdminProviderQuotaRefreshRequest,
OAUTH_ACCOUNT_BLOCK_PREFIX,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,
@@ -74,7 +76,13 @@ pub(super) async fn maybe_build_local_admin_endpoints_keys_response(
));
};
return Ok(Some(match build_admin_reveal_key_payload(state, &key) {
Ok(payload) => Json(payload).into_response(),
Ok(payload) => attach_admin_audit_response(
Json(payload).into_response(),
"admin_provider_key_revealed",
"reveal_provider_key",
"provider_key",
&key_id,
),
Err(detail) => (
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": detail })),
@@ -115,7 +123,13 @@ pub(super) async fn maybe_build_local_admin_endpoints_keys_response(
};
return Ok(Some(
match build_admin_export_key_payload(state, &key).await {
Ok(payload) => Json(payload).into_response(),
Ok(payload) => attach_admin_audit_response(
Json(payload).into_response(),
"admin_provider_key_exported",
"export_provider_key",
"provider_key_export",
&key_id,
),
Err(detail) => (
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": detail })),

View File

@@ -5,11 +5,12 @@ use super::super::{
build_admin_update_provider_endpoint_record, endpoint_key_counts_by_format,
key_api_formats_without_entry,
};
use crate::gateway::api::ai::admin_default_body_rules_for_signature;
use crate::gateway::handlers::{
use crate::api::ai::admin_default_body_rules_for_signature;
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{
query_param_value, AdminProviderEndpointCreateRequest, AdminProviderEndpointUpdateRequest,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -1,5 +1,6 @@
use super::super::{admin_rpm_key_id, build_admin_key_rpm_payload};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use axum::{
body::Body,
http,

View File

@@ -1,9 +1,9 @@
use crate::gateway::api::ai::{
use crate::api::ai::{
admin_default_body_rules_for_signature, admin_endpoint_signature_parts,
};
use crate::gateway::handlers::public::{admin_requested_force_stream, normalize_admin_base_url};
use crate::gateway::provider_transport::provider_type_is_fixed;
use crate::gateway::AppState;
use crate::handlers::public::{admin_requested_force_stream, normalize_admin_base_url};
use crate::provider_transport::provider_types::provider_type_is_fixed;
use crate::AppState;
use aether_data::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
};
@@ -12,7 +12,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid;
use super::super::{build_admin_provider_endpoint_response, endpoint_key_counts_by_format};
use crate::gateway::handlers::{
use crate::handlers::{
AdminProviderEndpointCreateRequest, AdminProviderEndpointUpdateRequest,
};

View File

@@ -1,6 +1,6 @@
use crate::gateway::handlers::public::provider_key_api_formats;
use crate::gateway::scheduler::count_recent_rpm_requests_for_provider_key_since;
use crate::gateway::AppState;
use crate::handlers::public::provider_key_api_formats;
use crate::scheduler::count_recent_rpm_requests_for_provider_key_since;
use crate::AppState;
use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};

View File

@@ -1,9 +1,9 @@
use crate::gateway::handlers::public::{
use crate::handlers::public::{
api_format_display_name, build_public_health_timeline, provider_key_api_formats,
};
use crate::gateway::handlers::unix_secs_to_rfc3339;
use crate::gateway::scheduler::{is_provider_key_circuit_open, provider_key_health_score};
use crate::gateway::AppState;
use crate::handlers::unix_secs_to_rfc3339;
use crate::scheduler::{is_provider_key_circuit_open, provider_key_health_score};
use crate::AppState;
use aether_data::repository::candidates::PublicHealthTimelineBucket;
use serde_json::json;
use std::collections::{BTreeMap, BTreeSet};

View File

@@ -1,4 +1,5 @@
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use aether_data::repository::provider_catalog::StoredProviderCatalogKey;
use axum::body::{Body, Bytes};
use axum::http::{self, Response};

View File

@@ -4,12 +4,13 @@ use super::{
ADMIN_GEMINI_FILES_DEFAULT_PAGE, ADMIN_GEMINI_FILES_DEFAULT_PAGE_SIZE,
ADMIN_GEMINI_FILES_MAX_PAGE_SIZE,
};
use crate::gateway::handlers::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{
admin_gemini_file_mapping_id_from_path, is_admin_gemini_files_capable_keys_root,
is_admin_gemini_files_mappings_root, is_admin_gemini_files_stats_root,
query_param_optional_bool, query_param_value, unix_secs_to_rfc3339,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::body::Body;
use axum::http::{self, Response};
use axum::response::IntoResponse;

View File

@@ -2,8 +2,9 @@ use super::{
admin_gemini_files_error_response, admin_gemini_files_key_capable,
ADMIN_GEMINI_FILES_DATA_UNAVAILABLE_DETAIL,
};
use crate::gateway::handlers::{is_admin_gemini_files_upload_root, query_param_value};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{is_admin_gemini_files_upload_root, query_param_value};
use crate::{AppState, GatewayError};
use aether_contracts::{ExecutionPlan, ExecutionResult, RequestBody};
use aether_data::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
@@ -368,7 +369,7 @@ async fn admin_gemini_files_upload_single_key(
.await
.map_err(|err| format!("{err:?}"))?
.ok_or_else(|| "无法读取 Key 传输配置".to_string())?;
if !crate::gateway::provider_transport::supports_local_gemini_transport_with_network(
if !crate::provider_transport::policy::supports_local_gemini_transport_with_network(
&transport,
"gemini:chat",
) {
@@ -378,11 +379,11 @@ async fn admin_gemini_files_upload_single_key(
return Err("Gemini Files 二进制上传暂不支持 endpoint body_rules".to_string());
}
let (auth_header, auth_value) =
crate::gateway::provider_transport::resolve_local_gemini_auth(&transport)
crate::provider_transport::auth::resolve_local_gemini_auth(&transport)
.ok_or_else(|| "Key 缺少可用的 Gemini 认证信息".to_string())?;
let mut provider_request_headers =
crate::gateway::provider_transport::build_passthrough_headers_with_auth(
crate::provider_transport::auth::build_passthrough_headers_with_auth(
&http::HeaderMap::new(),
&auth_header,
&auth_value,
@@ -392,7 +393,7 @@ async fn admin_gemini_files_upload_single_key(
let original_request_body = json!({
"body_bytes_b64": upload.body_bytes_b64,
});
if !crate::gateway::provider_transport::apply_local_header_rules(
if !crate::provider_transport::apply_local_header_rules(
&mut provider_request_headers,
transport.endpoint.header_rules.as_ref(),
&[auth_header.as_str(), "content-type"],
@@ -414,7 +415,7 @@ async fn admin_gemini_files_upload_single_key(
} else {
Some("uploadType=resumable")
};
let upstream_url = crate::gateway::provider_transport::build_gemini_files_passthrough_url(
let upstream_url = crate::provider_transport::url::build_gemini_files_passthrough_url(
&transport.endpoint.base_url,
upload_path,
upload_query,
@@ -442,9 +443,9 @@ async fn admin_gemini_files_upload_single_key(
client_api_format: "gemini:files".to_string(),
provider_api_format: "gemini:files".to_string(),
model_name: Some("gemini-files".to_string()),
proxy: crate::gateway::provider_transport::resolve_transport_proxy_snapshot_with_tunnel_affinity(state, &transport).await,
tls_profile: crate::gateway::provider_transport::resolve_transport_tls_profile(&transport),
timeouts: crate::gateway::provider_transport::resolve_transport_execution_timeouts(
proxy: crate::provider_transport::resolve_transport_proxy_snapshot_with_tunnel_affinity(state, &transport).await,
tls_profile: crate::provider_transport::resolve_transport_tls_profile(&transport),
timeouts: crate::provider_transport::resolve_transport_execution_timeouts(
&transport,
),
};
@@ -459,7 +460,7 @@ async fn admin_gemini_files_upload_single_key(
.ok_or_else(|| "上传成功但上游响应缺少 JSON body".to_string())?;
let success = admin_gemini_files_upload_success_from_body(&body_json, upload)
.ok_or_else(|| admin_gemini_files_execution_error_message(&result))?;
crate::gateway::usage::store_local_gemini_file_mapping(
crate::usage::reporting::store_local_gemini_file_mapping(
state,
success.file_name.as_str(),
key.id.as_str(),
@@ -483,7 +484,12 @@ async fn admin_gemini_files_execute_upload_plan(
trace_id: &str,
plan: &ExecutionPlan,
) -> Result<ExecutionResult, GatewayError> {
crate::gateway::execute_execution_runtime_sync_plan(state, Some(trace_id), plan).await
crate::execution_runtime::execute_execution_runtime_sync_plan(
state,
Some(trace_id),
plan,
)
.await
}
fn admin_gemini_files_execution_json_body(result: &ExecutionResult) -> Option<serde_json::Value> {

View File

@@ -9,13 +9,15 @@ use super::global_models_helpers::{
build_admin_global_models_data_unavailable_response,
ADMIN_GLOBAL_MODELS_DATA_UNAVAILABLE_DETAIL,
};
use crate::gateway::handlers::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::handlers::{
admin_global_model_assign_to_providers_id, admin_global_model_id_from_path,
admin_global_model_providers_id, admin_global_model_routing_id, is_admin_global_models_root,
query_param_optional_bool, query_param_value, AdminBatchAssignToProvidersRequest,
AdminBatchDeleteIdsRequest, AdminGlobalModelCreateRequest, AdminGlobalModelUpdateRequest,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,
@@ -170,15 +172,21 @@ pub(crate) async fn maybe_build_local_admin_global_models_response(
.ok()
.map(|duration| duration.as_secs())
.unwrap_or(0);
(
http::StatusCode::CREATED,
Json(build_admin_global_model_response(
&created,
&provider_models,
now_unix_secs,
)),
attach_admin_audit_response(
(
http::StatusCode::CREATED,
Json(build_admin_global_model_response(
&created,
&provider_models,
now_unix_secs,
)),
)
.into_response(),
"admin_global_model_created",
"create_global_model",
"global_model",
&created.id,
)
.into_response()
}
None => (
http::StatusCode::SERVICE_UNAVAILABLE,
@@ -288,12 +296,18 @@ pub(crate) async fn maybe_build_local_admin_global_models_response(
.ok()
.map(|duration| duration.as_secs())
.unwrap_or(0);
Json(build_admin_global_model_response(
&updated,
&provider_models,
now_unix_secs,
))
.into_response()
attach_admin_audit_response(
Json(build_admin_global_model_response(
&updated,
&provider_models,
now_unix_secs,
))
.into_response(),
"admin_global_model_updated",
"update_global_model",
"global_model",
&updated.id,
)
}
None => (
http::StatusCode::NOT_FOUND,
@@ -343,7 +357,13 @@ pub(crate) async fn maybe_build_local_admin_global_models_response(
.into_response(),
));
}
return Ok(Some(http::StatusCode::NO_CONTENT.into_response()));
return Ok(Some(attach_admin_audit_response(
http::StatusCode::NO_CONTENT.into_response(),
"admin_global_model_deleted",
"delete_global_model",
"global_model",
&existing.id,
)));
}
if decision.route_family.as_deref() == Some("global_models_manage")
@@ -393,13 +413,17 @@ pub(crate) async fn maybe_build_local_admin_global_models_response(
failed.push(json!({"id": existing.id, "error": "delete failed"}));
}
}
return Ok(Some(
return Ok(Some(attach_admin_audit_response(
Json(json!({
"success_count": success_count,
"failed": failed,
}))
.into_response(),
));
"admin_global_models_batch_deleted",
"batch_delete_global_models",
"global_models_batch",
"batch",
)));
}
if decision.route_family.as_deref() == Some("global_models_manage")
@@ -458,7 +482,13 @@ pub(crate) async fn maybe_build_local_admin_global_models_response(
));
}
};
return Ok(Some(Json(payload).into_response()));
return Ok(Some(attach_admin_audit_response(
Json(payload).into_response(),
"admin_global_model_assigned_to_providers",
"assign_global_model_to_providers",
"global_model",
&global_model_id,
)));
}
if decision.route_family.as_deref() == Some("global_models_manage")

View File

@@ -1,4 +1,5 @@
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
response::Response,

View File

@@ -1,8 +1,8 @@
use super::ldap_shared::*;
use crate::gateway::handlers::{
use crate::handlers::{
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks,
};
use crate::gateway::{AppState, GatewayError};
use crate::{AppState, GatewayError};
use serde::Deserialize;
#[derive(Debug, Deserialize)]

View File

@@ -3,7 +3,9 @@ use super::ldap_builders::{
AdminLdapConfigTestRequest, AdminLdapConfigUpdateRequest,
};
use super::ldap_shared::*;
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,
@@ -29,12 +31,16 @@ pub(super) async fn maybe_build_local_admin_ldap_response(
if request_context.request_method == http::Method::GET
&& is_admin_ldap_config_root(&request_context.request_path) =>
{
return Ok(Some(
return Ok(Some(attach_admin_audit_response(
Json(build_admin_ldap_config_payload(
state.get_ldap_module_config().await?.as_ref(),
))
.into_response(),
));
"admin_ldap_config_viewed",
"view_ldap_config",
"ldap_config",
"ldap",
)));
}
Some("set_config")
if request_context.request_method == http::Method::PUT
@@ -91,7 +97,13 @@ pub(super) async fn maybe_build_local_admin_ldap_response(
"message": "缺少必要字段: server_url, bind_dn, base_dn, bind_password",
}),
};
return Ok(Some(Json(response).into_response()));
return Ok(Some(attach_admin_audit_response(
Json(response).into_response(),
"admin_ldap_connection_tested",
"test_ldap_connection",
"ldap_config",
"ldap",
)));
}
_ => {}
}

View File

@@ -1,5 +1,6 @@
use crate::gateway::handlers::{json_string_list, unix_secs_to_rfc3339};
use crate::gateway::GatewayPublicRequestContext;
use crate::audit::attach_admin_audit_event;
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{json_string_list, unix_secs_to_rfc3339};
use aether_data::repository::provider_catalog::StoredProviderCatalogKey;
use axum::{
body::Body,
@@ -42,6 +43,17 @@ pub(crate) fn build_admin_proxy_auth_required_response(
.into_response()
}
pub(crate) fn attach_admin_audit_response(
mut response: Response<Body>,
event_name: &'static str,
action: &'static str,
target_type: &'static str,
target_id: &str,
) -> Response<Body> {
attach_admin_audit_event(&mut response, event_name, action, target_type, target_id);
response
}
pub(crate) fn provider_catalog_key_supports_format(
key: &StoredProviderCatalogKey,
api_format: &str,

View File

@@ -10,11 +10,11 @@ mod billing;
mod catalog_write_helpers;
mod core;
mod endpoints;
mod endpoints_health_helpers;
pub(crate) mod endpoints_health_helpers;
mod gemini_files;
mod global_models;
mod ldap;
mod misc_helpers;
pub(crate) mod misc_helpers;
mod models_helpers;
mod monitoring;
mod oauth_helpers;
@@ -26,17 +26,17 @@ mod provider_oauth_dispatch;
#[path = "provider_oauth/quota.rs"]
mod provider_oauth_quota;
#[path = "provider_oauth/refresh.rs"]
mod provider_oauth_refresh;
pub(crate) mod provider_oauth_refresh;
#[path = "provider_oauth/state.rs"]
mod provider_oauth_state;
mod provider_ops;
pub(crate) mod provider_ops;
mod provider_query;
mod provider_strategy;
mod providers;
mod providers_helpers;
mod proxy_nodes;
mod security;
mod stats;
pub(crate) mod stats;
mod usage;
mod users;
mod video_tasks;
@@ -53,21 +53,28 @@ pub(crate) use self::core::maybe_build_local_admin_core_response;
use self::core::*;
pub(crate) use self::endpoints::maybe_build_local_admin_endpoints_response;
use self::endpoints::*;
pub(crate) use self::endpoints_health_helpers::build_admin_endpoint_health_status_payload;
use self::endpoints_health_helpers::*;
use self::endpoints_health_helpers::{
build_admin_create_provider_endpoint_record, build_admin_endpoint_health_status_payload,
build_admin_endpoint_payload, build_admin_health_summary_payload,
build_admin_key_health_payload, build_admin_key_rpm_payload,
build_admin_provider_endpoints_payload, build_admin_update_provider_endpoint_record,
recover_admin_key_health, recover_all_admin_key_health,
};
pub(crate) use self::gemini_files::maybe_build_local_admin_gemini_files_response;
use self::gemini_files::*;
pub(crate) use self::global_models::maybe_build_local_admin_global_models_response;
use self::global_models::*;
pub(crate) use self::ldap::maybe_build_local_admin_ldap_response;
use self::ldap::*;
use self::misc_helpers::*;
pub(crate) use self::misc_helpers::{
build_admin_proxy_auth_required_response, build_unhandled_admin_proxy_response,
provider_catalog_key_supports_format,
use self::misc_helpers::{
admin_default_body_rules_api_format, admin_endpoint_id, admin_health_key_id,
admin_provider_id_for_endpoints, admin_recover_key_id, admin_rpm_key_id,
attach_admin_audit_response, build_admin_provider_endpoint_response,
endpoint_key_counts_by_format, endpoint_timestamp_or_now, json_truthy,
key_api_formats_without_entry,
};
use self::models_helpers::*;
pub(crate) use self::monitoring::maybe_build_local_admin_monitoring_root_response as maybe_build_local_admin_monitoring_response;
pub(crate) use self::monitoring::maybe_build_local_admin_monitoring_response;
use self::oauth_helpers::*;
pub(crate) use self::payments::maybe_build_local_admin_payments_response;
use self::payments::*;
@@ -77,10 +84,13 @@ pub(crate) use self::provider_models::maybe_build_local_admin_provider_models_re
use self::provider_models::*;
pub(crate) use self::provider_oauth_dispatch::maybe_build_local_admin_provider_oauth_response;
use self::provider_oauth_dispatch::*;
use self::provider_oauth_quota::*;
pub(crate) use self::provider_oauth_refresh::build_internal_control_error_response;
use self::provider_oauth_refresh::*;
use self::provider_oauth_state::*;
use self::provider_oauth_quota::{
normalize_string_id_list, refresh_antigravity_provider_quota_locally,
refresh_codex_provider_quota_locally, refresh_kiro_provider_quota_locally,
};
use self::provider_oauth_refresh::{
build_internal_control_error_response, normalize_provider_oauth_refresh_error_message,
};
pub(crate) use self::provider_ops::admin_provider_ops_local_action_response;
pub(crate) use self::provider_ops::maybe_build_local_admin_provider_ops_response;
pub(crate) use self::provider_query::maybe_build_local_admin_provider_query_response;
@@ -94,11 +104,10 @@ pub(crate) use self::proxy_nodes::maybe_build_local_admin_proxy_nodes_response;
use self::proxy_nodes::*;
pub(crate) use self::security::maybe_build_local_admin_security_response;
use self::security::*;
use self::stats::*;
pub(crate) use self::stats::{
admin_stats_bad_request_response, list_usage_for_optional_range,
maybe_build_local_admin_stats_response, parse_bounded_u32, round_to, AdminStatsTimeRange,
AdminStatsUsageFilter,
pub(crate) use self::stats::maybe_build_local_admin_stats_response;
use self::stats::{
aggregate_usage_stats, list_usage_for_optional_range, parse_bounded_u32, round_to,
AdminStatsTimeRange, AdminStatsUsageFilter,
};
pub(crate) use self::usage::maybe_build_local_admin_usage_response;
use self::usage::*;

View File

@@ -1,8 +1,8 @@
use crate::gateway::handlers::{
use crate::handlers::{
ADMIN_EXTERNAL_MODELS_CACHE_KEY, ADMIN_EXTERNAL_MODELS_CACHE_TTL_SECS,
OFFICIAL_EXTERNAL_MODEL_PROVIDERS,
};
use crate::gateway::{AppState, GatewayError};
use crate::{AppState, GatewayError};
use serde_json::json;
fn mark_admin_external_models_official(mut payload: serde_json::Value) -> serde_json::Value {

View File

@@ -3,11 +3,12 @@ use super::{
admin_provider_model_effective_output_price, model_tiered_pricing_first_tier_value,
timestamp_or_now,
};
use crate::gateway::handlers::json_string_list;
use crate::gateway::AppState;
use crate::handlers::json_string_list;
use crate::AppState;
use aether_data::repository::global_models::{
AdminGlobalModelListQuery, StoredAdminGlobalModel, StoredAdminProviderModel,
};
use futures_util::stream::{self, StreamExt};
use serde_json::json;
use std::collections::{BTreeMap, BTreeSet};
use std::time::{SystemTime, UNIX_EPOCH};
@@ -79,6 +80,29 @@ fn build_admin_global_model_price_range(
})
}
async fn admin_global_model_provider_models_by_global_model_id(
state: &AppState,
global_model_ids: &[String],
) -> BTreeMap<String, Vec<StoredAdminProviderModel>> {
let state = state.clone();
stream::iter(global_model_ids.iter().cloned().map(|global_model_id| {
let state = state.clone();
async move {
let provider_models = state
.list_admin_provider_models_by_global_model_id(&global_model_id)
.await
.ok()
.unwrap_or_default();
(global_model_id, provider_models)
}
}))
.buffer_unordered(32)
.collect::<Vec<_>>()
.await
.into_iter()
.collect()
}
pub(crate) fn build_admin_global_model_response(
global_model: &StoredAdminGlobalModel,
provider_models: &[StoredAdminProviderModel],
@@ -132,12 +156,16 @@ pub(crate) async fn build_admin_global_models_payload(
.cmp(&right.name)
.then_with(|| left.id.cmp(&right.id))
});
let global_model_ids = models
.iter()
.map(|model| model.id.clone())
.collect::<Vec<_>>();
let mut provider_models_by_global_model =
admin_global_model_provider_models_by_global_model_id(state, &global_model_ids).await;
let mut payload_models = Vec::with_capacity(models.len());
for model in models {
let provider_models = state
.list_admin_provider_models_by_global_model_id(&model.id)
.await
.ok()
let provider_models = provider_models_by_global_model
.remove(&model.id)
.unwrap_or_default();
payload_models.push(build_admin_global_model_response(
&model,

View File

@@ -1,8 +1,8 @@
use super::resolve_admin_global_model_by_id_or_err;
use crate::gateway::handlers::admin::provider_catalog_key_supports_format;
use crate::gateway::handlers::{json_string_list, masked_catalog_api_key};
use crate::gateway::scheduler::{is_provider_key_circuit_open, provider_key_health_score};
use crate::gateway::AppState;
use crate::handlers::admin::misc_helpers::provider_catalog_key_supports_format;
use crate::handlers::{json_string_list, masked_catalog_api_key};
use crate::scheduler::{is_provider_key_circuit_open, provider_key_health_score};
use crate::AppState;
use aether_data::repository::global_models::{
AdminProviderModelListQuery, UpsertAdminProviderModelRecord,
};

View File

@@ -5,11 +5,11 @@ use super::{
normalize_optional_price, normalize_required_trimmed_string,
resolve_admin_global_model_by_id_or_err,
};
use crate::gateway::handlers::{
use crate::handlers::{
AdminGlobalModelCreateRequest, AdminGlobalModelUpdateRequest, AdminImportProviderModelsRequest,
AdminProviderModelCreateRequest, AdminProviderModelUpdateRequest,
};
use crate::gateway::AppState;
use crate::AppState;
use aether_data::repository::global_models::{
AdminProviderModelListQuery, CreateAdminGlobalModelRecord, StoredAdminGlobalModel,
StoredAdminProviderModel, UpdateAdminGlobalModelRecord, UpsertAdminProviderModelRecord,

View File

@@ -1,5 +1,5 @@
use crate::gateway::handlers::unix_secs_to_rfc3339;
use crate::gateway::{AppState, GatewayError};
use crate::handlers::unix_secs_to_rfc3339;
use crate::{AppState, GatewayError};
use aether_data::repository::global_models::{
AdminProviderModelListQuery, StoredAdminProviderModel,
};

View File

@@ -1,6 +1,7 @@
use super::INTERNAL_GATEWAY_PATH_PREFIXES;
use crate::gateway::handlers::{query_param_value, unix_secs_to_rfc3339};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{query_param_value, unix_secs_to_rfc3339};
use crate::{AppState, GatewayError};
use aether_crypto::decrypt_python_fernet_ciphertext;
#[cfg(test)]
use aether_crypto::DEVELOPMENT_ENCRYPTION_KEY;
@@ -91,7 +92,7 @@ use self::common::{
admin_monitoring_bad_request_response, admin_monitoring_data_unavailable_response,
admin_monitoring_not_found_response, admin_monitoring_usage_is_error,
admin_monitoring_user_behavior_user_id_from_path, AdminMonitoringCacheAffinityRecord,
AdminMonitoringCacheSnapshot,
AdminMonitoringCacheSnapshot, AdminMonitoringResilienceSnapshot,
};
use self::resilience::{
build_admin_monitoring_reset_error_stats_response,
@@ -105,7 +106,7 @@ use self::route_filters::{
parse_admin_monitoring_username_filter,
};
use self::routes::{
match_admin_monitoring_route, maybe_build_local_admin_monitoring_response, AdminMonitoringRoute,
match_admin_monitoring_route, AdminMonitoringRoute,
};
use self::trace::{
build_admin_monitoring_trace_provider_stats_response,
@@ -120,7 +121,7 @@ const ADMIN_MONITORING_CACHE_AFFINITY_DEFAULT_TTL_SECS: u64 = 300;
const ADMIN_MONITORING_CACHE_RESERVATION_RATIO: f64 = 0.1;
const ADMIN_MONITORING_DYNAMIC_RESERVATION_PROBE_PHASE_REQUESTS: u64 = 100;
pub(crate) async fn maybe_build_local_admin_monitoring_root_response(
pub(crate) async fn maybe_build_local_admin_monitoring_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
) -> Result<Option<Response<Body>>, GatewayError> {

View File

@@ -6,14 +6,15 @@ use super::{
parse_admin_monitoring_hours, parse_admin_monitoring_limit, parse_admin_monitoring_offset,
parse_admin_monitoring_username_filter,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::query::monitoring as monitoring_query;
use crate::{AppState, GatewayError};
use axum::{
body::Body,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use sqlx::Row;
fn build_admin_monitoring_audit_logs_payload(
items: Vec<serde_json::Value>,
@@ -121,86 +122,18 @@ pub(super) async fn build_admin_monitoring_audit_logs_response(
.map(admin_monitoring_escape_like_pattern)
.map(|value| format!("%{value}%"));
let total = sqlx::query_scalar::<_, i64>(
r#"
SELECT COUNT(*)
FROM audit_logs AS a
LEFT JOIN users AS u ON a.user_id = u.id
WHERE a.created_at >= $1
AND ($2::text IS NULL OR u.username ILIKE $2 ESCAPE '\')
AND ($3::text IS NULL OR a.event_type = $3)
"#,
)
.bind(cutoff_time)
.bind(username_pattern.as_deref())
.bind(event_type.as_deref())
.fetch_one(&pool)
.await
.map_err(|err| GatewayError::Internal(format!("admin audit logs count failed: {err}")))?;
let rows = sqlx::query(
r#"
SELECT
a.id,
a.event_type,
a.user_id,
u.email AS user_email,
u.username AS user_username,
a.description,
a.ip_address,
a.status_code,
a.error_message,
a.event_metadata AS metadata,
a.created_at
FROM audit_logs AS a
LEFT JOIN users AS u ON a.user_id = u.id
WHERE a.created_at >= $1
AND ($2::text IS NULL OR u.username ILIKE $2 ESCAPE '\')
AND ($3::text IS NULL OR a.event_type = $3)
ORDER BY a.created_at DESC
LIMIT $4 OFFSET $5
"#,
)
.bind(cutoff_time)
.bind(username_pattern.as_deref())
.bind(event_type.as_deref())
.bind(i64::try_from(limit).unwrap_or(i64::MAX))
.bind(i64::try_from(offset).unwrap_or(i64::MAX))
.fetch_all(&pool)
.await
.map_err(|err| GatewayError::Internal(format!("admin audit logs read failed: {err}")))?;
let items = rows
.into_iter()
.map(|row| {
let created_at = row
.try_get::<chrono::DateTime<chrono::Utc>, _>("created_at")
.ok()
.map(|value| value.to_rfc3339());
json!({
"id": row.try_get::<String, _>("id").ok(),
"event_type": row.try_get::<String, _>("event_type").ok(),
"user_id": row.try_get::<Option<String>, _>("user_id").ok().flatten(),
"user_email": row.try_get::<Option<String>, _>("user_email").ok().flatten(),
"user_username": row.try_get::<Option<String>, _>("user_username").ok().flatten(),
"description": row.try_get::<Option<String>, _>("description").ok().flatten(),
"ip_address": row.try_get::<Option<String>, _>("ip_address").ok().flatten(),
"status_code": row.try_get::<Option<i32>, _>("status_code").ok().flatten(),
"error_message": row.try_get::<Option<String>, _>("error_message").ok().flatten(),
"metadata": row.try_get::<Option<serde_json::Value>, _>("metadata").ok().flatten(),
"created_at": created_at,
})
})
.collect::<Vec<_>>();
Ok(build_admin_monitoring_audit_logs_payload(
items,
usize::try_from(total.max(0)).unwrap_or(usize::MAX),
let (items, total) = monitoring_query::list_admin_audit_logs(
&pool,
cutoff_time,
username_pattern.as_deref(),
event_type.as_deref(),
limit,
offset,
username,
event_type,
days,
)
.await?;
Ok(build_admin_monitoring_audit_logs_payload(
items, total, limit, offset, username, event_type, days,
))
}
@@ -222,54 +155,7 @@ pub(super) async fn build_admin_monitoring_suspicious_activities_response(
};
let cutoff_time = chrono::Utc::now() - chrono::Duration::hours(hours);
let rows = sqlx::query(
r#"
SELECT
id,
event_type,
user_id,
description,
ip_address,
event_metadata AS metadata,
created_at
FROM audit_logs
WHERE created_at >= $1
AND event_type = ANY($2)
ORDER BY created_at DESC
LIMIT 100
"#,
)
.bind(cutoff_time)
.bind(vec![
"suspicious_activity",
"unauthorized_access",
"login_failed",
"request_rate_limited",
])
.fetch_all(&pool)
.await
.map_err(|err| {
GatewayError::Internal(format!("admin suspicious activities read failed: {err}"))
})?;
let activities = rows
.into_iter()
.map(|row| {
let created_at = row
.try_get::<chrono::DateTime<chrono::Utc>, _>("created_at")
.ok()
.map(|value| value.to_rfc3339());
json!({
"id": row.try_get::<String, _>("id").ok(),
"event_type": row.try_get::<String, _>("event_type").ok(),
"user_id": row.try_get::<Option<String>, _>("user_id").ok().flatten(),
"description": row.try_get::<Option<String>, _>("description").ok().flatten(),
"ip_address": row.try_get::<Option<String>, _>("ip_address").ok().flatten(),
"metadata": row.try_get::<Option<serde_json::Value>, _>("metadata").ok().flatten(),
"created_at": created_at,
})
})
.collect::<Vec<_>>();
let activities = monitoring_query::list_admin_suspicious_activities(&pool, cutoff_time).await?;
Ok(build_admin_monitoring_suspicious_activities_payload(
activities, hours,
@@ -303,33 +189,9 @@ pub(super) async fn build_admin_monitoring_user_behavior_response(
let cutoff_time = chrono::Utc::now() - chrono::Duration::days(days);
let event_rows = sqlx::query(
r#"
SELECT event_type, COUNT(*)::bigint AS count
FROM audit_logs
WHERE user_id = $1
AND created_at >= $2
GROUP BY event_type
"#,
)
.bind(&user_id)
.bind(cutoff_time)
.fetch_all(&pool)
.await
.map_err(|err| GatewayError::Internal(format!("admin user behavior read failed: {err}")))?;
let event_counts = event_rows
.into_iter()
.filter_map(|row| {
let event_type = row.try_get::<String, _>("event_type").ok()?;
let count = row
.try_get::<i64, _>("count")
.ok()
.and_then(|value| u64::try_from(value.max(0)).ok())
.unwrap_or(0);
Some((event_type, count))
})
.collect::<std::collections::BTreeMap<_, _>>();
let event_counts =
monitoring_query::read_admin_user_behavior_event_counts(&pool, &user_id, cutoff_time)
.await?;
let failed_requests = event_counts
.get("request_failed")

View File

@@ -29,7 +29,8 @@ use super::{
ADMIN_MONITORING_DYNAMIC_RESERVATION_STABLE_MIN_RESERVATION,
ADMIN_MONITORING_REDIS_CACHE_CATEGORIES,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use axum::{
body::Body,
http,

View File

@@ -1,5 +1,5 @@
use super::AdminMonitoringCacheAffinityRecord;
use crate::gateway::{AppState, GatewayError};
use crate::{AppState, GatewayError};
fn parse_admin_monitoring_cache_affinity_key(raw_key: &str) -> Option<(String, String, String)> {
let parts = raw_key.split(':').collect::<Vec<_>>();

View File

@@ -1,5 +1,5 @@
use super::AdminMonitoringCacheAffinityRecord;
use crate::gateway::{AppState, GatewayError};
use crate::{AppState, GatewayError};
pub(super) async fn admin_monitoring_list_export_api_key_records_by_ids(
state: &AppState,

View File

@@ -1,4 +1,4 @@
use crate::gateway::AppState;
use crate::AppState;
use aether_crypto::decrypt_python_fernet_ciphertext;
#[cfg(test)]
use aether_crypto::DEVELOPMENT_ENCRYPTION_KEY;

View File

@@ -1,7 +1,7 @@
use super::{
ADMIN_MONITORING_CACHE_AFFINITY_REDIS_REQUIRED_DETAIL, ADMIN_MONITORING_REDIS_REQUIRED_DETAIL,
};
use crate::gateway::handlers::query_param_value;
use crate::handlers::query_param_value;
use axum::{
body::Body,
http,

View File

@@ -1,7 +1,7 @@
use super::super::round_to;
use super::cache_affinity::admin_monitoring_cache_affinity_record;
use super::{AdminMonitoringCacheAffinityRecord, AdminMonitoringCacheSnapshot};
use crate::gateway::{AppState, GatewayError};
use crate::handlers::round_to;
use crate::{AppState, GatewayError};
async fn count_admin_monitoring_cache_affinity_entries(state: &AppState) -> usize {
let Some(runner) = state.redis_kv_runner() else {

View File

@@ -35,6 +35,16 @@ pub(super) struct AdminMonitoringCacheAffinityRecord {
pub(super) request_count: u64,
}
pub(super) struct AdminMonitoringResilienceSnapshot {
pub(super) timestamp: chrono::DateTime<chrono::Utc>,
pub(super) health_score: i64,
pub(super) status: &'static str,
pub(super) error_statistics: serde_json::Value,
pub(super) recent_errors: Vec<serde_json::Value>,
pub(super) recommendations: Vec<String>,
pub(super) previous_stats: serde_json::Value,
}
pub(super) fn admin_monitoring_data_unavailable_response() -> Response<Body> {
(
http::StatusCode::SERVICE_UNAVAILABLE,

View File

@@ -1,8 +1,12 @@
use super::{admin_monitoring_bad_request_response, admin_monitoring_usage_is_error};
use crate::gateway::handlers::{
use super::{
admin_monitoring_bad_request_response, admin_monitoring_usage_is_error,
AdminMonitoringResilienceSnapshot,
};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{
provider_key_health_summary, query_param_value, unix_secs_to_rfc3339,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::Body,
response::{IntoResponse, Response},
@@ -11,16 +15,6 @@ use axum::{
use serde_json::json;
use std::collections::BTreeMap;
struct AdminMonitoringResilienceSnapshot {
timestamp: chrono::DateTime<chrono::Utc>,
health_score: i64,
status: &'static str,
error_statistics: serde_json::Value,
recent_errors: Vec<serde_json::Value>,
recommendations: Vec<String>,
previous_stats: serde_json::Value,
}
fn build_admin_monitoring_resilience_recommendations(
total_errors: usize,
health_score: i64,

View File

@@ -1,4 +1,4 @@
use crate::gateway::handlers::query_param_value;
use crate::handlers::query_param_value;
pub(super) fn admin_monitoring_escape_like_pattern(value: &str) -> String {
value

View File

@@ -20,7 +20,9 @@ use super::{
build_admin_monitoring_trace_provider_stats_response,
build_admin_monitoring_trace_request_response, build_admin_monitoring_user_behavior_response,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::{AppState, GatewayError};
use axum::{body::Body, http, response::Response};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -63,9 +65,13 @@ pub(super) async fn maybe_build_local_admin_monitoring_response(
};
match route {
AdminMonitoringRoute::AuditLogs => Ok(Some(
AdminMonitoringRoute::AuditLogs => Ok(Some(attach_admin_audit_response(
build_admin_monitoring_audit_logs_response(state, request_context).await?,
)),
"admin_monitoring_audit_logs_viewed",
"view_admin_audit_logs",
"audit_log",
&admin_monitoring_audit_target_id(request_context),
))),
AdminMonitoringRoute::ResilienceStatus => Ok(Some(
build_admin_monitoring_resilience_status_response(state).await?,
)),
@@ -79,12 +85,20 @@ pub(super) async fn maybe_build_local_admin_monitoring_response(
AdminMonitoringRoute::CacheStats => Ok(Some(
build_admin_monitoring_cache_stats_response(state).await?,
)),
AdminMonitoringRoute::CacheAffinities => Ok(Some(
AdminMonitoringRoute::CacheAffinities => Ok(Some(attach_admin_audit_response(
build_admin_monitoring_cache_affinities_response(state, request_context).await?,
)),
AdminMonitoringRoute::CacheAffinity => Ok(Some(
"admin_monitoring_cache_affinities_viewed",
"view_cache_affinities",
"cache_affinity",
&admin_monitoring_audit_target_id(request_context),
))),
AdminMonitoringRoute::CacheAffinity => Ok(Some(attach_admin_audit_response(
build_admin_monitoring_cache_affinity_response(state, request_context).await?,
)),
"admin_monitoring_cache_affinity_viewed",
"view_cache_affinity",
"cache_affinity",
&admin_monitoring_audit_target_id(request_context),
))),
AdminMonitoringRoute::CacheUsersDelete => Ok(Some(
build_admin_monitoring_cache_users_delete_response(state, request_context).await?,
)),
@@ -126,18 +140,43 @@ pub(super) async fn maybe_build_local_admin_monitoring_response(
AdminMonitoringRoute::SystemStatus => Ok(Some(
build_admin_monitoring_system_status_response(state).await?,
)),
AdminMonitoringRoute::SuspiciousActivities => Ok(Some(
AdminMonitoringRoute::SuspiciousActivities => Ok(Some(attach_admin_audit_response(
build_admin_monitoring_suspicious_activities_response(state, request_context).await?,
)),
AdminMonitoringRoute::UserBehavior => Ok(Some(
"admin_monitoring_suspicious_activities_viewed",
"view_suspicious_activities",
"suspicious_activity",
&admin_monitoring_audit_target_id(request_context),
))),
AdminMonitoringRoute::UserBehavior => Ok(Some(attach_admin_audit_response(
build_admin_monitoring_user_behavior_response(state, request_context).await?,
)),
AdminMonitoringRoute::TraceRequest => Ok(Some(
"admin_monitoring_user_behavior_viewed",
"view_user_behavior",
"user",
&admin_monitoring_audit_target_id(request_context),
))),
AdminMonitoringRoute::TraceRequest => Ok(Some(attach_admin_audit_response(
build_admin_monitoring_trace_request_response(state, request_context).await?,
)),
AdminMonitoringRoute::TraceProviderStats => Ok(Some(
"admin_monitoring_request_trace_viewed",
"view_request_trace",
"request_trace",
&admin_monitoring_audit_target_id(request_context),
))),
AdminMonitoringRoute::TraceProviderStats => Ok(Some(attach_admin_audit_response(
build_admin_monitoring_trace_provider_stats_response(state, request_context).await?,
)),
"admin_monitoring_provider_trace_stats_viewed",
"view_provider_trace_stats",
"provider",
&admin_monitoring_audit_target_id(request_context),
))),
}
}
fn admin_monitoring_audit_target_id(request_context: &GatewayPublicRequestContext) -> String {
match request_context.request_query_string.as_deref() {
Some(query) if !query.trim().is_empty() => {
format!("{}?{query}", request_context.request_path)
}
_ => request_context.request_path.clone(),
}
}

View File

@@ -1,4 +1,4 @@
use crate::gateway::GatewayPublicRequestContext;
use crate::control::GatewayPublicRequestContext;
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
use axum::http::{self, Uri};
use serde_json::json;

View File

@@ -58,7 +58,7 @@ async fn admin_monitoring_cache_affinity_returns_not_found_without_runtime_or_te
let state = AppState::new()
.expect("state should build")
.with_data_state_for_tests(
crate::gateway::gateway_data::GatewayDataState::with_user_reader_for_tests(
crate::data::GatewayDataState::with_user_reader_for_tests(
user_repository,
)
.with_auth_api_key_reader(auth_repository),
@@ -106,7 +106,7 @@ async fn admin_monitoring_cache_affinities_and_affinity_return_local_payload_fro
let state = AppState::new()
.expect("state should build")
.with_data_state_for_tests(
crate::gateway::gateway_data::GatewayDataState::with_provider_catalog_reader_for_tests(
crate::data::GatewayDataState::with_provider_catalog_reader_for_tests(
provider_catalog,
)
.with_user_reader(user_repository)
@@ -212,7 +212,7 @@ async fn admin_monitoring_cache_users_delete_returns_local_payload_from_test_sto
let state = AppState::new()
.expect("state should build")
.with_data_state_for_tests(
crate::gateway::gateway_data::GatewayDataState::with_user_reader_for_tests(
crate::data::GatewayDataState::with_user_reader_for_tests(
user_repository,
)
.with_auth_api_key_reader(auth_repository),
@@ -600,7 +600,7 @@ async fn admin_monitoring_cache_affinity_delete_returns_local_payload_from_test_
let state = AppState::new()
.expect("state should build")
.with_data_state_for_tests(
crate::gateway::gateway_data::GatewayDataState::with_user_reader_for_tests(
crate::data::GatewayDataState::with_user_reader_for_tests(
user_repository,
)
.with_auth_api_key_reader(auth_repository),
@@ -720,7 +720,7 @@ async fn admin_monitoring_cache_metrics_returns_local_payload() {
let state = AppState::new()
.expect("state should build")
.with_data_state_for_tests(
crate::gateway::gateway_data::GatewayDataState::with_usage_reader_for_tests(
crate::data::GatewayDataState::with_usage_reader_for_tests(
usage_repository,
)
.with_system_config_values_for_tests([
@@ -852,7 +852,7 @@ async fn admin_monitoring_reset_error_stats_returns_local_payload_and_clears_fut
let state = AppState::new()
.expect("state should build")
.with_data_state_for_tests(
crate::gateway::gateway_data::GatewayDataState::with_provider_catalog_and_usage_reader_for_tests(
crate::data::GatewayDataState::with_provider_catalog_and_usage_reader_for_tests(
provider_catalog,
usage_repository,
),
@@ -969,7 +969,7 @@ async fn admin_monitoring_circuit_history_returns_local_payload() {
let state = AppState::new()
.expect("state should build")
.with_data_state_for_tests(
crate::gateway::gateway_data::GatewayDataState::with_provider_catalog_reader_for_tests(
crate::data::GatewayDataState::with_provider_catalog_reader_for_tests(
provider_catalog,
),
);

View File

@@ -3,7 +3,7 @@ use super::super::{
match_admin_monitoring_route, maybe_build_local_admin_monitoring_response,
AdminMonitoringRoute, ADMIN_MONITORING_REDIS_REQUIRED_DETAIL,
};
use crate::gateway::AppState;
use crate::AppState;
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
use aether_data::repository::usage::InMemoryUsageReadRepository;
use axum::body::to_bytes;
@@ -217,7 +217,7 @@ async fn admin_monitoring_resilience_status_returns_local_payload() {
let state = AppState::new()
.expect("state should build")
.with_data_state_for_tests(
crate::gateway::gateway_data::GatewayDataState::with_provider_catalog_and_usage_reader_for_tests(
crate::data::GatewayDataState::with_provider_catalog_and_usage_reader_for_tests(
provider_catalog,
usage_repository,
),
@@ -288,7 +288,7 @@ async fn admin_monitoring_cache_stats_returns_local_payload() {
let state = AppState::new()
.expect("state should build")
.with_data_state_for_tests(
crate::gateway::gateway_data::GatewayDataState::with_usage_reader_for_tests(
crate::data::GatewayDataState::with_usage_reader_for_tests(
usage_repository,
)
.with_system_config_values_for_tests([

View File

@@ -2,7 +2,7 @@ use super::super::maybe_build_local_admin_monitoring_response;
use super::super::test_support::{
request_context, sample_candidate, sample_endpoint, sample_key, sample_provider,
};
use crate::gateway::AppState;
use crate::AppState;
use axum::body::to_bytes;
use serde_json::json;
use std::sync::Arc;

View File

@@ -2,8 +2,9 @@ use super::{
admin_monitoring_bad_request_response, admin_monitoring_not_found_response,
parse_admin_monitoring_limit,
};
use crate::gateway::handlers::{query_param_value, unix_secs_to_rfc3339};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{query_param_value, unix_secs_to_rfc3339};
use crate::{AppState, GatewayError};
use axum::{
body::Body,
response::{IntoResponse, Response},

View File

@@ -1,7 +1,7 @@
use crate::gateway::handlers::{
use crate::handlers::{
encrypt_catalog_secret_with_fallbacks, AdminOAuthProviderUpsertRequest,
};
use crate::gateway::AppState;
use crate::AppState;
use aether_data::repository::oauth_providers::{
EncryptedSecretUpdate, UpsertOAuthProviderConfigRecord,
};

View File

@@ -1,4 +1,5 @@
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use axum::{body::Body, response::Response};
#[path = "payment/postgres.rs"]

View File

@@ -3,15 +3,15 @@ use super::{
build_admin_payments_bad_request_response, parse_admin_payments_limit,
parse_admin_payments_offset,
};
use crate::gateway::handlers::query_param_value;
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::query_param_value;
use crate::{AppState, GatewayError};
use axum::{
body::Body,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use sqlx::Row;
pub(super) async fn maybe_build_local_admin_payment_callbacks_response(
state: &AppState,
@@ -41,85 +41,16 @@ async fn build_admin_payment_callbacks_response(
};
let payment_method = query_param_value(query, "payment_method");
if let Some((items, total)) = state
let (items, total) = state
.list_admin_payment_callbacks(payment_method.as_deref(), limit, offset)
.await?
{
return Ok(Json(json!({
"items": items
.iter()
.map(build_admin_payment_callback_payload_from_record)
.collect::<Vec<_>>(),
"total": total,
"limit": limit,
"offset": offset,
}))
.into_response());
}
let mut total = 0_u64;
let mut items = Vec::new();
if let Some(pool) = state.postgres_pool() {
let count_row = sqlx::query(
r#"
SELECT COUNT(*) AS total
FROM payment_callbacks
WHERE ($1::TEXT IS NULL OR payment_method = $1)
"#,
)
.bind(payment_method.as_deref())
.fetch_one(&pool)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
total = count_row
.try_get::<i64, _>("total")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.max(0) as u64;
let rows = sqlx::query(
r#"
SELECT
id,
payment_order_id,
payment_method,
callback_key,
order_no,
gateway_order_id,
payload_hash,
signature_valid,
status,
payload,
error_message,
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
CAST(EXTRACT(EPOCH FROM processed_at) AS BIGINT) AS processed_at_unix_secs
FROM payment_callbacks
WHERE ($1::TEXT IS NULL OR payment_method = $1)
ORDER BY created_at DESC
OFFSET $2
LIMIT $3
"#,
)
.bind(payment_method.as_deref())
.bind(i64::try_from(offset).map_err(|err| GatewayError::Internal(err.to_string()))?)
.bind(i64::try_from(limit).map_err(|err| GatewayError::Internal(err.to_string()))?)
.fetch_all(&pool)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
items = rows
.iter()
.map(build_admin_payment_callback_payload)
.collect::<Result<Vec<_>, GatewayError>>()?;
} else {
return Ok(Json(json!({
"items": [],
"total": 0,
"limit": limit,
"offset": offset,
}))
.into_response());
}
.unwrap_or_default();
Ok(Json(json!({
"items": items,
"items": items
.iter()
.map(build_admin_payment_callback_payload_from_record)
.collect::<Vec<_>>(),
"total": total,
"limit": limit,
"offset": offset,

View File

@@ -7,8 +7,10 @@ use super::{
normalize_admin_payment_positive_number, parse_admin_payments_limit,
parse_admin_payments_offset, AdminPaymentOrderCreditRequest,
};
use crate::gateway::handlers::query_param_value;
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::handlers::query_param_value;
use crate::{AppState, GatewayError};
use axum::{
body::Body,
response::{IntoResponse, Response},
@@ -90,17 +92,17 @@ async fn build_admin_payment_get_order_response(
return Ok(build_admin_payment_order_not_found_response());
};
match state.read_admin_payment_order(&order_id).await? {
crate::gateway::AdminWalletMutationOutcome::Applied(order) => Ok(Json(json!({
crate::AdminWalletMutationOutcome::Applied(order) => Ok(Json(json!({
"order": build_admin_payment_order_payload(&order),
}))
.into_response()),
crate::gateway::AdminWalletMutationOutcome::NotFound => {
crate::AdminWalletMutationOutcome::NotFound => {
Ok(build_admin_payment_order_not_found_response())
}
crate::gateway::AdminWalletMutationOutcome::Invalid(detail) => {
crate::AdminWalletMutationOutcome::Invalid(detail) => {
Ok(build_admin_payments_bad_request_response(detail))
}
crate::gateway::AdminWalletMutationOutcome::Unavailable => {
crate::AdminWalletMutationOutcome::Unavailable => {
Ok(build_admin_payments_backend_unavailable_response(
"Payment order read backend unavailable",
))
@@ -118,18 +120,26 @@ async fn build_admin_payment_expire_order_response(
return Ok(build_admin_payment_order_not_found_response());
};
match state.admin_expire_payment_order(&order_id).await? {
crate::gateway::AdminWalletMutationOutcome::Applied((order, expired)) => Ok(Json(json!({
"order": build_admin_payment_order_payload(&order),
"expired": expired,
}))
.into_response()),
crate::gateway::AdminWalletMutationOutcome::NotFound => {
crate::AdminWalletMutationOutcome::Applied((order, expired)) => {
Ok(attach_admin_audit_response(
Json(json!({
"order": build_admin_payment_order_payload(&order),
"expired": expired,
}))
.into_response(),
"admin_payment_order_expired",
"expire_payment_order",
"payment_order",
&order_id,
))
}
crate::AdminWalletMutationOutcome::NotFound => {
Ok(build_admin_payment_order_not_found_response())
}
crate::gateway::AdminWalletMutationOutcome::Invalid(detail) => {
crate::AdminWalletMutationOutcome::Invalid(detail) => {
Ok(build_admin_payments_bad_request_response(detail))
}
crate::gateway::AdminWalletMutationOutcome::Unavailable => {
crate::AdminWalletMutationOutcome::Unavailable => {
Ok(build_admin_payments_backend_unavailable_response(
"Payment order write backend unavailable",
))
@@ -204,18 +214,26 @@ async fn build_admin_payment_credit_order_response(
)
.await?
{
crate::gateway::AdminWalletMutationOutcome::Applied((order, credited)) => Ok(Json(json!({
"order": build_admin_payment_order_payload(&order),
"credited": credited,
}))
.into_response()),
crate::gateway::AdminWalletMutationOutcome::NotFound => {
crate::AdminWalletMutationOutcome::Applied((order, credited)) => {
Ok(attach_admin_audit_response(
Json(json!({
"order": build_admin_payment_order_payload(&order),
"credited": credited,
}))
.into_response(),
"admin_payment_order_credited",
"credit_payment_order",
"payment_order",
&order_id,
))
}
crate::AdminWalletMutationOutcome::NotFound => {
Ok(build_admin_payment_order_not_found_response())
}
crate::gateway::AdminWalletMutationOutcome::Invalid(detail) => {
crate::AdminWalletMutationOutcome::Invalid(detail) => {
Ok(build_admin_payments_bad_request_response(detail))
}
crate::gateway::AdminWalletMutationOutcome::Unavailable => {
crate::AdminWalletMutationOutcome::Unavailable => {
Ok(build_admin_payments_backend_unavailable_response(
"Payment order write backend unavailable",
))
@@ -233,17 +251,25 @@ async fn build_admin_payment_fail_order_response(
return Ok(build_admin_payment_order_not_found_response());
};
match state.admin_fail_payment_order(&order_id).await? {
crate::gateway::AdminWalletMutationOutcome::Applied(order) => Ok(Json(json!({
"order": build_admin_payment_order_payload(&order),
}))
.into_response()),
crate::gateway::AdminWalletMutationOutcome::NotFound => {
crate::AdminWalletMutationOutcome::Applied(order) => {
Ok(attach_admin_audit_response(
Json(json!({
"order": build_admin_payment_order_payload(&order),
}))
.into_response(),
"admin_payment_order_failed",
"fail_payment_order",
"payment_order",
&order_id,
))
}
crate::AdminWalletMutationOutcome::NotFound => {
Ok(build_admin_payment_order_not_found_response())
}
crate::gateway::AdminWalletMutationOutcome::Invalid(detail) => {
crate::AdminWalletMutationOutcome::Invalid(detail) => {
Ok(build_admin_payments_bad_request_response(detail))
}
crate::gateway::AdminWalletMutationOutcome::Unavailable => {
crate::AdminWalletMutationOutcome::Unavailable => {
Ok(build_admin_payments_backend_unavailable_response(
"Payment order write backend unavailable",
))

View File

@@ -3,7 +3,8 @@ use super::{
payments_callbacks::maybe_build_local_admin_payment_callbacks_response,
payments_orders::maybe_build_local_admin_payment_orders_response,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use axum::{body::Body, http, response::Response};
pub(super) async fn maybe_build_local_admin_payments_response(

View File

@@ -1,5 +1,6 @@
use crate::gateway::handlers::{query_param_value, unix_secs_to_rfc3339};
use crate::gateway::{GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{query_param_value, unix_secs_to_rfc3339};
use crate::GatewayError;
use axum::{
body::Body,
http,
@@ -196,7 +197,7 @@ pub(super) fn admin_payment_effective_status(
}
pub(super) fn build_admin_payment_order_payload(
record: &crate::gateway::AdminWalletPaymentOrderRecord,
record: &crate::AdminWalletPaymentOrderRecord,
) -> serde_json::Value {
json!({
"id": record.id,
@@ -249,7 +250,7 @@ pub(super) fn build_admin_payment_callback_payload(
}
pub(super) fn build_admin_payment_callback_payload_from_record(
record: &crate::gateway::state::AdminPaymentCallbackRecord,
record: &crate::state::AdminPaymentCallbackRecord,
) -> serde_json::Value {
json!({
"id": record.id,

View File

@@ -1,5 +1,6 @@
use crate::gateway::handlers::query_param_value;
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::query_param_value;
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -6,10 +6,10 @@ use super::{
ADMIN_POOL_PROVIDER_CATALOG_WRITER_UNAVAILABLE_DETAIL,
};
use super::{pool_payloads, pool_selection};
use crate::gateway::handlers::encrypt_catalog_secret_with_fallbacks;
use crate::gateway::{
AppState, GatewayError, GatewayPublicRequestContext, LocalProviderDeleteTaskState,
};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::handlers::encrypt_catalog_secret_with_fallbacks;
use crate::{AppState, GatewayError, LocalProviderDeleteTaskState};
use aether_data::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
};
@@ -90,6 +90,31 @@ fn build_admin_pool_batch_delete_task_payload(
})
}
fn attach_admin_pool_batch_delete_task_terminal_audit(
provider_id: &str,
task_id: &str,
task_status: &str,
response: Response<Body>,
) -> Response<Body> {
match task_status {
"completed" => attach_admin_audit_response(
response,
"admin_pool_batch_delete_task_completed_viewed",
"view_pool_batch_delete_task_terminal_state",
"provider_key_batch_delete_task",
&format!("{provider_id}:{task_id}"),
),
"failed" => attach_admin_audit_response(
response,
"admin_pool_batch_delete_task_failed_viewed",
"view_pool_batch_delete_task_terminal_state",
"provider_key_batch_delete_task",
&format!("{provider_id}:{task_id}"),
),
_ => response,
}
}
fn admin_pool_resolved_api_formats(
endpoints: &[StoredProviderCatalogEndpoint],
existing_keys: &[StoredProviderCatalogKey],
@@ -547,7 +572,12 @@ async fn build_admin_pool_batch_delete_task_status_response(
));
}
Ok(Json(build_admin_pool_batch_delete_task_payload(&task)).into_response())
Ok(attach_admin_pool_batch_delete_task_terminal_audit(
&provider_id,
&task_id,
task.status.as_str(),
Json(build_admin_pool_batch_delete_task_payload(&task)).into_response(),
))
}
pub(super) async fn maybe_build_local_admin_pool_batch_response(

View File

@@ -1,4 +1,4 @@
use crate::gateway::handlers::{
use crate::handlers::{
unix_secs_to_rfc3339, AdminProviderPoolConfig, AdminProviderPoolRuntimeState,
};
use aether_data::repository::provider_catalog::StoredProviderCatalogKey;

View File

@@ -1,6 +1,6 @@
use super::super::{
admin_provider_pool_config, read_admin_provider_pool_cooldown_count,
read_admin_provider_pool_cooldown_key_ids, read_admin_provider_pool_runtime_state,
admin_provider_pool_config, read_admin_provider_pool_cooldown_key_ids,
read_admin_provider_pool_runtime_state,
};
use super::{
admin_pool_provider_id_from_path, build_admin_pool_error_response, parse_admin_pool_page,
@@ -8,8 +8,9 @@ use super::{
AdminPoolResolveSelectionRequest, ADMIN_POOL_PROVIDER_CATALOG_READER_UNAVAILABLE_DETAIL,
};
use super::{pool_payloads, pool_selection};
use crate::gateway::handlers::AdminProviderPoolRuntimeState;
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::AdminProviderPoolRuntimeState;
use crate::{AppState, GatewayError};
use aether_data::repository::provider_catalog::ProviderCatalogKeyListQuery;
use axum::{
body::{Body, Bytes},
@@ -46,17 +47,22 @@ async fn build_admin_pool_overview_payload(
.map(|item| (item.provider_id.clone(), item))
.collect::<BTreeMap<_, _>>();
let redis_runner = state.redis_kv_runner();
let cooldown_counts_by_provider = match redis_runner.as_ref() {
Some(runner) if !provider_ids.is_empty() => {
super::super::read_admin_provider_pool_cooldown_counts(runner, &provider_ids).await
}
_ => BTreeMap::new(),
};
let mut items = Vec::with_capacity(pool_enabled_providers.len());
for (provider, _pool_config) in pool_enabled_providers {
let stats = key_stats_by_provider.get(&provider.id);
let total_keys = stats.map(|item| item.total_keys as usize).unwrap_or(0);
let active_keys = stats.map(|item| item.active_keys as usize).unwrap_or(0);
let cooldown_count = if let Some(runner) = redis_runner.as_ref() {
read_admin_provider_pool_cooldown_count(runner, &provider.id).await
} else {
0
};
let cooldown_count = cooldown_counts_by_provider
.get(&provider.id)
.copied()
.unwrap_or(0);
items.push(json!({
"provider_id": provider.id,

View File

@@ -1,5 +1,5 @@
use crate::gateway::handlers::decrypt_catalog_secret_with_fallbacks;
use crate::gateway::AppState;
use crate::handlers::decrypt_catalog_secret_with_fallbacks;
use crate::AppState;
use aether_data::repository::provider_catalog::StoredProviderCatalogKey;
fn admin_pool_reason_indicates_ban(reason: &str) -> bool {

View File

@@ -1,4 +1,6 @@
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayControlDecision;
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use axum::body::{Body, Bytes};
use axum::http::Response;

View File

@@ -1,8 +1,10 @@
use super::super::build_admin_batch_assign_global_models_payload;
use crate::gateway::handlers::{
use crate::control::GatewayControlDecision;
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{
admin_provider_assign_global_models_path, AdminBatchAssignGlobalModelsRequest,
};
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -1,6 +1,8 @@
use super::super::build_admin_provider_available_source_models_payload;
use crate::gateway::handlers::admin_provider_available_source_models_path;
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayControlDecision;
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin_provider_available_source_models_path;
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -2,8 +2,10 @@ use super::super::{
admin_provider_model_name_exists, build_admin_provider_model_create_record,
build_admin_provider_model_response,
};
use crate::gateway::handlers::{admin_provider_models_batch_path, AdminProviderModelCreateRequest};
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayControlDecision;
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{admin_provider_models_batch_path, AdminProviderModelCreateRequest};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -1,8 +1,10 @@
use super::super::{build_admin_provider_model_create_record, build_admin_provider_model_response};
use crate::gateway::handlers::{
use crate::control::GatewayControlDecision;
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{
admin_provider_id_for_models_list, AdminProviderModelCreateRequest,
};
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -1,5 +1,7 @@
use crate::gateway::handlers::admin_provider_model_route_parts;
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayControlDecision;
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin_provider_model_route_parts;
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -1,6 +1,8 @@
use super::super::build_admin_provider_model_payload;
use crate::gateway::handlers::admin_provider_model_route_parts;
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayControlDecision;
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin_provider_model_route_parts;
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -1,8 +1,10 @@
use super::super::build_admin_import_provider_models_payload;
use crate::gateway::handlers::{
use crate::control::GatewayControlDecision;
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{
admin_provider_import_models_path, AdminImportProviderModelsRequest,
};
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -1,8 +1,10 @@
use super::super::build_admin_provider_models_payload;
use crate::gateway::handlers::{
use crate::control::GatewayControlDecision;
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{
admin_provider_id_for_models_list, query_param_optional_bool, query_param_value,
};
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -1,6 +1,8 @@
use super::super::{build_admin_provider_model_response, build_admin_provider_model_update_record};
use crate::gateway::handlers::{admin_provider_model_route_parts, AdminProviderModelUpdateRequest};
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayControlDecision;
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{admin_provider_model_route_parts, AdminProviderModelUpdateRequest};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -2,7 +2,16 @@ use super::provider_oauth_state::{
build_admin_provider_oauth_backend_unavailable_response,
build_admin_provider_oauth_supported_types_payload,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::handlers::{
admin_provider_oauth_batch_import_provider_id,
admin_provider_oauth_batch_import_task_provider_id, admin_provider_oauth_complete_key_id,
admin_provider_oauth_complete_provider_id, admin_provider_oauth_device_authorize_provider_id,
admin_provider_oauth_import_provider_id, admin_provider_oauth_refresh_key_id,
admin_provider_oauth_start_key_id, admin_provider_oauth_start_provider_id,
};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,
@@ -50,16 +59,28 @@ pub(crate) async fn maybe_build_local_admin_provider_oauth_response(
}
if route_kind == Some("start_key_oauth") && *method == http::Method::POST {
return Ok(Some(
dispatch_start::handle_admin_provider_oauth_start_key(state, request_context).await?,
));
let response =
dispatch_start::handle_admin_provider_oauth_start_key(state, request_context).await?;
return Ok(Some(attach_admin_provider_oauth_audit_response(
response,
"admin_provider_oauth_authorization_started",
"start_provider_oauth_for_key",
"provider_key",
admin_provider_oauth_start_key_id(&request_context.request_path),
)));
}
if route_kind == Some("start_provider_oauth") && *method == http::Method::POST {
return Ok(Some(
let response =
dispatch_start::handle_admin_provider_oauth_start_provider(state, request_context)
.await?,
));
.await?;
return Ok(Some(attach_admin_provider_oauth_audit_response(
response,
"admin_provider_oauth_authorization_started",
"start_provider_oauth_for_provider",
"provider",
admin_provider_oauth_start_provider_id(&request_context.request_path),
)));
}
if route_kind == Some("get_batch_import_task_status") && *method == http::Method::GET {
@@ -73,76 +94,112 @@ pub(crate) async fn maybe_build_local_admin_provider_oauth_response(
}
if route_kind == Some("complete_key_oauth") && *method == http::Method::POST {
return Ok(Some(
dispatch_complete::handle_admin_provider_oauth_complete_key(
state,
request_context,
request_body,
)
.await?,
));
let response = dispatch_complete::handle_admin_provider_oauth_complete_key(
state,
request_context,
request_body,
)
.await?;
return Ok(Some(attach_admin_provider_oauth_audit_response(
response,
"admin_provider_oauth_completed",
"complete_provider_oauth_for_key",
"provider_key",
admin_provider_oauth_complete_key_id(&request_context.request_path),
)));
}
if route_kind == Some("refresh_key_oauth") && *method == http::Method::POST {
return Ok(Some(
let response =
dispatch_refresh::handle_admin_provider_oauth_refresh_key(state, request_context)
.await?,
));
.await?;
return Ok(Some(attach_admin_provider_oauth_audit_response(
response,
"admin_provider_oauth_refreshed",
"refresh_provider_oauth_for_key",
"provider_key",
admin_provider_oauth_refresh_key_id(&request_context.request_path),
)));
}
if route_kind == Some("complete_provider_oauth") && *method == http::Method::POST {
return Ok(Some(
dispatch_complete::handle_admin_provider_oauth_complete_provider(
state,
request_context,
request_body,
)
.await?,
));
let response = dispatch_complete::handle_admin_provider_oauth_complete_provider(
state,
request_context,
request_body,
)
.await?;
return Ok(Some(attach_admin_provider_oauth_audit_response(
response,
"admin_provider_oauth_completed",
"complete_provider_oauth_for_provider",
"provider",
admin_provider_oauth_complete_provider_id(&request_context.request_path),
)));
}
if route_kind == Some("import_refresh_token") && *method == http::Method::POST {
return Ok(Some(
dispatch_import::handle_admin_provider_oauth_import_refresh_token(
state,
request_context,
request_body,
)
.await?,
));
let response = dispatch_import::handle_admin_provider_oauth_import_refresh_token(
state,
request_context,
request_body,
)
.await?;
return Ok(Some(attach_admin_provider_oauth_audit_response(
response,
"admin_provider_oauth_refresh_token_imported",
"import_provider_oauth_refresh_token",
"provider",
admin_provider_oauth_import_provider_id(&request_context.request_path),
)));
}
if route_kind == Some("batch_import_oauth") && *method == http::Method::POST {
return Ok(Some(
dispatch_batch::handle_admin_provider_oauth_batch_import(
state,
request_context,
request_body,
)
.await?,
));
let response = dispatch_batch::handle_admin_provider_oauth_batch_import(
state,
request_context,
request_body,
)
.await?;
return Ok(Some(attach_admin_provider_oauth_audit_response(
response,
"admin_provider_oauth_batch_import_completed",
"batch_import_provider_oauth",
"provider",
admin_provider_oauth_batch_import_provider_id(&request_context.request_path),
)));
}
if route_kind == Some("start_batch_import_oauth_task") && *method == http::Method::POST {
return Ok(Some(
dispatch_batch::handle_admin_provider_oauth_start_batch_import_task(
state,
request_context,
request_body,
)
.await?,
));
let response = dispatch_batch::handle_admin_provider_oauth_start_batch_import_task(
state,
request_context,
request_body,
)
.await?;
return Ok(Some(attach_admin_provider_oauth_audit_response(
response,
"admin_provider_oauth_batch_import_started",
"start_provider_oauth_batch_import",
"provider",
admin_provider_oauth_batch_import_task_provider_id(&request_context.request_path),
)));
}
if route_kind == Some("device_authorize") && *method == http::Method::POST {
return Ok(Some(
dispatch_device::handle_admin_provider_oauth_device_authorize(
state,
request_context,
request_body,
)
.await?,
));
let response = dispatch_device::handle_admin_provider_oauth_device_authorize(
state,
request_context,
request_body,
)
.await?;
return Ok(Some(attach_admin_provider_oauth_audit_response(
response,
"admin_provider_oauth_device_authorization_started",
"start_provider_oauth_device_authorization",
"provider",
admin_provider_oauth_device_authorize_provider_id(&request_context.request_path),
)));
}
if route_kind == Some("device_poll") && *method == http::Method::POST {
@@ -167,3 +224,19 @@ pub(crate) async fn maybe_build_local_admin_provider_oauth_response(
Ok(None)
}
fn attach_admin_provider_oauth_audit_response(
response: Response<Body>,
event_name: &'static str,
action: &'static str,
target_type: &'static str,
target_id: Option<String>,
) -> Response<Body> {
if !response.status().is_success() {
return response;
}
let Some(target_id) = target_id else {
return response;
};
attach_admin_audit_response(response, event_name, action, target_type, &target_id)
}

View File

@@ -9,12 +9,13 @@ use super::super::provider_oauth_state::{
current_unix_secs, decode_jwt_claims, exchange_admin_provider_oauth_refresh_token,
is_fixed_provider_type_for_provider_oauth, save_provider_oauth_batch_task_payload,
};
use crate::gateway::handlers::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{
admin_provider_oauth_batch_import_provider_id,
admin_provider_oauth_batch_import_task_provider_id,
ADMIN_PROVIDER_OAUTH_DATA_UNAVAILABLE_DETAIL,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::{to_bytes, Body, Bytes},
http,
@@ -420,7 +421,7 @@ async fn execute_admin_provider_oauth_kiro_batch_import(
.await?;
let api_formats = provider_oauth_active_api_formats(&endpoints);
let key_proxy = provider_oauth_key_proxy_value(proxy_node_id);
let adapter = crate::gateway::provider_transport::KiroOAuthRefreshAdapter::default()
let adapter = crate::provider_transport::kiro::KiroOAuthRefreshAdapter::default()
.with_refresh_base_urls(
admin_provider_oauth_kiro_refresh_base_url_override(state, "kiro_social_refresh"),
admin_provider_oauth_kiro_refresh_base_url_override(state, "kiro_idc_refresh"),
@@ -431,7 +432,7 @@ async fn execute_admin_provider_oauth_kiro_batch_import(
for (index, entry) in entries.iter().enumerate() {
let Some(mut refreshed_auth_config) =
crate::gateway::provider_transport::KiroAuthConfig::from_json_value(entry)
crate::provider_transport::kiro::KiroAuthConfig::from_json_value(entry)
else {
failed += 1;
results.push(json!({

View File

@@ -11,11 +11,12 @@ use super::super::provider_oauth_state::{
exchange_admin_provider_oauth_code, is_fixed_provider_type_for_provider_oauth,
json_non_empty_string, json_u64_value, parse_provider_oauth_callback_params,
};
use crate::gateway::handlers::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{
admin_provider_oauth_complete_key_id, admin_provider_oauth_complete_provider_id,
encrypt_catalog_secret_with_fallbacks,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,

View File

@@ -11,12 +11,16 @@ use super::super::provider_oauth_state::{
json_u64_value, normalize_kiro_device_region, poll_admin_kiro_device_token,
read_provider_oauth_device_session, register_admin_kiro_device_oidc_client,
save_provider_oauth_device_session, start_admin_kiro_device_authorization,
StoredAdminProviderOAuthDeviceSession, KIRO_DEVICE_AUTH_SESSION_TTL_BUFFER_SECS,
};
use crate::gateway::handlers::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::handlers::{
admin_provider_oauth_device_authorize_provider_id, admin_provider_oauth_device_poll_provider_id,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use aether_data::repository::provider_oauth::{
StoredAdminProviderOAuthDeviceSession, KIRO_DEVICE_AUTH_SESSION_TTL_BUFFER_SECS,
};
use axum::{
body::{Body, Bytes},
http,
@@ -295,12 +299,16 @@ pub(super) async fn handle_admin_provider_oauth_device_poll(
session.status = "expired".to_string();
session.error_msg = Some("设备码已过期".to_string());
let _ = save_provider_oauth_device_session(state, session_id, &session, 30).await;
return Ok(Json(json!({
"status": "expired",
"error": "设备码已过期",
"replaced": false,
}))
.into_response());
return Ok(attach_admin_provider_oauth_device_poll_terminal_response(
session_id,
"expired",
Json(json!({
"status": "expired",
"error": "设备码已过期",
"replaced": false,
}))
.into_response(),
));
}
let Some(provider) = state
@@ -344,23 +352,31 @@ pub(super) async fn handle_admin_provider_oauth_device_poll(
session.status = "expired".to_string();
session.error_msg = Some("设备码已过期".to_string());
let _ = save_provider_oauth_device_session(state, session_id, &session, 30).await;
return Ok(Json(json!({
"status": "expired",
"error": "设备码已过期",
"replaced": false,
}))
.into_response());
return Ok(attach_admin_provider_oauth_device_poll_terminal_response(
session_id,
"expired",
Json(json!({
"status": "expired",
"error": "设备码已过期",
"replaced": false,
}))
.into_response(),
));
}
if error_code == "access_denied" {
session.status = "error".to_string();
session.error_msg = Some("用户拒绝授权".to_string());
let _ = save_provider_oauth_device_session(state, session_id, &session, 30).await;
return Ok(Json(json!({
"status": "error",
"error": "用户拒绝授权",
"replaced": false,
}))
.into_response());
return Ok(attach_admin_provider_oauth_device_poll_terminal_response(
session_id,
"error",
Json(json!({
"status": "error",
"error": "用户拒绝授权",
"replaced": false,
}))
.into_response(),
));
}
let error_message = json_non_empty_string(token_result.get("error_description"))
.or_else(|| (!error_code.is_empty()).then_some(error_code.clone()))
@@ -489,11 +505,46 @@ pub(super) async fn handle_admin_provider_oauth_device_poll(
session.error_msg = None;
let _ = save_provider_oauth_device_session(state, session_id, &session, 60).await;
Ok(Json(json!({
"status": "authorized",
"key_id": persisted_key.id,
"email": email,
"replaced": replaced,
}))
.into_response())
Ok(attach_admin_provider_oauth_device_poll_terminal_response(
session_id,
"authorized",
Json(json!({
"status": "authorized",
"key_id": persisted_key.id,
"email": email,
"replaced": replaced,
}))
.into_response(),
))
}
fn attach_admin_provider_oauth_device_poll_terminal_response(
session_id: &str,
status: &str,
response: Response<Body>,
) -> Response<Body> {
match status {
"authorized" => attach_admin_audit_response(
response,
"admin_provider_oauth_device_authorization_completed",
"poll_provider_oauth_device_authorization_terminal_state",
"provider_oauth_device_session",
session_id,
),
"expired" => attach_admin_audit_response(
response,
"admin_provider_oauth_device_authorization_expired",
"poll_provider_oauth_device_authorization_terminal_state",
"provider_oauth_device_session",
session_id,
),
"error" => attach_admin_audit_response(
response,
"admin_provider_oauth_device_authorization_failed",
"poll_provider_oauth_device_authorization_terminal_state",
"provider_oauth_device_session",
session_id,
),
_ => response,
}
}

View File

@@ -8,8 +8,9 @@ use super::super::provider_oauth_state::{
admin_provider_oauth_template, build_admin_provider_oauth_backend_unavailable_response,
exchange_admin_provider_oauth_refresh_token, is_fixed_provider_type_for_provider_oauth,
};
use crate::gateway::handlers::admin_provider_oauth_import_provider_id;
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin_provider_oauth_import_provider_id;
use crate::{AppState, GatewayError};
use axum::{
body::Body,
http,

View File

@@ -5,11 +5,12 @@ use super::super::provider_oauth_refresh::{
refresh_provider_oauth_account_state_after_update,
};
use super::super::provider_oauth_state::is_fixed_provider_type_for_provider_oauth;
use crate::gateway::handlers::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{
admin_provider_oauth_refresh_key_id, decrypt_catalog_secret_with_fallbacks,
OAUTH_ACCOUNT_BLOCK_PREFIX, OAUTH_REFRESH_FAILED_PREFIX,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::Body,
http,
@@ -125,7 +126,7 @@ pub(super) async fn handle_admin_provider_oauth_refresh_key(
"缺少 refresh_token需要重新授权",
));
}
Err(crate::gateway::provider_transport::LocalOAuthRefreshError::HttpStatus {
Err(crate::provider_transport::LocalOAuthRefreshError::HttpStatus {
status_code,
body_excerpt,
..
@@ -164,7 +165,7 @@ pub(super) async fn handle_admin_provider_oauth_refresh_key(
format!("Token 刷新失败:{error_reason}"),
));
}
Err(crate::gateway::provider_transport::LocalOAuthRefreshError::Transport {
Err(crate::provider_transport::LocalOAuthRefreshError::Transport {
source,
..
}) => {
@@ -173,7 +174,7 @@ pub(super) async fn handle_admin_provider_oauth_refresh_key(
format!("Token 刷新失败:{}", source),
));
}
Err(crate::gateway::provider_transport::LocalOAuthRefreshError::InvalidResponse {
Err(crate::provider_transport::LocalOAuthRefreshError::InvalidResponse {
message,
..
}) => {

View File

@@ -4,10 +4,11 @@ use super::super::provider_oauth_state::{
generate_provider_oauth_pkce_verifier, is_fixed_provider_type_for_provider_oauth,
provider_oauth_pkce_s256, save_provider_oauth_state,
};
use crate::gateway::handlers::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{
admin_provider_oauth_start_key_id, admin_provider_oauth_start_provider_id,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use axum::{
body::Body,
http,

View File

@@ -1,7 +1,9 @@
use super::super::provider_oauth_refresh::build_internal_control_error_response;
use super::super::provider_oauth_state::read_provider_oauth_batch_task_payload;
use crate::gateway::handlers::admin_provider_oauth_batch_import_task_path;
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
use crate::handlers::admin_provider_oauth_batch_import_task_path;
use crate::{AppState, GatewayError};
use axum::{
body::Body,
http,
@@ -37,5 +39,27 @@ pub(super) async fn handle_admin_provider_oauth_batch_import_task_status(
));
}
};
Ok(Json(payload).into_response())
let status = payload
.get("status")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned)
.unwrap_or_default();
let response = Json(payload).into_response();
Ok(match status.as_str() {
"completed" => attach_admin_audit_response(
response,
"admin_provider_oauth_batch_task_completed_viewed",
"view_provider_oauth_batch_task_terminal_state",
"provider_oauth_batch_task",
&format!("{provider_id}:{task_id}"),
),
"failed" => attach_admin_audit_response(
response,
"admin_provider_oauth_batch_task_failed_viewed",
"view_provider_oauth_batch_task_terminal_state",
"provider_oauth_batch_task",
&format!("{provider_id}:{task_id}"),
),
_ => response,
})
}

View File

@@ -3,8 +3,8 @@ use super::{
extract_execution_error_message, persist_provider_quota_refresh_state,
quota_refresh_success_invalid_state,
};
use crate::gateway::handlers::ANTIGRAVITY_FETCH_AVAILABLE_MODELS_PATH;
use crate::gateway::{AppState, GatewayError};
use crate::handlers::ANTIGRAVITY_FETCH_AVAILABLE_MODELS_PATH;
use crate::{AppState, GatewayError};
use aether_contracts::{ExecutionPlan, ExecutionResult, ExecutionTimeouts, RequestBody};
use aether_data::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
@@ -64,21 +64,22 @@ fn parse_antigravity_usage_response(
async fn execute_antigravity_quota_plan(
state: &AppState,
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
authorization: (String, String),
project_id: &str,
auth: &crate::gateway::provider_transport::AntigravityRequestAuthSupport,
auth: &crate::provider_transport::antigravity::AntigravityRequestAuthSupport,
) -> Result<Option<ExecutionResult>, GatewayError> {
let supported_auth = match auth {
crate::gateway::provider_transport::AntigravityRequestAuthSupport::Supported(auth) => auth,
crate::gateway::provider_transport::AntigravityRequestAuthSupport::Unsupported(_) => {
crate::provider_transport::antigravity::AntigravityRequestAuthSupport::Supported(auth) => auth,
crate::provider_transport::antigravity::AntigravityRequestAuthSupport::Unsupported(_) => {
return Ok(None);
}
};
let mut headers = crate::gateway::provider_transport::build_antigravity_static_identity_headers(
supported_auth,
);
let mut headers =
crate::provider_transport::antigravity::build_antigravity_static_identity_headers(
supported_auth,
);
headers.insert("authorization".to_string(), authorization.1);
headers.insert("content-type".to_string(), "application/json".to_string());
headers.insert("accept".to_string(), "application/json".to_string());
@@ -112,9 +113,9 @@ async fn execute_antigravity_quota_plan(
client_api_format: "gemini:chat".to_string(),
provider_api_format: "antigravity:fetch_available_models".to_string(),
model_name: Some("fetchAvailableModels".to_string()),
proxy: crate::gateway::provider_transport::resolve_transport_proxy_snapshot_with_tunnel_affinity(state, transport).await,
tls_profile: crate::gateway::provider_transport::resolve_transport_tls_profile(transport),
timeouts: crate::gateway::provider_transport::resolve_transport_execution_timeouts(
proxy: crate::provider_transport::resolve_transport_proxy_snapshot_with_tunnel_affinity(state, transport).await,
tls_profile: crate::provider_transport::resolve_transport_tls_profile(transport),
timeouts: crate::provider_transport::resolve_transport_execution_timeouts(
transport,
)
.or(Some(ExecutionTimeouts {
@@ -159,7 +160,7 @@ pub(crate) async fn refresh_antigravity_provider_quota_locally(
};
let authorization = match state.resolve_local_oauth_request_auth(&transport).await? {
Some(crate::gateway::provider_transport::LocalResolvedOAuthRequestAuth::Header {
Some(crate::provider_transport::LocalResolvedOAuthRequestAuth::Header {
name,
value,
}) => (name, value),
@@ -176,12 +177,14 @@ pub(crate) async fn refresh_antigravity_provider_quota_locally(
};
let antigravity_auth =
crate::gateway::provider_transport::resolve_local_antigravity_request_auth(&transport);
crate::provider_transport::antigravity::resolve_local_antigravity_request_auth(
&transport,
);
let project_id = match &antigravity_auth {
crate::gateway::provider_transport::AntigravityRequestAuthSupport::Supported(auth) => {
crate::provider_transport::antigravity::AntigravityRequestAuthSupport::Supported(auth) => {
auth.project_id.clone()
}
crate::gateway::provider_transport::AntigravityRequestAuthSupport::Unsupported(_) => {
crate::provider_transport::antigravity::AntigravityRequestAuthSupport::Unsupported(_) => {
failed_count += 1;
results.push(json!({
"key_id": key.id,

View File

@@ -4,11 +4,11 @@ use super::{
persist_provider_quota_refresh_state, provider_auto_remove_banned_keys,
quota_refresh_success_invalid_state, should_auto_remove_structured_reason,
};
use crate::gateway::handlers::{
use crate::handlers::{
CODEX_WHAM_USAGE_URL, OAUTH_ACCOUNT_BLOCK_PREFIX, OAUTH_EXPIRED_PREFIX,
OAUTH_REQUEST_FAILED_PREFIX,
};
use crate::gateway::{AppState, GatewayError};
use crate::{AppState, GatewayError};
use aether_contracts::{ExecutionPlan, ExecutionResult, ExecutionTimeouts, RequestBody};
use aether_data::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
@@ -331,7 +331,7 @@ fn codex_soft_request_failure_reason(status_code: u16, upstream_message: Option<
}
fn build_codex_refresh_headers(
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
resolved_oauth_auth: Option<(String, String)>,
) -> Result<BTreeMap<String, String>, String> {
let mut headers = BTreeMap::new();
@@ -379,7 +379,7 @@ fn build_codex_refresh_headers(
async fn execute_codex_quota_plan(
state: &AppState,
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
headers: BTreeMap<String, String>,
) -> Result<Option<ExecutionResult>, GatewayError> {
let plan = ExecutionPlan {
@@ -403,9 +403,9 @@ async fn execute_codex_quota_plan(
client_api_format: "openai:cli".to_string(),
provider_api_format: "openai:cli".to_string(),
model_name: Some("codex-wham-usage".to_string()),
proxy: crate::gateway::provider_transport::resolve_transport_proxy_snapshot_with_tunnel_affinity(state, transport).await,
tls_profile: crate::gateway::provider_transport::resolve_transport_tls_profile(transport),
timeouts: crate::gateway::provider_transport::resolve_transport_execution_timeouts(
proxy: crate::provider_transport::resolve_transport_proxy_snapshot_with_tunnel_affinity(state, transport).await,
tls_profile: crate::provider_transport::resolve_transport_tls_profile(transport),
timeouts: crate::provider_transport::resolve_transport_execution_timeouts(
transport,
)
.or(Some(ExecutionTimeouts {
@@ -453,7 +453,7 @@ pub(crate) async fn refresh_codex_provider_quota_locally(
let resolved_oauth_auth = if key.auth_type.trim().eq_ignore_ascii_case("oauth") {
match state.resolve_local_oauth_request_auth(&transport).await? {
Some(
crate::gateway::provider_transport::LocalResolvedOAuthRequestAuth::Header {
crate::provider_transport::LocalResolvedOAuthRequestAuth::Header {
name,
value,
},

View File

@@ -2,10 +2,10 @@ use super::{
coerce_json_f64, execute_provider_quota_plan, extract_execution_error_message,
persist_provider_quota_refresh_state, quota_refresh_success_invalid_state,
};
use crate::gateway::handlers::{
use crate::handlers::{
encrypt_catalog_secret_with_fallbacks, KIRO_USAGE_LIMITS_PATH, KIRO_USAGE_SDK_VERSION,
};
use crate::gateway::{AppState, GatewayError};
use crate::{AppState, GatewayError};
use aether_contracts::{ExecutionPlan, ExecutionResult, ExecutionTimeouts, RequestBody};
use aether_data::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
@@ -167,7 +167,7 @@ fn parse_kiro_usage_response(
}
fn build_kiro_usage_headers(
auth: &crate::gateway::provider_transport::KiroRequestAuth,
auth: &crate::provider_transport::kiro::KiroRequestAuth,
) -> BTreeMap<String, String> {
let kiro_version = auth.auth_config.effective_kiro_version();
let machine_id = auth.machine_id.trim();
@@ -200,7 +200,9 @@ fn build_kiro_usage_headers(
])
}
fn build_kiro_usage_url(auth: &crate::gateway::provider_transport::KiroRequestAuth) -> String {
fn build_kiro_usage_url(
auth: &crate::provider_transport::kiro::KiroRequestAuth,
) -> String {
let host = format!(
"q.{}.amazonaws.com",
auth.auth_config.effective_api_region()
@@ -220,8 +222,8 @@ fn build_kiro_usage_url(auth: &crate::gateway::provider_transport::KiroRequestAu
async fn execute_kiro_quota_plan(
state: &AppState,
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
auth: &crate::gateway::provider_transport::KiroRequestAuth,
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
auth: &crate::provider_transport::kiro::KiroRequestAuth,
) -> Result<Option<ExecutionResult>, GatewayError> {
let plan = ExecutionPlan {
request_id: format!("kiro-quota:{}", transport.key.id),
@@ -244,9 +246,9 @@ async fn execute_kiro_quota_plan(
client_api_format: "claude:cli".to_string(),
provider_api_format: "kiro:usage".to_string(),
model_name: Some("kiro-usage-limits".to_string()),
proxy: crate::gateway::provider_transport::resolve_transport_proxy_snapshot_with_tunnel_affinity(state, transport).await,
tls_profile: crate::gateway::provider_transport::resolve_transport_tls_profile(transport),
timeouts: crate::gateway::provider_transport::resolve_transport_execution_timeouts(
proxy: crate::provider_transport::resolve_transport_proxy_snapshot_with_tunnel_affinity(state, transport).await,
tls_profile: crate::provider_transport::resolve_transport_tls_profile(transport),
timeouts: crate::provider_transport::resolve_transport_execution_timeouts(
transport,
)
.or(Some(ExecutionTimeouts {
@@ -291,7 +293,7 @@ pub(crate) async fn refresh_kiro_provider_quota_locally(
};
let Some(auth) = (match state.resolve_local_oauth_request_auth(&transport).await? {
Some(crate::gateway::provider_transport::LocalResolvedOAuthRequestAuth::Kiro(auth)) => {
Some(crate::provider_transport::LocalResolvedOAuthRequestAuth::Kiro(auth)) => {
Some(auth)
}
_ => None,

View File

@@ -1,5 +1,5 @@
use crate::gateway::handlers::{OAUTH_ACCOUNT_BLOCK_PREFIX, OAUTH_REFRESH_FAILED_PREFIX};
use crate::gateway::{AppState, GatewayError};
use crate::handlers::{OAUTH_ACCOUNT_BLOCK_PREFIX, OAUTH_REFRESH_FAILED_PREFIX};
use crate::{AppState, GatewayError};
use aether_contracts::{ExecutionPlan, ExecutionResult};
use aether_data::repository::provider_catalog::StoredProviderCatalogKey;
use std::collections::BTreeSet;
@@ -176,11 +176,13 @@ pub(crate) async fn persist_provider_quota_refresh_state(
pub(super) async fn execute_provider_quota_plan(
state: &AppState,
transport: &crate::gateway::provider_transport::GatewayProviderTransportSnapshot,
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
plan: ExecutionPlan,
quota_kind: &str,
) -> Result<Option<ExecutionResult>, GatewayError> {
match crate::gateway::execute_execution_runtime_sync_plan(state, None, &plan).await {
match crate::execution_runtime::execute_execution_runtime_sync_plan(state, None, &plan)
.await
{
Ok(result) => Ok(Some(result)),
Err(err) => {
warn!(

View File

@@ -5,12 +5,12 @@ use super::provider_oauth_quota::{
use super::provider_oauth_state::{
enrich_admin_provider_oauth_auth_config, json_non_empty_string, json_u64_value,
};
use crate::gateway::handlers::{
use crate::handlers::{
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks,
parse_catalog_auth_config_json, OAUTH_ACCOUNT_BLOCK_PREFIX, OAUTH_EXPIRED_PREFIX,
OAUTH_REFRESH_FAILED_PREFIX, OAUTH_REQUEST_FAILED_PREFIX,
};
use crate::gateway::{AppState, GatewayError};
use crate::{AppState, GatewayError};
use aether_data::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
};

View File

@@ -1,15 +1,20 @@
use super::{
build_internal_control_error_response, normalize_provider_oauth_refresh_error_message,
};
use crate::gateway::handlers::ADMIN_PROVIDER_OAUTH_DATA_UNAVAILABLE_DETAIL;
use crate::gateway::provider_transport::{
use crate::handlers::ADMIN_PROVIDER_OAUTH_DATA_UNAVAILABLE_DETAIL;
use crate::provider_transport::provider_types::{
provider_type_admin_oauth_template, provider_type_is_fixed_for_admin_oauth,
ProviderOAuthTemplate, ADMIN_PROVIDER_OAUTH_TEMPLATE_TYPES,
};
use crate::gateway::{AppState, GatewayError};
use crate::{AppState, GatewayError};
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::{body::Body, http, response::Response};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use serde::Deserialize;
use serde_json::json;
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
@@ -50,32 +55,11 @@ pub(crate) fn build_admin_provider_oauth_backend_unavailable_response() -> Respo
)
}
const KIRO_DEVICE_AUTH_SESSION_PREFIX: &str = "device_auth_session:";
pub(crate) const KIRO_DEVICE_AUTH_SESSION_TTL_BUFFER_SECS: u64 = 60;
const PROVIDER_OAUTH_BATCH_TASK_TTL_SECS: u64 = 24 * 60 * 60;
const KIRO_DEVICE_DEFAULT_START_URL: &str = "https://view.awsapps.com/start";
const KIRO_DEVICE_DEFAULT_REGION: &str = "us-east-1";
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";
#[derive(Debug, Clone, serde::Serialize, Deserialize)]
pub(crate) struct StoredAdminProviderOAuthDeviceSession {
pub(crate) provider_id: String,
pub(crate) region: String,
pub(crate) client_id: String,
pub(crate) client_secret: String,
pub(crate) device_code: String,
pub(crate) interval: u64,
pub(crate) expires_at_unix_secs: u64,
pub(crate) status: String,
pub(crate) proxy_node_id: Option<String>,
pub(crate) created_at_unix_secs: u64,
pub(crate) key_id: Option<String>,
pub(crate) email: Option<String>,
pub(crate) replaced: bool,
pub(crate) error_msg: Option<String>,
}
pub(crate) fn default_kiro_device_start_url() -> String {
KIRO_DEVICE_DEFAULT_START_URL.to_string()
}
@@ -95,10 +79,6 @@ pub(crate) fn normalize_kiro_device_region(value: Option<&str>) -> Option<String
.then(|| value.to_string())
}
fn provider_oauth_device_session_key(session_id: &str) -> String {
format!("{KIRO_DEVICE_AUTH_SESSION_PREFIX}{session_id}")
}
pub(crate) fn current_unix_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -113,7 +93,7 @@ pub(crate) async fn save_provider_oauth_device_session(
session: &StoredAdminProviderOAuthDeviceSession,
ttl_seconds: u64,
) -> Result<(), Response<Body>> {
let key = provider_oauth_device_session_key(session_id);
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,
@@ -145,7 +125,7 @@ pub(crate) async fn read_provider_oauth_device_session(
state: &AppState,
session_id: &str,
) -> Result<Option<StoredAdminProviderOAuthDeviceSession>, GatewayError> {
let key = provider_oauth_device_session_key(session_id);
let key = provider_oauth_device_session_storage_key(session_id);
let raw = if let Some(runner) = state.redis_kv_runner() {
let mut connection = runner
.client()
@@ -330,14 +310,6 @@ pub(crate) fn build_kiro_device_key_name(
format!("kiro_{fallback} (idc)")
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct StoredAdminProviderOAuthState {
pub(crate) key_id: String,
pub(crate) provider_id: String,
pub(crate) provider_type: String,
pub(crate) pkce_verifier: Option<String>,
}
pub(crate) fn generate_provider_oauth_nonce() -> String {
format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple())
}
@@ -375,11 +347,11 @@ pub(crate) async fn save_provider_oauth_state(
.map(|duration| duration.as_secs())
.unwrap_or(0),
});
let key = format!("provider_oauth_state:{nonce}");
let key = provider_oauth_state_storage_key(&nonce);
let value = payload.to_string();
if let Some(runner) = state.redis_kv_runner() {
runner
.setex(&key, &value, Some(600))
.setex(&key, &value, Some(PROVIDER_OAUTH_STATE_TTL_SECS))
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
return Ok(nonce);
@@ -422,7 +394,7 @@ pub(crate) async fn consume_provider_oauth_state(
state: &AppState,
nonce: &str,
) -> Result<Option<StoredAdminProviderOAuthState>, GatewayError> {
let key = format!("provider_oauth_state:{nonce}");
let key = provider_oauth_state_storage_key(nonce);
let raw = if let Some(runner) = state.redis_kv_runner() {
let mut connection = runner
.client()
@@ -731,83 +703,12 @@ pub(crate) fn build_provider_oauth_start_response(
})
}
fn build_provider_oauth_batch_task_status_payload(
provider_id: &str,
state: &serde_json::Map<String, serde_json::Value>,
) -> serde_json::Value {
let now_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0);
let raw_status = state
.get("status")
.and_then(serde_json::Value::as_str)
.unwrap_or("failed");
let normalized_status = match raw_status {
"submitted" | "processing" | "completed" | "failed" => raw_status,
_ => "failed",
};
let error_samples = state
.get("error_samples")
.and_then(serde_json::Value::as_array)
.map(|items| {
items
.iter()
.filter(|item| item.is_object())
.cloned()
.collect::<Vec<_>>()
})
.unwrap_or_default();
json!({
"task_id": state
.get("task_id")
.and_then(serde_json::Value::as_str)
.unwrap_or_default(),
"provider_id": provider_id,
"provider_type": state
.get("provider_type")
.and_then(serde_json::Value::as_str)
.unwrap_or_default(),
"status": normalized_status,
"total": state.get("total").and_then(serde_json::Value::as_i64).unwrap_or(0),
"processed": state.get("processed").and_then(serde_json::Value::as_i64).unwrap_or(0),
"success": state.get("success").and_then(serde_json::Value::as_i64).unwrap_or(0),
"failed": state.get("failed").and_then(serde_json::Value::as_i64).unwrap_or(0),
"progress_percent": state
.get("progress_percent")
.and_then(serde_json::Value::as_i64)
.unwrap_or(0)
.clamp(0, 100),
"message": state.get("message").cloned().unwrap_or(serde_json::Value::Null),
"error": state.get("error").cloned().unwrap_or(serde_json::Value::Null),
"error_samples": error_samples,
"created_at": state
.get("created_at")
.and_then(serde_json::Value::as_u64)
.unwrap_or(now_unix_secs),
"started_at": state.get("started_at").cloned().unwrap_or(serde_json::Value::Null),
"finished_at": state
.get("finished_at")
.cloned()
.unwrap_or(serde_json::Value::Null),
"updated_at": state
.get("updated_at")
.and_then(serde_json::Value::as_u64)
.unwrap_or(now_unix_secs),
})
}
fn provider_oauth_batch_task_key(task_id: &str) -> String {
format!("provider_oauth_batch_task:{task_id}")
}
pub(crate) async fn save_provider_oauth_batch_task_payload(
state: &AppState,
task_id: &str,
task_state: &serde_json::Value,
) -> Result<(), GatewayError> {
let key = provider_oauth_batch_task_key(task_id);
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()))?;
@@ -843,7 +744,7 @@ pub(crate) async fn read_provider_oauth_batch_task_payload(
provider_id: &str,
task_id: &str,
) -> Result<Option<serde_json::Value>, GatewayError> {
let key = provider_oauth_batch_task_key(task_id);
let key = provider_oauth_batch_task_storage_key(task_id);
let raw = if let Some(runner) = state.redis_kv_runner() {
let Ok(mut connection) = runner.client().get_multiplexed_async_connection().await else {
return Err(GatewayError::Internal(

View File

@@ -1,4 +1,5 @@
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use axum::body::{Body, Bytes};
use axum::http::Response;

View File

@@ -1,7 +1,8 @@
use crate::gateway::handlers::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{
admin_provider_ops_architecture_id_from_path, is_admin_provider_ops_architectures_root,
};
use crate::gateway::{GatewayError, GatewayPublicRequestContext};
use crate::GatewayError;
use axum::{
body::Body,
http,

View File

@@ -1,11 +1,12 @@
use crate::gateway::handlers::{
use crate::control::GatewayPublicRequestContext;
use crate::handlers::{
admin_provider_id_for_provider_ops_balance, admin_provider_id_for_provider_ops_checkin,
admin_provider_id_for_provider_ops_config, admin_provider_id_for_provider_ops_connect,
admin_provider_id_for_provider_ops_disconnect, admin_provider_id_for_provider_ops_status,
admin_provider_id_for_provider_ops_verify, admin_provider_ops_action_route_parts,
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks,
};
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
use crate::{AppState, GatewayError};
use aether_data::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
};

View File

@@ -5,7 +5,7 @@ use super::{
resolve_admin_provider_ops_base_url, AdminProviderOpsCheckinOutcome,
ADMIN_PROVIDER_OPS_ACTION_RUST_ONLY_MESSAGE,
};
use crate::gateway::AppState;
use crate::AppState;
use aether_data::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
};

View File

@@ -1,8 +1,8 @@
use super::{AdminProviderOpsSaveConfigRequest, ADMIN_PROVIDER_OPS_SENSITIVE_FIELDS};
use crate::gateway::handlers::{
use crate::handlers::{
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks,
};
use crate::gateway::AppState;
use crate::AppState;
use aether_data::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
};

Some files were not shown because too many files have changed in this diff Show More