mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 大规模模块拆分与代码精简,新增 ai-pipeline/data-contracts 独立 crate
- 新增 aether-ai-pipeline 和 aether-data-contracts crate,将 pipeline 逻辑与数据契约从 gateway 中解耦 - 重构 admin handlers:拆分单体模块为 auth/billing/endpoint/features/model/observability/provider/system 等独立子模块 - 合并 chat/cli 重复代码路径:精简 conversion、finalize、planner 中的 sync/chat/cli 分支 - 重构 scheduler/executor/data 层,引入 facade 模式降低模块间耦合 - 移除冗余的 intent 模块,将 plan_fallback/policy/stream_path/sync_path 迁移至 executor - 前端适配:调整 admin API 调用和 provider 模型测试对话框
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::{
|
||||
use super::super::users::{
|
||||
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::handlers::public::serialize_admin_system_users_export_wallet;
|
||||
use crate::handlers::{
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::shared::{
|
||||
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks, query_param_bool,
|
||||
query_param_optional_bool, query_param_value,
|
||||
};
|
||||
use crate::handlers::admin::system::shared::serialize_admin_system_users_export_wallet;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -21,23 +21,17 @@ use serde_json::json;
|
||||
|
||||
const ADMIN_API_KEYS_DATA_UNAVAILABLE_DETAIL: &str = "Admin standalone API key data unavailable";
|
||||
|
||||
#[path = "api_keys/mutation_routes.rs"]
|
||||
mod admin_api_keys_mutation_routes;
|
||||
#[path = "api_keys/read_routes.rs"]
|
||||
mod admin_api_keys_read_routes;
|
||||
#[path = "api_keys/routes.rs"]
|
||||
mod admin_api_keys_routes;
|
||||
#[path = "api_keys/shared.rs"]
|
||||
mod admin_api_keys_shared;
|
||||
mod mutation_routes;
|
||||
mod read_routes;
|
||||
mod routes;
|
||||
mod shared;
|
||||
|
||||
use self::admin_api_keys_mutation_routes::{
|
||||
use self::mutation_routes::{
|
||||
build_admin_create_api_key_response, build_admin_delete_api_key_response,
|
||||
build_admin_toggle_api_key_response, build_admin_update_api_key_response,
|
||||
};
|
||||
use self::admin_api_keys_read_routes::{
|
||||
build_admin_api_key_detail_response, build_admin_list_api_keys_response,
|
||||
};
|
||||
use self::admin_api_keys_shared::{
|
||||
use self::read_routes::{build_admin_api_key_detail_response, build_admin_list_api_keys_response};
|
||||
use self::shared::{
|
||||
admin_api_key_total_tokens_by_ids, admin_api_keys_id_from_path, admin_api_keys_operator_id,
|
||||
admin_api_keys_parse_limit, admin_api_keys_parse_skip, build_admin_api_key_detail_payload,
|
||||
build_admin_api_key_list_item_payload, build_admin_api_keys_bad_request_response,
|
||||
@@ -51,10 +45,6 @@ pub(crate) async fn maybe_build_local_admin_api_keys_response(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
admin_api_keys_routes::maybe_build_local_admin_api_keys_routes_response(
|
||||
state,
|
||||
request_context,
|
||||
request_body,
|
||||
)
|
||||
.await
|
||||
routes::maybe_build_local_admin_api_keys_routes_response(state, request_context, request_body)
|
||||
.await
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::admin_api_keys_shared::{
|
||||
use super::shared::{
|
||||
admin_api_key_total_tokens_by_ids, admin_api_keys_id_from_path, admin_api_keys_operator_id,
|
||||
build_admin_api_key_detail_payload, build_admin_api_keys_bad_request_response,
|
||||
build_admin_api_keys_data_unavailable_response, build_admin_api_keys_not_found_response,
|
||||
@@ -12,7 +12,7 @@ use super::{
|
||||
normalize_admin_user_api_formats, normalize_admin_user_string_list,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
|
||||
use crate::handlers::admin::shared::attach_admin_audit_response;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -27,7 +27,7 @@ pub(super) async fn build_admin_create_api_key_response(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if !state.has_auth_api_key_writer() {
|
||||
if !state.data.has_auth_api_key_writer() {
|
||||
return Ok(build_admin_api_keys_data_unavailable_response());
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ pub(super) async fn build_admin_update_api_key_response(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if !state.has_auth_api_key_writer() {
|
||||
if !state.data.has_auth_api_key_writer() {
|
||||
return Ok(build_admin_api_keys_data_unavailable_response());
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ pub(super) async fn build_admin_toggle_api_key_response(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if !state.has_auth_api_key_writer() {
|
||||
if !state.data.has_auth_api_key_writer() {
|
||||
return Ok(build_admin_api_keys_data_unavailable_response());
|
||||
}
|
||||
|
||||
@@ -290,8 +290,10 @@ pub(super) async fn build_admin_toggle_api_key_response(
|
||||
};
|
||||
|
||||
let Some(snapshot) = state
|
||||
.read_auth_api_key_snapshots_by_ids(std::slice::from_ref(&api_key_id))
|
||||
.await?
|
||||
.data
|
||||
.list_auth_api_key_snapshots_by_ids(std::slice::from_ref(&api_key_id))
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.into_iter()
|
||||
.find(|snapshot| snapshot.api_key_id == api_key_id)
|
||||
else {
|
||||
@@ -327,7 +329,7 @@ pub(super) async fn build_admin_delete_api_key_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if !state.has_auth_api_key_writer() {
|
||||
if !state.data.has_auth_api_key_writer() {
|
||||
return Ok(build_admin_api_keys_data_unavailable_response());
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::admin_api_keys_shared::{
|
||||
use super::shared::{
|
||||
admin_api_key_total_tokens_by_ids, admin_api_keys_id_from_path, admin_api_keys_parse_limit,
|
||||
admin_api_keys_parse_skip, build_admin_api_key_detail_payload,
|
||||
build_admin_api_key_list_item_payload, build_admin_api_keys_bad_request_response,
|
||||
@@ -6,7 +6,7 @@ use super::admin_api_keys_shared::{
|
||||
};
|
||||
use super::{decrypt_catalog_secret_with_fallbacks, query_param_bool, query_param_optional_bool};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
|
||||
use crate::handlers::admin::shared::attach_admin_audit_response;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -15,11 +15,14 @@ use axum::{
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::time::Instant;
|
||||
use tracing::info;
|
||||
|
||||
pub(super) async fn build_admin_list_api_keys_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let handler_started_at = Instant::now();
|
||||
let query = request_context.request_query_string.as_deref();
|
||||
let skip = match admin_api_keys_parse_skip(query) {
|
||||
Ok(value) => value,
|
||||
@@ -30,24 +33,43 @@ pub(super) async fn build_admin_list_api_keys_response(
|
||||
Err(detail) => return Ok(build_admin_api_keys_bad_request_response(detail)),
|
||||
};
|
||||
let is_active = query_param_optional_bool(query, "is_active");
|
||||
let include_usage_summary = query_param_bool(query, "include_usage_summary", false);
|
||||
|
||||
let total = state
|
||||
.count_auth_api_key_export_standalone_records(is_active)
|
||||
.await? as usize;
|
||||
let paged_records = state
|
||||
.list_auth_api_key_export_standalone_records_page(
|
||||
&aether_data::repository::auth::StandaloneApiKeyExportListQuery {
|
||||
skip,
|
||||
limit,
|
||||
is_active,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let list_query = aether_data::repository::auth::StandaloneApiKeyExportListQuery {
|
||||
skip,
|
||||
limit,
|
||||
is_active,
|
||||
};
|
||||
let count_and_page_started_at = Instant::now();
|
||||
let (total, paged_records) = tokio::try_join!(
|
||||
state.count_auth_api_key_export_standalone_records(is_active),
|
||||
state.list_auth_api_key_export_standalone_records_page(&list_query),
|
||||
)?;
|
||||
let count_and_page_ms = count_and_page_started_at.elapsed().as_millis() as u64;
|
||||
let api_key_ids = paged_records
|
||||
.iter()
|
||||
.map(|record| record.api_key_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let total_tokens_by_api_key_id = admin_api_key_total_tokens_by_ids(state, &api_key_ids).await?;
|
||||
let wallet_lookup_started_at = Instant::now();
|
||||
let wallets_by_api_key_id = state
|
||||
.list_wallet_snapshots_by_api_key_ids(&api_key_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(|wallet| {
|
||||
wallet
|
||||
.api_key_id
|
||||
.clone()
|
||||
.map(|api_key_id| (api_key_id, wallet))
|
||||
})
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
let wallet_lookup_ms = wallet_lookup_started_at.elapsed().as_millis() as u64;
|
||||
let usage_summary_started_at = Instant::now();
|
||||
let total_tokens_by_api_key_id = if include_usage_summary {
|
||||
Some(admin_api_key_total_tokens_by_ids(state, &api_key_ids).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let usage_summary_ms = usage_summary_started_at.elapsed().as_millis() as u64;
|
||||
|
||||
let api_keys = paged_records
|
||||
.iter()
|
||||
@@ -56,16 +78,30 @@ pub(super) async fn build_admin_list_api_keys_response(
|
||||
state,
|
||||
record,
|
||||
total_tokens_by_api_key_id
|
||||
.get(&record.api_key_id)
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
.as_ref()
|
||||
.and_then(|totals| totals.get(&record.api_key_id).copied()),
|
||||
wallets_by_api_key_id.get(&record.api_key_id),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
info!(
|
||||
event_name = "admin_api_keys_list_timing",
|
||||
log_type = "event",
|
||||
trace_id = request_context.trace_id.as_str(),
|
||||
returned_items = api_keys.len(),
|
||||
total,
|
||||
include_usage_summary,
|
||||
count_and_page_ms,
|
||||
wallet_lookup_ms,
|
||||
usage_summary_ms,
|
||||
handler_ms = handler_started_at.elapsed().as_millis() as u64,
|
||||
"measured admin api keys list handler timing"
|
||||
);
|
||||
|
||||
Ok(Json(json!({
|
||||
"api_keys": api_keys,
|
||||
"total": total,
|
||||
"total": total as usize,
|
||||
"limit": limit,
|
||||
"skip": skip,
|
||||
}))
|
||||
@@ -81,8 +117,10 @@ pub(super) async fn build_admin_api_key_detail_response(
|
||||
};
|
||||
|
||||
if state
|
||||
.read_auth_api_key_snapshots_by_ids(std::slice::from_ref(&api_key_id))
|
||||
.await?
|
||||
.data
|
||||
.list_auth_api_key_snapshots_by_ids(std::slice::from_ref(&api_key_id))
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.into_iter()
|
||||
.any(|snapshot| snapshot.api_key_id == api_key_id && !snapshot.api_key_is_standalone)
|
||||
{
|
||||
@@ -1,11 +1,9 @@
|
||||
use super::admin_api_keys_mutation_routes::{
|
||||
use super::mutation_routes::{
|
||||
build_admin_create_api_key_response, build_admin_delete_api_key_response,
|
||||
build_admin_toggle_api_key_response, build_admin_update_api_key_response,
|
||||
};
|
||||
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 super::read_routes::{build_admin_api_key_detail_response, build_admin_list_api_keys_response};
|
||||
use super::shared::build_admin_api_keys_data_unavailable_response;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{body::Body, http, response::Response};
|
||||
@@ -1,9 +1,9 @@
|
||||
use super::ADMIN_API_KEYS_DATA_UNAVAILABLE_DETAIL;
|
||||
use super::{
|
||||
format_optional_unix_secs_iso8601, http, json, masked_user_api_key_display, query_param_value,
|
||||
serialize_admin_system_users_export_wallet, AppState, Body, GatewayError,
|
||||
GatewayPublicRequestContext, IntoResponse, Json, Response,
|
||||
};
|
||||
use crate::handlers::admin::api_keys::ADMIN_API_KEYS_DATA_UNAVAILABLE_DETAIL;
|
||||
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
pub(super) struct AdminStandaloneApiKeyCreateRequest {
|
||||
@@ -121,7 +121,8 @@ pub(super) fn admin_api_keys_parse_limit(query: Option<&str>) -> Result<usize, S
|
||||
pub(super) fn build_admin_api_key_list_item_payload(
|
||||
state: &AppState,
|
||||
record: &aether_data::repository::auth::StoredAuthApiKeyExportRecord,
|
||||
total_tokens: u64,
|
||||
total_tokens: Option<u64>,
|
||||
wallet: Option<&aether_data::repository::wallet::StoredWalletSnapshot>,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"id": record.api_key_id,
|
||||
@@ -142,6 +143,7 @@ pub(super) fn build_admin_api_key_list_item_payload(
|
||||
"created_at": serde_json::Value::Null,
|
||||
"updated_at": serde_json::Value::Null,
|
||||
"auto_delete_on_expiry": record.auto_delete_on_expiry,
|
||||
"wallet": serialize_admin_system_users_export_wallet(wallet),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::ldap_shared::*;
|
||||
use crate::handlers::{
|
||||
use super::shared::*;
|
||||
use crate::handlers::admin::shared::{
|
||||
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks,
|
||||
};
|
||||
use crate::{AppState, GatewayError};
|
||||
@@ -5,17 +5,14 @@ use axum::{
|
||||
response::Response,
|
||||
};
|
||||
|
||||
#[path = "ldap/builders.rs"]
|
||||
mod ldap_builders;
|
||||
#[path = "ldap/routes.rs"]
|
||||
mod ldap_routes;
|
||||
#[path = "ldap/shared.rs"]
|
||||
mod ldap_shared;
|
||||
mod builders;
|
||||
mod routes;
|
||||
mod shared;
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_ldap_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
ldap_routes::maybe_build_local_admin_ldap_response(state, request_context, request_body).await
|
||||
routes::maybe_build_local_admin_ldap_response(state, request_context, request_body).await
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
use super::ldap_builders::{
|
||||
use super::builders::{
|
||||
admin_ldap_test_connection, build_admin_ldap_test_config, build_admin_ldap_update_config,
|
||||
AdminLdapConfigTestRequest, AdminLdapConfigUpdateRequest,
|
||||
};
|
||||
use super::ldap_shared::*;
|
||||
use super::shared::*;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
|
||||
use crate::handlers::admin::shared::attach_admin_audit_response;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
14
apps/aether-gateway/src/handlers/admin/auth/mod.rs
Normal file
14
apps/aether-gateway/src/handlers/admin/auth/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
mod api_keys;
|
||||
mod ldap;
|
||||
mod oauth_config;
|
||||
mod oauth_routes;
|
||||
mod security;
|
||||
|
||||
pub(crate) use self::api_keys::maybe_build_local_admin_api_keys_response;
|
||||
pub(crate) use self::ldap::maybe_build_local_admin_ldap_response;
|
||||
pub(crate) use self::oauth_config::{
|
||||
build_admin_oauth_provider_payload, build_admin_oauth_supported_types_payload,
|
||||
build_admin_oauth_upsert_record, build_proxy_error_response,
|
||||
};
|
||||
pub(crate) use self::oauth_routes::maybe_build_local_admin_oauth_response;
|
||||
pub(crate) use self::security::maybe_build_local_admin_security_response;
|
||||
@@ -1,6 +1,4 @@
|
||||
use crate::handlers::{
|
||||
encrypt_catalog_secret_with_fallbacks, AdminOAuthProviderUpsertRequest,
|
||||
};
|
||||
use crate::handlers::admin::shared::encrypt_catalog_secret_with_fallbacks;
|
||||
use crate::AppState;
|
||||
use aether_data::repository::oauth_providers::{
|
||||
EncryptedSecretUpdate, UpsertOAuthProviderConfigRecord,
|
||||
@@ -11,9 +9,36 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct AdminOAuthProviderUpsertRequest {
|
||||
pub(crate) display_name: String,
|
||||
pub(crate) client_id: String,
|
||||
#[serde(default)]
|
||||
pub(crate) client_secret: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) authorization_url_override: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) token_url_override: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) userinfo_url_override: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) scopes: Option<Vec<String>>,
|
||||
pub(crate) redirect_uri: String,
|
||||
pub(crate) frontend_callback_url: String,
|
||||
#[serde(default)]
|
||||
pub(crate) attribute_mapping: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub(crate) extra_config: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub(crate) is_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub(crate) force: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn build_admin_oauth_supported_types_payload() -> Vec<serde_json::Value> {
|
||||
vec![json!({
|
||||
"provider_type": "linuxdo",
|
||||
@@ -65,6 +90,19 @@ pub(crate) fn build_proxy_error_response(
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(crate) fn admin_oauth_provider_type_from_path(request_path: &str) -> Option<String> {
|
||||
let provider_type = request_path.strip_prefix("/api/admin/oauth/providers/")?;
|
||||
(!provider_type.is_empty() && !provider_type.contains('/')).then_some(provider_type.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn admin_oauth_test_provider_type_from_path(request_path: &str) -> Option<String> {
|
||||
request_path
|
||||
.strip_prefix("/api/admin/oauth/providers/")?
|
||||
.strip_suffix("/test")
|
||||
.filter(|provider_type| !provider_type.is_empty() && !provider_type.contains('/'))
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn admin_oauth_is_supported_provider(provider_type: &str) -> bool {
|
||||
provider_type.eq_ignore_ascii_case("linuxdo")
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
use super::super::{
|
||||
use super::oauth_config::{
|
||||
admin_oauth_provider_type_from_path, admin_oauth_test_provider_type_from_path,
|
||||
build_admin_oauth_provider_payload, build_admin_oauth_supported_types_payload,
|
||||
build_admin_oauth_upsert_record, build_proxy_error_response,
|
||||
build_admin_oauth_upsert_record, build_proxy_error_response, AdminOAuthProviderUpsertRequest,
|
||||
};
|
||||
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::handlers::admin::shared::attach_admin_audit_response;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
@@ -17,7 +14,7 @@ use axum::{
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) async fn maybe_build_local_admin_core_oauth_response(
|
||||
pub(crate) async fn maybe_build_local_admin_oauth_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_body: Option<&Bytes>,
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
|
||||
use crate::handlers::admin::shared::attach_admin_audit_response;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
@@ -7,7 +7,7 @@ use super::{
|
||||
normalize_admin_billing_required_text,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::unix_secs_to_rfc3339;
|
||||
use crate::handlers::admin::shared::unix_secs_to_rfc3339;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
@@ -288,12 +288,12 @@ async fn build_admin_create_dimension_collector_response(
|
||||
crate::LocalMutationOutcome::Invalid(detail) => {
|
||||
Ok(build_admin_billing_bad_request_response(detail))
|
||||
}
|
||||
crate::LocalMutationOutcome::NotFound => Ok(
|
||||
build_admin_billing_not_found_response("Dimension collector not found"),
|
||||
),
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(
|
||||
build_admin_billing_read_only_response("当前为只读模式,无法创建维度采集器"),
|
||||
),
|
||||
crate::LocalMutationOutcome::NotFound => Ok(build_admin_billing_not_found_response(
|
||||
"Dimension collector not found",
|
||||
)),
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(build_admin_billing_read_only_response(
|
||||
"当前为只读模式,无法创建维度采集器",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,15 +321,15 @@ async fn build_admin_update_dimension_collector_response(
|
||||
crate::LocalMutationOutcome::Applied(record) => {
|
||||
Ok(Json(build_admin_billing_collector_payload_from_record(&record)).into_response())
|
||||
}
|
||||
crate::LocalMutationOutcome::NotFound => Ok(
|
||||
build_admin_billing_not_found_response("Dimension collector not found"),
|
||||
),
|
||||
crate::LocalMutationOutcome::NotFound => Ok(build_admin_billing_not_found_response(
|
||||
"Dimension collector not found",
|
||||
)),
|
||||
crate::LocalMutationOutcome::Invalid(detail) => {
|
||||
Ok(build_admin_billing_bad_request_response(detail))
|
||||
}
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(
|
||||
build_admin_billing_read_only_response("当前为只读模式,无法更新维度采集器"),
|
||||
),
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(build_admin_billing_read_only_response(
|
||||
"当前为只读模式,无法更新维度采集器",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::{query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::handlers::admin::shared::{query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
@@ -13,12 +13,14 @@ use sqlx::Row;
|
||||
|
||||
const ADMIN_BILLING_DATA_UNAVAILABLE_DETAIL: &str = "Admin billing data unavailable";
|
||||
|
||||
#[path = "billing/collectors.rs"]
|
||||
mod billing_collectors;
|
||||
#[path = "billing/presets.rs"]
|
||||
mod billing_presets;
|
||||
#[path = "billing/rules.rs"]
|
||||
mod billing_rules;
|
||||
mod collectors;
|
||||
mod payments;
|
||||
mod presets;
|
||||
mod rules;
|
||||
mod wallets;
|
||||
|
||||
pub(crate) use self::payments::maybe_build_local_admin_payments_response;
|
||||
pub(crate) use self::wallets::maybe_build_local_admin_wallets_response;
|
||||
|
||||
fn default_admin_billing_true() -> bool {
|
||||
true
|
||||
@@ -256,7 +258,7 @@ pub(crate) async fn maybe_build_local_admin_billing_response(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let Some(response) = billing_presets::maybe_build_local_admin_billing_presets_response(
|
||||
if let Some(response) = presets::maybe_build_local_admin_billing_presets_response(
|
||||
state,
|
||||
request_context,
|
||||
request_body,
|
||||
@@ -265,16 +267,13 @@ pub(crate) async fn maybe_build_local_admin_billing_response(
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
if let Some(response) = billing_rules::maybe_build_local_admin_billing_rules_response(
|
||||
state,
|
||||
request_context,
|
||||
request_body,
|
||||
)
|
||||
.await?
|
||||
if let Some(response) =
|
||||
rules::maybe_build_local_admin_billing_rules_response(state, request_context, request_body)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
if let Some(response) = billing_collectors::maybe_build_local_admin_billing_collectors_response(
|
||||
if let Some(response) = collectors::maybe_build_local_admin_billing_collectors_response(
|
||||
state,
|
||||
request_context,
|
||||
request_body,
|
||||
@@ -4,7 +4,7 @@ use super::{
|
||||
parse_admin_payments_offset,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::query_param_value;
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -2,18 +2,14 @@ use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
#[path = "payment/postgres.rs"]
|
||||
mod callbacks;
|
||||
mod orders;
|
||||
#[path = "../../payment/postgres.rs"]
|
||||
mod payment_postgres;
|
||||
#[path = "payments/callbacks.rs"]
|
||||
mod payments_callbacks;
|
||||
#[path = "payments/orders.rs"]
|
||||
mod payments_orders;
|
||||
#[path = "payments/routes.rs"]
|
||||
mod payments_routes;
|
||||
#[path = "payments/shared.rs"]
|
||||
mod payments_shared;
|
||||
mod routes;
|
||||
mod shared;
|
||||
|
||||
use self::payments_shared::{
|
||||
use self::shared::{
|
||||
admin_payment_operator_id, admin_payment_order_id_from_detail_path,
|
||||
admin_payment_order_id_from_suffix_path, build_admin_payment_callback_payload,
|
||||
build_admin_payment_callback_payload_from_record, build_admin_payment_order_not_found_response,
|
||||
@@ -29,6 +25,5 @@ pub(crate) async fn maybe_build_local_admin_payments_response(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
payments_routes::maybe_build_local_admin_payments_response(state, request_context, request_body)
|
||||
.await
|
||||
routes::maybe_build_local_admin_payments_response(state, request_context, request_body).await
|
||||
}
|
||||
@@ -8,8 +8,7 @@ use super::{
|
||||
parse_admin_payments_offset, AdminPaymentOrderCreditRequest,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
|
||||
use crate::handlers::query_param_value;
|
||||
use crate::handlers::admin::shared::{attach_admin_audit_response, query_param_value};
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -251,18 +250,16 @@ 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::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::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())
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{
|
||||
build_admin_payments_data_unavailable_response,
|
||||
payments_callbacks::maybe_build_local_admin_payment_callbacks_response,
|
||||
payments_orders::maybe_build_local_admin_payment_orders_response,
|
||||
callbacks::maybe_build_local_admin_payment_callbacks_response,
|
||||
orders::maybe_build_local_admin_payment_orders_response,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::{query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::GatewayError;
|
||||
use crate::handlers::admin::shared::{query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::{GatewayAdminPaymentCallbackView, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
@@ -250,7 +250,7 @@ pub(super) fn build_admin_payment_callback_payload(
|
||||
}
|
||||
|
||||
pub(super) fn build_admin_payment_callback_payload_from_record(
|
||||
record: &crate::state::AdminPaymentCallbackRecord,
|
||||
record: &GatewayAdminPaymentCallbackView,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"id": record.id,
|
||||
@@ -3,7 +3,7 @@ use super::{
|
||||
build_admin_billing_read_only_response, normalize_admin_billing_required_text,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
|
||||
use crate::handlers::admin::shared::attach_admin_audit_response;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
@@ -37,8 +37,7 @@ fn build_admin_billing_presets_payload() -> serde_json::Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_admin_billing_aether_core_collectors(
|
||||
) -> Vec<crate::AdminBillingCollectorWriteInput> {
|
||||
fn build_admin_billing_aether_core_collectors() -> Vec<crate::AdminBillingCollectorWriteInput> {
|
||||
vec![
|
||||
crate::AdminBillingCollectorWriteInput {
|
||||
api_format: "OPENAI:CHAT".to_string(),
|
||||
@@ -237,10 +236,7 @@ fn build_admin_billing_aether_core_collectors(
|
||||
|
||||
fn resolve_admin_billing_preset_collectors(
|
||||
preset: &str,
|
||||
) -> Option<(
|
||||
&'static str,
|
||||
Vec<crate::AdminBillingCollectorWriteInput>,
|
||||
)> {
|
||||
) -> Option<(&'static str, Vec<crate::AdminBillingCollectorWriteInput>)> {
|
||||
let normalized = preset.trim().to_ascii_lowercase();
|
||||
match normalized.as_str() {
|
||||
"aether-core" | "default" => {
|
||||
@@ -323,15 +319,15 @@ async fn build_admin_apply_billing_preset_response(
|
||||
resolved_preset,
|
||||
))
|
||||
}
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(
|
||||
build_admin_billing_read_only_response("当前为只读模式,无法应用计费预设"),
|
||||
),
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(build_admin_billing_read_only_response(
|
||||
"当前为只读模式,无法应用计费预设",
|
||||
)),
|
||||
crate::LocalMutationOutcome::Invalid(detail) => {
|
||||
Ok(build_admin_billing_bad_request_response(detail))
|
||||
}
|
||||
crate::LocalMutationOutcome::NotFound => Ok(
|
||||
build_admin_billing_not_found_response("Billing preset not found"),
|
||||
),
|
||||
crate::LocalMutationOutcome::NotFound => Ok(build_admin_billing_not_found_response(
|
||||
"Billing preset not found",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::{
|
||||
normalize_admin_billing_optional_text, normalize_admin_billing_required_text,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::unix_secs_to_rfc3339;
|
||||
use crate::handlers::admin::shared::unix_secs_to_rfc3339;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
@@ -245,12 +245,12 @@ async fn build_admin_create_billing_rule_response(
|
||||
crate::LocalMutationOutcome::Invalid(detail) => {
|
||||
Ok(build_admin_billing_bad_request_response(detail))
|
||||
}
|
||||
crate::LocalMutationOutcome::NotFound => Ok(
|
||||
build_admin_billing_not_found_response("Billing rule not found"),
|
||||
),
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(
|
||||
build_admin_billing_read_only_response("当前为只读模式,无法创建计费规则"),
|
||||
),
|
||||
crate::LocalMutationOutcome::NotFound => Ok(build_admin_billing_not_found_response(
|
||||
"Billing rule not found",
|
||||
)),
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(build_admin_billing_read_only_response(
|
||||
"当前为只读模式,无法创建计费规则",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,15 +270,15 @@ async fn build_admin_update_billing_rule_response(
|
||||
crate::LocalMutationOutcome::Applied(record) => {
|
||||
Ok(Json(build_admin_billing_rule_payload_from_record(&record)).into_response())
|
||||
}
|
||||
crate::LocalMutationOutcome::NotFound => Ok(
|
||||
build_admin_billing_not_found_response("Billing rule not found"),
|
||||
),
|
||||
crate::LocalMutationOutcome::NotFound => Ok(build_admin_billing_not_found_response(
|
||||
"Billing rule not found",
|
||||
)),
|
||||
crate::LocalMutationOutcome::Invalid(detail) => {
|
||||
Ok(build_admin_billing_bad_request_response(detail))
|
||||
}
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(
|
||||
build_admin_billing_read_only_response("当前为只读模式,无法更新计费规则"),
|
||||
),
|
||||
crate::LocalMutationOutcome::Unavailable => Ok(build_admin_billing_read_only_response(
|
||||
"当前为只读模式,无法更新计费规则",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,24 +2,16 @@ use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
#[path = "wallets/routes.rs"]
|
||||
mod admin_wallets_routes;
|
||||
#[path = "wallets/shared.rs"]
|
||||
mod admin_wallets_shared;
|
||||
#[path = "wallets/mutations.rs"]
|
||||
mod mutations;
|
||||
#[path = "wallets/reads.rs"]
|
||||
mod reads;
|
||||
mod routes;
|
||||
mod shared;
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_wallets_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
admin_wallets_routes::maybe_build_local_admin_wallets_routes_response(
|
||||
state,
|
||||
request_context,
|
||||
request_body,
|
||||
)
|
||||
.await
|
||||
routes::maybe_build_local_admin_wallets_routes_response(state, request_context, request_body)
|
||||
.await
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::admin_wallets_shared::{
|
||||
use super::shared::{
|
||||
admin_wallet_id_from_suffix_path, admin_wallet_operator_id,
|
||||
admin_wallet_refund_ids_from_suffix_path, build_admin_wallet_not_found_response,
|
||||
build_admin_wallet_payment_order_payload, build_admin_wallet_refund_not_found_response,
|
||||
@@ -14,8 +14,7 @@ use super::admin_wallets_shared::{
|
||||
ADMIN_WALLETS_API_KEY_REFUND_DETAIL,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
|
||||
use crate::handlers::unix_secs_to_rfc3339;
|
||||
use crate::handlers::admin::shared::{attach_admin_audit_response, unix_secs_to_rfc3339};
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::admin_wallets_shared::{
|
||||
use super::shared::{
|
||||
admin_wallet_id_from_detail_path, admin_wallet_id_from_suffix_path,
|
||||
build_admin_wallet_not_found_response, build_admin_wallet_refund_payload,
|
||||
build_admin_wallet_summary_payload, build_admin_wallets_bad_request_response,
|
||||
@@ -7,7 +7,7 @@ use super::admin_wallets_shared::{
|
||||
ADMIN_WALLETS_API_KEY_REFUND_DETAIL,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::{query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::handlers::admin::shared::{query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::admin_wallets_shared::build_admin_wallets_data_unavailable_response;
|
||||
use super::mutations::{
|
||||
build_admin_wallet_adjust_response, build_admin_wallet_complete_refund_response,
|
||||
build_admin_wallet_fail_refund_response, build_admin_wallet_process_refund_response,
|
||||
@@ -9,6 +8,7 @@ use super::reads::{
|
||||
build_admin_wallet_list_response, build_admin_wallet_refund_requests_response,
|
||||
build_admin_wallet_refunds_response, build_admin_wallet_transactions_response,
|
||||
};
|
||||
use super::shared::build_admin_wallets_data_unavailable_response;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{body::Body, http, response::Response};
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::{query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::handlers::admin::shared::{query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -495,8 +495,10 @@ pub(super) async fn resolve_admin_wallet_owner_summary(
|
||||
} else if let Some(api_key_id) = wallet.api_key_id.as_deref() {
|
||||
let api_key_ids = vec![api_key_id.to_string()];
|
||||
let snapshots = state
|
||||
.read_auth_api_key_snapshots_by_ids(&api_key_ids)
|
||||
.await?;
|
||||
.data
|
||||
.list_auth_api_key_snapshots_by_ids(&api_key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
let owner_name = snapshots
|
||||
.into_iter()
|
||||
.find(|snapshot| snapshot.api_key_id == api_key_id)
|
||||
@@ -0,0 +1,29 @@
|
||||
pub(super) fn admin_health_key_id(request_path: &str) -> Option<String> {
|
||||
let raw = request_path.strip_prefix("/api/admin/endpoints/health/key/")?;
|
||||
let normalized = raw.trim().trim_matches('/');
|
||||
if normalized.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(normalized.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn admin_recover_key_id(request_path: &str) -> Option<String> {
|
||||
let raw = request_path.strip_prefix("/api/admin/endpoints/health/keys/")?;
|
||||
let normalized = raw.trim().trim_matches('/');
|
||||
if normalized.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(normalized.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn admin_rpm_key_id(request_path: &str) -> Option<String> {
|
||||
let raw = request_path.strip_prefix("/api/admin/endpoints/rpm/key/")?;
|
||||
let normalized = raw.trim().trim_matches('/');
|
||||
if normalized.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(normalized.to_string())
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
use super::super::{
|
||||
admin_health_key_id, admin_recover_key_id, build_admin_endpoint_health_status_payload,
|
||||
build_admin_health_summary_payload, build_admin_key_health_payload, recover_admin_key_health,
|
||||
recover_all_admin_key_health,
|
||||
use super::extractors::{admin_health_key_id, admin_recover_key_id};
|
||||
use super::health_builders::{
|
||||
build_admin_endpoint_health_status_payload, build_admin_health_summary_payload,
|
||||
build_admin_key_health_payload, recover_admin_key_health, recover_all_admin_key_health,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use crate::handlers::public::{
|
||||
build_api_format_health_monitor_payload, ApiFormatHealthMonitorOptions,
|
||||
};
|
||||
use crate::handlers::query_param_value;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::handlers::public::provider_key_api_formats;
|
||||
use crate::scheduler::count_recent_rpm_requests_for_provider_key_since;
|
||||
use crate::AppState;
|
||||
use aether_scheduler_core::count_recent_rpm_requests_for_provider_key_since;
|
||||
use serde_json::json;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
mod keys;
|
||||
mod status;
|
||||
|
||||
pub(super) use self::keys::{
|
||||
build_admin_key_health_payload, build_admin_key_rpm_payload, recover_admin_key_health,
|
||||
recover_all_admin_key_health,
|
||||
};
|
||||
pub(crate) use self::status::build_admin_endpoint_health_status_payload;
|
||||
pub(super) use self::status::build_admin_health_summary_payload;
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::handlers::admin::shared::unix_secs_to_rfc3339;
|
||||
use crate::handlers::public::{
|
||||
api_format_display_name, build_public_health_timeline, provider_key_api_formats,
|
||||
};
|
||||
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 aether_data_contracts::repository::candidates::PublicHealthTimelineBucket;
|
||||
use aether_scheduler_core::{is_provider_key_circuit_open, provider_key_health_score};
|
||||
use serde_json::json;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
1
apps/aether-gateway/src/handlers/admin/endpoint/keys.rs
Normal file
1
apps/aether-gateway/src/handlers/admin/endpoint/keys.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub(super) use crate::handlers::admin::provider::endpoint_keys::maybe_build_local_admin_endpoints_keys_response;
|
||||
@@ -3,14 +3,14 @@ use crate::{AppState, GatewayError};
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::http::Response;
|
||||
|
||||
#[path = "endpoints/health.rs"]
|
||||
mod endpoints_health;
|
||||
#[path = "endpoints/keys.rs"]
|
||||
mod endpoints_keys;
|
||||
#[path = "endpoints/routes.rs"]
|
||||
mod endpoints_routes;
|
||||
#[path = "endpoints/rpm.rs"]
|
||||
mod endpoints_rpm;
|
||||
mod extractors;
|
||||
mod health;
|
||||
mod health_builders;
|
||||
mod keys;
|
||||
mod routes;
|
||||
mod rpm;
|
||||
|
||||
pub(crate) use self::health_builders::build_admin_endpoint_health_status_payload;
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_endpoints_response(
|
||||
state: &AppState,
|
||||
@@ -18,30 +18,25 @@ pub(crate) async fn maybe_build_local_admin_endpoints_response(
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
if let Some(response) =
|
||||
endpoints_health::maybe_build_local_admin_endpoints_health_response(state, request_context)
|
||||
.await?
|
||||
health::maybe_build_local_admin_endpoints_health_response(state, request_context).await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) =
|
||||
endpoints_rpm::maybe_build_local_admin_endpoints_rpm_response(state, request_context)
|
||||
rpm::maybe_build_local_admin_endpoints_rpm_response(state, request_context).await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) =
|
||||
keys::maybe_build_local_admin_endpoints_keys_response(state, request_context, request_body)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) = endpoints_keys::maybe_build_local_admin_endpoints_keys_response(
|
||||
state,
|
||||
request_context,
|
||||
request_body,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) = endpoints_routes::maybe_build_local_admin_endpoints_routes_response(
|
||||
if let Some(response) = routes::maybe_build_local_admin_endpoints_routes_response(
|
||||
state,
|
||||
request_context,
|
||||
request_body,
|
||||
@@ -0,0 +1 @@
|
||||
pub(super) use crate::handlers::admin::provider::endpoints_admin::maybe_build_local_admin_endpoints_routes_response;
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::super::{admin_rpm_key_id, build_admin_key_rpm_payload};
|
||||
use super::extractors::admin_rpm_key_id;
|
||||
use super::health_builders::build_admin_key_rpm_payload;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
@@ -1,18 +0,0 @@
|
||||
#[path = "endpoints_health_helpers/endpoints.rs"]
|
||||
mod endpoints;
|
||||
#[path = "endpoints_health_helpers/keys.rs"]
|
||||
mod keys;
|
||||
#[path = "endpoints_health_helpers/status.rs"]
|
||||
mod status;
|
||||
|
||||
pub(crate) use self::endpoints::{
|
||||
build_admin_create_provider_endpoint_record, build_admin_endpoint_payload,
|
||||
build_admin_provider_endpoints_payload, build_admin_update_provider_endpoint_record,
|
||||
};
|
||||
pub(crate) use self::keys::{
|
||||
build_admin_key_health_payload, build_admin_key_rpm_payload, recover_admin_key_health,
|
||||
recover_all_admin_key_health,
|
||||
};
|
||||
pub(crate) use self::status::{
|
||||
build_admin_endpoint_health_status_payload, build_admin_health_summary_payload,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data::repository::provider_catalog::StoredProviderCatalogKey;
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::http::{self, Response};
|
||||
use axum::response::IntoResponse;
|
||||
@@ -14,10 +14,8 @@ const ADMIN_GEMINI_FILES_DEFAULT_PAGE: usize = 1;
|
||||
const ADMIN_GEMINI_FILES_DEFAULT_PAGE_SIZE: usize = 20;
|
||||
const ADMIN_GEMINI_FILES_MAX_PAGE_SIZE: usize = 100;
|
||||
|
||||
#[path = "gemini_files/read_routes.rs"]
|
||||
mod admin_gemini_files_read_routes;
|
||||
#[path = "gemini_files/upload.rs"]
|
||||
mod admin_gemini_files_upload;
|
||||
mod read_routes;
|
||||
mod upload;
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_gemini_files_response(
|
||||
state: &AppState,
|
||||
@@ -32,22 +30,18 @@ pub(crate) async fn maybe_build_local_admin_gemini_files_response(
|
||||
}
|
||||
|
||||
if let Some(response) =
|
||||
admin_gemini_files_read_routes::maybe_build_local_admin_gemini_files_read_response(
|
||||
state,
|
||||
request_context,
|
||||
)
|
||||
.await?
|
||||
read_routes::maybe_build_local_admin_gemini_files_read_response(state, request_context)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) =
|
||||
admin_gemini_files_upload::maybe_build_local_admin_gemini_files_upload_response(
|
||||
state,
|
||||
request_context,
|
||||
request_body,
|
||||
)
|
||||
.await?
|
||||
if let Some(response) = upload::maybe_build_local_admin_gemini_files_upload_response(
|
||||
state,
|
||||
request_context,
|
||||
request_body,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
@@ -5,12 +5,13 @@ use super::{
|
||||
ADMIN_GEMINI_FILES_MAX_PAGE_SIZE,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::{
|
||||
use crate::handlers::admin::shared::{
|
||||
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::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
||||
use axum::body::Body;
|
||||
use axum::http::{self, Response};
|
||||
use axum::response::IntoResponse;
|
||||
@@ -113,8 +114,7 @@ async fn admin_gemini_files_capable_keys(
|
||||
|
||||
async fn admin_gemini_files_all_keys(
|
||||
state: &AppState,
|
||||
) -> Result<Vec<aether_data::repository::provider_catalog::StoredProviderCatalogKey>, GatewayError>
|
||||
{
|
||||
) -> Result<Vec<StoredProviderCatalogKey>, GatewayError> {
|
||||
let providers = state.list_provider_catalog_providers(false).await?;
|
||||
let provider_ids = providers
|
||||
.into_iter()
|
||||
@@ -3,10 +3,10 @@ use super::{
|
||||
ADMIN_GEMINI_FILES_DATA_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::{is_admin_gemini_files_upload_root, query_param_value};
|
||||
use crate::handlers::admin::shared::{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::{
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
};
|
||||
use axum::body::{Body, Bytes};
|
||||
@@ -443,11 +443,12 @@ 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::provider_transport::resolve_transport_proxy_snapshot_with_tunnel_affinity(state, &transport).await,
|
||||
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,
|
||||
),
|
||||
timeouts: crate::provider_transport::resolve_transport_execution_timeouts(&transport),
|
||||
};
|
||||
|
||||
let result = admin_gemini_files_execute_upload_plan(state, trace_id, &plan)
|
||||
@@ -484,12 +485,7 @@ async fn admin_gemini_files_execute_upload_plan(
|
||||
trace_id: &str,
|
||||
plan: &ExecutionPlan,
|
||||
) -> Result<ExecutionResult, GatewayError> {
|
||||
crate::execution_runtime::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> {
|
||||
5
apps/aether-gateway/src/handlers/admin/features/mod.rs
Normal file
5
apps/aether-gateway/src/handlers/admin/features/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod gemini_files;
|
||||
mod video_tasks;
|
||||
|
||||
pub(crate) use self::gemini_files::maybe_build_local_admin_gemini_files_response;
|
||||
pub(crate) use self::video_tasks::maybe_build_local_admin_video_tasks_response;
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::video_tasks::{StoredVideoTask, VideoTaskStatus};
|
||||
use axum::http;
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use serde_json::json;
|
||||
@@ -6,19 +7,17 @@ use serde_json::Value;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(super) fn admin_video_task_status_name(
|
||||
status: aether_data::repository::video_tasks::VideoTaskStatus,
|
||||
) -> &'static str {
|
||||
pub(super) fn admin_video_task_status_name(status: VideoTaskStatus) -> &'static str {
|
||||
match status {
|
||||
aether_data::repository::video_tasks::VideoTaskStatus::Pending => "pending",
|
||||
aether_data::repository::video_tasks::VideoTaskStatus::Submitted => "submitted",
|
||||
aether_data::repository::video_tasks::VideoTaskStatus::Queued => "queued",
|
||||
aether_data::repository::video_tasks::VideoTaskStatus::Processing => "processing",
|
||||
aether_data::repository::video_tasks::VideoTaskStatus::Completed => "completed",
|
||||
aether_data::repository::video_tasks::VideoTaskStatus::Failed => "failed",
|
||||
aether_data::repository::video_tasks::VideoTaskStatus::Cancelled => "cancelled",
|
||||
aether_data::repository::video_tasks::VideoTaskStatus::Expired => "expired",
|
||||
aether_data::repository::video_tasks::VideoTaskStatus::Deleted => "deleted",
|
||||
VideoTaskStatus::Pending => "pending",
|
||||
VideoTaskStatus::Submitted => "submitted",
|
||||
VideoTaskStatus::Queued => "queued",
|
||||
VideoTaskStatus::Processing => "processing",
|
||||
VideoTaskStatus::Completed => "completed",
|
||||
VideoTaskStatus::Failed => "failed",
|
||||
VideoTaskStatus::Cancelled => "cancelled",
|
||||
VideoTaskStatus::Expired => "expired",
|
||||
VideoTaskStatus::Deleted => "deleted",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +42,7 @@ pub(super) fn truncate_admin_video_task_prompt(prompt: Option<&str>) -> Option<S
|
||||
|
||||
pub(super) async fn build_admin_video_task_provider_names(
|
||||
state: &AppState,
|
||||
tasks: &[aether_data::repository::video_tasks::StoredVideoTask],
|
||||
tasks: &[StoredVideoTask],
|
||||
) -> Result<BTreeMap<String, String>, GatewayError> {
|
||||
let provider_ids = tasks
|
||||
.iter()
|
||||
@@ -64,7 +63,7 @@ pub(super) async fn build_admin_video_task_provider_names(
|
||||
}
|
||||
|
||||
pub(super) fn build_admin_video_task_list_item(
|
||||
task: &aether_data::repository::video_tasks::StoredVideoTask,
|
||||
task: &StoredVideoTask,
|
||||
provider_names: &BTreeMap<String, String>,
|
||||
) -> Value {
|
||||
let provider_name = task
|
||||
@@ -2,14 +2,12 @@ use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
#[path = "video_tasks/builders.rs"]
|
||||
mod video_tasks_builders;
|
||||
#[path = "video_tasks/routes.rs"]
|
||||
mod video_tasks_routes;
|
||||
mod builders;
|
||||
mod routes;
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_video_tasks_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
video_tasks_routes::maybe_build_local_admin_video_tasks_response(state, request_context).await
|
||||
routes::maybe_build_local_admin_video_tasks_response(state, request_context).await
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::async_task;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::query_param_value;
|
||||
use crate::handlers::admin::shared::{attach_admin_audit_response, query_param_value};
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::video_tasks::{VideoTaskQueryFilter, VideoTaskStatus};
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
@@ -10,8 +11,8 @@ use axum::{
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::super::{attach_admin_audit_response, build_proxy_error_response};
|
||||
use super::video_tasks_builders::{
|
||||
use super::super::super::auth::build_proxy_error_response;
|
||||
use super::builders::{
|
||||
admin_video_task_detail_id_from_path, admin_video_task_nested_id_from_path,
|
||||
admin_video_task_status_name, admin_video_task_timestamp, build_admin_video_task_list_item,
|
||||
build_admin_video_task_provider_names, current_admin_video_task_unix_secs,
|
||||
@@ -36,24 +37,20 @@ pub(super) async fn maybe_build_local_admin_video_tasks_response(
|
||||
{
|
||||
let status =
|
||||
match query_param_value(request_context.request_query_string.as_deref(), "status") {
|
||||
Some(value) => {
|
||||
match aether_data::repository::video_tasks::VideoTaskStatus::from_database(
|
||||
&value,
|
||||
) {
|
||||
Ok(status) => Some(status),
|
||||
Err(err) => {
|
||||
return Ok(Some(build_proxy_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid_request",
|
||||
err.to_string(),
|
||||
None,
|
||||
)));
|
||||
}
|
||||
Some(value) => match VideoTaskStatus::from_database(&value) {
|
||||
Ok(status) => Some(status),
|
||||
Err(err) => {
|
||||
return Ok(Some(build_proxy_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid_request",
|
||||
err.to_string(),
|
||||
None,
|
||||
)));
|
||||
}
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let filter = aether_data::repository::video_tasks::VideoTaskQueryFilter {
|
||||
let filter = VideoTaskQueryFilter {
|
||||
user_id: query_param_value(request_context.request_query_string.as_deref(), "user_id"),
|
||||
status,
|
||||
model_substring: query_param_value(
|
||||
@@ -93,18 +90,15 @@ pub(super) async fn maybe_build_local_admin_video_tasks_response(
|
||||
"/api/admin/video-tasks/stats" | "/api/admin/video-tasks/stats/"
|
||||
)
|
||||
{
|
||||
let filter = aether_data::repository::video_tasks::VideoTaskQueryFilter {
|
||||
let filter = VideoTaskQueryFilter {
|
||||
user_id: None,
|
||||
status: None,
|
||||
model_substring: None,
|
||||
client_api_format: None,
|
||||
};
|
||||
let stats = async_task::read_video_task_stats(
|
||||
state,
|
||||
&filter,
|
||||
current_admin_video_task_unix_secs(),
|
||||
)
|
||||
.await?;
|
||||
let stats =
|
||||
async_task::read_video_task_stats(state, &filter, current_admin_video_task_unix_secs())
|
||||
.await?;
|
||||
let active_users = state.count_distinct_video_task_users(&filter).await?;
|
||||
return Ok(Some(
|
||||
Json(json!({
|
||||
@@ -179,8 +173,7 @@ pub(super) async fn maybe_build_local_admin_video_tasks_response(
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(task) = async_task::read_video_task_detail(state, task_id).await?
|
||||
else {
|
||||
let Some(task) = async_task::read_video_task_detail(state, task_id).await? else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
@@ -336,9 +329,7 @@ pub(super) async fn maybe_build_local_admin_video_tasks_response(
|
||||
.into_response(),
|
||||
));
|
||||
}
|
||||
let Some(source) =
|
||||
async_task::read_video_task_video_source(state, task_id).await?
|
||||
else {
|
||||
let Some(source) = async_task::read_video_task_video_source(state, task_id).await? else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
@@ -1,7 +0,0 @@
|
||||
#[path = "global_models/helpers.rs"]
|
||||
mod global_models_helpers;
|
||||
#[path = "global_models/routes.rs"]
|
||||
mod global_models_routes;
|
||||
|
||||
use global_models_helpers::*;
|
||||
pub(crate) use global_models_routes::maybe_build_local_admin_global_models_response;
|
||||
@@ -1,238 +0,0 @@
|
||||
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,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(crate) fn build_unhandled_admin_proxy_response(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Response<Body> {
|
||||
let decision = request_context.control_decision.as_ref();
|
||||
(
|
||||
http::StatusCode::NOT_IMPLEMENTED,
|
||||
Json(json!({
|
||||
"detail": "admin proxy route not implemented in rust frontdoor",
|
||||
"route_family": decision.and_then(|value| value.route_family.as_deref()),
|
||||
"route_kind": decision.and_then(|value| value.route_kind.as_deref()),
|
||||
"request_path": request_context.request_path,
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(crate) fn build_admin_proxy_auth_required_response(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Response<Body> {
|
||||
let decision = request_context.control_decision.as_ref();
|
||||
(
|
||||
http::StatusCode::UNAUTHORIZED,
|
||||
Json(json!({
|
||||
"detail": "admin authentication required",
|
||||
"route_family": decision.and_then(|value| value.route_family.as_deref()),
|
||||
"route_kind": decision.and_then(|value| value.route_kind.as_deref()),
|
||||
"request_path": request_context.request_path,
|
||||
})),
|
||||
)
|
||||
.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,
|
||||
) -> bool {
|
||||
let Some(value) = key.api_formats.as_ref() else {
|
||||
return true;
|
||||
};
|
||||
let Some(values) = value.as_array() else {
|
||||
return true;
|
||||
};
|
||||
values
|
||||
.iter()
|
||||
.filter_map(serde_json::Value::as_str)
|
||||
.any(|candidate| candidate.trim().eq_ignore_ascii_case(api_format))
|
||||
}
|
||||
|
||||
pub(crate) fn key_api_formats_without_entry(
|
||||
key: &StoredProviderCatalogKey,
|
||||
api_format: &str,
|
||||
) -> Option<Vec<String>> {
|
||||
let current_formats = json_string_list(key.api_formats.as_ref());
|
||||
if !current_formats
|
||||
.iter()
|
||||
.any(|candidate| candidate == api_format)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
current_formats
|
||||
.into_iter()
|
||||
.filter(|candidate| candidate != api_format)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_health_key_id(request_path: &str) -> Option<String> {
|
||||
let raw = request_path.strip_prefix("/api/admin/endpoints/health/key/")?;
|
||||
let normalized = raw.trim().trim_matches('/');
|
||||
if normalized.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(normalized.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn admin_recover_key_id(request_path: &str) -> Option<String> {
|
||||
let raw = request_path.strip_prefix("/api/admin/endpoints/health/keys/")?;
|
||||
let normalized = raw.trim().trim_matches('/');
|
||||
if normalized.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(normalized.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn admin_rpm_key_id(request_path: &str) -> Option<String> {
|
||||
let raw = request_path.strip_prefix("/api/admin/endpoints/rpm/key/")?;
|
||||
let normalized = raw.trim().trim_matches('/');
|
||||
if normalized.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(normalized.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn admin_provider_id_for_endpoints(request_path: &str) -> Option<String> {
|
||||
let raw = request_path.strip_prefix("/api/admin/endpoints/providers/")?;
|
||||
let raw = raw.strip_suffix("/endpoints")?;
|
||||
let normalized = raw.trim().trim_matches('/');
|
||||
if normalized.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(normalized.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn admin_endpoint_id(request_path: &str) -> Option<String> {
|
||||
let raw = request_path.strip_prefix("/api/admin/endpoints/")?;
|
||||
let normalized = raw.trim().trim_matches('/');
|
||||
if normalized.is_empty() || normalized.contains('/') {
|
||||
None
|
||||
} else {
|
||||
Some(normalized.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn admin_default_body_rules_api_format(request_path: &str) -> Option<String> {
|
||||
let raw = request_path.strip_prefix("/api/admin/endpoints/defaults/")?;
|
||||
let raw = raw.strip_suffix("/body-rules")?;
|
||||
let normalized = raw.trim().trim_matches('/');
|
||||
if normalized.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(normalized.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn masked_proxy_value(proxy: Option<&serde_json::Value>) -> serde_json::Value {
|
||||
let Some(proxy) = proxy.and_then(serde_json::Value::as_object) else {
|
||||
return serde_json::Value::Null;
|
||||
};
|
||||
let mut masked = proxy.clone();
|
||||
if masked
|
||||
.get("password")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|value| !value.trim().is_empty())
|
||||
{
|
||||
masked.insert("password".to_string(), json!("***"));
|
||||
}
|
||||
serde_json::Value::Object(masked)
|
||||
}
|
||||
|
||||
pub(crate) fn json_truthy(value: &serde_json::Value) -> bool {
|
||||
match value {
|
||||
serde_json::Value::Null => false,
|
||||
serde_json::Value::Bool(value) => *value,
|
||||
serde_json::Value::Number(value) => value.as_f64().is_some_and(|value| value != 0.0),
|
||||
serde_json::Value::String(value) => !value.trim().is_empty(),
|
||||
serde_json::Value::Array(value) => !value.is_empty(),
|
||||
serde_json::Value::Object(value) => !value.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn endpoint_timestamp_or_now(
|
||||
value: Option<u64>,
|
||||
now_unix_secs: u64,
|
||||
) -> serde_json::Value {
|
||||
unix_secs_to_rfc3339(value.unwrap_or(now_unix_secs))
|
||||
.map(serde_json::Value::String)
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
}
|
||||
|
||||
pub(crate) fn endpoint_key_counts_by_format(
|
||||
keys: &[aether_data::repository::provider_catalog::StoredProviderCatalogKey],
|
||||
) -> (BTreeMap<String, usize>, BTreeMap<String, usize>) {
|
||||
let mut total = BTreeMap::new();
|
||||
let mut active = BTreeMap::new();
|
||||
for key in keys {
|
||||
let Some(formats) = key
|
||||
.api_formats
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_array)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for api_format in formats.iter().filter_map(serde_json::Value::as_str) {
|
||||
*total.entry(api_format.to_string()).or_insert(0) += 1;
|
||||
if key.is_active {
|
||||
*active.entry(api_format.to_string()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
(total, active)
|
||||
}
|
||||
|
||||
pub(crate) fn build_admin_provider_endpoint_response(
|
||||
endpoint: &aether_data::repository::provider_catalog::StoredProviderCatalogEndpoint,
|
||||
provider_name: &str,
|
||||
total_keys: usize,
|
||||
active_keys: usize,
|
||||
now_unix_secs: u64,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"id": endpoint.id,
|
||||
"provider_id": endpoint.provider_id,
|
||||
"provider_name": provider_name,
|
||||
"api_format": endpoint.api_format,
|
||||
"base_url": endpoint.base_url,
|
||||
"custom_path": endpoint.custom_path,
|
||||
"header_rules": endpoint.header_rules,
|
||||
"body_rules": endpoint.body_rules,
|
||||
"max_retries": endpoint.max_retries.unwrap_or(2),
|
||||
"is_active": endpoint.is_active,
|
||||
"config": endpoint.config,
|
||||
"proxy": masked_proxy_value(endpoint.proxy.as_ref()),
|
||||
"format_acceptance_config": endpoint.format_acceptance_config,
|
||||
"total_keys": total_keys,
|
||||
"active_keys": active_keys,
|
||||
"created_at": endpoint_timestamp_or_now(endpoint.created_at_unix_secs, now_unix_secs),
|
||||
"updated_at": endpoint_timestamp_or_now(endpoint.updated_at_unix_secs, now_unix_secs),
|
||||
})
|
||||
}
|
||||
@@ -1,119 +1,11 @@
|
||||
use super::{
|
||||
admin_clear_oauth_invalid_key_id, admin_export_key_id, admin_provider_id_for_refresh_quota,
|
||||
admin_reveal_key_id, admin_update_key_id, build_admin_provider_key_response,
|
||||
INTERNAL_GATEWAY_PATH_PREFIXES,
|
||||
};
|
||||
pub(crate) mod shared;
|
||||
|
||||
mod adaptive;
|
||||
mod api_keys;
|
||||
mod billing;
|
||||
mod catalog_write_helpers;
|
||||
mod core;
|
||||
mod endpoints;
|
||||
pub(crate) mod endpoints_health_helpers;
|
||||
mod gemini_files;
|
||||
mod global_models;
|
||||
mod ldap;
|
||||
pub(crate) mod misc_helpers;
|
||||
mod models_helpers;
|
||||
mod monitoring;
|
||||
mod oauth_helpers;
|
||||
mod payments;
|
||||
mod pool;
|
||||
mod provider_models;
|
||||
#[path = "provider_oauth/dispatch.rs"]
|
||||
mod provider_oauth_dispatch;
|
||||
#[path = "provider_oauth/quota.rs"]
|
||||
mod provider_oauth_quota;
|
||||
#[path = "provider_oauth/refresh.rs"]
|
||||
pub(crate) mod provider_oauth_refresh;
|
||||
#[path = "provider_oauth/state.rs"]
|
||||
mod provider_oauth_state;
|
||||
pub(crate) mod provider_ops;
|
||||
mod provider_query;
|
||||
mod provider_strategy;
|
||||
mod providers;
|
||||
mod providers_helpers;
|
||||
mod proxy_nodes;
|
||||
mod security;
|
||||
pub(crate) mod stats;
|
||||
mod usage;
|
||||
mod users;
|
||||
mod video_tasks;
|
||||
mod wallets;
|
||||
|
||||
pub(crate) use self::adaptive::maybe_build_local_admin_adaptive_response;
|
||||
use self::adaptive::*;
|
||||
pub(crate) use self::api_keys::maybe_build_local_admin_api_keys_response;
|
||||
use self::api_keys::*;
|
||||
pub(crate) use self::billing::maybe_build_local_admin_billing_response;
|
||||
use self::billing::*;
|
||||
use self::catalog_write_helpers::*;
|
||||
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::*;
|
||||
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::{
|
||||
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_response;
|
||||
use self::oauth_helpers::*;
|
||||
pub(crate) use self::payments::maybe_build_local_admin_payments_response;
|
||||
use self::payments::*;
|
||||
pub(crate) use self::pool::maybe_build_local_admin_pool_response;
|
||||
use self::pool::*;
|
||||
pub(crate) use self::provider_models::maybe_build_local_admin_provider_models_response;
|
||||
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::{
|
||||
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;
|
||||
use self::provider_query::*;
|
||||
pub(crate) use self::provider_strategy::maybe_build_local_admin_provider_strategy_response;
|
||||
use self::provider_strategy::*;
|
||||
pub(crate) use self::providers::maybe_build_local_admin_providers_response;
|
||||
use self::providers::*;
|
||||
use self::providers_helpers::*;
|
||||
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::*;
|
||||
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::*;
|
||||
pub(crate) use self::users::maybe_build_local_admin_users_response;
|
||||
use self::users::*;
|
||||
pub(crate) use self::video_tasks::maybe_build_local_admin_video_tasks_response;
|
||||
use self::video_tasks::*;
|
||||
pub(crate) use self::wallets::maybe_build_local_admin_wallets_response;
|
||||
use self::wallets::*;
|
||||
pub(crate) mod auth;
|
||||
pub(crate) mod billing;
|
||||
pub(crate) mod endpoint;
|
||||
pub(crate) mod features;
|
||||
pub(crate) mod model;
|
||||
pub(crate) mod observability;
|
||||
pub(crate) mod provider;
|
||||
pub(crate) mod system;
|
||||
pub(crate) mod users;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use super::super::{
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::model::{
|
||||
build_admin_model_catalog_payload, clear_admin_external_models_cache,
|
||||
read_admin_external_models_cache,
|
||||
};
|
||||
use super::build_admin_model_catalog_data_unavailable_response;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -13,7 +12,17 @@ use axum::{
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) async fn maybe_build_local_admin_core_model_response(
|
||||
const ADMIN_MODEL_CATALOG_DATA_UNAVAILABLE_DETAIL: &str = "Admin model catalog data unavailable";
|
||||
|
||||
fn build_admin_model_catalog_data_unavailable_response() -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(json!({ "detail": ADMIN_MODEL_CATALOG_DATA_UNAVAILABLE_DETAIL })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_model_catalog_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
231
apps/aether-gateway/src/handlers/admin/model/external_cache.rs
Normal file
231
apps/aether-gateway/src/handlers/admin/model/external_cache.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use crate::handlers::shared::mark_external_models_official_providers;
|
||||
use crate::{AppState, GatewayError};
|
||||
use serde_json::json;
|
||||
use tracing::warn;
|
||||
|
||||
const ADMIN_EXTERNAL_MODELS_CACHE_KEY: &str = "aether:external:models_dev";
|
||||
const ADMIN_EXTERNAL_MODELS_CACHE_TTL_SECS: u64 = 15 * 60;
|
||||
const ADMIN_EXTERNAL_MODELS_SOURCE_URL_ENV: &str = "AETHER_GATEWAY_EXTERNAL_MODELS_URL";
|
||||
const ADMIN_EXTERNAL_MODELS_SOURCE_URL_DEFAULT: &str = "https://models.dev/api.json";
|
||||
|
||||
fn admin_external_models_source_url() -> String {
|
||||
std::env::var(ADMIN_EXTERNAL_MODELS_SOURCE_URL_ENV)
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| ADMIN_EXTERNAL_MODELS_SOURCE_URL_DEFAULT.to_string())
|
||||
}
|
||||
|
||||
fn normalize_admin_external_models_payload(payload: serde_json::Value) -> serde_json::Value {
|
||||
mark_external_models_official_providers(&payload).unwrap_or(payload)
|
||||
}
|
||||
|
||||
async fn store_admin_external_models_cache(
|
||||
state: &AppState,
|
||||
payload: &serde_json::Value,
|
||||
) -> Result<(), GatewayError> {
|
||||
let Some(runner) = state.redis_kv_runner() else {
|
||||
return Ok(());
|
||||
};
|
||||
let serialized =
|
||||
serde_json::to_string(payload).map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
runner
|
||||
.setex(
|
||||
ADMIN_EXTERNAL_MODELS_CACHE_KEY,
|
||||
&serialized,
|
||||
Some(ADMIN_EXTERNAL_MODELS_CACHE_TTL_SECS),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_admin_external_models_from_source(
|
||||
state: &AppState,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let url = admin_external_models_source_url();
|
||||
let response = state
|
||||
.client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
let response = response
|
||||
.error_for_status()
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
let payload = response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
Ok(normalize_admin_external_models_payload(payload))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_external_models_cache(
|
||||
state: &AppState,
|
||||
) -> Result<Option<serde_json::Value>, GatewayError> {
|
||||
if let Some(runner) = state.redis_kv_runner() {
|
||||
match runner.client().get_multiplexed_async_connection().await {
|
||||
Ok(mut connection) => {
|
||||
let namespaced_key = runner.keyspace().key(ADMIN_EXTERNAL_MODELS_CACHE_KEY);
|
||||
match redis::cmd("GET")
|
||||
.arg(&namespaced_key)
|
||||
.query_async::<Option<String>>(&mut connection)
|
||||
.await
|
||||
{
|
||||
Ok(Some(raw)) => match serde_json::from_str::<serde_json::Value>(&raw) {
|
||||
Ok(payload) => {
|
||||
let payload = normalize_admin_external_models_payload(payload);
|
||||
if let Err(err) =
|
||||
store_admin_external_models_cache(state, &payload).await
|
||||
{
|
||||
warn!(error = ?err, "failed to refresh external models cache ttl");
|
||||
}
|
||||
return Ok(Some(payload));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(error = %err, "failed to parse cached external models payload");
|
||||
}
|
||||
},
|
||||
Ok(None) => {}
|
||||
Err(err) => {
|
||||
warn!(error = %err, "failed to read external models cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(error = %err, "failed to connect to redis for external models cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match fetch_admin_external_models_from_source(state).await {
|
||||
Ok(payload) => {
|
||||
if let Err(err) = store_admin_external_models_cache(state, &payload).await {
|
||||
warn!(error = ?err, "failed to store fetched external models cache");
|
||||
}
|
||||
Ok(Some(payload))
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(error = ?err, "failed to fetch external models catalog");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_admin_external_models_cache(
|
||||
state: &AppState,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let Some(runner) = state.redis_kv_runner() else {
|
||||
return Ok(json!({
|
||||
"cleared": false,
|
||||
"message": "Redis 未启用",
|
||||
}));
|
||||
};
|
||||
let deleted = runner
|
||||
.del(ADMIN_EXTERNAL_MODELS_CACHE_KEY)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
Ok(json!({
|
||||
"cleared": deleted > 0,
|
||||
"message": if deleted > 0 { "缓存已清除" } else { "缓存不存在" },
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
admin_external_models_source_url, normalize_admin_external_models_payload,
|
||||
read_admin_external_models_cache, ADMIN_EXTERNAL_MODELS_SOURCE_URL_ENV,
|
||||
};
|
||||
use crate::tests::{start_server, AppState};
|
||||
use axum::routing::get;
|
||||
use axum::{Json, Router};
|
||||
use serde_json::json;
|
||||
|
||||
struct TestEnvVarGuard {
|
||||
key: &'static str,
|
||||
previous: Option<String>,
|
||||
}
|
||||
|
||||
impl Drop for TestEnvVarGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(previous) = self.previous.as_deref() {
|
||||
std::env::set_var(self.key, previous);
|
||||
} else {
|
||||
std::env::remove_var(self.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_test_env_var(key: &'static str, value: &str) -> TestEnvVarGuard {
|
||||
let previous = std::env::var(key).ok();
|
||||
std::env::set_var(key, value);
|
||||
TestEnvVarGuard { key, previous }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_external_models_payload_with_official_flags() {
|
||||
let payload = json!({
|
||||
"openai": {
|
||||
"name": "OpenAI",
|
||||
"models": {}
|
||||
},
|
||||
"openrouter": {
|
||||
"name": "OpenRouter",
|
||||
"models": {}
|
||||
}
|
||||
});
|
||||
|
||||
let normalized = normalize_admin_external_models_payload(payload);
|
||||
|
||||
assert_eq!(normalized["openai"]["official"], json!(true));
|
||||
assert_eq!(normalized["openrouter"]["official"], json!(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_models_source_url_uses_env_override_when_present() {
|
||||
let _guard = set_test_env_var(
|
||||
ADMIN_EXTERNAL_MODELS_SOURCE_URL_ENV,
|
||||
"http://127.0.0.1:12345/api",
|
||||
);
|
||||
assert_eq!(
|
||||
admin_external_models_source_url(),
|
||||
"http://127.0.0.1:12345/api"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_external_models_fetches_remote_payload_when_cache_missing() {
|
||||
let upstream = Router::new().route(
|
||||
"/api.json",
|
||||
get(|| async {
|
||||
Json(json!({
|
||||
"openai": {
|
||||
"name": "OpenAI",
|
||||
"models": {
|
||||
"gpt-5": {
|
||||
"name": "GPT-5"
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}),
|
||||
);
|
||||
let (upstream_url, upstream_handle) = start_server(upstream).await;
|
||||
let _guard = set_test_env_var(
|
||||
ADMIN_EXTERNAL_MODELS_SOURCE_URL_ENV,
|
||||
&format!("{upstream_url}/api.json"),
|
||||
);
|
||||
|
||||
let state = AppState::new().expect("gateway should build");
|
||||
let payload = read_admin_external_models_cache(&state)
|
||||
.await
|
||||
.expect("external models read should succeed")
|
||||
.expect("payload should be fetched");
|
||||
|
||||
assert_eq!(payload["openai"]["official"], json!(true));
|
||||
assert_eq!(payload["openai"]["models"]["gpt-5"]["name"], json!("GPT-5"));
|
||||
|
||||
upstream_handle.abort();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
use super::{
|
||||
use super::payloads::{
|
||||
admin_provider_model_effective_capability, admin_provider_model_effective_input_price,
|
||||
admin_provider_model_effective_output_price, model_tiered_pricing_first_tier_value,
|
||||
timestamp_or_now,
|
||||
};
|
||||
use crate::handlers::json_string_list;
|
||||
use crate::handlers::admin::shared::json_string_list;
|
||||
use crate::AppState;
|
||||
use aether_data::repository::global_models::{
|
||||
use aether_data_contracts::repository::global_models::{
|
||||
AdminGlobalModelListQuery, StoredAdminGlobalModel, StoredAdminProviderModel,
|
||||
};
|
||||
use futures_util::stream::{self, StreamExt};
|
||||
@@ -0,0 +1,5 @@
|
||||
mod helpers;
|
||||
mod routes;
|
||||
|
||||
use self::helpers::*;
|
||||
pub(crate) use self::routes::maybe_build_local_admin_global_models_response;
|
||||
@@ -5,18 +5,19 @@ use super::super::{
|
||||
build_admin_global_model_update_record, build_admin_global_models_payload,
|
||||
resolve_admin_global_model_by_id_or_err,
|
||||
};
|
||||
use super::global_models_helpers::{
|
||||
use super::helpers::{
|
||||
build_admin_global_models_data_unavailable_response,
|
||||
ADMIN_GLOBAL_MODELS_DATA_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
|
||||
use crate::handlers::{
|
||||
use crate::handlers::admin::model::shared::{
|
||||
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,
|
||||
AdminBatchAssignToProvidersRequest, AdminBatchDeleteIdsRequest, AdminGlobalModelCreateRequest,
|
||||
AdminGlobalModelUpdateRequest,
|
||||
};
|
||||
use crate::handlers::admin::shared::attach_admin_audit_response;
|
||||
use crate::handlers::admin::shared::{query_param_optional_bool, query_param_value};
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
36
apps/aether-gateway/src/handlers/admin/model/mod.rs
Normal file
36
apps/aether-gateway/src/handlers/admin/model/mod.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
pub(crate) mod shared;
|
||||
|
||||
mod catalog_routes;
|
||||
mod external_cache;
|
||||
mod global;
|
||||
mod global_models;
|
||||
mod payloads;
|
||||
mod routing;
|
||||
mod write;
|
||||
|
||||
pub(crate) use self::catalog_routes::maybe_build_local_admin_model_catalog_response;
|
||||
pub(crate) use self::external_cache::{
|
||||
clear_admin_external_models_cache, read_admin_external_models_cache,
|
||||
};
|
||||
pub(crate) use self::global::{
|
||||
build_admin_global_model_payload, build_admin_global_model_providers_payload,
|
||||
build_admin_global_model_response, build_admin_global_models_payload,
|
||||
build_admin_model_catalog_payload, resolve_admin_global_model_by_id_or_err,
|
||||
};
|
||||
pub(crate) use self::global_models::maybe_build_local_admin_global_models_response;
|
||||
pub(crate) use self::payloads::{
|
||||
admin_provider_model_effective_capability, admin_provider_model_effective_input_price,
|
||||
admin_provider_model_effective_output_price, admin_provider_model_name_exists,
|
||||
build_admin_provider_model_payload, build_admin_provider_model_response,
|
||||
build_admin_provider_models_payload, normalize_optional_price,
|
||||
normalize_required_trimmed_string,
|
||||
};
|
||||
pub(crate) use self::routing::{
|
||||
build_admin_assign_global_model_to_providers_payload, build_admin_global_model_routing_payload,
|
||||
};
|
||||
pub(crate) use self::write::{
|
||||
build_admin_batch_assign_global_models_payload, build_admin_global_model_create_record,
|
||||
build_admin_global_model_update_record, build_admin_import_provider_models_payload,
|
||||
build_admin_provider_available_source_models_payload, build_admin_provider_model_create_record,
|
||||
build_admin_provider_model_update_record,
|
||||
};
|
||||
@@ -1,39 +1,12 @@
|
||||
use crate::handlers::unix_secs_to_rfc3339;
|
||||
use crate::handlers::admin::shared::unix_secs_to_rfc3339;
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data::repository::global_models::{
|
||||
use aether_data_contracts::repository::global_models::{
|
||||
AdminProviderModelListQuery, StoredAdminProviderModel,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[path = "models/external_cache.rs"]
|
||||
mod models_external_cache;
|
||||
#[path = "models/global.rs"]
|
||||
mod models_global;
|
||||
#[path = "models/routing.rs"]
|
||||
mod models_routing;
|
||||
#[path = "models/write.rs"]
|
||||
mod models_write;
|
||||
|
||||
pub(crate) use self::models_external_cache::{
|
||||
clear_admin_external_models_cache, read_admin_external_models_cache,
|
||||
};
|
||||
pub(crate) use self::models_global::{
|
||||
build_admin_global_model_payload, build_admin_global_model_providers_payload,
|
||||
build_admin_global_model_response, build_admin_global_models_payload,
|
||||
build_admin_model_catalog_payload, resolve_admin_global_model_by_id_or_err,
|
||||
};
|
||||
pub(crate) use self::models_routing::{
|
||||
build_admin_assign_global_model_to_providers_payload, build_admin_global_model_routing_payload,
|
||||
};
|
||||
pub(crate) use self::models_write::{
|
||||
build_admin_batch_assign_global_models_payload, build_admin_global_model_create_record,
|
||||
build_admin_global_model_update_record, build_admin_import_provider_models_payload,
|
||||
build_admin_provider_available_source_models_payload, build_admin_provider_model_create_record,
|
||||
build_admin_provider_model_update_record,
|
||||
};
|
||||
|
||||
fn model_tiered_pricing_first_tier_value(
|
||||
pub(crate) fn model_tiered_pricing_first_tier_value(
|
||||
tiered_pricing: Option<&serde_json::Value>,
|
||||
field_name: &str,
|
||||
) -> Option<f64> {
|
||||
@@ -94,13 +67,16 @@ fn merge_admin_provider_model_effective_config(
|
||||
}
|
||||
}
|
||||
|
||||
fn timestamp_or_now(value: Option<u64>, now_unix_secs: u64) -> serde_json::Value {
|
||||
pub(crate) fn timestamp_or_now(value: Option<u64>, now_unix_secs: u64) -> serde_json::Value {
|
||||
unix_secs_to_rfc3339(value.unwrap_or(now_unix_secs))
|
||||
.map(serde_json::Value::String)
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
}
|
||||
|
||||
fn normalize_required_trimmed_string(value: &str, field_name: &str) -> Result<String, String> {
|
||||
pub(crate) fn normalize_required_trimmed_string(
|
||||
value: &str,
|
||||
field_name: &str,
|
||||
) -> Result<String, String> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(format!("{field_name} 不能为空"));
|
||||
@@ -108,7 +84,10 @@ fn normalize_required_trimmed_string(value: &str, field_name: &str) -> Result<St
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
|
||||
fn normalize_optional_price(value: Option<f64>, field_name: &str) -> Result<Option<f64>, String> {
|
||||
pub(crate) fn normalize_optional_price(
|
||||
value: Option<f64>,
|
||||
field_name: &str,
|
||||
) -> Result<Option<f64>, String> {
|
||||
let Some(value) = value else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -118,7 +97,9 @@ fn normalize_optional_price(value: Option<f64>, field_name: &str) -> Result<Opti
|
||||
Ok(Some(value))
|
||||
}
|
||||
|
||||
fn admin_provider_model_effective_input_price(model: &StoredAdminProviderModel) -> Option<f64> {
|
||||
pub(crate) fn admin_provider_model_effective_input_price(
|
||||
model: &StoredAdminProviderModel,
|
||||
) -> Option<f64> {
|
||||
model_tiered_pricing_first_tier_value(model.tiered_pricing.as_ref(), "input_price_per_1m")
|
||||
.or_else(|| {
|
||||
model_tiered_pricing_first_tier_value(
|
||||
@@ -128,7 +109,9 @@ fn admin_provider_model_effective_input_price(model: &StoredAdminProviderModel)
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_provider_model_effective_output_price(model: &StoredAdminProviderModel) -> Option<f64> {
|
||||
pub(crate) fn admin_provider_model_effective_output_price(
|
||||
model: &StoredAdminProviderModel,
|
||||
) -> Option<f64> {
|
||||
model_tiered_pricing_first_tier_value(model.tiered_pricing.as_ref(), "output_price_per_1m")
|
||||
.or_else(|| {
|
||||
model_tiered_pricing_first_tier_value(
|
||||
@@ -138,7 +121,7 @@ fn admin_provider_model_effective_output_price(model: &StoredAdminProviderModel)
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_provider_model_effective_capability(
|
||||
pub(crate) fn admin_provider_model_effective_capability(
|
||||
model: &StoredAdminProviderModel,
|
||||
capability: &str,
|
||||
) -> bool {
|
||||
@@ -202,10 +185,19 @@ pub(crate) fn build_admin_provider_model_response(
|
||||
"supports_extended_thinking": model.supports_extended_thinking,
|
||||
"supports_image_generation": model.supports_image_generation,
|
||||
"effective_supports_vision": admin_provider_model_effective_capability(model, "vision"),
|
||||
"effective_supports_function_calling": admin_provider_model_effective_capability(model, "function_calling"),
|
||||
"effective_supports_function_calling": admin_provider_model_effective_capability(
|
||||
model,
|
||||
"function_calling",
|
||||
),
|
||||
"effective_supports_streaming": admin_provider_model_effective_capability(model, "streaming"),
|
||||
"effective_supports_extended_thinking": admin_provider_model_effective_capability(model, "extended_thinking"),
|
||||
"effective_supports_image_generation": admin_provider_model_effective_capability(model, "image_generation"),
|
||||
"effective_supports_extended_thinking": admin_provider_model_effective_capability(
|
||||
model,
|
||||
"extended_thinking",
|
||||
),
|
||||
"effective_supports_image_generation": admin_provider_model_effective_capability(
|
||||
model,
|
||||
"image_generation",
|
||||
),
|
||||
"is_active": model.is_active,
|
||||
"is_available": model.is_available,
|
||||
"config": model.config.clone(),
|
||||
@@ -1,14 +1,15 @@
|
||||
use super::resolve_admin_global_model_by_id_or_err;
|
||||
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::handlers::admin::shared::{
|
||||
json_string_list, masked_catalog_api_key, provider_catalog_key_supports_format,
|
||||
};
|
||||
use crate::AppState;
|
||||
use aether_data::repository::global_models::{
|
||||
use aether_data_contracts::repository::global_models::{
|
||||
AdminProviderModelListQuery, UpsertAdminProviderModelRecord,
|
||||
};
|
||||
use aether_data::repository::provider_catalog::{
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
};
|
||||
use aether_scheduler_core::{is_provider_key_circuit_open, provider_key_health_score};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use uuid::Uuid;
|
||||
@@ -0,0 +1,5 @@
|
||||
mod paths;
|
||||
mod payloads;
|
||||
|
||||
pub(crate) use self::paths::*;
|
||||
pub(crate) use self::payloads::*;
|
||||
46
apps/aether-gateway/src/handlers/admin/model/shared/paths.rs
Normal file
46
apps/aether-gateway/src/handlers/admin/model/shared/paths.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
pub(crate) fn is_admin_global_models_root(request_path: &str) -> bool {
|
||||
matches!(
|
||||
request_path,
|
||||
"/api/admin/models/global" | "/api/admin/models/global/"
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_global_model_id_from_path(request_path: &str) -> Option<String> {
|
||||
let raw = request_path.strip_prefix("/api/admin/models/global/")?;
|
||||
let normalized = raw.trim().trim_matches('/');
|
||||
if normalized.is_empty()
|
||||
|| normalized.contains('/')
|
||||
|| normalized == "batch-delete"
|
||||
|| normalized.ends_with("/providers")
|
||||
|| normalized.ends_with("/assign-to-providers")
|
||||
|| normalized.ends_with("/routing")
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(normalized.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn admin_global_model_assign_to_providers_id(request_path: &str) -> Option<String> {
|
||||
request_path
|
||||
.strip_prefix("/api/admin/models/global/")?
|
||||
.strip_suffix("/assign-to-providers")
|
||||
.map(|value| value.trim().trim_matches('/').to_string())
|
||||
.filter(|value| !value.is_empty() && !value.contains('/'))
|
||||
}
|
||||
|
||||
pub(crate) fn admin_global_model_routing_id(request_path: &str) -> Option<String> {
|
||||
request_path
|
||||
.strip_prefix("/api/admin/models/global/")?
|
||||
.strip_suffix("/routing")
|
||||
.map(|value| value.trim().trim_matches('/').to_string())
|
||||
.filter(|value| !value.is_empty() && !value.contains('/'))
|
||||
}
|
||||
|
||||
pub(crate) fn admin_global_model_providers_id(request_path: &str) -> Option<String> {
|
||||
request_path
|
||||
.strip_prefix("/api/admin/models/global/")?
|
||||
.strip_suffix("/providers")
|
||||
.map(|value| value.trim().trim_matches('/').to_string())
|
||||
.filter(|value| !value.is_empty() && !value.contains('/'))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct AdminGlobalModelCreateRequest {
|
||||
pub(crate) name: String,
|
||||
pub(crate) display_name: String,
|
||||
#[serde(default)]
|
||||
pub(crate) default_price_per_request: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub(crate) default_tiered_pricing: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub(crate) supported_capabilities: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub(crate) config: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub(crate) is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct AdminGlobalModelUpdateRequest {
|
||||
#[serde(default)]
|
||||
pub(crate) display_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) is_active: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub(crate) default_price_per_request: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub(crate) default_tiered_pricing: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub(crate) supported_capabilities: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub(crate) config: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct AdminBatchDeleteIdsRequest {
|
||||
pub(crate) ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct AdminBatchAssignToProvidersRequest {
|
||||
pub(crate) provider_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) create_models: Option<bool>,
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
use super::super::{normalize_json_array, normalize_json_object, normalize_string_list};
|
||||
use super::{
|
||||
admin_provider_model_effective_capability, admin_provider_model_effective_input_price,
|
||||
admin_provider_model_effective_output_price, admin_provider_model_name_exists,
|
||||
normalize_optional_price, normalize_required_trimmed_string,
|
||||
resolve_admin_global_model_by_id_or_err,
|
||||
};
|
||||
use crate::handlers::{
|
||||
AdminGlobalModelCreateRequest, AdminGlobalModelUpdateRequest, AdminImportProviderModelsRequest,
|
||||
AdminProviderModelCreateRequest, AdminProviderModelUpdateRequest,
|
||||
use crate::handlers::admin::model::shared::{
|
||||
AdminGlobalModelCreateRequest, AdminGlobalModelUpdateRequest,
|
||||
};
|
||||
use crate::handlers::admin::provider::shared::{
|
||||
AdminImportProviderModelsRequest, AdminProviderModelCreateRequest,
|
||||
AdminProviderModelUpdateRequest,
|
||||
};
|
||||
use crate::handlers::admin::shared::{
|
||||
normalize_json_array, normalize_json_object, normalize_string_list,
|
||||
};
|
||||
use crate::AppState;
|
||||
use aether_data::repository::global_models::{
|
||||
use aether_data_contracts::repository::global_models::{
|
||||
AdminProviderModelListQuery, CreateAdminGlobalModelRecord, StoredAdminGlobalModel,
|
||||
StoredAdminProviderModel, UpdateAdminGlobalModelRecord, UpsertAdminProviderModelRecord,
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
use crate::handlers::{
|
||||
ADMIN_EXTERNAL_MODELS_CACHE_KEY, ADMIN_EXTERNAL_MODELS_CACHE_TTL_SECS,
|
||||
OFFICIAL_EXTERNAL_MODEL_PROVIDERS,
|
||||
};
|
||||
use crate::{AppState, GatewayError};
|
||||
use serde_json::json;
|
||||
|
||||
fn mark_admin_external_models_official(mut payload: serde_json::Value) -> serde_json::Value {
|
||||
let Some(models) = payload
|
||||
.get_mut("models")
|
||||
.and_then(serde_json::Value::as_array_mut)
|
||||
else {
|
||||
return payload;
|
||||
};
|
||||
for item in models {
|
||||
let provider_name = item
|
||||
.get("provider")
|
||||
.or_else(|| item.get("provider_name"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
if let Some(object) = item.as_object_mut() {
|
||||
object.insert(
|
||||
"official".to_string(),
|
||||
json!(OFFICIAL_EXTERNAL_MODEL_PROVIDERS.contains(&provider_name.as_str())),
|
||||
);
|
||||
}
|
||||
}
|
||||
payload
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_external_models_cache(
|
||||
state: &AppState,
|
||||
) -> Result<Option<serde_json::Value>, GatewayError> {
|
||||
let Some(runner) = state.redis_kv_runner() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut connection = runner
|
||||
.client()
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
let namespaced_key = runner.keyspace().key(ADMIN_EXTERNAL_MODELS_CACHE_KEY);
|
||||
let raw = redis::cmd("GET")
|
||||
.arg(&namespaced_key)
|
||||
.query_async::<Option<String>>(&mut connection)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
let Some(raw) = raw else {
|
||||
return Ok(None);
|
||||
};
|
||||
let payload = serde_json::from_str::<serde_json::Value>(&raw)
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
let payload = mark_admin_external_models_official(payload);
|
||||
let serialized =
|
||||
serde_json::to_string(&payload).map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
runner
|
||||
.setex(
|
||||
ADMIN_EXTERNAL_MODELS_CACHE_KEY,
|
||||
&serialized,
|
||||
Some(ADMIN_EXTERNAL_MODELS_CACHE_TTL_SECS),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
Ok(Some(payload))
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_admin_external_models_cache(
|
||||
state: &AppState,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let Some(runner) = state.redis_kv_runner() else {
|
||||
return Ok(json!({
|
||||
"cleared": false,
|
||||
"message": "Redis 未启用",
|
||||
}));
|
||||
};
|
||||
let deleted = runner
|
||||
.del(ADMIN_EXTERNAL_MODELS_CACHE_KEY)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
Ok(json!({
|
||||
"cleared": deleted > 0,
|
||||
"message": if deleted > 0 { "缓存已清除" } else { "缓存不存在" },
|
||||
}))
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
use super::INTERNAL_GATEWAY_PATH_PREFIXES;
|
||||
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;
|
||||
use axum::{body::Body, response::Response, Json};
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
|
||||
#[path = "monitoring/activity.rs"]
|
||||
mod activity;
|
||||
#[path = "monitoring/cache.rs"]
|
||||
mod cache;
|
||||
#[path = "monitoring/cache_affinity.rs"]
|
||||
mod cache_affinity;
|
||||
#[path = "monitoring/cache_identity.rs"]
|
||||
mod cache_identity;
|
||||
#[path = "monitoring/cache_payloads.rs"]
|
||||
mod cache_payloads;
|
||||
#[path = "monitoring/cache_route_helpers.rs"]
|
||||
mod cache_route_helpers;
|
||||
#[path = "monitoring/cache_store.rs"]
|
||||
mod cache_store;
|
||||
#[path = "monitoring/common.rs"]
|
||||
mod common;
|
||||
#[path = "monitoring/resilience.rs"]
|
||||
mod resilience;
|
||||
#[path = "monitoring/route_filters.rs"]
|
||||
mod route_filters;
|
||||
#[path = "monitoring/routes.rs"]
|
||||
mod routes;
|
||||
#[cfg(test)]
|
||||
#[path = "monitoring/test_support.rs"]
|
||||
mod test_support;
|
||||
#[path = "monitoring/trace.rs"]
|
||||
mod trace;
|
||||
use self::activity::{
|
||||
build_admin_monitoring_audit_logs_response,
|
||||
build_admin_monitoring_suspicious_activities_response,
|
||||
build_admin_monitoring_system_status_response, build_admin_monitoring_user_behavior_response,
|
||||
};
|
||||
use self::cache::{
|
||||
build_admin_monitoring_cache_affinities_response,
|
||||
build_admin_monitoring_cache_affinity_delete_response,
|
||||
build_admin_monitoring_cache_affinity_response, build_admin_monitoring_cache_config_response,
|
||||
build_admin_monitoring_cache_flush_response, build_admin_monitoring_cache_metrics_response,
|
||||
build_admin_monitoring_cache_provider_delete_response,
|
||||
build_admin_monitoring_cache_stats_response,
|
||||
build_admin_monitoring_cache_users_delete_response,
|
||||
build_admin_monitoring_model_mapping_delete_model_response,
|
||||
build_admin_monitoring_model_mapping_delete_provider_response,
|
||||
build_admin_monitoring_model_mapping_delete_response,
|
||||
build_admin_monitoring_model_mapping_stats_response,
|
||||
build_admin_monitoring_redis_cache_categories_response,
|
||||
build_admin_monitoring_redis_keys_delete_response,
|
||||
};
|
||||
use self::cache_affinity::{
|
||||
admin_monitoring_cache_affinity_record, admin_monitoring_scheduler_affinity_cache_key,
|
||||
clear_admin_monitoring_scheduler_affinity_entries,
|
||||
delete_admin_monitoring_cache_affinity_entries_for_tests,
|
||||
delete_admin_monitoring_cache_affinity_raw_keys,
|
||||
};
|
||||
use self::cache_identity::{
|
||||
admin_monitoring_find_user_summary_by_id, admin_monitoring_list_export_api_key_records_by_ids,
|
||||
admin_monitoring_load_affinity_identity_maps,
|
||||
};
|
||||
use self::cache_payloads::{
|
||||
admin_monitoring_cache_affinity_sort_value, admin_monitoring_masked_provider_key_prefix,
|
||||
admin_monitoring_masked_user_api_key_prefix,
|
||||
};
|
||||
use self::cache_route_helpers::{
|
||||
admin_monitoring_cache_affinity_delete_params_from_path,
|
||||
admin_monitoring_cache_affinity_not_found_response,
|
||||
admin_monitoring_cache_affinity_unavailable_response,
|
||||
admin_monitoring_cache_affinity_user_identifier_from_path,
|
||||
admin_monitoring_cache_model_mapping_provider_params_from_path,
|
||||
admin_monitoring_cache_model_name_from_path, admin_monitoring_cache_provider_id_from_path,
|
||||
admin_monitoring_cache_redis_category_from_path,
|
||||
admin_monitoring_cache_users_not_found_response,
|
||||
admin_monitoring_cache_users_user_identifier_from_path,
|
||||
admin_monitoring_redis_unavailable_response, parse_admin_monitoring_keyword_filter,
|
||||
};
|
||||
use self::cache_store::{
|
||||
admin_monitoring_has_test_redis_keys, build_admin_monitoring_cache_snapshot,
|
||||
delete_admin_monitoring_namespaced_keys, list_admin_monitoring_cache_affinity_records,
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys,
|
||||
list_admin_monitoring_namespaced_keys, load_admin_monitoring_cache_affinity_entries_for_tests,
|
||||
};
|
||||
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, AdminMonitoringResilienceSnapshot,
|
||||
};
|
||||
use self::resilience::{
|
||||
build_admin_monitoring_reset_error_stats_response,
|
||||
build_admin_monitoring_resilience_circuit_history_response,
|
||||
build_admin_monitoring_resilience_status_response,
|
||||
};
|
||||
use self::route_filters::{
|
||||
admin_monitoring_escape_like_pattern, parse_admin_monitoring_days,
|
||||
parse_admin_monitoring_event_type_filter, parse_admin_monitoring_hours,
|
||||
parse_admin_monitoring_limit, parse_admin_monitoring_offset,
|
||||
parse_admin_monitoring_username_filter,
|
||||
};
|
||||
use self::routes::{
|
||||
match_admin_monitoring_route, AdminMonitoringRoute,
|
||||
};
|
||||
use self::trace::{
|
||||
build_admin_monitoring_trace_provider_stats_response,
|
||||
build_admin_monitoring_trace_request_response,
|
||||
};
|
||||
|
||||
const ADMIN_MONITORING_DATA_UNAVAILABLE_DETAIL: &str = "Admin monitoring data unavailable";
|
||||
const ADMIN_MONITORING_CACHE_AFFINITY_REDIS_REQUIRED_DETAIL: &str =
|
||||
"Redis未初始化,无法获取缓存亲和性";
|
||||
const ADMIN_MONITORING_REDIS_REQUIRED_DETAIL: &str = "Redis 未启用";
|
||||
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_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
routes::maybe_build_local_admin_monitoring_response(state, request_context).await
|
||||
}
|
||||
const ADMIN_MONITORING_DYNAMIC_RESERVATION_PROBE_RESERVATION: f64 = 0.1;
|
||||
const ADMIN_MONITORING_DYNAMIC_RESERVATION_STABLE_MIN_RESERVATION: f64 = 0.1;
|
||||
const ADMIN_MONITORING_DYNAMIC_RESERVATION_STABLE_MAX_RESERVATION: f64 = 0.35;
|
||||
const ADMIN_MONITORING_DYNAMIC_RESERVATION_LOW_LOAD_THRESHOLD: f64 = 0.5;
|
||||
const ADMIN_MONITORING_DYNAMIC_RESERVATION_HIGH_LOAD_THRESHOLD: f64 = 0.8;
|
||||
const ADMIN_MONITORING_REDIS_CACHE_CATEGORIES: &[(&str, &str, &str, &str)] = &[
|
||||
(
|
||||
"upstream_models",
|
||||
"上游模型",
|
||||
"upstream_models:*",
|
||||
"Provider 上游获取的模型列表缓存",
|
||||
),
|
||||
("model_id", "模型 ID", "model:id:*", "Model 按 ID 缓存"),
|
||||
(
|
||||
"model_provider_global",
|
||||
"模型映射",
|
||||
"model:provider_global:*",
|
||||
"Provider-GlobalModel 模型映射缓存",
|
||||
),
|
||||
(
|
||||
"provider_mapping_preview",
|
||||
"映射预览",
|
||||
"admin:providers:mapping-preview:*",
|
||||
"Provider 详情页 mapping-preview 缓存",
|
||||
),
|
||||
(
|
||||
"global_model",
|
||||
"全局模型",
|
||||
"global_model:*",
|
||||
"GlobalModel 缓存(ID/名称/解析)",
|
||||
),
|
||||
(
|
||||
"models_list",
|
||||
"模型列表",
|
||||
"models:list:*",
|
||||
"/v1/models 端点模型列表缓存",
|
||||
),
|
||||
("user", "用户", "user:*", "用户信息缓存(ID/Email)"),
|
||||
(
|
||||
"apikey",
|
||||
"API Key",
|
||||
"apikey:*",
|
||||
"API Key 认证缓存(Hash/Auth)",
|
||||
),
|
||||
(
|
||||
"api_key_id",
|
||||
"API Key ID",
|
||||
"api_key:id:*",
|
||||
"API Key 按 ID 缓存",
|
||||
),
|
||||
(
|
||||
"cache_affinity",
|
||||
"缓存亲和性",
|
||||
"cache_affinity:*",
|
||||
"请求路由亲和性缓存",
|
||||
),
|
||||
(
|
||||
"provider_billing",
|
||||
"Provider 计费",
|
||||
"provider:billing_type:*",
|
||||
"Provider 计费类型缓存",
|
||||
),
|
||||
(
|
||||
"provider_rate",
|
||||
"Provider 费率",
|
||||
"provider_api_key:rate_multiplier:*",
|
||||
"ProviderAPIKey 费率倍数缓存",
|
||||
),
|
||||
(
|
||||
"provider_balance",
|
||||
"Provider 余额",
|
||||
"provider_ops:balance:*",
|
||||
"Provider 余额查询缓存",
|
||||
),
|
||||
("health", "健康检查", "health:*", "端点健康状态缓存"),
|
||||
(
|
||||
"endpoint_status",
|
||||
"端点状态",
|
||||
"endpoint_status:*",
|
||||
"用户端点状态缓存",
|
||||
),
|
||||
("dashboard", "仪表盘", "dashboard:*", "仪表盘统计缓存"),
|
||||
(
|
||||
"activity_heatmap",
|
||||
"活动热力图",
|
||||
"activity_heatmap:*",
|
||||
"用户活动热力图缓存",
|
||||
),
|
||||
(
|
||||
"gemini_files",
|
||||
"Gemini 文件映射",
|
||||
"gemini_files:*",
|
||||
"Gemini Files API 文件-Key 映射缓存",
|
||||
),
|
||||
(
|
||||
"provider_oauth",
|
||||
"OAuth 状态",
|
||||
"provider_oauth_state:*",
|
||||
"Provider OAuth 授权流程临时状态",
|
||||
),
|
||||
(
|
||||
"oauth_refresh_lock",
|
||||
"OAuth 刷新锁",
|
||||
"provider_oauth_refresh_lock:*",
|
||||
"OAuth Token 刷新分布式锁",
|
||||
),
|
||||
(
|
||||
"concurrency_lock",
|
||||
"并发锁",
|
||||
"concurrency:*",
|
||||
"请求并发控制锁",
|
||||
),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "monitoring/tests.rs"]
|
||||
mod tests;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,95 +0,0 @@
|
||||
use super::ADMIN_MONITORING_DATA_UNAVAILABLE_DETAIL;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) struct AdminMonitoringCacheSnapshot {
|
||||
pub(super) scheduler_name: String,
|
||||
pub(super) scheduling_mode: String,
|
||||
pub(super) provider_priority_mode: String,
|
||||
pub(super) storage_type: &'static str,
|
||||
pub(super) total_affinities: usize,
|
||||
pub(super) cache_hits: usize,
|
||||
pub(super) cache_misses: usize,
|
||||
pub(super) cache_hit_rate: f64,
|
||||
pub(super) provider_switches: usize,
|
||||
pub(super) key_switches: usize,
|
||||
pub(super) cache_invalidations: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct AdminMonitoringCacheAffinityRecord {
|
||||
pub(super) raw_key: String,
|
||||
pub(super) affinity_key: String,
|
||||
pub(super) api_format: String,
|
||||
pub(super) model_name: String,
|
||||
pub(super) provider_id: Option<String>,
|
||||
pub(super) endpoint_id: Option<String>,
|
||||
pub(super) key_id: Option<String>,
|
||||
pub(super) created_at: Option<serde_json::Value>,
|
||||
pub(super) expire_at: Option<serde_json::Value>,
|
||||
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,
|
||||
Json(json!({ "detail": ADMIN_MONITORING_DATA_UNAVAILABLE_DETAIL })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(super) fn admin_monitoring_bad_request_response(detail: impl Into<String>) -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": detail.into() })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(super) fn admin_monitoring_not_found_response(detail: &'static str) -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": detail })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(super) fn admin_monitoring_usage_is_error(
|
||||
item: &aether_data::repository::usage::StoredRequestUsageAudit,
|
||||
) -> bool {
|
||||
item.status_code.is_some_and(|value| value >= 400)
|
||||
|| item.status.trim().eq_ignore_ascii_case("failed")
|
||||
|| item.status.trim().eq_ignore_ascii_case("error")
|
||||
|| item.error_message.is_some()
|
||||
|| item.error_category.is_some()
|
||||
}
|
||||
|
||||
pub(super) fn admin_monitoring_user_behavior_user_id_from_path(
|
||||
request_path: &str,
|
||||
) -> Option<String> {
|
||||
let value = request_path
|
||||
.strip_prefix("/api/admin/monitoring/user-behavior/")?
|
||||
.trim()
|
||||
.trim_matches('/')
|
||||
.to_string();
|
||||
if value.is_empty() || value.contains('/') {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod monitoring;
|
||||
pub(crate) mod stats;
|
||||
mod usage;
|
||||
|
||||
pub(crate) use self::monitoring::maybe_build_local_admin_monitoring_response;
|
||||
pub(crate) use self::stats::maybe_build_local_admin_stats_response;
|
||||
pub(crate) use self::usage::maybe_build_local_admin_usage_response;
|
||||
@@ -1,14 +1,16 @@
|
||||
use super::INTERNAL_GATEWAY_PATH_PREFIXES;
|
||||
use super::{
|
||||
admin_monitoring_bad_request_response, admin_monitoring_escape_like_pattern,
|
||||
admin_monitoring_usage_is_error, admin_monitoring_user_behavior_user_id_from_path,
|
||||
parse_admin_monitoring_days, parse_admin_monitoring_event_type_filter,
|
||||
parse_admin_monitoring_hours, parse_admin_monitoring_limit, parse_admin_monitoring_offset,
|
||||
use super::responses::admin_monitoring_bad_request_response;
|
||||
use super::route_filters::{
|
||||
admin_monitoring_escape_like_pattern, parse_admin_monitoring_days,
|
||||
parse_admin_monitoring_event_type_filter, parse_admin_monitoring_hours,
|
||||
parse_admin_monitoring_limit, parse_admin_monitoring_offset,
|
||||
parse_admin_monitoring_username_filter,
|
||||
};
|
||||
use super::usage_helpers::admin_monitoring_usage_is_error;
|
||||
use crate::constants::INTERNAL_GATEWAY_PATH_PREFIXES;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::query::monitoring as monitoring_query;
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::usage::UsageAuditListQuery;
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
@@ -84,6 +86,19 @@ fn build_admin_monitoring_user_behavior_payload(
|
||||
.into_response()
|
||||
}
|
||||
|
||||
fn admin_monitoring_user_behavior_user_id_from_path(request_path: &str) -> Option<String> {
|
||||
let value = request_path
|
||||
.strip_prefix("/api/admin/monitoring/user-behavior/")?
|
||||
.trim()
|
||||
.trim_matches('/')
|
||||
.to_string();
|
||||
if value.is_empty() || value.contains('/') {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_audit_logs_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
@@ -260,7 +275,7 @@ pub(super) async fn build_admin_monitoring_system_status_response(
|
||||
.saturating_add(standalone_api_key_summary.active);
|
||||
|
||||
let today_usage = state
|
||||
.list_usage_audits(&aether_data::repository::usage::UsageAuditListQuery {
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: Some(today_start.timestamp().max(0) as u64),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -276,7 +291,7 @@ pub(super) async fn build_admin_monitoring_system_status_response(
|
||||
.sum::<f64>();
|
||||
|
||||
let recent_errors = state
|
||||
.list_usage_audits(&aether_data::repository::usage::UsageAuditListQuery {
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: Some(recent_error_from.timestamp().max(0) as u64),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -0,0 +1,208 @@
|
||||
use super::cache_config::{
|
||||
ADMIN_MONITORING_CACHE_AFFINITY_DEFAULT_TTL_SECS, ADMIN_MONITORING_CACHE_RESERVATION_RATIO,
|
||||
ADMIN_MONITORING_DYNAMIC_RESERVATION_HIGH_LOAD_THRESHOLD,
|
||||
ADMIN_MONITORING_DYNAMIC_RESERVATION_LOW_LOAD_THRESHOLD,
|
||||
ADMIN_MONITORING_DYNAMIC_RESERVATION_PROBE_PHASE_REQUESTS,
|
||||
ADMIN_MONITORING_DYNAMIC_RESERVATION_PROBE_RESERVATION,
|
||||
ADMIN_MONITORING_DYNAMIC_RESERVATION_STABLE_MAX_RESERVATION,
|
||||
ADMIN_MONITORING_DYNAMIC_RESERVATION_STABLE_MIN_RESERVATION,
|
||||
};
|
||||
use super::cache_store::build_admin_monitoring_cache_snapshot;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_stats_response(
|
||||
state: &AppState,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let snapshot = build_admin_monitoring_cache_snapshot(state).await?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"scheduler": snapshot.scheduler_name,
|
||||
"total_affinities": snapshot.total_affinities,
|
||||
"cache_hit_rate": snapshot.cache_hit_rate,
|
||||
"provider_switches": snapshot.provider_switches,
|
||||
"key_switches": snapshot.key_switches,
|
||||
"cache_hits": snapshot.cache_hits,
|
||||
"cache_misses": snapshot.cache_misses,
|
||||
"scheduler_metrics": {
|
||||
"cache_hits": snapshot.cache_hits,
|
||||
"cache_misses": snapshot.cache_misses,
|
||||
"cache_hit_rate": snapshot.cache_hit_rate,
|
||||
"total_batches": 0,
|
||||
"last_batch_size": 0,
|
||||
"total_candidates": 0,
|
||||
"last_candidate_count": 0,
|
||||
"concurrency_denied": 0,
|
||||
"avg_candidates_per_batch": 0.0,
|
||||
"scheduling_mode": snapshot.scheduling_mode,
|
||||
"provider_priority_mode": snapshot.provider_priority_mode,
|
||||
},
|
||||
"affinity_stats": {
|
||||
"storage_type": snapshot.storage_type,
|
||||
"total_affinities": snapshot.total_affinities,
|
||||
"cache_hits": snapshot.cache_hits,
|
||||
"cache_misses": snapshot.cache_misses,
|
||||
"cache_hit_rate": snapshot.cache_hit_rate,
|
||||
"cache_invalidations": snapshot.cache_invalidations,
|
||||
"provider_switches": snapshot.provider_switches,
|
||||
"key_switches": snapshot.key_switches,
|
||||
"config": {
|
||||
"default_ttl": ADMIN_MONITORING_CACHE_AFFINITY_DEFAULT_TTL_SECS,
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_metrics_response(
|
||||
state: &AppState,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let snapshot = build_admin_monitoring_cache_snapshot(state).await?;
|
||||
let metrics = [
|
||||
(
|
||||
"cache_scheduler_total_batches",
|
||||
"Number of scheduling batches processed",
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
"cache_scheduler_last_batch_size",
|
||||
"Size of the most recent scheduling batch",
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
"cache_scheduler_total_candidates",
|
||||
"Total candidates seen during scheduling",
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
"cache_scheduler_last_candidate_count",
|
||||
"Number of candidates in the most recent batch",
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
"cache_scheduler_cache_hits",
|
||||
"Cache hits counted during scheduling",
|
||||
snapshot.cache_hits as f64,
|
||||
),
|
||||
(
|
||||
"cache_scheduler_cache_misses",
|
||||
"Cache misses counted during scheduling",
|
||||
snapshot.cache_misses as f64,
|
||||
),
|
||||
(
|
||||
"cache_scheduler_cache_hit_rate",
|
||||
"Cache hit rate during scheduling",
|
||||
snapshot.cache_hit_rate,
|
||||
),
|
||||
(
|
||||
"cache_scheduler_concurrency_denied",
|
||||
"Times candidate rejected due to concurrency limits",
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
"cache_scheduler_avg_candidates_per_batch",
|
||||
"Average candidates per batch",
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
"cache_affinity_total",
|
||||
"Total cache affinities stored",
|
||||
snapshot.total_affinities as f64,
|
||||
),
|
||||
(
|
||||
"cache_affinity_hits",
|
||||
"Affinity cache hits",
|
||||
snapshot.cache_hits as f64,
|
||||
),
|
||||
(
|
||||
"cache_affinity_misses",
|
||||
"Affinity cache misses",
|
||||
snapshot.cache_misses as f64,
|
||||
),
|
||||
(
|
||||
"cache_affinity_hit_rate",
|
||||
"Affinity cache hit rate",
|
||||
snapshot.cache_hit_rate,
|
||||
),
|
||||
(
|
||||
"cache_affinity_invalidations",
|
||||
"Affinity invalidations",
|
||||
snapshot.cache_invalidations as f64,
|
||||
),
|
||||
(
|
||||
"cache_affinity_provider_switches",
|
||||
"Affinity provider switches",
|
||||
snapshot.provider_switches as f64,
|
||||
),
|
||||
(
|
||||
"cache_affinity_key_switches",
|
||||
"Affinity key switches",
|
||||
snapshot.key_switches as f64,
|
||||
),
|
||||
];
|
||||
|
||||
let mut lines = Vec::with_capacity(metrics.len() * 3 + 1);
|
||||
for (name, help_text, value) in metrics {
|
||||
lines.push(format!("# HELP {name} {help_text}"));
|
||||
lines.push(format!("# TYPE {name} gauge"));
|
||||
lines.push(format!("{name} {value}"));
|
||||
}
|
||||
lines.push(format!(
|
||||
"cache_scheduler_info{{scheduler=\"{}\"}} 1",
|
||||
snapshot.scheduler_name
|
||||
));
|
||||
|
||||
Ok((
|
||||
[(
|
||||
http::header::CONTENT_TYPE,
|
||||
"text/plain; version=0.0.4; charset=utf-8",
|
||||
)],
|
||||
lines.join("\n") + "\n",
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_config_response(
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"cache_ttl_seconds": ADMIN_MONITORING_CACHE_AFFINITY_DEFAULT_TTL_SECS,
|
||||
"cache_reservation_ratio": ADMIN_MONITORING_CACHE_RESERVATION_RATIO,
|
||||
"dynamic_reservation": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"probe_phase_requests": ADMIN_MONITORING_DYNAMIC_RESERVATION_PROBE_PHASE_REQUESTS,
|
||||
"probe_reservation": ADMIN_MONITORING_DYNAMIC_RESERVATION_PROBE_RESERVATION,
|
||||
"stable_min_reservation": ADMIN_MONITORING_DYNAMIC_RESERVATION_STABLE_MIN_RESERVATION,
|
||||
"stable_max_reservation": ADMIN_MONITORING_DYNAMIC_RESERVATION_STABLE_MAX_RESERVATION,
|
||||
"low_load_threshold": ADMIN_MONITORING_DYNAMIC_RESERVATION_LOW_LOAD_THRESHOLD,
|
||||
"high_load_threshold": ADMIN_MONITORING_DYNAMIC_RESERVATION_HIGH_LOAD_THRESHOLD,
|
||||
},
|
||||
"description": {
|
||||
"probe_phase_requests": "探测阶段请求数阈值",
|
||||
"probe_reservation": "探测阶段预留比例",
|
||||
"stable_min_reservation": "稳定阶段最小预留比例",
|
||||
"stable_max_reservation": "稳定阶段最大预留比例",
|
||||
"low_load_threshold": "低负载阈值(低于此值使用最小预留)",
|
||||
"high_load_threshold": "高负载阈值(高于此值根据置信度使用较高预留)",
|
||||
},
|
||||
},
|
||||
"description": {
|
||||
"cache_ttl": "缓存亲和性有效期(秒)",
|
||||
"cache_reservation_ratio": "静态预留比例(已被动态预留替代)",
|
||||
"dynamic_reservation": "动态预留机制配置",
|
||||
},
|
||||
}
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::AdminMonitoringCacheAffinityRecord;
|
||||
use super::cache_types::AdminMonitoringCacheAffinityRecord;
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
fn parse_admin_monitoring_cache_affinity_key(raw_key: &str) -> Option<(String, String, String)> {
|
||||
@@ -0,0 +1,351 @@
|
||||
use super::cache_identity::{
|
||||
admin_monitoring_find_user_summary_by_id, admin_monitoring_list_export_api_key_records_by_ids,
|
||||
admin_monitoring_load_affinity_identity_maps,
|
||||
};
|
||||
use super::cache_payloads::{
|
||||
admin_monitoring_cache_affinity_sort_value, admin_monitoring_masked_provider_key_prefix,
|
||||
admin_monitoring_masked_user_api_key_prefix,
|
||||
};
|
||||
use super::cache_route_helpers::{
|
||||
admin_monitoring_cache_affinity_not_found_response,
|
||||
admin_monitoring_cache_affinity_user_identifier_from_path,
|
||||
parse_admin_monitoring_keyword_filter,
|
||||
};
|
||||
use super::cache_store::{
|
||||
list_admin_monitoring_cache_affinity_records,
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys,
|
||||
};
|
||||
use super::responses::admin_monitoring_bad_request_response;
|
||||
use super::route_filters::{parse_admin_monitoring_limit, parse_admin_monitoring_offset};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
fn normalize_keyword<'a>(keyword: Option<&'a String>) -> Option<String> {
|
||||
keyword.map(|value| value.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_affinities_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let limit = match parse_admin_monitoring_limit(request_context.request_query_string.as_deref())
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(detail) => return Ok(admin_monitoring_bad_request_response(detail)),
|
||||
};
|
||||
let offset =
|
||||
match parse_admin_monitoring_offset(request_context.request_query_string.as_deref()) {
|
||||
Ok(value) => value,
|
||||
Err(detail) => return Ok(admin_monitoring_bad_request_response(detail)),
|
||||
};
|
||||
let keyword =
|
||||
parse_admin_monitoring_keyword_filter(request_context.request_query_string.as_deref());
|
||||
|
||||
let mut matched_user_id = None::<String>;
|
||||
let mut matched_api_key_id = None::<String>;
|
||||
let filtered_affinities = if let Some(keyword_value) = keyword.as_deref() {
|
||||
let direct_affinity_keys =
|
||||
std::iter::once(keyword_value.to_string()).collect::<std::collections::BTreeSet<_>>();
|
||||
let direct_affinities = list_admin_monitoring_cache_affinity_records_by_affinity_keys(
|
||||
state,
|
||||
&direct_affinity_keys,
|
||||
)
|
||||
.await?;
|
||||
if !direct_affinities.is_empty() {
|
||||
matched_api_key_id = Some(keyword_value.to_string());
|
||||
matched_user_id = admin_monitoring_list_export_api_key_records_by_ids(
|
||||
state,
|
||||
&[keyword_value.to_string()],
|
||||
)
|
||||
.await?
|
||||
.get(keyword_value)
|
||||
.map(|item| item.user_id.clone());
|
||||
direct_affinities
|
||||
} else if let Some(user) = state.find_user_auth_by_identifier(keyword_value).await? {
|
||||
matched_user_id = Some(user.id.clone());
|
||||
let user_api_key_ids = state
|
||||
.list_auth_api_key_export_records_by_user_ids(std::slice::from_ref(&user.id))
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|item| item.api_key_id)
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys(state, &user_api_key_ids)
|
||||
.await?
|
||||
} else {
|
||||
list_admin_monitoring_cache_affinity_records(state).await?
|
||||
}
|
||||
} else {
|
||||
list_admin_monitoring_cache_affinity_records(state).await?
|
||||
};
|
||||
let (api_key_by_id, user_by_id) =
|
||||
admin_monitoring_load_affinity_identity_maps(state, &filtered_affinities).await?;
|
||||
|
||||
let provider_ids = filtered_affinities
|
||||
.iter()
|
||||
.filter_map(|item| item.provider_id.clone())
|
||||
.collect::<std::collections::BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let endpoint_ids = filtered_affinities
|
||||
.iter()
|
||||
.filter_map(|item| item.endpoint_id.clone())
|
||||
.collect::<std::collections::BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let key_ids = filtered_affinities
|
||||
.iter()
|
||||
.filter_map(|item| item.key_id.clone())
|
||||
.collect::<std::collections::BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let provider_by_id = state
|
||||
.data
|
||||
.list_provider_catalog_providers_by_ids(&provider_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.into_iter()
|
||||
.map(|item| (item.id.clone(), item))
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
let endpoint_by_id = state
|
||||
.data
|
||||
.list_provider_catalog_endpoints_by_ids(&endpoint_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.into_iter()
|
||||
.map(|item| (item.id.clone(), item))
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
let key_by_id = state
|
||||
.data
|
||||
.list_provider_catalog_keys_by_ids(&key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.into_iter()
|
||||
.map(|item| (item.id.clone(), item))
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
|
||||
let keyword_lower = normalize_keyword(keyword.as_ref());
|
||||
let mut items = Vec::new();
|
||||
for affinity in filtered_affinities {
|
||||
let user_api_key = api_key_by_id.get(&affinity.affinity_key);
|
||||
let user_id = user_api_key.map(|item| item.user_id.clone());
|
||||
let user = user_id.as_ref().and_then(|id| user_by_id.get(id));
|
||||
let provider = affinity
|
||||
.provider_id
|
||||
.as_ref()
|
||||
.and_then(|id| provider_by_id.get(id));
|
||||
let endpoint = affinity
|
||||
.endpoint_id
|
||||
.as_ref()
|
||||
.and_then(|id| endpoint_by_id.get(id));
|
||||
let key = affinity.key_id.as_ref().and_then(|id| key_by_id.get(id));
|
||||
|
||||
let user_api_key_name = user_api_key.and_then(|item| item.name.clone());
|
||||
let user_api_key_prefix = user_api_key.and_then(|item| {
|
||||
admin_monitoring_masked_user_api_key_prefix(state, item.key_encrypted.as_deref())
|
||||
});
|
||||
let provider_name = provider.map(|item| item.name.clone());
|
||||
let endpoint_url = endpoint
|
||||
.map(|item| item.base_url.clone())
|
||||
.filter(|value| !value.trim().is_empty());
|
||||
let key_name = key.map(|item| item.name.clone());
|
||||
let key_prefix =
|
||||
key.and_then(|item| admin_monitoring_masked_provider_key_prefix(state, item));
|
||||
let user_id_text = user_id.clone();
|
||||
let username = user.map(|item| item.username.clone());
|
||||
let email = user.and_then(|item| item.email.clone());
|
||||
let provider_id = affinity.provider_id.clone();
|
||||
let key_id = affinity.key_id.clone();
|
||||
|
||||
if let Some(keyword_value) = keyword_lower.as_deref() {
|
||||
if matched_user_id.is_none() && matched_api_key_id.is_none() {
|
||||
let searchable = [
|
||||
Some(affinity.affinity_key.as_str()),
|
||||
user_api_key_name.as_deref(),
|
||||
user_id_text.as_deref(),
|
||||
username.as_deref(),
|
||||
email.as_deref(),
|
||||
provider_id.as_deref(),
|
||||
key_id.as_deref(),
|
||||
];
|
||||
if !searchable
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.any(|value| value.to_ascii_lowercase().contains(keyword_value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items.push(json!({
|
||||
"affinity_key": affinity.affinity_key,
|
||||
"user_api_key_name": user_api_key_name,
|
||||
"user_api_key_prefix": user_api_key_prefix,
|
||||
"is_standalone": user_api_key.map(|item| item.is_standalone).unwrap_or(false),
|
||||
"user_id": user_id_text,
|
||||
"username": username,
|
||||
"email": email,
|
||||
"provider_id": provider_id,
|
||||
"provider_name": provider_name,
|
||||
"endpoint_id": affinity.endpoint_id,
|
||||
"endpoint_url": endpoint_url,
|
||||
"key_id": key_id,
|
||||
"key_name": key_name,
|
||||
"key_prefix": key_prefix,
|
||||
"rate_multipliers": key.and_then(|item| item.rate_multipliers.clone()),
|
||||
"global_model_id": affinity.model_name,
|
||||
"model_name": affinity.model_name,
|
||||
"model_display_name": serde_json::Value::Null,
|
||||
"api_format": affinity.api_format,
|
||||
"created_at": affinity.created_at,
|
||||
"expire_at": affinity.expire_at,
|
||||
"request_count": affinity.request_count,
|
||||
}));
|
||||
}
|
||||
|
||||
items.sort_by(|left, right| {
|
||||
admin_monitoring_cache_affinity_sort_value(right.get("expire_at"))
|
||||
.partial_cmp(&admin_monitoring_cache_affinity_sort_value(
|
||||
left.get("expire_at"),
|
||||
))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
let total = items.len();
|
||||
let paged_items = items
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.collect::<Vec<_>>();
|
||||
let paged_count = paged_items.len();
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"items": paged_items,
|
||||
"meta": {
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"count": paged_count,
|
||||
},
|
||||
"matched_user_id": matched_user_id,
|
||||
}
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_affinity_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(user_identifier) =
|
||||
admin_monitoring_cache_affinity_user_identifier_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response(
|
||||
"缺少 user_identifier",
|
||||
));
|
||||
};
|
||||
let direct_api_key_by_id =
|
||||
admin_monitoring_list_export_api_key_records_by_ids(state, &[user_identifier.clone()])
|
||||
.await?;
|
||||
let direct_affinity_keys =
|
||||
std::iter::once(user_identifier.clone()).collect::<std::collections::BTreeSet<_>>();
|
||||
let direct_affinities =
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys(state, &direct_affinity_keys)
|
||||
.await?;
|
||||
|
||||
let (resolved_user_id, username, email, filtered_affinities) = if !direct_affinities.is_empty()
|
||||
|| direct_api_key_by_id.contains_key(&user_identifier)
|
||||
{
|
||||
let user_id = direct_api_key_by_id
|
||||
.get(&user_identifier)
|
||||
.map(|item| item.user_id.clone());
|
||||
let user = match user_id.as_deref() {
|
||||
Some(user_id) => admin_monitoring_find_user_summary_by_id(state, user_id).await?,
|
||||
None => None,
|
||||
};
|
||||
(
|
||||
user_id,
|
||||
user.as_ref().map(|item| item.username.clone()),
|
||||
user.and_then(|item| item.email),
|
||||
direct_affinities,
|
||||
)
|
||||
} else if let Some(user) = state.find_user_auth_by_identifier(&user_identifier).await? {
|
||||
let user_api_key_ids = state
|
||||
.list_auth_api_key_export_records_by_user_ids(std::slice::from_ref(&user.id))
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|item| item.api_key_id)
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
let affinities =
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys(state, &user_api_key_ids)
|
||||
.await?;
|
||||
(Some(user.id), Some(user.username), user.email, affinities)
|
||||
} else {
|
||||
return Ok(admin_monitoring_cache_affinity_not_found_response(
|
||||
&user_identifier,
|
||||
));
|
||||
};
|
||||
|
||||
if filtered_affinities.is_empty() {
|
||||
let display_name = username.clone().unwrap_or_else(|| user_identifier.clone());
|
||||
return Ok(Json(json!({
|
||||
"status": "not_found",
|
||||
"message": format!(
|
||||
"用户 {} ({}) 没有缓存亲和性",
|
||||
display_name,
|
||||
email.clone().unwrap_or_else(|| "null".to_string()),
|
||||
),
|
||||
"user_info": {
|
||||
"user_id": resolved_user_id,
|
||||
"username": username,
|
||||
"email": email,
|
||||
},
|
||||
"affinities": [],
|
||||
}))
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let mut affinities = filtered_affinities
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
json!({
|
||||
"provider_id": item.provider_id,
|
||||
"endpoint_id": item.endpoint_id,
|
||||
"key_id": item.key_id,
|
||||
"api_format": item.api_format,
|
||||
"model_name": item.model_name,
|
||||
"created_at": item.created_at,
|
||||
"expire_at": item.expire_at,
|
||||
"request_count": item.request_count,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
affinities.sort_by(|left, right| {
|
||||
admin_monitoring_cache_affinity_sort_value(right.get("expire_at"))
|
||||
.partial_cmp(&admin_monitoring_cache_affinity_sort_value(
|
||||
left.get("expire_at"),
|
||||
))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
let total_endpoints = affinities.len();
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"user_info": {
|
||||
"user_id": resolved_user_id,
|
||||
"username": username,
|
||||
"email": email,
|
||||
},
|
||||
"affinities": affinities,
|
||||
"total_endpoints": total_endpoints,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
pub(crate) const ADMIN_MONITORING_CACHE_AFFINITY_REDIS_REQUIRED_DETAIL: &str =
|
||||
"Redis未初始化,无法获取缓存亲和性";
|
||||
pub(crate) const ADMIN_MONITORING_REDIS_REQUIRED_DETAIL: &str = "Redis 未启用";
|
||||
pub(crate) const ADMIN_MONITORING_CACHE_AFFINITY_DEFAULT_TTL_SECS: u64 = 300;
|
||||
pub(crate) const ADMIN_MONITORING_CACHE_RESERVATION_RATIO: f64 = 0.1;
|
||||
pub(crate) const ADMIN_MONITORING_DYNAMIC_RESERVATION_PROBE_PHASE_REQUESTS: u64 = 100;
|
||||
pub(crate) const ADMIN_MONITORING_DYNAMIC_RESERVATION_PROBE_RESERVATION: f64 = 0.1;
|
||||
pub(crate) const ADMIN_MONITORING_DYNAMIC_RESERVATION_STABLE_MIN_RESERVATION: f64 = 0.1;
|
||||
pub(crate) const ADMIN_MONITORING_DYNAMIC_RESERVATION_STABLE_MAX_RESERVATION: f64 = 0.35;
|
||||
pub(crate) const ADMIN_MONITORING_DYNAMIC_RESERVATION_LOW_LOAD_THRESHOLD: f64 = 0.5;
|
||||
pub(crate) const ADMIN_MONITORING_DYNAMIC_RESERVATION_HIGH_LOAD_THRESHOLD: f64 = 0.8;
|
||||
pub(crate) const ADMIN_MONITORING_REDIS_CACHE_CATEGORIES: &[(&str, &str, &str, &str)] = &[
|
||||
(
|
||||
"upstream_models",
|
||||
"上游模型",
|
||||
"upstream_models:*",
|
||||
"Provider 上游获取的模型列表缓存",
|
||||
),
|
||||
("model_id", "模型 ID", "model:id:*", "Model 按 ID 缓存"),
|
||||
(
|
||||
"model_provider_global",
|
||||
"模型映射",
|
||||
"model:provider_global:*",
|
||||
"Provider-GlobalModel 模型映射缓存",
|
||||
),
|
||||
(
|
||||
"provider_mapping_preview",
|
||||
"映射预览",
|
||||
"admin:providers:mapping-preview:*",
|
||||
"Provider 详情页 mapping-preview 缓存",
|
||||
),
|
||||
(
|
||||
"global_model",
|
||||
"全局模型",
|
||||
"global_model:*",
|
||||
"GlobalModel 缓存(ID/名称/解析)",
|
||||
),
|
||||
(
|
||||
"models_list",
|
||||
"模型列表",
|
||||
"models:list:*",
|
||||
"/v1/models 端点模型列表缓存",
|
||||
),
|
||||
("user", "用户", "user:*", "用户信息缓存(ID/Email)"),
|
||||
(
|
||||
"apikey",
|
||||
"API Key",
|
||||
"apikey:*",
|
||||
"API Key 认证缓存(Hash/Auth)",
|
||||
),
|
||||
(
|
||||
"api_key_id",
|
||||
"API Key ID",
|
||||
"api_key:id:*",
|
||||
"API Key 按 ID 缓存",
|
||||
),
|
||||
(
|
||||
"cache_affinity",
|
||||
"缓存亲和性",
|
||||
"cache_affinity:*",
|
||||
"请求路由亲和性缓存",
|
||||
),
|
||||
(
|
||||
"provider_billing",
|
||||
"Provider 计费",
|
||||
"provider:billing_type:*",
|
||||
"Provider 计费类型缓存",
|
||||
),
|
||||
(
|
||||
"provider_rate",
|
||||
"Provider 费率",
|
||||
"provider_api_key:rate_multiplier:*",
|
||||
"ProviderAPIKey 费率倍数缓存",
|
||||
),
|
||||
(
|
||||
"provider_balance",
|
||||
"Provider 余额",
|
||||
"provider_ops:balance:*",
|
||||
"Provider 余额查询缓存",
|
||||
),
|
||||
("health", "健康检查", "health:*", "端点健康状态缓存"),
|
||||
(
|
||||
"endpoint_status",
|
||||
"端点状态",
|
||||
"endpoint_status:*",
|
||||
"用户端点状态缓存",
|
||||
),
|
||||
("dashboard", "仪表盘", "dashboard:*", "仪表盘统计缓存"),
|
||||
(
|
||||
"activity_heatmap",
|
||||
"活动热力图",
|
||||
"activity_heatmap:*",
|
||||
"用户活动热力图缓存",
|
||||
),
|
||||
(
|
||||
"gemini_files",
|
||||
"Gemini 文件映射",
|
||||
"gemini_files:*",
|
||||
"Gemini Files API 文件-Key 映射缓存",
|
||||
),
|
||||
(
|
||||
"provider_oauth",
|
||||
"OAuth 状态",
|
||||
"provider_oauth_state:*",
|
||||
"Provider OAuth 授权流程临时状态",
|
||||
),
|
||||
(
|
||||
"oauth_refresh_lock",
|
||||
"OAuth 刷新锁",
|
||||
"provider_oauth_refresh_lock:*",
|
||||
"OAuth Token 刷新分布式锁",
|
||||
),
|
||||
(
|
||||
"concurrency_lock",
|
||||
"并发锁",
|
||||
"concurrency:*",
|
||||
"请求并发控制锁",
|
||||
),
|
||||
];
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::AdminMonitoringCacheAffinityRecord;
|
||||
use super::cache_types::AdminMonitoringCacheAffinityRecord;
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
pub(super) async fn admin_monitoring_list_export_api_key_records_by_ids(
|
||||
@@ -0,0 +1,109 @@
|
||||
use super::cache_config::ADMIN_MONITORING_REDIS_CACHE_CATEGORIES;
|
||||
use super::cache_store::{
|
||||
admin_monitoring_has_test_redis_keys, list_admin_monitoring_namespaced_keys,
|
||||
};
|
||||
use crate::AppState;
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) async fn build_admin_monitoring_model_mapping_stats_response(
|
||||
state: &AppState,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"available": false,
|
||||
"message": "Redis 未启用,模型映射缓存不可用",
|
||||
}
|
||||
}))
|
||||
.into_response());
|
||||
};
|
||||
|
||||
let model_id_keys = list_admin_monitoring_namespaced_keys(state, "model:id:*").await?;
|
||||
let global_model_id_keys =
|
||||
list_admin_monitoring_namespaced_keys(state, "global_model:id:*").await?;
|
||||
let global_model_name_keys =
|
||||
list_admin_monitoring_namespaced_keys(state, "global_model:name:*").await?;
|
||||
let global_model_resolve_keys =
|
||||
list_admin_monitoring_namespaced_keys(state, "global_model:resolve:*").await?;
|
||||
let provider_global_keys =
|
||||
list_admin_monitoring_namespaced_keys(state, "model:provider_global:*")
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|key| !key.starts_with("model:provider_global:hits:"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let total_keys = model_id_keys.len()
|
||||
+ global_model_id_keys.len()
|
||||
+ global_model_name_keys.len()
|
||||
+ global_model_resolve_keys.len()
|
||||
+ provider_global_keys.len();
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"available": true,
|
||||
"ttl_seconds": 300,
|
||||
"total_keys": total_keys,
|
||||
"breakdown": {
|
||||
"model_by_id": model_id_keys.len(),
|
||||
"model_by_provider_global": provider_global_keys.len(),
|
||||
"global_model_by_id": global_model_id_keys.len(),
|
||||
"global_model_by_name": global_model_name_keys.len(),
|
||||
"global_model_resolve": global_model_resolve_keys.len(),
|
||||
},
|
||||
"mappings": [],
|
||||
"provider_model_mappings": serde_json::Value::Null,
|
||||
"unmapped": serde_json::Value::Null,
|
||||
}
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_redis_cache_categories_response(
|
||||
state: &AppState,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"available": false,
|
||||
"message": "Redis 未启用",
|
||||
}
|
||||
}))
|
||||
.into_response());
|
||||
};
|
||||
|
||||
let mut categories = Vec::with_capacity(ADMIN_MONITORING_REDIS_CACHE_CATEGORIES.len());
|
||||
let mut total_keys = 0usize;
|
||||
|
||||
for (key, name, pattern, description) in ADMIN_MONITORING_REDIS_CACHE_CATEGORIES {
|
||||
let count = list_admin_monitoring_namespaced_keys(state, pattern)
|
||||
.await?
|
||||
.len();
|
||||
total_keys += count;
|
||||
categories.push(json!({
|
||||
"key": key,
|
||||
"name": name,
|
||||
"pattern": pattern,
|
||||
"description": description,
|
||||
"count": count,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"available": true,
|
||||
"categories": categories,
|
||||
"total_keys": total_keys,
|
||||
}
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
use super::cache_affinity::{
|
||||
clear_admin_monitoring_scheduler_affinity_entries,
|
||||
delete_admin_monitoring_cache_affinity_raw_keys,
|
||||
};
|
||||
use super::cache_config::ADMIN_MONITORING_REDIS_CACHE_CATEGORIES;
|
||||
use super::cache_identity::{
|
||||
admin_monitoring_find_user_summary_by_id, admin_monitoring_list_export_api_key_records_by_ids,
|
||||
};
|
||||
use super::cache_route_helpers::{
|
||||
admin_monitoring_cache_affinity_delete_params_from_path,
|
||||
admin_monitoring_cache_affinity_unavailable_response,
|
||||
admin_monitoring_cache_model_mapping_provider_params_from_path,
|
||||
admin_monitoring_cache_model_name_from_path, admin_monitoring_cache_provider_id_from_path,
|
||||
admin_monitoring_cache_redis_category_from_path,
|
||||
admin_monitoring_cache_users_not_found_response,
|
||||
admin_monitoring_cache_users_user_identifier_from_path,
|
||||
admin_monitoring_redis_unavailable_response,
|
||||
};
|
||||
use super::cache_store::{
|
||||
admin_monitoring_has_test_redis_keys, delete_admin_monitoring_namespaced_keys,
|
||||
list_admin_monitoring_cache_affinity_records,
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys,
|
||||
list_admin_monitoring_namespaced_keys, load_admin_monitoring_cache_affinity_entries_for_tests,
|
||||
};
|
||||
use super::responses::{
|
||||
admin_monitoring_bad_request_response, admin_monitoring_not_found_response,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_users_delete_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(user_identifier) =
|
||||
admin_monitoring_cache_users_user_identifier_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response(
|
||||
"缺少 user_identifier",
|
||||
));
|
||||
};
|
||||
|
||||
if state.redis_kv_runner().is_none()
|
||||
&& load_admin_monitoring_cache_affinity_entries_for_tests(state).is_empty()
|
||||
{
|
||||
return Ok(admin_monitoring_cache_affinity_unavailable_response());
|
||||
}
|
||||
|
||||
let direct_api_key_by_id =
|
||||
admin_monitoring_list_export_api_key_records_by_ids(state, &[user_identifier.clone()])
|
||||
.await?;
|
||||
|
||||
if let Some(api_key) = direct_api_key_by_id.get(&user_identifier) {
|
||||
let target_affinity_keys =
|
||||
std::iter::once(user_identifier.clone()).collect::<std::collections::BTreeSet<_>>();
|
||||
let target_affinities = list_admin_monitoring_cache_affinity_records_by_affinity_keys(
|
||||
state,
|
||||
&target_affinity_keys,
|
||||
)
|
||||
.await?;
|
||||
let raw_keys = target_affinities
|
||||
.iter()
|
||||
.map(|item| item.raw_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let _ = delete_admin_monitoring_cache_affinity_raw_keys(state, &raw_keys).await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(state, &target_affinities);
|
||||
|
||||
let user = admin_monitoring_find_user_summary_by_id(state, &api_key.user_id).await?;
|
||||
let api_key_name = api_key
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| user_identifier.clone());
|
||||
return Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": format!("已清除 API Key {api_key_name} 的缓存亲和性"),
|
||||
"user_info": {
|
||||
"user_id": Some(api_key.user_id.clone()),
|
||||
"username": user.as_ref().map(|item| item.username.clone()),
|
||||
"email": user.and_then(|item| item.email),
|
||||
"api_key_id": user_identifier,
|
||||
"api_key_name": api_key.name.clone(),
|
||||
},
|
||||
}))
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let Some(user) = state.find_user_auth_by_identifier(&user_identifier).await? else {
|
||||
return Ok(admin_monitoring_cache_users_not_found_response(
|
||||
&user_identifier,
|
||||
));
|
||||
};
|
||||
|
||||
let user_api_key_ids = state
|
||||
.list_auth_api_key_export_records_by_user_ids(std::slice::from_ref(&user.id))
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|item| item.api_key_id.clone())
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
let target_affinities =
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys(state, &user_api_key_ids)
|
||||
.await?;
|
||||
let raw_keys = target_affinities
|
||||
.iter()
|
||||
.map(|item| item.raw_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let _ = delete_admin_monitoring_cache_affinity_raw_keys(state, &raw_keys).await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(state, &target_affinities);
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": format!("已清除用户 {} 的所有缓存亲和性", user.username),
|
||||
"user_info": {
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
},
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_affinity_delete_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some((affinity_key, endpoint_id, model_id, api_format)) =
|
||||
admin_monitoring_cache_affinity_delete_params_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response(
|
||||
"缺少 affinity_key、endpoint_id、model_id 或 api_format",
|
||||
));
|
||||
};
|
||||
|
||||
if state.redis_kv_runner().is_none()
|
||||
&& load_admin_monitoring_cache_affinity_entries_for_tests(state).is_empty()
|
||||
{
|
||||
return Ok(admin_monitoring_cache_affinity_unavailable_response());
|
||||
}
|
||||
|
||||
let target_affinity_keys =
|
||||
std::iter::once(affinity_key.clone()).collect::<std::collections::BTreeSet<_>>();
|
||||
let target_affinity =
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys(state, &target_affinity_keys)
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|item| {
|
||||
item.affinity_key == affinity_key
|
||||
&& item.endpoint_id.as_deref() == Some(endpoint_id.as_str())
|
||||
&& item.model_name == model_id
|
||||
&& item.api_format.eq_ignore_ascii_case(&api_format)
|
||||
});
|
||||
let Some(target_affinity) = target_affinity else {
|
||||
return Ok(admin_monitoring_not_found_response(
|
||||
"未找到指定的缓存亲和性记录",
|
||||
));
|
||||
};
|
||||
|
||||
let _ = delete_admin_monitoring_cache_affinity_raw_keys(
|
||||
state,
|
||||
std::slice::from_ref(&target_affinity.raw_key),
|
||||
)
|
||||
.await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(
|
||||
state,
|
||||
std::slice::from_ref(&target_affinity),
|
||||
);
|
||||
|
||||
let mut api_key_by_id = admin_monitoring_list_export_api_key_records_by_ids(
|
||||
state,
|
||||
std::slice::from_ref(&affinity_key),
|
||||
)
|
||||
.await?;
|
||||
let api_key_name = api_key_by_id
|
||||
.remove(&affinity_key)
|
||||
.and_then(|item| item.name)
|
||||
.unwrap_or_else(|| affinity_key.chars().take(8).collect::<String>());
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": format!("已清除缓存亲和性: {api_key_name}"),
|
||||
"affinity_key": affinity_key,
|
||||
"endpoint_id": endpoint_id,
|
||||
"model_id": model_id,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_flush_response(
|
||||
state: &AppState,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let raw_affinities = list_admin_monitoring_cache_affinity_records(state).await?;
|
||||
if state.redis_kv_runner().is_none() && raw_affinities.is_empty() {
|
||||
return Ok(admin_monitoring_cache_affinity_unavailable_response());
|
||||
}
|
||||
|
||||
let raw_keys = raw_affinities
|
||||
.iter()
|
||||
.map(|item| item.raw_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let deleted = delete_admin_monitoring_cache_affinity_raw_keys(state, &raw_keys).await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(state, &raw_affinities);
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": "已清除全部缓存亲和性",
|
||||
"deleted_affinities": deleted,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_provider_delete_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(provider_id) =
|
||||
admin_monitoring_cache_provider_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response("缺少 provider_id"));
|
||||
};
|
||||
|
||||
let raw_affinities = list_admin_monitoring_cache_affinity_records(state).await?;
|
||||
if state.redis_kv_runner().is_none() && raw_affinities.is_empty() {
|
||||
return Ok(admin_monitoring_cache_affinity_unavailable_response());
|
||||
}
|
||||
|
||||
let target_affinities = raw_affinities
|
||||
.into_iter()
|
||||
.filter(|item| item.provider_id.as_deref() == Some(provider_id.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
if target_affinities.is_empty() {
|
||||
return Ok((
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({
|
||||
"detail": format!("未找到 provider {provider_id} 的缓存亲和性记录")
|
||||
})),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let raw_keys = target_affinities
|
||||
.iter()
|
||||
.map(|item| item.raw_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let deleted = delete_admin_monitoring_cache_affinity_raw_keys(state, &raw_keys).await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(state, &target_affinities);
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": format!("已清除 provider {provider_id} 的缓存亲和性"),
|
||||
"provider_id": provider_id,
|
||||
"deleted_affinities": deleted,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_model_mapping_delete_response(
|
||||
state: &AppState,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(admin_monitoring_redis_unavailable_response());
|
||||
}
|
||||
|
||||
let mut raw_keys = list_admin_monitoring_namespaced_keys(state, "model:*").await?;
|
||||
raw_keys.extend(list_admin_monitoring_namespaced_keys(state, "global_model:*").await?);
|
||||
raw_keys.sort();
|
||||
raw_keys.dedup();
|
||||
let deleted_count = delete_admin_monitoring_namespaced_keys(state, &raw_keys).await?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": "已清除所有模型映射缓存",
|
||||
"deleted_count": deleted_count,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_model_mapping_delete_model_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(model_name) =
|
||||
admin_monitoring_cache_model_name_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response("缺少 model_name"));
|
||||
};
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(admin_monitoring_redis_unavailable_response());
|
||||
}
|
||||
|
||||
let candidate_keys = [
|
||||
format!("global_model:resolve:{model_name}"),
|
||||
format!("global_model:name:{model_name}"),
|
||||
];
|
||||
let mut existing_keys = Vec::new();
|
||||
for key in candidate_keys {
|
||||
let matches = list_admin_monitoring_namespaced_keys(state, key.as_str()).await?;
|
||||
existing_keys.extend(matches);
|
||||
}
|
||||
existing_keys.sort();
|
||||
existing_keys.dedup();
|
||||
|
||||
let deleted_count = delete_admin_monitoring_namespaced_keys(state, &existing_keys).await?;
|
||||
let deleted_keys = if deleted_count == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
existing_keys
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": format!("已清除模型 {model_name} 的映射缓存"),
|
||||
"model_name": model_name,
|
||||
"deleted_keys": deleted_keys,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_model_mapping_delete_provider_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some((provider_id, global_model_id)) =
|
||||
admin_monitoring_cache_model_mapping_provider_params_from_path(
|
||||
&request_context.request_path,
|
||||
)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response(
|
||||
"缺少 provider_id 或 global_model_id",
|
||||
));
|
||||
};
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(admin_monitoring_redis_unavailable_response());
|
||||
}
|
||||
|
||||
let candidate_keys = [
|
||||
format!("model:provider_global:{provider_id}:{global_model_id}"),
|
||||
format!("model:provider_global:hits:{provider_id}:{global_model_id}"),
|
||||
];
|
||||
let mut existing_keys = Vec::new();
|
||||
for key in candidate_keys {
|
||||
let matches = list_admin_monitoring_namespaced_keys(state, key.as_str()).await?;
|
||||
existing_keys.extend(matches);
|
||||
}
|
||||
existing_keys.sort();
|
||||
existing_keys.dedup();
|
||||
|
||||
let _ = delete_admin_monitoring_namespaced_keys(state, &existing_keys).await?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": "已清除 Provider 模型映射缓存",
|
||||
"provider_id": provider_id,
|
||||
"global_model_id": global_model_id,
|
||||
"deleted_keys": existing_keys,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_redis_keys_delete_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(category) =
|
||||
admin_monitoring_cache_redis_category_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response("缺少 category"));
|
||||
};
|
||||
|
||||
let Some((cat_key, name, pattern, _description)) = ADMIN_MONITORING_REDIS_CACHE_CATEGORIES
|
||||
.iter()
|
||||
.find(|(cat_key, _, _, _)| *cat_key == category)
|
||||
else {
|
||||
return Ok((
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": format!("未知的缓存分类: {category}") })),
|
||||
)
|
||||
.into_response());
|
||||
};
|
||||
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(admin_monitoring_redis_unavailable_response());
|
||||
}
|
||||
|
||||
let raw_keys = list_admin_monitoring_namespaced_keys(state, pattern).await?;
|
||||
let deleted_count = delete_admin_monitoring_namespaced_keys(state, &raw_keys).await?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": format!("已清除 {name} 缓存"),
|
||||
"category": cat_key,
|
||||
"deleted_count": deleted_count,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use crate::AppState;
|
||||
use aether_crypto::decrypt_python_fernet_ciphertext;
|
||||
#[cfg(test)]
|
||||
use aether_crypto::DEVELOPMENT_ENCRYPTION_KEY;
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
||||
|
||||
pub(super) fn admin_monitoring_masked_user_api_key_prefix(
|
||||
state: &AppState,
|
||||
@@ -23,7 +24,7 @@ pub(super) fn admin_monitoring_masked_user_api_key_prefix(
|
||||
|
||||
pub(super) fn admin_monitoring_masked_provider_key_prefix(
|
||||
state: &AppState,
|
||||
key: &aether_data::repository::provider_catalog::StoredProviderCatalogKey,
|
||||
key: &StoredProviderCatalogKey,
|
||||
) -> Option<String> {
|
||||
match key.auth_type.trim() {
|
||||
"service_account" | "vertex_ai" => Some("[Service Account]".to_string()),
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{
|
||||
use super::cache_config::{
|
||||
ADMIN_MONITORING_CACHE_AFFINITY_REDIS_REQUIRED_DETAIL, ADMIN_MONITORING_REDIS_REQUIRED_DETAIL,
|
||||
};
|
||||
use crate::handlers::query_param_value;
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::cache_affinity::admin_monitoring_cache_affinity_record;
|
||||
use super::{AdminMonitoringCacheAffinityRecord, AdminMonitoringCacheSnapshot};
|
||||
use crate::handlers::round_to;
|
||||
use super::cache_types::{AdminMonitoringCacheAffinityRecord, AdminMonitoringCacheSnapshot};
|
||||
use crate::handlers::admin::observability::stats::round_to;
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::usage::UsageAuditListQuery;
|
||||
|
||||
async fn count_admin_monitoring_cache_affinity_entries(state: &AppState) -> usize {
|
||||
let Some(runner) = state.redis_kv_runner() else {
|
||||
@@ -287,7 +288,7 @@ pub(super) async fn build_admin_monitoring_cache_snapshot(
|
||||
let now = chrono::Utc::now();
|
||||
let usage = if state.has_usage_data_reader() {
|
||||
state
|
||||
.list_usage_audits(&aether_data::repository::usage::UsageAuditListQuery {
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: Some(
|
||||
(now - chrono::Duration::hours(24)).timestamp().max(0) as u64,
|
||||
),
|
||||
@@ -0,0 +1,27 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct AdminMonitoringCacheAffinityRecord {
|
||||
pub(super) raw_key: String,
|
||||
pub(super) affinity_key: String,
|
||||
pub(super) api_format: String,
|
||||
pub(super) model_name: String,
|
||||
pub(super) provider_id: Option<String>,
|
||||
pub(super) endpoint_id: Option<String>,
|
||||
pub(super) key_id: Option<String>,
|
||||
pub(super) created_at: Option<serde_json::Value>,
|
||||
pub(super) expire_at: Option<serde_json::Value>,
|
||||
pub(super) request_count: u64,
|
||||
}
|
||||
|
||||
pub(super) struct AdminMonitoringCacheSnapshot {
|
||||
pub(super) scheduler_name: String,
|
||||
pub(super) scheduling_mode: String,
|
||||
pub(super) provider_priority_mode: String,
|
||||
pub(super) storage_type: &'static str,
|
||||
pub(super) total_affinities: usize,
|
||||
pub(super) cache_hits: usize,
|
||||
pub(super) cache_misses: usize,
|
||||
pub(super) cache_hit_rate: f64,
|
||||
pub(super) provider_switches: usize,
|
||||
pub(super) key_switches: usize,
|
||||
pub(super) cache_invalidations: usize,
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
mod activity;
|
||||
mod cache;
|
||||
mod cache_affinity;
|
||||
mod cache_affinity_reads;
|
||||
mod cache_config;
|
||||
mod cache_identity;
|
||||
mod cache_model_mapping;
|
||||
mod cache_mutations;
|
||||
mod cache_payloads;
|
||||
mod cache_route_helpers;
|
||||
mod cache_store;
|
||||
mod cache_types;
|
||||
mod resilience;
|
||||
mod responses;
|
||||
mod route_filters;
|
||||
mod routes;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_support;
|
||||
mod trace;
|
||||
mod usage_helpers;
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_monitoring_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
routes::maybe_build_local_admin_monitoring_response(state, request_context).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -1,12 +1,13 @@
|
||||
use super::{
|
||||
admin_monitoring_bad_request_response, admin_monitoring_usage_is_error,
|
||||
AdminMonitoringResilienceSnapshot,
|
||||
};
|
||||
use super::responses::admin_monitoring_bad_request_response;
|
||||
use super::usage_helpers::admin_monitoring_usage_is_error;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::{
|
||||
use crate::handlers::admin::shared::{
|
||||
provider_key_health_summary, query_param_value, unix_secs_to_rfc3339,
|
||||
};
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::{
|
||||
provider_catalog::StoredProviderCatalogKey, usage::UsageAuditListQuery,
|
||||
};
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
@@ -15,6 +16,16 @@ 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,
|
||||
@@ -56,7 +67,7 @@ fn parse_admin_monitoring_circuit_history_limit(query: Option<&str>) -> Result<u
|
||||
}
|
||||
|
||||
fn build_admin_monitoring_circuit_history_items(
|
||||
keys: &[aether_data::repository::provider_catalog::StoredProviderCatalogKey],
|
||||
keys: &[StoredProviderCatalogKey],
|
||||
provider_name_by_id: &BTreeMap<String, String>,
|
||||
limit: usize,
|
||||
) -> Vec<serde_json::Value> {
|
||||
@@ -338,7 +349,7 @@ async fn build_admin_monitoring_resilience_snapshot(
|
||||
}
|
||||
|
||||
let mut recent_usage_errors = state
|
||||
.list_usage_audits(&aether_data::repository::usage::UsageAuditListQuery {
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: Some(recent_error_from.timestamp().max(0) as u64),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) fn admin_monitoring_bad_request_response(detail: impl Into<String>) -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": detail.into() })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(super) fn admin_monitoring_not_found_response(detail: &'static str) -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": detail })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::handlers::query_param_value;
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
|
||||
pub(super) fn admin_monitoring_escape_like_pattern(value: &str) -> String {
|
||||
value
|
||||
@@ -1,32 +1,46 @@
|
||||
use super::{
|
||||
build_admin_monitoring_audit_logs_response, build_admin_monitoring_cache_affinities_response,
|
||||
build_admin_monitoring_cache_affinity_delete_response,
|
||||
build_admin_monitoring_cache_affinity_response, build_admin_monitoring_cache_config_response,
|
||||
build_admin_monitoring_cache_flush_response, build_admin_monitoring_cache_metrics_response,
|
||||
build_admin_monitoring_cache_provider_delete_response,
|
||||
use super::activity::{
|
||||
build_admin_monitoring_audit_logs_response,
|
||||
build_admin_monitoring_suspicious_activities_response,
|
||||
build_admin_monitoring_system_status_response, build_admin_monitoring_user_behavior_response,
|
||||
};
|
||||
use super::cache::{
|
||||
build_admin_monitoring_cache_config_response, build_admin_monitoring_cache_metrics_response,
|
||||
build_admin_monitoring_cache_stats_response,
|
||||
};
|
||||
use super::cache_affinity_reads::{
|
||||
build_admin_monitoring_cache_affinities_response,
|
||||
build_admin_monitoring_cache_affinity_response,
|
||||
};
|
||||
use super::cache_model_mapping::{
|
||||
build_admin_monitoring_model_mapping_stats_response,
|
||||
build_admin_monitoring_redis_cache_categories_response,
|
||||
};
|
||||
use super::cache_mutations::{
|
||||
build_admin_monitoring_cache_affinity_delete_response,
|
||||
build_admin_monitoring_cache_flush_response,
|
||||
build_admin_monitoring_cache_provider_delete_response,
|
||||
build_admin_monitoring_cache_users_delete_response,
|
||||
build_admin_monitoring_model_mapping_delete_model_response,
|
||||
build_admin_monitoring_model_mapping_delete_provider_response,
|
||||
build_admin_monitoring_model_mapping_delete_response,
|
||||
build_admin_monitoring_model_mapping_stats_response,
|
||||
build_admin_monitoring_redis_cache_categories_response,
|
||||
build_admin_monitoring_redis_keys_delete_response,
|
||||
};
|
||||
use super::resilience::{
|
||||
build_admin_monitoring_reset_error_stats_response,
|
||||
build_admin_monitoring_resilience_circuit_history_response,
|
||||
build_admin_monitoring_resilience_status_response,
|
||||
build_admin_monitoring_suspicious_activities_response,
|
||||
build_admin_monitoring_system_status_response,
|
||||
};
|
||||
use super::trace::{
|
||||
build_admin_monitoring_trace_provider_stats_response,
|
||||
build_admin_monitoring_trace_request_response, build_admin_monitoring_user_behavior_response,
|
||||
build_admin_monitoring_trace_request_response,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::misc_helpers::attach_admin_audit_response;
|
||||
use crate::handlers::admin::shared::attach_admin_audit_response;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{body::Body, http, response::Response};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(super) enum AdminMonitoringRoute {
|
||||
pub(crate) enum AdminMonitoringRoute {
|
||||
AuditLogs,
|
||||
SystemStatus,
|
||||
SuspiciousActivities,
|
||||
@@ -53,7 +67,7 @@ pub(super) enum AdminMonitoringRoute {
|
||||
CacheRedisKeysDelete,
|
||||
}
|
||||
|
||||
pub(super) async fn maybe_build_local_admin_monitoring_response(
|
||||
pub(crate) async fn maybe_build_local_admin_monitoring_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
@@ -180,7 +194,7 @@ fn admin_monitoring_audit_target_id(request_context: &GatewayPublicRequestContex
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn match_admin_monitoring_route(
|
||||
pub(crate) fn match_admin_monitoring_route(
|
||||
method: &http::Method,
|
||||
path: &str,
|
||||
) -> Option<AdminMonitoringRoute> {
|
||||
@@ -1,14 +1,16 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
|
||||
use aether_data_contracts::repository::{
|
||||
candidates::{RequestCandidateStatus, StoredRequestCandidate},
|
||||
provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
},
|
||||
usage::StoredRequestUsageAudit,
|
||||
};
|
||||
use axum::http::{self, Uri};
|
||||
use serde_json::json;
|
||||
|
||||
use aether_data::repository::auth::StoredAuthApiKeyExportRecord;
|
||||
use aether_data::repository::candidates::{RequestCandidateStatus, StoredRequestCandidate};
|
||||
use aether_data::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
};
|
||||
use aether_data::repository::usage::StoredRequestUsageAudit;
|
||||
use aether_data::repository::users::{StoredUserAuthRecord, StoredUserExportRow};
|
||||
|
||||
pub(super) fn request_context(method: http::Method, uri: &str) -> GatewayPublicRequestContext {
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::super::test_support::{request_context, sample_key, sample_provider, sample_usage};
|
||||
use super::super::{
|
||||
match_admin_monitoring_route, maybe_build_local_admin_monitoring_response,
|
||||
AdminMonitoringRoute, ADMIN_MONITORING_REDIS_REQUIRED_DETAIL,
|
||||
use super::super::cache_config::ADMIN_MONITORING_REDIS_REQUIRED_DETAIL;
|
||||
use super::super::routes::{
|
||||
match_admin_monitoring_route, maybe_build_local_admin_monitoring_response, AdminMonitoringRoute,
|
||||
};
|
||||
use super::super::test_support::{request_context, sample_key, sample_provider, sample_usage};
|
||||
use crate::AppState;
|
||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||
use aether_data::repository::usage::InMemoryUsageReadRepository;
|
||||
@@ -288,13 +288,11 @@ async fn admin_monitoring_cache_stats_returns_local_payload() {
|
||||
let state = AppState::new()
|
||||
.expect("state should build")
|
||||
.with_data_state_for_tests(
|
||||
crate::data::GatewayDataState::with_usage_reader_for_tests(
|
||||
usage_repository,
|
||||
)
|
||||
.with_system_config_values_for_tests([
|
||||
("scheduling_mode".to_string(), json!("cache_affinity")),
|
||||
("provider_priority_mode".to_string(), json!("provider")),
|
||||
]),
|
||||
crate::data::GatewayDataState::with_usage_reader_for_tests(usage_repository)
|
||||
.with_system_config_values_for_tests([
|
||||
("scheduling_mode".to_string(), json!("cache_affinity")),
|
||||
("provider_priority_mode".to_string(), json!("provider")),
|
||||
]),
|
||||
);
|
||||
let context = request_context(http::Method::GET, "/api/admin/monitoring/cache/stats");
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
use super::test_support::*;
|
||||
use super::{maybe_build_local_admin_monitoring_response, AppState};
|
||||
use aether_data_contracts::repository::{
|
||||
candidates::{RequestCandidateStatus, StoredRequestCandidate},
|
||||
provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
},
|
||||
usage::StoredRequestUsageAudit,
|
||||
};
|
||||
use axum::body::to_bytes;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
@@ -7,19 +14,13 @@ use std::sync::Arc;
|
||||
use aether_data::repository::auth::{
|
||||
InMemoryAuthApiKeySnapshotRepository, StoredAuthApiKeyExportRecord,
|
||||
};
|
||||
use aether_data::repository::candidates::{
|
||||
InMemoryRequestCandidateRepository, RequestCandidateStatus, StoredRequestCandidate,
|
||||
};
|
||||
use aether_data::repository::provider_catalog::{
|
||||
InMemoryProviderCatalogReadRepository, StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
StoredProviderCatalogProvider,
|
||||
};
|
||||
use aether_data::repository::usage::{InMemoryUsageReadRepository, StoredRequestUsageAudit};
|
||||
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
|
||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||
use aether_data::repository::usage::InMemoryUsageReadRepository;
|
||||
use aether_data::repository::users::{
|
||||
InMemoryUserReadRepository, StoredUserAuthRecord, StoredUserExportRow,
|
||||
};
|
||||
|
||||
#[path = "tests_basics.rs"]
|
||||
mod basics;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -58,10 +59,8 @@ 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::data::GatewayDataState::with_user_reader_for_tests(
|
||||
user_repository,
|
||||
)
|
||||
.with_auth_api_key_reader(auth_repository),
|
||||
crate::data::GatewayDataState::with_user_reader_for_tests(user_repository)
|
||||
.with_auth_api_key_reader(auth_repository),
|
||||
);
|
||||
let context = request_context(
|
||||
http::Method::GET,
|
||||
@@ -106,11 +105,9 @@ 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::data::GatewayDataState::with_provider_catalog_reader_for_tests(
|
||||
provider_catalog,
|
||||
)
|
||||
.with_user_reader(user_repository)
|
||||
.with_auth_api_key_reader(auth_repository),
|
||||
crate::data::GatewayDataState::with_provider_catalog_reader_for_tests(provider_catalog)
|
||||
.with_user_reader(user_repository)
|
||||
.with_auth_api_key_reader(auth_repository),
|
||||
)
|
||||
.with_admin_monitoring_cache_affinity_entry_for_tests(
|
||||
"cache_affinity:user-key-1:openai:model-alpha",
|
||||
@@ -212,10 +209,8 @@ 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::data::GatewayDataState::with_user_reader_for_tests(
|
||||
user_repository,
|
||||
)
|
||||
.with_auth_api_key_reader(auth_repository),
|
||||
crate::data::GatewayDataState::with_user_reader_for_tests(user_repository)
|
||||
.with_auth_api_key_reader(auth_repository),
|
||||
)
|
||||
.with_admin_monitoring_cache_affinity_entry_for_tests(
|
||||
"cache_affinity:user-key-1:openai:model-alpha",
|
||||
@@ -600,10 +595,8 @@ 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::data::GatewayDataState::with_user_reader_for_tests(
|
||||
user_repository,
|
||||
)
|
||||
.with_auth_api_key_reader(auth_repository),
|
||||
crate::data::GatewayDataState::with_user_reader_for_tests(user_repository)
|
||||
.with_auth_api_key_reader(auth_repository),
|
||||
)
|
||||
.with_admin_monitoring_cache_affinity_entry_for_tests(
|
||||
"cache_affinity:user-key-1:openai:model-alpha",
|
||||
@@ -720,13 +713,11 @@ async fn admin_monitoring_cache_metrics_returns_local_payload() {
|
||||
let state = AppState::new()
|
||||
.expect("state should build")
|
||||
.with_data_state_for_tests(
|
||||
crate::data::GatewayDataState::with_usage_reader_for_tests(
|
||||
usage_repository,
|
||||
)
|
||||
.with_system_config_values_for_tests([
|
||||
("scheduling_mode".to_string(), json!("cache_affinity")),
|
||||
("provider_priority_mode".to_string(), json!("provider")),
|
||||
]),
|
||||
crate::data::GatewayDataState::with_usage_reader_for_tests(usage_repository)
|
||||
.with_system_config_values_for_tests([
|
||||
("scheduling_mode".to_string(), json!("cache_affinity")),
|
||||
("provider_priority_mode".to_string(), json!("provider")),
|
||||
]),
|
||||
);
|
||||
let context = request_context(http::Method::GET, "/api/admin/monitoring/cache/metrics");
|
||||
|
||||
@@ -969,9 +960,7 @@ async fn admin_monitoring_circuit_history_returns_local_payload() {
|
||||
let state = AppState::new()
|
||||
.expect("state should build")
|
||||
.with_data_state_for_tests(
|
||||
crate::data::GatewayDataState::with_provider_catalog_reader_for_tests(
|
||||
provider_catalog,
|
||||
),
|
||||
crate::data::GatewayDataState::with_provider_catalog_reader_for_tests(provider_catalog),
|
||||
);
|
||||
let context = request_context(
|
||||
http::Method::GET,
|
||||
@@ -1001,5 +990,4 @@ async fn admin_monitoring_circuit_history_returns_local_payload() {
|
||||
);
|
||||
}
|
||||
|
||||
#[path = "tests_trace.rs"]
|
||||
mod trace_cases;
|
||||
mod trace;
|
||||
@@ -1,15 +1,14 @@
|
||||
use super::super::maybe_build_local_admin_monitoring_response;
|
||||
use super::super::routes::maybe_build_local_admin_monitoring_response;
|
||||
use super::super::test_support::{
|
||||
request_context, sample_candidate, sample_endpoint, sample_key, sample_provider,
|
||||
};
|
||||
use crate::AppState;
|
||||
use aether_data_contracts::repository::candidates::RequestCandidateStatus;
|
||||
use axum::body::to_bytes;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
|
||||
use aether_data::repository::candidates::{
|
||||
InMemoryRequestCandidateRepository, RequestCandidateStatus,
|
||||
};
|
||||
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
|
||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -155,3 +154,26 @@ async fn admin_monitoring_trace_provider_stats_returns_local_payload() {
|
||||
assert_eq!(payload["failure_rate"], json!(50.0));
|
||||
assert_eq!(payload["avg_latency_ms"], json!(40.0));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_monitoring_trace_request_returns_contextual_not_found_payload() {
|
||||
let state = AppState::new().expect("state should build");
|
||||
let context = request_context(
|
||||
http::Method::GET,
|
||||
"/api/admin/monitoring/trace/provider-test-missing?attempted_only=false",
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
|
||||
assert_eq!(response.status(), http::StatusCode::NOT_FOUND);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json body should parse");
|
||||
assert_eq!(payload["detail"], json!("Request trace not found"));
|
||||
assert_eq!(payload["request_id"], json!("provider-test-missing"));
|
||||
assert_eq!(payload["attempted_only"], json!(false));
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
use super::{
|
||||
admin_monitoring_bad_request_response, admin_monitoring_not_found_response,
|
||||
parse_admin_monitoring_limit,
|
||||
};
|
||||
use super::responses::admin_monitoring_bad_request_response;
|
||||
use super::route_filters::parse_admin_monitoring_limit;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::{query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::handlers::admin::shared::{query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::log_ids::short_request_id;
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::candidates::RequestCandidateStatus;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tracing::warn;
|
||||
|
||||
fn admin_monitoring_trace_request_id_from_path(request_path: &str) -> Option<String> {
|
||||
let value = request_path
|
||||
@@ -49,6 +51,21 @@ fn parse_admin_monitoring_attempted_only(query: Option<&str>) -> Result<bool, St
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_monitoring_trace_not_found_response(
|
||||
request_id: &str,
|
||||
attempted_only: bool,
|
||||
) -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({
|
||||
"detail": "Request trace not found",
|
||||
"request_id": request_id,
|
||||
"attempted_only": attempted_only,
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_trace_request_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
@@ -66,10 +83,23 @@ pub(super) async fn build_admin_monitoring_trace_request_response(
|
||||
};
|
||||
|
||||
let Some(trace) = state
|
||||
.data
|
||||
.read_decision_trace(&request_id, attempted_only)
|
||||
.await?
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
else {
|
||||
return Ok(admin_monitoring_not_found_response("Request not found"));
|
||||
warn!(
|
||||
event_name = "admin_monitoring_request_trace_not_found",
|
||||
log_type = "admin_monitoring",
|
||||
request_id = %short_request_id(request_id.as_str()),
|
||||
attempted_only,
|
||||
path = %request_context.request_path,
|
||||
"admin monitoring request trace not found"
|
||||
);
|
||||
return Ok(admin_monitoring_trace_not_found_response(
|
||||
&request_id,
|
||||
attempted_only,
|
||||
));
|
||||
};
|
||||
|
||||
let candidates = trace
|
||||
@@ -142,45 +172,31 @@ pub(super) async fn build_admin_monitoring_trace_provider_stats_response(
|
||||
let total_attempts = candidates.len();
|
||||
let success_count = candidates
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
item.status == aether_data::repository::candidates::RequestCandidateStatus::Success
|
||||
})
|
||||
.filter(|item| item.status == RequestCandidateStatus::Success)
|
||||
.count();
|
||||
let failed_count = candidates
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
item.status == aether_data::repository::candidates::RequestCandidateStatus::Failed
|
||||
})
|
||||
.filter(|item| item.status == RequestCandidateStatus::Failed)
|
||||
.count();
|
||||
let cancelled_count = candidates
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
item.status == aether_data::repository::candidates::RequestCandidateStatus::Cancelled
|
||||
})
|
||||
.filter(|item| item.status == RequestCandidateStatus::Cancelled)
|
||||
.count();
|
||||
let skipped_count = candidates
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
item.status == aether_data::repository::candidates::RequestCandidateStatus::Skipped
|
||||
})
|
||||
.filter(|item| item.status == RequestCandidateStatus::Skipped)
|
||||
.count();
|
||||
let pending_count = candidates
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
item.status == aether_data::repository::candidates::RequestCandidateStatus::Pending
|
||||
})
|
||||
.filter(|item| item.status == RequestCandidateStatus::Pending)
|
||||
.count();
|
||||
let available_count = candidates
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
item.status == aether_data::repository::candidates::RequestCandidateStatus::Available
|
||||
})
|
||||
.filter(|item| item.status == RequestCandidateStatus::Available)
|
||||
.count();
|
||||
let unused_count = candidates
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
item.status == aether_data::repository::candidates::RequestCandidateStatus::Unused
|
||||
})
|
||||
.filter(|item| item.status == RequestCandidateStatus::Unused)
|
||||
.count();
|
||||
let completed_count = success_count + failed_count;
|
||||
let failure_rate = if completed_count == 0 {
|
||||
@@ -0,0 +1,9 @@
|
||||
use aether_data_contracts::repository::usage::StoredRequestUsageAudit;
|
||||
|
||||
pub(super) fn admin_monitoring_usage_is_error(item: &StoredRequestUsageAudit) -> bool {
|
||||
item.status_code.is_some_and(|value| value >= 400)
|
||||
|| item.status.trim().eq_ignore_ascii_case("failed")
|
||||
|| item.status.trim().eq_ignore_ascii_case("error")
|
||||
|| item.error_message.is_some()
|
||||
|| item.error_category.is_some()
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
use super::{
|
||||
use super::helpers::{
|
||||
round_to, AdminStatsComparisonType, AdminStatsGranularity, AdminStatsTimeRange,
|
||||
AdminStatsUsageFilter,
|
||||
};
|
||||
use super::range::{build_comparison_range, list_usage_for_range};
|
||||
use super::responses::{
|
||||
admin_stats_bad_request_response, admin_stats_comparison_empty_response,
|
||||
admin_stats_error_distribution_empty_response,
|
||||
admin_stats_performance_percentiles_empty_response, admin_stats_time_series_empty_response,
|
||||
aggregate_usage_stats, build_comparison_range, build_time_series_payload, list_usage_for_range,
|
||||
pct_change_value, percentile_cont, round_to, AdminStatsComparisonType, AdminStatsGranularity,
|
||||
AdminStatsTimeRange, AdminStatsUsageFilter,
|
||||
};
|
||||
use super::timeseries::{
|
||||
aggregate_usage_stats, build_time_series_payload, pct_change_value, percentile_cont,
|
||||
};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::query_param_value;
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -1,12 +1,17 @@
|
||||
use super::{
|
||||
admin_stats_bad_request_response, admin_stats_cost_forecast_empty_response,
|
||||
admin_stats_cost_savings_empty_response, build_daily_time_series_buckets,
|
||||
build_time_range_from_days, linear_regression, list_usage_for_range, parse_bounded_u32,
|
||||
parse_tz_offset_minutes, round_to, AdminStatsForecastPoint, AdminStatsGranularity,
|
||||
AdminStatsTimeRange, AdminStatsUsageFilter,
|
||||
use super::helpers::{
|
||||
round_to, AdminStatsForecastPoint, AdminStatsGranularity, AdminStatsTimeRange,
|
||||
AdminStatsUsageFilter,
|
||||
};
|
||||
use super::range::{
|
||||
build_time_range_from_days, list_usage_for_range, parse_bounded_u32, parse_tz_offset_minutes,
|
||||
};
|
||||
use super::responses::{
|
||||
admin_stats_bad_request_response, admin_stats_cost_forecast_empty_response,
|
||||
admin_stats_cost_savings_empty_response,
|
||||
};
|
||||
use super::timeseries::{build_daily_time_series_buckets, linear_regression};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::query_param_value;
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -1,60 +1,22 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::query_param_value;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{body::Body, response::Response};
|
||||
use super::range::{
|
||||
admin_usage_default_days, parse_naive_date, parse_tz_offset_minutes, resolve_preset_dates,
|
||||
user_today,
|
||||
};
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use aether_data_contracts::repository::usage::StoredRequestUsageAudit;
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
|
||||
const MIN_PERCENTILE_SAMPLES: usize = 10;
|
||||
|
||||
#[path = "stats/analytics_routes.rs"]
|
||||
mod analytics_routes;
|
||||
#[path = "stats/cost_routes.rs"]
|
||||
mod cost_routes;
|
||||
#[path = "stats/leaderboard.rs"]
|
||||
mod leaderboard;
|
||||
#[path = "stats/leaderboard_routes.rs"]
|
||||
mod leaderboard_routes;
|
||||
#[path = "stats/provider_quota_routes.rs"]
|
||||
mod provider_quota_routes;
|
||||
#[path = "stats/range.rs"]
|
||||
mod range;
|
||||
#[path = "stats/responses.rs"]
|
||||
mod responses;
|
||||
#[path = "stats/timeseries.rs"]
|
||||
mod timeseries;
|
||||
|
||||
use self::leaderboard::{
|
||||
build_api_key_leaderboard_items, build_model_leaderboard_items, build_user_leaderboard_items,
|
||||
compare_leaderboard_items, compute_dense_rank, load_user_leaderboard_metadata,
|
||||
};
|
||||
use self::range::{
|
||||
admin_usage_default_days, build_comparison_range, build_time_range_from_days,
|
||||
list_usage_for_range, parse_naive_date, parse_nonnegative_usize, parse_tz_offset_minutes,
|
||||
resolve_preset_dates, user_today,
|
||||
};
|
||||
pub(crate) use self::range::{list_usage_for_optional_range, parse_bounded_u32};
|
||||
pub(crate) use self::responses::admin_stats_bad_request_response;
|
||||
use self::responses::{
|
||||
admin_stats_comparison_empty_response, admin_stats_cost_forecast_empty_response,
|
||||
admin_stats_cost_savings_empty_response, admin_stats_error_distribution_empty_response,
|
||||
admin_stats_leaderboard_empty_response, admin_stats_performance_percentiles_empty_response,
|
||||
admin_stats_provider_quota_usage_empty_response, admin_stats_time_series_empty_response,
|
||||
};
|
||||
pub(crate) use self::timeseries::aggregate_usage_stats;
|
||||
use self::timeseries::{
|
||||
build_daily_time_series_buckets, build_time_series_payload, linear_regression,
|
||||
pct_change_value, percentile_cont,
|
||||
};
|
||||
pub(crate) const MIN_PERCENTILE_SAMPLES: usize = 10;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum AdminStatsComparisonType {
|
||||
pub(crate) enum AdminStatsComparisonType {
|
||||
Period,
|
||||
Year,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum AdminStatsGranularity {
|
||||
pub(crate) enum AdminStatsGranularity {
|
||||
Hour,
|
||||
Day,
|
||||
Week,
|
||||
@@ -86,39 +48,50 @@ pub(crate) struct AdminStatsAggregate {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct AdminStatsForecastPoint {
|
||||
date: chrono::NaiveDate,
|
||||
total_cost: f64,
|
||||
pub(crate) struct AdminStatsForecastPoint {
|
||||
pub(crate) date: chrono::NaiveDate,
|
||||
pub(crate) total_cost: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum AdminStatsLeaderboardMetric {
|
||||
pub(crate) enum AdminStatsLeaderboardMetric {
|
||||
Requests,
|
||||
Tokens,
|
||||
Cost,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum AdminStatsSortOrder {
|
||||
pub(crate) enum AdminStatsSortOrder {
|
||||
Asc,
|
||||
Desc,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct AdminStatsLeaderboardItem {
|
||||
id: String,
|
||||
name: String,
|
||||
requests: u64,
|
||||
tokens: u64,
|
||||
cost: f64,
|
||||
pub(crate) struct AdminStatsLeaderboardItem {
|
||||
pub(crate) id: String,
|
||||
pub(crate) name: String,
|
||||
pub(crate) requests: u64,
|
||||
pub(crate) tokens: u64,
|
||||
pub(crate) cost: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct AdminStatsUserMetadata {
|
||||
name: String,
|
||||
role: String,
|
||||
is_active: bool,
|
||||
is_deleted: bool,
|
||||
pub(crate) struct AdminStatsUserMetadata {
|
||||
pub(crate) name: String,
|
||||
pub(crate) role: String,
|
||||
pub(crate) is_active: bool,
|
||||
pub(crate) is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct AdminStatsTimeSeriesBucket {
|
||||
pub(crate) total_requests: u64,
|
||||
pub(crate) input_tokens: u64,
|
||||
pub(crate) output_tokens: u64,
|
||||
pub(crate) cache_creation_tokens: u64,
|
||||
pub(crate) cache_read_tokens: u64,
|
||||
pub(crate) total_cost: f64,
|
||||
pub(crate) total_response_time_ms: f64,
|
||||
}
|
||||
|
||||
impl AdminStatsAggregate {
|
||||
@@ -131,19 +104,8 @@ impl AdminStatsAggregate {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct AdminStatsTimeSeriesBucket {
|
||||
total_requests: u64,
|
||||
input_tokens: u64,
|
||||
output_tokens: u64,
|
||||
cache_creation_tokens: u64,
|
||||
cache_read_tokens: u64,
|
||||
total_cost: f64,
|
||||
total_response_time_ms: f64,
|
||||
}
|
||||
|
||||
impl AdminStatsGranularity {
|
||||
fn parse(query: Option<&str>) -> Result<Self, String> {
|
||||
pub(crate) fn parse(query: Option<&str>) -> Result<Self, String> {
|
||||
match query_param_value(query, "granularity").as_deref() {
|
||||
None | Some("day") => Ok(Self::Day),
|
||||
Some("hour") => Ok(Self::Hour),
|
||||
@@ -155,7 +117,7 @@ impl AdminStatsGranularity {
|
||||
}
|
||||
|
||||
impl AdminStatsLeaderboardMetric {
|
||||
fn parse(query: Option<&str>) -> Result<Self, String> {
|
||||
pub(crate) fn parse(query: Option<&str>) -> Result<Self, String> {
|
||||
match query_param_value(query, "metric").as_deref() {
|
||||
None | Some("requests") => Ok(Self::Requests),
|
||||
Some("tokens") => Ok(Self::Tokens),
|
||||
@@ -164,7 +126,7 @@ impl AdminStatsLeaderboardMetric {
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(self) -> &'static str {
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Requests => "requests",
|
||||
Self::Tokens => "tokens",
|
||||
@@ -174,7 +136,7 @@ impl AdminStatsLeaderboardMetric {
|
||||
}
|
||||
|
||||
impl AdminStatsSortOrder {
|
||||
fn parse(query: Option<&str>) -> Result<Self, String> {
|
||||
pub(crate) fn parse(query: Option<&str>) -> Result<Self, String> {
|
||||
match query_param_value(query, "order").as_deref() {
|
||||
None | Some("desc") => Ok(Self::Desc),
|
||||
Some("asc") => Ok(Self::Asc),
|
||||
@@ -184,7 +146,7 @@ impl AdminStatsSortOrder {
|
||||
}
|
||||
|
||||
impl AdminStatsUsageFilter {
|
||||
fn from_query(query: Option<&str>) -> Self {
|
||||
pub(crate) fn from_query(query: Option<&str>) -> Self {
|
||||
Self {
|
||||
user_id: query_param_value(query, "user_id"),
|
||||
provider_name: query_param_value(query, "provider_name"),
|
||||
@@ -194,7 +156,7 @@ impl AdminStatsUsageFilter {
|
||||
}
|
||||
|
||||
impl AdminStatsTimeSeriesBucket {
|
||||
fn add_usage(&mut self, item: &aether_data::repository::usage::StoredRequestUsageAudit) {
|
||||
pub(crate) fn add_usage(&mut self, item: &StoredRequestUsageAudit) {
|
||||
self.total_requests = self.total_requests.saturating_add(1);
|
||||
self.input_tokens = self.input_tokens.saturating_add(item.input_tokens);
|
||||
self.output_tokens = self.output_tokens.saturating_add(item.output_tokens);
|
||||
@@ -208,7 +170,7 @@ impl AdminStatsTimeSeriesBucket {
|
||||
self.total_response_time_ms += item.response_time_ms.unwrap_or(0) as f64;
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &Self) {
|
||||
pub(crate) fn merge(&mut self, other: &Self) {
|
||||
self.total_requests = self.total_requests.saturating_add(other.total_requests);
|
||||
self.input_tokens = self.input_tokens.saturating_add(other.input_tokens);
|
||||
self.output_tokens = self.output_tokens.saturating_add(other.output_tokens);
|
||||
@@ -222,7 +184,7 @@ impl AdminStatsTimeSeriesBucket {
|
||||
self.total_response_time_ms += other.total_response_time_ms;
|
||||
}
|
||||
|
||||
fn avg_response_time_ms(&self) -> f64 {
|
||||
pub(crate) fn avg_response_time_ms(&self) -> f64 {
|
||||
if self.total_requests == 0 {
|
||||
0.0
|
||||
} else {
|
||||
@@ -230,7 +192,7 @@ impl AdminStatsTimeSeriesBucket {
|
||||
}
|
||||
}
|
||||
|
||||
fn to_json_with_avg(&self, date: String) -> serde_json::Value {
|
||||
pub(crate) fn to_json_with_avg(&self, date: String) -> serde_json::Value {
|
||||
json!({
|
||||
"date": date,
|
||||
"total_requests": self.total_requests,
|
||||
@@ -243,7 +205,7 @@ impl AdminStatsTimeSeriesBucket {
|
||||
})
|
||||
}
|
||||
|
||||
fn to_json_without_avg(&self, date: String) -> serde_json::Value {
|
||||
pub(crate) fn to_json_without_avg(&self, date: String) -> serde_json::Value {
|
||||
json!({
|
||||
"date": date,
|
||||
"total_requests": self.total_requests,
|
||||
@@ -314,7 +276,7 @@ impl AdminStatsTimeRange {
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_required(
|
||||
pub(crate) fn resolve_required(
|
||||
query: Option<&str>,
|
||||
start_key: &str,
|
||||
end_key: &str,
|
||||
@@ -338,7 +300,7 @@ impl AdminStatsTimeRange {
|
||||
})
|
||||
}
|
||||
|
||||
fn to_unix_bounds(&self) -> Option<(u64, u64)> {
|
||||
pub(crate) fn to_unix_bounds(&self) -> Option<(u64, u64)> {
|
||||
let offset = chrono::Duration::minutes(i64::from(self.tz_offset_minutes));
|
||||
let start_local = self.start_date.and_hms_opt(0, 0, 0)?;
|
||||
let end_local = self
|
||||
@@ -356,7 +318,9 @@ impl AdminStatsTimeRange {
|
||||
Some((start_utc as u64, end_utc as u64))
|
||||
}
|
||||
|
||||
fn to_utc_datetime_bounds(&self) -> Option<(chrono::DateTime<Utc>, chrono::DateTime<Utc>)> {
|
||||
pub(crate) fn to_utc_datetime_bounds(
|
||||
&self,
|
||||
) -> Option<(chrono::DateTime<Utc>, chrono::DateTime<Utc>)> {
|
||||
let offset = chrono::Duration::minutes(i64::from(self.tz_offset_minutes));
|
||||
let start_local = self.start_date.and_hms_opt(0, 0, 0)?;
|
||||
let end_local = self
|
||||
@@ -369,7 +333,10 @@ impl AdminStatsTimeRange {
|
||||
))
|
||||
}
|
||||
|
||||
fn validate_for_time_series(&self, granularity: AdminStatsGranularity) -> Result<(), String> {
|
||||
pub(crate) fn validate_for_time_series(
|
||||
&self,
|
||||
granularity: AdminStatsGranularity,
|
||||
) -> Result<(), String> {
|
||||
if granularity == AdminStatsGranularity::Hour && self.start_date != self.end_date {
|
||||
return Err("Hour granularity only supports single day query".to_string());
|
||||
}
|
||||
@@ -382,7 +349,7 @@ impl AdminStatsTimeRange {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn local_dates(&self) -> Vec<chrono::NaiveDate> {
|
||||
pub(crate) fn local_dates(&self) -> Vec<chrono::NaiveDate> {
|
||||
let mut current = self.start_date;
|
||||
let mut dates = Vec::new();
|
||||
while current <= self.end_date {
|
||||
@@ -395,75 +362,25 @@ impl AdminStatsTimeRange {
|
||||
dates
|
||||
}
|
||||
|
||||
fn local_date_strings(&self) -> Vec<String> {
|
||||
pub(crate) fn local_date_strings(&self) -> Vec<String> {
|
||||
self.local_dates()
|
||||
.into_iter()
|
||||
.map(|date| date.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn local_date_for_unix_secs(&self, unix_secs: u64) -> Option<chrono::NaiveDate> {
|
||||
pub(crate) fn local_date_for_unix_secs(&self, unix_secs: u64) -> Option<chrono::NaiveDate> {
|
||||
let timestamp = chrono::DateTime::<Utc>::from_timestamp(i64::try_from(unix_secs).ok()?, 0)?;
|
||||
let local = timestamp
|
||||
.checked_add_signed(chrono::Duration::minutes(i64::from(self.tz_offset_minutes)))?;
|
||||
Some(local.date_naive())
|
||||
}
|
||||
|
||||
fn local_date_string_for_unix_secs(&self, unix_secs: u64) -> Option<String> {
|
||||
pub(crate) fn local_date_string_for_unix_secs(&self, unix_secs: u64) -> Option<String> {
|
||||
Some(self.local_date_for_unix_secs(unix_secs)?.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_stats_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(decision) = request_context.control_decision.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if decision.route_family.as_deref() != Some("stats_manage") {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let Some(response) =
|
||||
provider_quota_routes::maybe_build_local_admin_stats_provider_quota_response(
|
||||
state,
|
||||
request_context,
|
||||
decision,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) = analytics_routes::maybe_build_local_admin_stats_analytics_response(
|
||||
state,
|
||||
request_context,
|
||||
decision,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) =
|
||||
cost_routes::maybe_build_local_admin_stats_cost_response(state, request_context).await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) = leaderboard_routes::maybe_build_local_admin_stats_leaderboard_response(
|
||||
state,
|
||||
request_context,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) fn round_to(value: f64, decimals: u32) -> f64 {
|
||||
let factor = 10_f64.powi(i32::try_from(decimals).unwrap_or(0));
|
||||
(value * factor).round() / factor
|
||||
@@ -1,11 +1,12 @@
|
||||
use super::{
|
||||
use super::helpers::{
|
||||
AdminStatsLeaderboardItem, AdminStatsLeaderboardMetric, AdminStatsSortOrder,
|
||||
AdminStatsUserMetadata,
|
||||
};
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::usage::StoredRequestUsageAudit;
|
||||
|
||||
pub(super) fn build_model_leaderboard_items(
|
||||
items: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
items: &[StoredRequestUsageAudit],
|
||||
) -> Vec<AdminStatsLeaderboardItem> {
|
||||
let mut grouped: std::collections::BTreeMap<String, AdminStatsLeaderboardItem> =
|
||||
std::collections::BTreeMap::new();
|
||||
@@ -38,7 +39,7 @@ pub(super) fn build_model_leaderboard_items(
|
||||
}
|
||||
|
||||
pub(super) fn build_api_key_leaderboard_items(
|
||||
items: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
items: &[StoredRequestUsageAudit],
|
||||
snapshots: Option<&[aether_data::repository::auth::StoredAuthApiKeySnapshot]>,
|
||||
include_inactive: bool,
|
||||
exclude_admin: bool,
|
||||
@@ -110,7 +111,7 @@ pub(super) fn build_api_key_leaderboard_items(
|
||||
}
|
||||
|
||||
pub(super) fn build_user_leaderboard_items(
|
||||
items: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
items: &[StoredRequestUsageAudit],
|
||||
users: &std::collections::BTreeMap<String, AdminStatsUserMetadata>,
|
||||
include_inactive: bool,
|
||||
exclude_admin: bool,
|
||||
@@ -1,12 +1,15 @@
|
||||
use super::{
|
||||
admin_stats_bad_request_response, admin_stats_leaderboard_empty_response,
|
||||
build_api_key_leaderboard_items, build_model_leaderboard_items, build_user_leaderboard_items,
|
||||
compare_leaderboard_items, compute_dense_rank, list_usage_for_optional_range,
|
||||
load_user_leaderboard_metadata, parse_bounded_u32, parse_nonnegative_usize, round_to,
|
||||
AdminStatsLeaderboardMetric, AdminStatsSortOrder, AdminStatsTimeRange, AdminStatsUsageFilter,
|
||||
use super::helpers::{
|
||||
round_to, AdminStatsLeaderboardMetric, AdminStatsSortOrder, AdminStatsTimeRange,
|
||||
AdminStatsUsageFilter,
|
||||
};
|
||||
use super::leaderboard::{
|
||||
build_api_key_leaderboard_items, build_model_leaderboard_items, build_user_leaderboard_items,
|
||||
compare_leaderboard_items, compute_dense_rank, load_user_leaderboard_metadata,
|
||||
};
|
||||
use super::range::{list_usage_for_optional_range, parse_bounded_u32, parse_nonnegative_usize};
|
||||
use super::responses::{admin_stats_bad_request_response, admin_stats_leaderboard_empty_response};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::{query_param_bool, query_param_value};
|
||||
use crate::handlers::admin::shared::{query_param_bool, query_param_value};
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -167,8 +170,10 @@ pub(super) async fn maybe_build_local_admin_stats_leaderboard_response(
|
||||
.collect();
|
||||
Some(
|
||||
state
|
||||
.read_auth_api_key_snapshots_by_ids(&api_key_ids)
|
||||
.await?,
|
||||
.data
|
||||
.list_auth_api_key_snapshots_by_ids(&api_key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@@ -0,0 +1,67 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
mod analytics_routes;
|
||||
mod cost_routes;
|
||||
mod helpers;
|
||||
mod leaderboard;
|
||||
mod leaderboard_routes;
|
||||
mod provider_quota_routes;
|
||||
mod range;
|
||||
mod responses;
|
||||
mod timeseries;
|
||||
pub(crate) use self::helpers::{round_to, AdminStatsTimeRange, AdminStatsUsageFilter};
|
||||
pub(crate) use self::range::{list_usage_for_optional_range, parse_bounded_u32};
|
||||
pub(crate) use self::responses::admin_stats_bad_request_response;
|
||||
pub(crate) use self::timeseries::aggregate_usage_stats;
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_stats_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(decision) = request_context.control_decision.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if decision.route_family.as_deref() != Some("stats_manage") {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let Some(response) =
|
||||
provider_quota_routes::maybe_build_local_admin_stats_provider_quota_response(
|
||||
state,
|
||||
request_context,
|
||||
decision,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) = analytics_routes::maybe_build_local_admin_stats_analytics_response(
|
||||
state,
|
||||
request_context,
|
||||
decision,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) =
|
||||
cost_routes::maybe_build_local_admin_stats_cost_response(state, request_context).await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(response) = leaderboard_routes::maybe_build_local_admin_stats_leaderboard_response(
|
||||
state,
|
||||
request_context,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::admin_stats_provider_quota_usage_empty_response;
|
||||
use super::responses::admin_stats_provider_quota_usage_empty_response;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::unix_secs_to_rfc3339;
|
||||
use crate::handlers::admin::shared::unix_secs_to_rfc3339;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::{AdminStatsComparisonType, AdminStatsTimeRange, AdminStatsUsageFilter};
|
||||
use crate::handlers::query_param_value;
|
||||
use super::helpers::{AdminStatsComparisonType, AdminStatsTimeRange, AdminStatsUsageFilter};
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageAuditListQuery};
|
||||
use chrono::{Datelike, Utc};
|
||||
|
||||
pub(super) fn parse_tz_offset_minutes(query: Option<&str>) -> Result<i32, String> {
|
||||
@@ -183,14 +184,14 @@ pub(super) async fn list_usage_for_range(
|
||||
state: &AppState,
|
||||
time_range: &AdminStatsTimeRange,
|
||||
filters: &AdminStatsUsageFilter,
|
||||
) -> Result<Vec<aether_data::repository::usage::StoredRequestUsageAudit>, GatewayError> {
|
||||
) -> Result<Vec<StoredRequestUsageAudit>, GatewayError> {
|
||||
let Some((created_from_unix_secs, created_until_unix_secs)) = time_range.to_unix_bounds()
|
||||
else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
state
|
||||
.list_usage_audits(&aether_data::repository::usage::UsageAuditListQuery {
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: Some(created_from_unix_secs),
|
||||
created_until_unix_secs: Some(created_until_unix_secs),
|
||||
user_id: filters.user_id.clone(),
|
||||
@@ -204,12 +205,12 @@ pub(crate) async fn list_usage_for_optional_range(
|
||||
state: &AppState,
|
||||
time_range: Option<&AdminStatsTimeRange>,
|
||||
filters: &AdminStatsUsageFilter,
|
||||
) -> Result<Vec<aether_data::repository::usage::StoredRequestUsageAudit>, GatewayError> {
|
||||
) -> Result<Vec<StoredRequestUsageAudit>, GatewayError> {
|
||||
match time_range {
|
||||
Some(time_range) => list_usage_for_range(state, time_range, filters).await,
|
||||
None => {
|
||||
state
|
||||
.list_usage_audits(&aether_data::repository::usage::UsageAuditListQuery {
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: None,
|
||||
created_until_unix_secs: None,
|
||||
user_id: filters.user_id.clone(),
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{AdminStatsLeaderboardMetric, AdminStatsTimeRange};
|
||||
use super::helpers::{AdminStatsLeaderboardMetric, AdminStatsTimeRange};
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
@@ -1,14 +1,15 @@
|
||||
use super::{
|
||||
use super::helpers::{
|
||||
round_to, AdminStatsAggregate, AdminStatsGranularity, AdminStatsTimeRange,
|
||||
AdminStatsTimeSeriesBucket, MIN_PERCENTILE_SAMPLES,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::StoredRequestUsageAudit;
|
||||
use chrono::{Datelike, Utc};
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) fn build_time_series_payload(
|
||||
time_range: &AdminStatsTimeRange,
|
||||
granularity: AdminStatsGranularity,
|
||||
items: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
items: &[StoredRequestUsageAudit],
|
||||
) -> Vec<serde_json::Value> {
|
||||
match granularity {
|
||||
AdminStatsGranularity::Hour => build_hourly_time_series_payload(time_range, items),
|
||||
@@ -20,7 +21,7 @@ pub(super) fn build_time_series_payload(
|
||||
|
||||
pub(super) fn build_daily_time_series_buckets(
|
||||
time_range: &AdminStatsTimeRange,
|
||||
items: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
items: &[StoredRequestUsageAudit],
|
||||
) -> std::collections::BTreeMap<chrono::NaiveDate, AdminStatsTimeSeriesBucket> {
|
||||
let mut buckets: std::collections::BTreeMap<chrono::NaiveDate, AdminStatsTimeSeriesBucket> =
|
||||
time_range
|
||||
@@ -44,7 +45,7 @@ pub(super) fn build_daily_time_series_buckets(
|
||||
|
||||
fn build_daily_time_series_payload(
|
||||
time_range: &AdminStatsTimeRange,
|
||||
items: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
items: &[StoredRequestUsageAudit],
|
||||
) -> Vec<serde_json::Value> {
|
||||
build_daily_time_series_buckets(time_range, items)
|
||||
.into_iter()
|
||||
@@ -54,7 +55,7 @@ fn build_daily_time_series_payload(
|
||||
|
||||
fn build_weekly_time_series_payload(
|
||||
time_range: &AdminStatsTimeRange,
|
||||
items: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
items: &[StoredRequestUsageAudit],
|
||||
) -> Vec<serde_json::Value> {
|
||||
let mut weekly: std::collections::BTreeMap<
|
||||
(i32, u32),
|
||||
@@ -78,7 +79,7 @@ fn build_weekly_time_series_payload(
|
||||
|
||||
fn build_monthly_time_series_payload(
|
||||
time_range: &AdminStatsTimeRange,
|
||||
items: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
items: &[StoredRequestUsageAudit],
|
||||
) -> Vec<serde_json::Value> {
|
||||
let mut monthly: std::collections::BTreeMap<
|
||||
(i32, u32),
|
||||
@@ -104,7 +105,7 @@ fn build_monthly_time_series_payload(
|
||||
|
||||
fn build_hourly_time_series_payload(
|
||||
time_range: &AdminStatsTimeRange,
|
||||
items: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
items: &[StoredRequestUsageAudit],
|
||||
) -> Vec<serde_json::Value> {
|
||||
let Some((mut current, end)) = time_range.to_utc_datetime_bounds() else {
|
||||
return Vec::new();
|
||||
@@ -147,9 +148,7 @@ fn build_hourly_time_series_payload(
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn aggregate_usage_stats(
|
||||
items: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
) -> AdminStatsAggregate {
|
||||
pub(crate) fn aggregate_usage_stats(items: &[StoredRequestUsageAudit]) -> AdminStatsAggregate {
|
||||
let mut aggregate = AdminStatsAggregate::default();
|
||||
for item in items {
|
||||
aggregate.total_requests = aggregate.total_requests.saturating_add(1);
|
||||
@@ -1,12 +1,11 @@
|
||||
use super::super::{parse_bounded_u32, round_to};
|
||||
use crate::handlers::{query_param_value, unix_secs_to_rfc3339};
|
||||
use super::super::stats::{parse_bounded_u32, round_to};
|
||||
use crate::handlers::admin::shared::{query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageAuditListQuery};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
pub(super) fn admin_usage_total_tokens(
|
||||
item: &aether_data::repository::usage::StoredRequestUsageAudit,
|
||||
) -> u64 {
|
||||
pub(super) fn admin_usage_total_tokens(item: &StoredRequestUsageAudit) -> u64 {
|
||||
item.input_tokens
|
||||
.saturating_add(item.output_tokens)
|
||||
.saturating_add(item.cache_creation_input_tokens)
|
||||
@@ -103,7 +102,7 @@ pub(super) fn admin_usage_token_cache_hit_rate(input_tokens: u64, cache_read_tok
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_aggregation_by_model_json(
|
||||
usage: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
usage: &[StoredRequestUsageAudit],
|
||||
limit: usize,
|
||||
) -> serde_json::Value {
|
||||
let mut grouped: BTreeMap<String, (u64, u64, u64, u64, f64, f64)> = BTreeMap::new();
|
||||
@@ -155,7 +154,7 @@ pub(super) fn admin_usage_aggregation_by_model_json(
|
||||
|
||||
pub(super) async fn admin_usage_aggregation_by_user_json(
|
||||
state: &AppState,
|
||||
usage: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
usage: &[StoredRequestUsageAudit],
|
||||
limit: usize,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let mut grouped: BTreeMap<String, (u64, u64, f64)> = BTreeMap::new();
|
||||
@@ -214,7 +213,7 @@ pub(super) async fn admin_usage_aggregation_by_user_json(
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_aggregation_by_provider_json(
|
||||
usage: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
usage: &[StoredRequestUsageAudit],
|
||||
limit: usize,
|
||||
) -> serde_json::Value {
|
||||
let mut grouped: BTreeMap<String, (u64, u64, u64, u64, f64, f64, u64, u64)> = BTreeMap::new();
|
||||
@@ -289,7 +288,7 @@ pub(super) fn admin_usage_aggregation_by_provider_json(
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_aggregation_by_api_format_json(
|
||||
usage: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
usage: &[StoredRequestUsageAudit],
|
||||
limit: usize,
|
||||
) -> serde_json::Value {
|
||||
let mut grouped: BTreeMap<String, (u64, u64, u64, u64, f64, f64, u64)> = BTreeMap::new();
|
||||
@@ -351,9 +350,7 @@ pub(super) fn admin_usage_aggregation_by_api_format_json(
|
||||
json!(items)
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_heatmap_json(
|
||||
usage: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
) -> serde_json::Value {
|
||||
pub(super) fn admin_usage_heatmap_json(usage: &[StoredRequestUsageAudit]) -> serde_json::Value {
|
||||
let today = chrono::Utc::now().date_naive();
|
||||
let start_date = today
|
||||
.checked_sub_signed(chrono::Duration::days(364))
|
||||
@@ -407,9 +404,7 @@ pub(super) fn admin_usage_heatmap_json(
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_is_success(
|
||||
item: &aether_data::repository::usage::StoredRequestUsageAudit,
|
||||
) -> bool {
|
||||
pub(super) fn admin_usage_is_success(item: &StoredRequestUsageAudit) -> bool {
|
||||
matches!(
|
||||
item.status.as_str(),
|
||||
"completed" | "success" | "ok" | "billed" | "settled"
|
||||
@@ -427,11 +422,11 @@ pub(super) async fn list_recent_completed_usage_for_cache_affinity(
|
||||
state: &AppState,
|
||||
hours: u32,
|
||||
user_id: Option<&str>,
|
||||
) -> Result<Vec<aether_data::repository::usage::StoredRequestUsageAudit>, GatewayError> {
|
||||
) -> Result<Vec<StoredRequestUsageAudit>, GatewayError> {
|
||||
let now_unix_secs = u64::try_from(chrono::Utc::now().timestamp()).unwrap_or_default();
|
||||
let created_from_unix_secs = now_unix_secs.saturating_sub(u64::from(hours) * 3600);
|
||||
let mut items = state
|
||||
.list_usage_audits(&aether_data::repository::usage::UsageAuditListQuery {
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: Some(created_from_unix_secs),
|
||||
created_until_unix_secs: None,
|
||||
user_id: user_id.map(ToOwned::to_owned),
|
||||
@@ -449,8 +444,8 @@ pub(super) async fn list_recent_completed_usage_for_cache_affinity(
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_group_completed_by_user(
|
||||
items: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
) -> BTreeMap<String, Vec<aether_data::repository::usage::StoredRequestUsageAudit>> {
|
||||
items: &[StoredRequestUsageAudit],
|
||||
) -> BTreeMap<String, Vec<StoredRequestUsageAudit>> {
|
||||
let mut grouped = BTreeMap::new();
|
||||
for item in items.iter().filter(|item| item.user_id.is_some()) {
|
||||
grouped
|
||||
@@ -462,9 +457,9 @@ pub(super) fn admin_usage_group_completed_by_user(
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_group_completed_by_api_key(
|
||||
items: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
items: &[StoredRequestUsageAudit],
|
||||
api_key_id: Option<&str>,
|
||||
) -> BTreeMap<String, Vec<aether_data::repository::usage::StoredRequestUsageAudit>> {
|
||||
) -> BTreeMap<String, Vec<StoredRequestUsageAudit>> {
|
||||
let mut grouped = BTreeMap::new();
|
||||
for item in items.iter().filter(|item| item.api_key_id.is_some()) {
|
||||
if !admin_usage_matches_optional_id(item.api_key_id.as_deref(), api_key_id) {
|
||||
@@ -479,7 +474,7 @@ pub(super) fn admin_usage_group_completed_by_api_key(
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_collect_request_intervals_minutes(
|
||||
items: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
items: &[StoredRequestUsageAudit],
|
||||
) -> Vec<f64> {
|
||||
let mut previous_created_at_unix_secs = None;
|
||||
let mut intervals = Vec::new();
|
||||
@@ -588,7 +583,7 @@ pub(super) fn admin_usage_point_sort_key(
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_matches_search(
|
||||
item: &aether_data::repository::usage::StoredRequestUsageAudit,
|
||||
item: &StoredRequestUsageAudit,
|
||||
search: Option<&str>,
|
||||
) -> bool {
|
||||
let Some(search) = search.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
@@ -610,7 +605,7 @@ pub(super) fn admin_usage_matches_search(
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_matches_username(
|
||||
item: &aether_data::repository::usage::StoredRequestUsageAudit,
|
||||
item: &StoredRequestUsageAudit,
|
||||
username: Option<&str>,
|
||||
) -> bool {
|
||||
let Some(username) = username.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
@@ -634,7 +629,7 @@ pub(super) fn admin_usage_matches_eq(value: &str, query: Option<&str>) -> bool {
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_matches_api_format(
|
||||
item: &aether_data::repository::usage::StoredRequestUsageAudit,
|
||||
item: &StoredRequestUsageAudit,
|
||||
api_format: Option<&str>,
|
||||
) -> bool {
|
||||
let Some(api_format) = api_format.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
@@ -646,7 +641,7 @@ pub(super) fn admin_usage_matches_api_format(
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_matches_status(
|
||||
item: &aether_data::repository::usage::StoredRequestUsageAudit,
|
||||
item: &StoredRequestUsageAudit,
|
||||
status: Option<&str>,
|
||||
) -> bool {
|
||||
let Some(status) = status.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
@@ -671,7 +666,7 @@ pub(super) fn admin_usage_matches_status(
|
||||
|
||||
pub(super) async fn admin_usage_provider_key_names(
|
||||
state: &AppState,
|
||||
usage: &[aether_data::repository::usage::StoredRequestUsageAudit],
|
||||
usage: &[StoredRequestUsageAudit],
|
||||
) -> Result<BTreeMap<String, String>, GatewayError> {
|
||||
if !state.has_provider_catalog_data_reader() {
|
||||
return Ok(BTreeMap::new());
|
||||
@@ -696,7 +691,7 @@ pub(super) async fn admin_usage_provider_key_names(
|
||||
}
|
||||
|
||||
fn admin_usage_request_metadata_string(
|
||||
item: &aether_data::repository::usage::StoredRequestUsageAudit,
|
||||
item: &StoredRequestUsageAudit,
|
||||
key: &str,
|
||||
) -> Option<String> {
|
||||
item.request_metadata
|
||||
@@ -710,7 +705,7 @@ fn admin_usage_request_metadata_string(
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_provider_key_name(
|
||||
item: &aether_data::repository::usage::StoredRequestUsageAudit,
|
||||
item: &StoredRequestUsageAudit,
|
||||
provider_key_names: &BTreeMap<String, String>,
|
||||
) -> Option<String> {
|
||||
item.provider_api_key_id
|
||||
@@ -721,7 +716,7 @@ pub(super) fn admin_usage_provider_key_name(
|
||||
}
|
||||
|
||||
pub(super) fn admin_usage_record_json(
|
||||
item: &aether_data::repository::usage::StoredRequestUsageAudit,
|
||||
item: &StoredRequestUsageAudit,
|
||||
users_by_id: &BTreeMap<String, aether_data::repository::users::StoredUserSummary>,
|
||||
provider_key_name: Option<&str>,
|
||||
) -> Value {
|
||||
@@ -1,21 +1,24 @@
|
||||
use super::super::{
|
||||
use super::super::stats::{
|
||||
list_usage_for_optional_range, round_to, AdminStatsTimeRange, AdminStatsUsageFilter,
|
||||
};
|
||||
use super::{
|
||||
use super::analytics::{
|
||||
admin_usage_aggregation_by_api_format_json, admin_usage_aggregation_by_model_json,
|
||||
admin_usage_aggregation_by_provider_json, admin_usage_aggregation_by_user_json,
|
||||
admin_usage_bad_request_response, admin_usage_calculate_recommended_ttl,
|
||||
admin_usage_collect_request_intervals_minutes, admin_usage_data_unavailable_response,
|
||||
admin_usage_calculate_recommended_ttl, admin_usage_collect_request_intervals_minutes,
|
||||
admin_usage_group_completed_by_api_key, admin_usage_group_completed_by_user,
|
||||
admin_usage_heatmap_json, admin_usage_matches_optional_id, admin_usage_parse_aggregation_limit,
|
||||
admin_usage_parse_recent_hours, admin_usage_parse_timeline_limit, admin_usage_percentile_cont,
|
||||
admin_usage_point_sort_key, admin_usage_proportional_limits,
|
||||
admin_usage_ttl_recommendation_reason, list_recent_completed_usage_for_cache_affinity,
|
||||
};
|
||||
use super::helpers::{
|
||||
admin_usage_bad_request_response, admin_usage_data_unavailable_response,
|
||||
ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::{query_param_bool, query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::handlers::admin::shared::{query_param_bool, query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::usage::UsageAuditListQuery;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
@@ -102,7 +105,7 @@ pub(super) async fn maybe_build_local_admin_usage_analytics_response(
|
||||
let now_unix_secs = u64::try_from(chrono::Utc::now().timestamp()).unwrap_or_default();
|
||||
let created_from_unix_secs = now_unix_secs.saturating_sub(365 * 24 * 3600);
|
||||
let mut usage = state
|
||||
.list_usage_audits(&aether_data::repository::usage::UsageAuditListQuery {
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: Some(created_from_unix_secs),
|
||||
..Default::default()
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user