Merge remote-tracking branch 'origin/aether-rust-pioneer' into pr-375-session-scope

This commit is contained in:
fawney19
2026-05-05 22:34:47 +08:00
468 changed files with 88906 additions and 21501 deletions

View File

@@ -1,5 +1,5 @@
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::{query_param_value, unix_secs_to_rfc3339};
use crate::handlers::admin::shared::query_param_value;
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
@@ -9,7 +9,6 @@ use axum::{
};
use regex::Regex;
use serde_json::json;
use sqlx::Row;
const ADMIN_BILLING_DATA_UNAVAILABLE_DETAIL: &str = "Admin billing data unavailable";
@@ -185,20 +184,6 @@ fn admin_billing_validate_safe_expression(expression: &str) -> Result<(), String
Ok(())
}
fn admin_billing_optional_epoch_value(
row: &sqlx::postgres::PgRow,
field: &str,
) -> Result<Option<String>, GatewayError> {
let value = row
.try_get::<Option<i64>, _>(field)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
match value {
None => Ok(None),
Some(value) if value < 0 => Ok(None),
Some(value) => Ok(unix_secs_to_rfc3339(value as u64)),
}
}
pub(crate) async fn maybe_build_local_admin_billing_response(
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,

View File

@@ -1,7 +1,6 @@
use super::{
build_admin_payment_callback_payload, build_admin_payment_callback_payload_from_record,
build_admin_payments_bad_request_response, parse_admin_payments_limit,
parse_admin_payments_offset,
build_admin_payment_callback_payload_from_record, build_admin_payments_bad_request_response,
parse_admin_payments_limit, parse_admin_payments_offset,
};
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::query_param_value;

View File

@@ -12,13 +12,13 @@ mod 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,
build_admin_payment_order_payload, build_admin_payment_orders_page_response,
build_admin_payments_backend_unavailable_response, build_admin_payments_bad_request_response,
build_admin_payments_data_unavailable_response, normalize_admin_payment_currency,
normalize_admin_payment_optional_string, normalize_admin_payment_positive_number,
parse_admin_payments_limit, parse_admin_payments_offset, AdminPaymentOrderCreditRequest,
admin_payment_order_id_from_suffix_path, build_admin_payment_callback_payload_from_record,
build_admin_payment_order_not_found_response, build_admin_payment_order_payload,
build_admin_payment_orders_page_response, build_admin_payments_backend_unavailable_response,
build_admin_payments_bad_request_response, build_admin_payments_data_unavailable_response,
normalize_admin_payment_currency, normalize_admin_payment_optional_string,
normalize_admin_payment_positive_number, parse_admin_payments_limit,
parse_admin_payments_offset, AdminPaymentOrderCreditRequest,
};
pub(crate) async fn maybe_build_local_admin_payments_response(

View File

@@ -1,6 +1,6 @@
use crate::handlers::admin::request::AdminRequestContext;
use crate::handlers::admin::shared::{query_param_value, unix_secs_to_rfc3339};
use crate::{GatewayAdminPaymentCallbackView, GatewayError};
use crate::GatewayAdminPaymentCallbackView;
use axum::{
body::Body,
http,
@@ -8,7 +8,6 @@ use axum::{
Json,
};
use serde_json::json;
use sqlx::Row;
const ADMIN_PAYMENTS_DATA_UNAVAILABLE_DETAIL: &str = "Admin payments data unavailable";
@@ -220,34 +219,6 @@ pub(super) fn build_admin_payment_order_payload(
})
}
pub(super) fn build_admin_payment_callback_payload(
row: &sqlx::postgres::PgRow,
) -> Result<serde_json::Value, GatewayError> {
Ok(json!({
"id": row.try_get::<String, _>("id").map_err(|err| GatewayError::Internal(err.to_string()))?,
"payment_order_id": row.try_get::<Option<String>, _>("payment_order_id").map_err(|err| GatewayError::Internal(err.to_string()))?,
"payment_method": row.try_get::<String, _>("payment_method").map_err(|err| GatewayError::Internal(err.to_string()))?,
"callback_key": row.try_get::<String, _>("callback_key").map_err(|err| GatewayError::Internal(err.to_string()))?,
"order_no": row.try_get::<Option<String>, _>("order_no").map_err(|err| GatewayError::Internal(err.to_string()))?,
"gateway_order_id": row.try_get::<Option<String>, _>("gateway_order_id").map_err(|err| GatewayError::Internal(err.to_string()))?,
"payload_hash": row.try_get::<Option<String>, _>("payload_hash").map_err(|err| GatewayError::Internal(err.to_string()))?,
"signature_valid": row.try_get::<bool, _>("signature_valid").map_err(|err| GatewayError::Internal(err.to_string()))?,
"status": row.try_get::<String, _>("status").map_err(|err| GatewayError::Internal(err.to_string()))?,
"payload": row.try_get::<Option<serde_json::Value>, _>("payload").map_err(|err| GatewayError::Internal(err.to_string()))?,
"error_message": row.try_get::<Option<String>, _>("error_message").map_err(|err| GatewayError::Internal(err.to_string()))?,
"created_at": row
.try_get::<Option<i64>, _>("created_at_unix_ms")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.and_then(|value| u64::try_from(value).ok())
.and_then(unix_secs_to_rfc3339),
"processed_at": row
.try_get::<Option<i64>, _>("processed_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.and_then(|value| u64::try_from(value).ok())
.and_then(unix_secs_to_rfc3339),
}))
}
pub(super) fn build_admin_payment_callback_payload_from_record(
record: &GatewayAdminPaymentCallbackView,
) -> serde_json::Value {

View File

@@ -61,7 +61,7 @@ pub(in super::super) async fn build_admin_wallet_adjust_response(
));
}
let operator_id = admin_wallet_operator_id(request_context);
let has_postgres = state.has_postgres_pool();
let has_wallet_writer = state.has_wallet_data_writer();
let Some((wallet, transaction)) = state
.admin_adjust_wallet_balance(
&wallet_id,
@@ -72,7 +72,7 @@ pub(in super::super) async fn build_admin_wallet_adjust_response(
)
.await?
else {
return if has_postgres {
return if has_wallet_writer {
Ok(build_admin_wallet_not_found_response())
} else {
Ok(build_admin_wallets_data_unavailable_response())

View File

@@ -61,7 +61,7 @@ pub(in super::super) async fn build_admin_wallet_recharge_response(
));
}
let operator_id = admin_wallet_operator_id(request_context);
let has_postgres = state.has_postgres_pool();
let has_wallet_writer = state.has_wallet_data_writer();
let Some((wallet, payment_order)) = state
.admin_create_manual_wallet_recharge(
&wallet_id,
@@ -72,7 +72,7 @@ pub(in super::super) async fn build_admin_wallet_recharge_response(
)
.await?
else {
return if has_postgres {
return if has_wallet_writer {
Ok(build_admin_wallet_not_found_response())
} else {
Ok(build_admin_wallets_data_unavailable_response())

View File

@@ -1,8 +1,6 @@
use super::requests::ADMIN_WALLETS_API_KEY_GIFT_ADJUST_DETAIL;
use crate::handlers::admin::request::AdminRequestContext;
use crate::handlers::admin::shared::query_param_value;
use crate::GatewayError;
use sqlx::Row;
pub(in super::super) fn admin_wallet_operator_id(
request_context: &AdminRequestContext<'_>,
@@ -198,17 +196,6 @@ pub(in super::super) fn parse_admin_wallets_owner_type_filter(
}
}
pub(in super::super) fn optional_epoch_value(
row: &sqlx::postgres::PgRow,
key: &str,
) -> Result<Option<String>, GatewayError> {
Ok(row
.try_get::<Option<i64>, _>(key)
.map_err(|err| GatewayError::Internal(err.to_string()))?
.and_then(|value| u64::try_from(value).ok())
.and_then(crate::handlers::admin::shared::unix_secs_to_rfc3339))
}
pub(in super::super) fn admin_wallet_build_order_no(now: chrono::DateTime<chrono::Utc>) -> String {
format!(
"po_{}_{}",

View File

@@ -210,7 +210,7 @@ async fn admin_gemini_files_upload_single_key(
proxy: state
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport)
.await,
tls_profile: state.resolve_transport_tls_profile(&transport),
transport_profile: state.resolve_transport_profile(&transport),
timeouts: state.resolve_transport_execution_timeouts(&transport),
};

View File

@@ -6,7 +6,6 @@ use super::route_filters::{
};
use crate::constants::INTERNAL_GATEWAY_PATH_PREFIXES;
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::query::monitoring as monitoring_query;
use crate::GatewayError;
use aether_admin::observability::monitoring::{
admin_monitoring_bad_request_response, admin_monitoring_user_behavior_user_id_from_path,
@@ -41,33 +40,21 @@ pub(super) async fn build_admin_monitoring_audit_logs_response(
Err(detail) => return Ok(admin_monitoring_bad_request_response(detail)),
};
let Some(pool) = state.postgres_pool() else {
return Ok(build_admin_monitoring_audit_logs_payload_response(
Vec::new(),
0,
limit,
offset,
username,
event_type,
days,
));
};
let cutoff_time = chrono::Utc::now() - chrono::Duration::days(days);
let username_pattern = username
.as_deref()
.map(admin_monitoring_escape_like_pattern)
.map(|value| format!("%{value}%"));
let (items, total) = monitoring_query::list_admin_audit_logs(
&pool,
cutoff_time,
username_pattern.as_deref(),
event_type.as_deref(),
limit,
offset,
)
.await?;
let (items, total) = state
.list_admin_audit_logs(
cutoff_time,
username_pattern.as_deref(),
event_type.as_deref(),
limit,
offset,
)
.await?;
Ok(build_admin_monitoring_audit_logs_payload_response(
items, total, limit, offset, username, event_type, days,
@@ -85,14 +72,8 @@ pub(super) async fn build_admin_monitoring_suspicious_activities_response(
Err(detail) => return Ok(admin_monitoring_bad_request_response(detail)),
};
let Some(pool) = state.postgres_pool() else {
return Ok(
build_admin_monitoring_suspicious_activities_payload_response(Vec::new(), hours),
);
};
let cutoff_time = chrono::Utc::now() - chrono::Duration::hours(hours);
let activities = monitoring_query::list_admin_suspicious_activities(&pool, cutoff_time).await?;
let activities = state.list_admin_suspicious_activities(cutoff_time).await?;
Ok(build_admin_monitoring_suspicious_activities_payload_response(activities, hours))
}
@@ -112,22 +93,11 @@ pub(super) async fn build_admin_monitoring_user_behavior_response(
Err(detail) => return Ok(admin_monitoring_bad_request_response(detail)),
};
let Some(pool) = state.postgres_pool() else {
return Ok(build_admin_monitoring_user_behavior_payload_response(
user_id,
days,
std::collections::BTreeMap::new(),
0,
0,
0,
));
};
let cutoff_time = chrono::Utc::now() - chrono::Duration::days(days);
let event_counts =
monitoring_query::read_admin_user_behavior_event_counts(&pool, &user_id, cutoff_time)
.await?;
let event_counts = state
.read_admin_user_behavior_event_counts(&user_id, cutoff_time)
.await?;
let failed_requests = event_counts
.get("request_failed")

View File

@@ -23,7 +23,7 @@ async fn count_admin_monitoring_cache_affinity_entries(state: &AdminAppState<'_>
}
async fn scan_admin_monitoring_namespaced_keys(
runner: &aether_data::redis::RedisKvRunner,
runner: &aether_data::driver::redis::RedisKvRunner,
pattern: &str,
) -> Result<Vec<String>, GatewayError> {
let mut connection = runner

View File

@@ -1,4 +1,4 @@
use super::range::build_comparison_range;
use super::range::{build_comparison_range, parse_bounded_u32};
use super::resolve_admin_usage_time_range;
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::query_param_value;
@@ -6,16 +6,18 @@ use crate::GatewayError;
use aether_admin::observability::stats::{
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,
admin_stats_performance_percentiles_empty_response,
admin_stats_provider_performance_empty_response, admin_stats_time_series_empty_response,
build_admin_stats_comparison_response_from_aggregates,
build_admin_stats_error_distribution_response_from_summaries,
build_admin_stats_performance_percentiles_response_from_summaries,
build_admin_stats_provider_performance_response,
build_admin_stats_time_series_response_from_summaries, AdminStatsAggregate,
AdminStatsComparisonType, AdminStatsGranularity, AdminStatsTimeRange, AdminStatsUsageFilter,
};
use aether_data_contracts::repository::usage::{
UsageAuditSummaryQuery, UsageErrorDistributionQuery, UsagePerformancePercentilesQuery,
UsageTimeSeriesGranularity, UsageTimeSeriesQuery,
UsageProviderPerformanceQuery, UsageTimeSeriesGranularity, UsageTimeSeriesQuery,
};
use axum::{body::Body, http, response::Response};
@@ -173,6 +175,56 @@ pub(super) async fn maybe_build_local_admin_stats_analytics_response(
));
}
if request_context.route_kind() == Some("provider_performance")
&& request_context.method() == http::Method::GET
&& matches!(
request_context.path(),
"/api/admin/stats/performance/providers" | "/api/admin/stats/performance/providers/"
)
{
let time_range = match resolve_admin_usage_time_range(request_context.query_string()) {
Ok(value) => value,
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
};
let granularity =
match query_param_value(request_context.query_string(), "granularity").as_deref() {
None | Some("day") => UsageTimeSeriesGranularity::Day,
Some("hour") => UsageTimeSeriesGranularity::Hour,
Some(_) => {
return Ok(Some(admin_stats_bad_request_response(
"granularity must be one of: day, hour".to_string(),
)));
}
};
let limit = match query_param_value(request_context.query_string(), "limit")
.map(|value| parse_bounded_u32("limit", &value, 1, 20))
.transpose()
{
Ok(value) => value.unwrap_or(8) as usize,
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
};
if !state.has_usage_data_reader() {
return Ok(Some(admin_stats_provider_performance_empty_response()));
}
let Some((created_from_unix_secs, created_until_unix_secs)) = time_range.to_unix_bounds()
else {
return Ok(Some(admin_stats_provider_performance_empty_response()));
};
let performance = state
.summarize_usage_provider_performance(&UsageProviderPerformanceQuery {
created_from_unix_secs,
created_until_unix_secs,
granularity,
tz_offset_minutes: time_range.tz_offset_minutes,
limit,
})
.await?;
return Ok(Some(build_admin_stats_provider_performance_response(
&performance,
)));
}
if request_context.route_kind() == Some("time_series")
&& request_context.method() == http::Method::GET
&& matches!(

View File

@@ -1,7 +1,4 @@
use crate::handlers::admin::request::AdminAppState;
use crate::query::usage_heatmap::{
list_usage_heatmap_aggregate_rows, read_stats_daily_cutoff_date,
};
use crate::GatewayError;
use aether_admin::observability::stats::round_to;
use aether_admin::observability::usage::{
@@ -89,49 +86,15 @@ pub(super) async fn build_admin_usage_heatmap_response(
async fn build_admin_heatmap_summaries(
state: &AdminAppState<'_>,
created_from_unix_secs: u64,
start_date: chrono::NaiveDate,
today: chrono::NaiveDate,
_start_date: chrono::NaiveDate,
_today: chrono::NaiveDate,
) -> Result<Vec<StoredUsageDailySummary>, GatewayError> {
let query = UsageDailyHeatmapQuery {
created_from_unix_secs,
user_id: None,
admin_mode: true,
};
let Some(pool) = state.app().postgres_pool() else {
return state.summarize_usage_daily_heatmap(&query).await;
};
let Some(cutoff_date) = read_stats_daily_cutoff_date(&pool).await? else {
return state.summarize_usage_daily_heatmap(&query).await;
};
let cutoff_day = cutoff_date.date_naive().min(today);
let mut summaries =
list_usage_heatmap_aggregate_rows(&pool, start_date, cutoff_day, None).await?;
let raw_start_date = start_date.max(cutoff_day);
if raw_start_date <= today {
let raw_start_of_day = raw_start_date
.and_hms_opt(0, 0, 0)
.expect("heatmap day start should be valid");
let raw_created_from_unix_secs = u64::try_from(
chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
raw_start_of_day,
chrono::Utc,
)
.timestamp(),
)
.unwrap_or_default();
summaries.extend(
state
.summarize_usage_daily_heatmap(&UsageDailyHeatmapQuery {
created_from_unix_secs: raw_created_from_unix_secs,
user_id: None,
admin_mode: true,
})
.await?,
);
}
let mut summaries = state.summarize_usage_daily_heatmap(&query).await?;
summaries.sort_by(|left, right| left.date.cmp(&right.date));
Ok(summaries)
}

View File

@@ -3,7 +3,9 @@ use crate::handlers::admin::provider::shared::support::{
ADMIN_PROVIDER_MAPPING_PREVIEW_MAX_KEYS, ADMIN_PROVIDER_MAPPING_PREVIEW_MAX_MODELS,
};
use crate::handlers::admin::request::AdminAppState;
use crate::handlers::admin::shared::{decrypt_catalog_secret_with_fallbacks, json_string_list};
use crate::handlers::admin::shared::{
decrypt_catalog_secret_with_fallbacks, json_string_list, take_secret_prefix, take_secret_suffix,
};
use crate::handlers::public::matches_model_mapping_for_models;
use crate::{GatewayError, LocalProviderDeleteTaskState};
use aether_data_contracts::repository::global_models::{
@@ -175,14 +177,15 @@ pub(crate) fn mapping_preview_masked_catalog_api_key(
decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext)
.map(|value| {
if value.len() > 8 {
let char_count = value.chars().count();
if char_count > 8 {
format!(
"{}***{}",
&value[..4],
&value[value.len().saturating_sub(4)..]
take_secret_prefix(&value, 4),
take_secret_suffix(&value, 4)
)
} else if value.len() >= 2 {
format!("{}***", &value[..2])
} else if char_count >= 2 {
format!("{}***", take_secret_prefix(&value, 2))
} else {
"***".to_string()
}

View File

@@ -72,7 +72,7 @@ async fn execute_antigravity_quota_plan(
provider_api_format: "antigravity:fetch_available_models".to_string(),
model_name: Some("fetchAvailableModels".to_string()),
proxy,
tls_profile: state.resolve_transport_tls_profile(transport),
transport_profile: state.resolve_transport_profile(transport),
timeouts,
};

View File

@@ -97,7 +97,7 @@ pub(super) async fn execute_codex_quota_plan(
provider_api_format: "openai:responses".to_string(),
model_name: Some("codex-wham-usage".to_string()),
proxy,
tls_profile: state.resolve_transport_tls_profile(transport),
transport_profile: state.resolve_transport_profile(transport),
timeouts,
};
execute_provider_quota_plan(state, transport, plan, "codex").await

View File

@@ -103,7 +103,7 @@ pub(super) async fn execute_kiro_quota_plan(
provider_api_format: "kiro:usage".to_string(),
model_name: Some("kiro-usage-limits".to_string()),
proxy,
tls_profile: state.resolve_transport_tls_profile(transport),
transport_profile: state.resolve_transport_profile(transport),
timeouts,
};

View File

@@ -194,7 +194,6 @@ pub(super) async fn execute_provider_quota_plan(
key_id = %transport.key.id,
endpoint_id = %transport.endpoint.id,
url = %plan.url,
tls_profile = ?plan.tls_profile.as_deref(),
proxy_source = ?proxy_source,
proxy_node_id = ?proxy_node_id,
proxy_url_present,

View File

@@ -179,7 +179,7 @@ async fn admin_provider_ops_execute_request(
provider_api_format: "provider_ops:verify".to_string(),
model_name: Some("verify-auth".to_string()),
proxy: proxy_snapshot.cloned(),
tls_profile: None,
transport_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(ADMIN_PROVIDER_OPS_VERIFY_TIMEOUT_MS),
read_ms: Some(ADMIN_PROVIDER_OPS_VERIFY_TIMEOUT_MS),

View File

@@ -1,4 +1,4 @@
use aether_data::redis::RedisKeyspace;
use aether_data::driver::redis::RedisKeyspace;
pub(super) fn pool_sticky_pattern(keyspace: &RedisKeyspace, provider_id: &str) -> String {
keyspace.key(&format!("ap:{provider_id}:sticky:*"))

View File

@@ -7,7 +7,7 @@ use crate::handlers::admin::provider::shared::support::{
AdminProviderPoolConfig, AdminProviderPoolRuntimeState, ADMIN_PROVIDER_POOL_SCAN_BATCH,
};
use crate::GatewayError;
use aether_data::redis::RedisKvRunner;
use aether_data::driver::redis::RedisKvRunner;
use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::warn;

View File

@@ -5,7 +5,7 @@ use super::keys::{
use crate::handlers::admin::provider::shared::support::{
AdminProviderPoolConfig, AdminProviderPoolUnschedulableRule,
};
use aether_data::redis::RedisKvRunner;
use aether_data::driver::redis::RedisKvRunner;
use regex::Regex;
use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};

View File

@@ -849,7 +849,7 @@ async fn provider_query_execute_kiro_test_candidate(
proxy: state
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport)
.await,
tls_profile: state.resolve_transport_tls_profile(&transport),
transport_profile: state.resolve_transport_profile(&transport),
timeouts: state.resolve_transport_execution_timeouts(&transport),
};
@@ -1214,7 +1214,7 @@ async fn provider_query_execute_standard_test_candidate(
proxy: state
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport)
.await,
tls_profile: state.resolve_transport_tls_profile(&transport),
transport_profile: state.resolve_transport_profile(&transport),
timeouts: state.resolve_transport_execution_timeouts(&transport),
};

View File

@@ -44,6 +44,8 @@ pub(crate) struct AdminProviderKeyCreateRequest {
pub(crate) model_include_patterns: Option<Vec<String>>,
#[serde(default)]
pub(crate) model_exclude_patterns: Option<Vec<String>>,
#[serde(default)]
pub(crate) fingerprint: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]

View File

@@ -170,7 +170,7 @@ pub(crate) async fn build_admin_create_provider_key_record(
normalize_string_list(payload.allowed_models).map(|value| json!(value)),
None,
None,
None,
normalize_json_object(payload.fingerprint, "fingerprint")?,
)
.map_err(|err| err.to_string())?;
key.note = payload

View File

@@ -116,7 +116,7 @@ impl<'a> AdminAppState<'a> {
self.app.mark_provider_key_rpm_reset(key_id, now_unix_secs)
}
pub(crate) fn redis_kv_runner(&self) -> Option<aether_data::redis::RedisKvRunner> {
pub(crate) fn redis_kv_runner(&self) -> Option<aether_data::driver::redis::RedisKvRunner> {
self.app.redis_kv_runner()
}
@@ -128,8 +128,8 @@ impl<'a> AdminAppState<'a> {
self.app.provider_key_rpm_reset_at(key_id, now_unix_secs)
}
pub(crate) fn has_postgres_pool(&self) -> bool {
self.app.postgres_pool().is_some()
pub(crate) fn has_wallet_data_writer(&self) -> bool {
self.app.has_wallet_data_writer()
}
pub(crate) fn mark_admin_monitoring_error_stats_reset(&self, now_unix_secs: u64) {

View File

@@ -168,6 +168,16 @@ impl<'a> AdminAppState<'a> {
.await
}
pub(crate) async fn summarize_usage_provider_performance(
&self,
query: &aether_data_contracts::repository::usage::UsageProviderPerformanceQuery,
) -> Result<
aether_data_contracts::repository::usage::StoredUsageProviderPerformance,
GatewayError,
> {
self.app.summarize_usage_provider_performance(query).await
}
pub(crate) async fn summarize_usage_cost_savings(
&self,
query: &aether_data_contracts::repository::usage::UsageCostSavingsSummaryQuery,

View File

@@ -221,6 +221,13 @@ impl<'a> AdminAppState<'a> {
crate::provider_transport::resolve_transport_tls_profile(transport)
}
pub(crate) fn resolve_transport_profile(
&self,
transport: &AdminGatewayProviderTransportSnapshot,
) -> Option<aether_contracts::ResolvedTransportProfile> {
crate::provider_transport::resolve_transport_profile(transport)
}
pub(crate) fn resolve_transport_execution_timeouts(
&self,
transport: &AdminGatewayProviderTransportSnapshot,

View File

@@ -12,6 +12,6 @@ pub(crate) use crate::handlers::shared::{
masked_catalog_api_key, normalize_json_array, normalize_json_object, normalize_string_list,
parse_catalog_auth_config_json, provider_catalog_key_supports_format,
provider_key_health_summary, provider_key_status_snapshot_payload, query_param_bool,
query_param_optional_bool, query_param_value, unix_secs_to_rfc3339,
OFFICIAL_EXTERNAL_MODEL_PROVIDERS,
query_param_optional_bool, query_param_value, take_secret_prefix, take_secret_suffix,
unix_secs_to_rfc3339, OFFICIAL_EXTERNAL_MODEL_PROVIDERS,
};

View File

@@ -1261,12 +1261,16 @@ fn build_tunnel_probe_relay_envelope(
timeout_secs: u64,
) -> Result<Vec<u8>, String> {
let meta = crate::tunnel::tunnel_protocol::RequestMeta {
provider_id: None,
endpoint_id: None,
key_id: None,
method: "GET".to_string(),
url: probe_url.trim().to_string(),
headers: std::collections::HashMap::new(),
timeout: timeout_secs,
follow_redirects: Some(false),
http1_only: false,
transport_profile: None,
};
let meta_bytes = serde_json::to_vec(&meta)
.map_err(|error| format!("encode tunnel probe metadata failed: {error}"))?;

View File

@@ -4,7 +4,6 @@ use super::support::{
};
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::{query_param_optional_bool, query_param_value};
use crate::query::user_rollups::list_user_usage_totals_from_stats_summary;
use crate::GatewayError;
use axum::{
body::Body,
@@ -43,19 +42,10 @@ pub(in super::super) async fn build_admin_list_users_response(
.iter()
.map(|row| row.id.clone())
.collect::<Vec<_>>();
let usage_totals_future = async {
let Some(pool) = state.app().postgres_pool() else {
return state.summarize_usage_totals_by_user_ids(&user_ids).await;
};
match list_user_usage_totals_from_stats_summary(&pool, &user_ids).await? {
Some(items) => Ok(items),
None => state.summarize_usage_totals_by_user_ids(&user_ids).await,
}
};
let (auth_rows_result, wallet_rows_result, usage_totals_result) = tokio::join!(
state.list_user_auth_by_ids(&user_ids),
state.list_wallet_snapshots_by_user_ids(&user_ids),
usage_totals_future,
state.summarize_usage_totals_by_user_ids(&user_ids),
);
let auth_by_user_id = auth_rows_result?
.into_iter()

View File

@@ -71,7 +71,7 @@ use self::support_test_connection::maybe_build_local_test_connection_response;
use self::support_user_me::maybe_build_local_users_me_response;
use self::support_wallet::{
maybe_build_local_wallet_response, sanitize_wallet_gateway_response,
wallet_normalize_optional_string_field, wallet_payment_order_payload_from_row,
wallet_normalize_optional_string_field,
};
pub(crate) fn build_unhandled_public_support_response(

View File

@@ -18,18 +18,6 @@ use chrono::Datelike;
use serde_json::json;
use std::collections::{BTreeMap, BTreeSet};
use crate::query::dashboard_stats::{
list_admin_dashboard_daily_model_aggregates, list_admin_dashboard_daily_provider_aggregates,
list_admin_dashboard_daily_totals_aggregates, list_admin_dashboard_hourly_model_aggregates,
list_admin_dashboard_hourly_provider_aggregates, list_admin_dashboard_hourly_totals_aggregates,
list_user_dashboard_daily_model_aggregates, list_user_dashboard_daily_totals_aggregates,
list_user_dashboard_hourly_model_aggregates, list_user_dashboard_hourly_totals_aggregates,
read_stats_hourly_cutoff, summarize_dashboard_usage_from_daily_aggregates,
DashboardDailyModelAggregateRow, DashboardDailyProviderAggregateRow,
DashboardDailyTotalsAggregateRow,
};
use crate::query::usage_heatmap::read_stats_daily_cutoff_date;
#[derive(Debug, Clone, Copy)]
struct DashboardDateRange {
start_date: chrono::NaiveDate,
@@ -438,93 +426,6 @@ fn dashboard_range_bounds_unix(range: DashboardDateRange) -> Option<(u64, u64)>
Some((start_utc.max(0) as u64, end_utc.max(0) as u64))
}
fn dashboard_range_bounds_utc(
range: DashboardDateRange,
) -> Option<(chrono::DateTime<chrono::Utc>, chrono::DateTime<chrono::Utc>)> {
let (created_from_unix_secs, created_until_unix_secs) = dashboard_range_bounds_unix(range)?;
let start_utc =
chrono::DateTime::<chrono::Utc>::from_timestamp(created_from_unix_secs as i64, 0)?;
let end_utc =
chrono::DateTime::<chrono::Utc>::from_timestamp(created_until_unix_secs as i64, 0)?;
Some((start_utc, end_utc))
}
fn dashboard_local_day_bounds_utc_exclusive(
range: DashboardDateRange,
) -> Option<(chrono::DateTime<chrono::Utc>, chrono::DateTime<chrono::Utc>)> {
let offset = chrono::Duration::minutes(i64::from(range.tz_offset_minutes));
let start_local = range.start_date.and_hms_opt(0, 0, 0)?;
let end_exclusive_local = range
.end_date
.checked_add_signed(chrono::Duration::days(1))?
.and_hms_opt(0, 0, 0)?;
let start_utc = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
start_local.checked_sub_signed(offset)?,
chrono::Utc,
);
let end_exclusive_utc = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
end_exclusive_local.checked_sub_signed(offset)?,
chrono::Utc,
);
Some((start_utc, end_exclusive_utc))
}
fn dashboard_utc_midnight(value: chrono::DateTime<chrono::Utc>) -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
value
.date_naive()
.and_hms_opt(0, 0, 0)
.expect("midnight should be valid"),
chrono::Utc,
)
}
fn dashboard_next_utc_midnight(
value: chrono::DateTime<chrono::Utc>,
) -> chrono::DateTime<chrono::Utc> {
let midnight = dashboard_utc_midnight(value);
if value == midnight {
midnight
} else {
midnight + chrono::Duration::days(1)
}
}
fn dashboard_unix_secs(value: chrono::DateTime<chrono::Utc>) -> u64 {
value.timestamp().max(0) as u64
}
fn dashboard_absorb_dashboard_summary(
target: &mut StoredUsageDashboardSummary,
part: &StoredUsageDashboardSummary,
) {
target.total_requests = target.total_requests.saturating_add(part.total_requests);
target.input_tokens = target.input_tokens.saturating_add(part.input_tokens);
target.effective_input_tokens = target
.effective_input_tokens
.saturating_add(part.effective_input_tokens);
target.output_tokens = target.output_tokens.saturating_add(part.output_tokens);
target.total_tokens = target.total_tokens.saturating_add(part.total_tokens);
target.cache_creation_tokens = target
.cache_creation_tokens
.saturating_add(part.cache_creation_tokens);
target.cache_read_tokens = target
.cache_read_tokens
.saturating_add(part.cache_read_tokens);
target.total_input_context = target
.total_input_context
.saturating_add(part.total_input_context);
target.cache_creation_cost_usd += part.cache_creation_cost_usd;
target.cache_read_cost_usd += part.cache_read_cost_usd;
target.total_cost_usd += part.total_cost_usd;
target.actual_total_cost_usd += part.actual_total_cost_usd;
target.error_requests = target.error_requests.saturating_add(part.error_requests);
target.response_time_sum_ms += part.response_time_sum_ms;
target.response_time_samples = target
.response_time_samples
.saturating_add(part.response_time_samples);
}
async fn dashboard_summary_for_unix_range_raw(
state: &AppState,
created_from_unix_secs: u64,
@@ -607,87 +508,7 @@ async fn dashboard_summary_for_range(
user_id: Option<&str>,
error_context: &str,
) -> Result<StoredUsageDashboardSummary, Response<Body>> {
let Some(pool) = state.postgres_pool() else {
return dashboard_summary_for_range_raw(state, range, user_id, error_context).await;
};
let cutoff_date = match read_stats_daily_cutoff_date(&pool).await {
Ok(value) => value,
Err(err) => {
return Err(build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
))
}
};
let Some(cutoff_date) = cutoff_date else {
return dashboard_summary_for_range_raw(state, range, user_id, error_context).await;
};
let Some((start_utc, end_utc)) = dashboard_range_bounds_utc(range) else {
return Err(build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: invalid time range"),
false,
));
};
let aggregate_start = dashboard_next_utc_midnight(start_utc);
let aggregate_end = dashboard_utc_midnight(end_utc).min(cutoff_date);
let mut summary = StoredUsageDashboardSummary::default();
if aggregate_start < aggregate_end {
let leading_end = aggregate_start.min(end_utc);
if start_utc < leading_end {
let raw = dashboard_summary_for_unix_range_raw(
state,
dashboard_unix_secs(start_utc),
dashboard_unix_secs(leading_end),
user_id,
error_context,
)
.await?;
dashboard_absorb_dashboard_summary(&mut summary, &raw);
}
let aggregate = summarize_dashboard_usage_from_daily_aggregates(
&pool,
aggregate_start,
aggregate_end,
user_id,
)
.await
.map_err(|err| {
build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
)
})?;
dashboard_absorb_dashboard_summary(&mut summary, &aggregate);
if aggregate_end < end_utc {
let raw = dashboard_summary_for_unix_range_raw(
state,
dashboard_unix_secs(aggregate_end),
dashboard_unix_secs(end_utc),
user_id,
error_context,
)
.await?;
dashboard_absorb_dashboard_summary(&mut summary, &raw);
}
} else if start_utc < end_utc {
let raw = dashboard_summary_for_unix_range_raw(
state,
dashboard_unix_secs(start_utc),
dashboard_unix_secs(end_utc),
user_id,
error_context,
)
.await?;
dashboard_absorb_dashboard_summary(&mut summary, &raw);
}
Ok(summary)
dashboard_summary_for_range_raw(state, range, user_id, error_context).await
}
async fn dashboard_daily_breakdown_for_range(
@@ -753,75 +574,6 @@ fn dashboard_apply_daily_breakdown_rows(
}
}
fn dashboard_record_daily_totals_aggregate(
by_date: &mut std::collections::BTreeMap<chrono::NaiveDate, DashboardDailyAggregate>,
row: &DashboardDailyTotalsAggregateRow,
) {
let Ok(date) = chrono::NaiveDate::parse_from_str(&row.date, "%Y-%m-%d") else {
return;
};
let aggregate = by_date.entry(date).or_default();
aggregate.totals.requests = aggregate.totals.requests.saturating_add(row.requests);
aggregate.totals.total_tokens = aggregate
.totals
.total_tokens
.saturating_add(row.total_tokens);
aggregate.totals.total_cost_usd += row.total_cost_usd;
aggregate.totals.response_time_sum_ms += row.response_time_sum_ms;
aggregate.totals.response_time_samples = aggregate
.totals
.response_time_samples
.saturating_add(row.response_time_samples);
}
fn dashboard_record_daily_model_aggregate(
by_date: &mut std::collections::BTreeMap<chrono::NaiveDate, DashboardDailyAggregate>,
model_summary: &mut std::collections::BTreeMap<String, DashboardModelAggregate>,
row: &DashboardDailyModelAggregateRow,
) {
let Ok(date) = chrono::NaiveDate::parse_from_str(&row.date, "%Y-%m-%d") else {
return;
};
let aggregate = by_date.entry(date).or_default();
let model = aggregate.models.entry(row.model.clone()).or_default();
model.requests = model.requests.saturating_add(row.requests);
model.tokens = model.tokens.saturating_add(row.total_tokens);
model.cost += row.total_cost_usd;
model.response_time_sum_ms += row.response_time_sum_ms;
model.response_time_samples = model
.response_time_samples
.saturating_add(row.response_time_samples);
let summary = model_summary.entry(row.model.clone()).or_default();
summary.requests = summary.requests.saturating_add(row.requests);
summary.tokens = summary.tokens.saturating_add(row.total_tokens);
summary.cost += row.total_cost_usd;
summary.response_time_sum_ms += row.response_time_sum_ms;
summary.response_time_samples = summary
.response_time_samples
.saturating_add(row.response_time_samples);
}
fn dashboard_record_daily_provider_aggregate(
by_date: &mut std::collections::BTreeMap<chrono::NaiveDate, DashboardDailyAggregate>,
provider_summary: &mut std::collections::BTreeMap<String, DashboardProviderAggregate>,
row: &DashboardDailyProviderAggregateRow,
) {
let Ok(date) = chrono::NaiveDate::parse_from_str(&row.date, "%Y-%m-%d") else {
return;
};
let aggregate = by_date.entry(date).or_default();
let provider = aggregate.providers.entry(row.provider.clone()).or_default();
provider.requests = provider.requests.saturating_add(row.requests);
provider.tokens = provider.tokens.saturating_add(row.total_tokens);
provider.cost += row.total_cost_usd;
let summary = provider_summary.entry(row.provider.clone()).or_default();
summary.requests = summary.requests.saturating_add(row.requests);
summary.tokens = summary.tokens.saturating_add(row.total_tokens);
summary.cost += row.total_cost_usd;
}
fn dashboard_build_daily_stats_payload(
range: DashboardDateRange,
is_admin: bool,
@@ -972,477 +724,6 @@ fn dashboard_build_daily_stats_payload(
payload
}
fn dashboard_range_supports_hourly_rollup(range: DashboardDateRange) -> bool {
range.tz_offset_minutes % 60 == 0
}
async fn dashboard_admin_hourly_stats_aggregate_payload(
state: &AppState,
range: DashboardDateRange,
error_context: &str,
) -> Result<Option<serde_json::Value>, Response<Body>> {
if !dashboard_range_supports_hourly_rollup(range) {
return Ok(None);
}
let Some(pool) = state.postgres_pool() else {
return Ok(None);
};
let cutoff_utc = match read_stats_hourly_cutoff(&pool).await {
Ok(value) => value,
Err(err) => {
return Err(build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
))
}
};
let Some(cutoff_utc) = cutoff_utc else {
return Ok(None);
};
let Some((range_start_utc, range_end_exclusive_utc)) =
dashboard_local_day_bounds_utc_exclusive(range)
else {
return Ok(None);
};
let aggregate_end_utc = range_end_exclusive_utc.min(cutoff_utc);
if range_start_utc >= aggregate_end_utc {
return Ok(None);
}
let daily_totals = list_admin_dashboard_hourly_totals_aggregates(
&pool,
range_start_utc,
aggregate_end_utc,
range.tz_offset_minutes,
)
.await
.map_err(|err| {
build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
)
})?;
let daily_models = list_admin_dashboard_hourly_model_aggregates(
&pool,
range_start_utc,
aggregate_end_utc,
range.tz_offset_minutes,
)
.await
.map_err(|err| {
build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
)
})?;
let daily_providers = list_admin_dashboard_hourly_provider_aggregates(
&pool,
range_start_utc,
aggregate_end_utc,
range.tz_offset_minutes,
)
.await
.map_err(|err| {
build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
)
})?;
let mut by_date =
std::collections::BTreeMap::<chrono::NaiveDate, DashboardDailyAggregate>::new();
let mut model_summary = std::collections::BTreeMap::<String, DashboardModelAggregate>::new();
let mut provider_summary =
std::collections::BTreeMap::<String, DashboardProviderAggregate>::new();
for row in &daily_totals {
dashboard_record_daily_totals_aggregate(&mut by_date, row);
}
for row in &daily_models {
dashboard_record_daily_model_aggregate(&mut by_date, &mut model_summary, row);
}
for row in &daily_providers {
dashboard_record_daily_provider_aggregate(&mut by_date, &mut provider_summary, row);
}
if aggregate_end_utc < range_end_exclusive_utc {
let raw_rows = dashboard_daily_breakdown_for_unix_range_raw(
state,
dashboard_unix_secs(aggregate_end_utc),
dashboard_unix_secs(range_end_exclusive_utc),
range.tz_offset_minutes,
None,
error_context,
)
.await?;
dashboard_apply_daily_breakdown_rows(
&raw_rows,
&mut by_date,
&mut model_summary,
&mut provider_summary,
);
}
Ok(Some(dashboard_build_daily_stats_payload(
range,
true,
&by_date,
&model_summary,
&provider_summary,
)))
}
async fn dashboard_user_hourly_stats_aggregate_payload(
state: &AppState,
range: DashboardDateRange,
user_id: &str,
error_context: &str,
) -> Result<Option<serde_json::Value>, Response<Body>> {
if !dashboard_range_supports_hourly_rollup(range) {
return Ok(None);
}
let Some(pool) = state.postgres_pool() else {
return Ok(None);
};
let cutoff_utc = match read_stats_hourly_cutoff(&pool).await {
Ok(value) => value,
Err(err) => {
return Err(build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
))
}
};
let Some(cutoff_utc) = cutoff_utc else {
return Ok(None);
};
let Some((range_start_utc, range_end_exclusive_utc)) =
dashboard_local_day_bounds_utc_exclusive(range)
else {
return Ok(None);
};
let aggregate_end_utc = range_end_exclusive_utc.min(cutoff_utc);
if range_start_utc >= aggregate_end_utc {
return Ok(None);
}
let daily_totals = list_user_dashboard_hourly_totals_aggregates(
&pool,
range_start_utc,
aggregate_end_utc,
range.tz_offset_minutes,
user_id,
)
.await
.map_err(|err| {
build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
)
})?;
let daily_models = list_user_dashboard_hourly_model_aggregates(
&pool,
range_start_utc,
aggregate_end_utc,
range.tz_offset_minutes,
user_id,
)
.await
.map_err(|err| {
build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
)
})?;
let mut by_date =
std::collections::BTreeMap::<chrono::NaiveDate, DashboardDailyAggregate>::new();
let mut model_summary = std::collections::BTreeMap::<String, DashboardModelAggregate>::new();
let mut provider_summary =
std::collections::BTreeMap::<String, DashboardProviderAggregate>::new();
for row in &daily_totals {
dashboard_record_daily_totals_aggregate(&mut by_date, row);
}
for row in &daily_models {
dashboard_record_daily_model_aggregate(&mut by_date, &mut model_summary, row);
}
if aggregate_end_utc < range_end_exclusive_utc {
let raw_rows = dashboard_daily_breakdown_for_unix_range_raw(
state,
dashboard_unix_secs(aggregate_end_utc),
dashboard_unix_secs(range_end_exclusive_utc),
range.tz_offset_minutes,
Some(user_id),
error_context,
)
.await?;
dashboard_apply_daily_breakdown_rows(
&raw_rows,
&mut by_date,
&mut model_summary,
&mut provider_summary,
);
}
Ok(Some(dashboard_build_daily_stats_payload(
range,
false,
&by_date,
&model_summary,
&provider_summary,
)))
}
async fn dashboard_admin_daily_stats_aggregate_payload(
state: &AppState,
range: DashboardDateRange,
error_context: &str,
) -> Result<Option<serde_json::Value>, Response<Body>> {
if range.tz_offset_minutes != 0 {
return dashboard_admin_hourly_stats_aggregate_payload(state, range, error_context).await;
}
let Some(pool) = state.postgres_pool() else {
return Ok(None);
};
let cutoff_date = match read_stats_daily_cutoff_date(&pool).await {
Ok(value) => value,
Err(err) => {
return Err(build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
))
}
};
let Some(cutoff_date) = cutoff_date else {
return Ok(None);
};
let Some(range_end_exclusive) = range.end_date.checked_add_signed(chrono::Duration::days(1))
else {
return Ok(None);
};
let aggregate_end_exclusive = range_end_exclusive.min(cutoff_date.date_naive());
if range.start_date >= aggregate_end_exclusive {
return Ok(None);
}
let aggregate_start_utc = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
range
.start_date
.and_hms_opt(0, 0, 0)
.expect("midnight should be valid"),
chrono::Utc,
);
let aggregate_end_utc = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
aggregate_end_exclusive
.and_hms_opt(0, 0, 0)
.expect("midnight should be valid"),
chrono::Utc,
);
let daily_totals =
list_admin_dashboard_daily_totals_aggregates(&pool, aggregate_start_utc, aggregate_end_utc)
.await
.map_err(|err| {
build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
)
})?;
let daily_models =
list_admin_dashboard_daily_model_aggregates(&pool, aggregate_start_utc, aggregate_end_utc)
.await
.map_err(|err| {
build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
)
})?;
let daily_providers = list_admin_dashboard_daily_provider_aggregates(
&pool,
aggregate_start_utc,
aggregate_end_utc,
)
.await
.map_err(|err| {
build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
)
})?;
let mut by_date =
std::collections::BTreeMap::<chrono::NaiveDate, DashboardDailyAggregate>::new();
let mut model_summary = std::collections::BTreeMap::<String, DashboardModelAggregate>::new();
let mut provider_summary =
std::collections::BTreeMap::<String, DashboardProviderAggregate>::new();
for row in &daily_totals {
dashboard_record_daily_totals_aggregate(&mut by_date, row);
}
for row in &daily_models {
dashboard_record_daily_model_aggregate(&mut by_date, &mut model_summary, row);
}
for row in &daily_providers {
dashboard_record_daily_provider_aggregate(&mut by_date, &mut provider_summary, row);
}
if aggregate_end_exclusive <= range.end_date {
let raw_range = DashboardDateRange {
start_date: aggregate_end_exclusive,
end_date: range.end_date,
tz_offset_minutes: range.tz_offset_minutes,
};
let raw_rows =
dashboard_daily_breakdown_for_range(state, raw_range, None, error_context).await?;
dashboard_apply_daily_breakdown_rows(
&raw_rows,
&mut by_date,
&mut model_summary,
&mut provider_summary,
);
}
Ok(Some(dashboard_build_daily_stats_payload(
range,
true,
&by_date,
&model_summary,
&provider_summary,
)))
}
async fn dashboard_user_daily_stats_aggregate_payload(
state: &AppState,
range: DashboardDateRange,
user_id: &str,
error_context: &str,
) -> Result<Option<serde_json::Value>, Response<Body>> {
if range.tz_offset_minutes != 0 {
return dashboard_user_hourly_stats_aggregate_payload(state, range, user_id, error_context)
.await;
}
let Some(pool) = state.postgres_pool() else {
return Ok(None);
};
let cutoff_date = match read_stats_daily_cutoff_date(&pool).await {
Ok(value) => value,
Err(err) => {
return Err(build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
))
}
};
let Some(cutoff_date) = cutoff_date else {
return Ok(None);
};
let Some(range_end_exclusive) = range.end_date.checked_add_signed(chrono::Duration::days(1))
else {
return Ok(None);
};
let aggregate_end_exclusive = range_end_exclusive.min(cutoff_date.date_naive());
if range.start_date >= aggregate_end_exclusive {
return Ok(None);
}
let aggregate_start_utc = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
range
.start_date
.and_hms_opt(0, 0, 0)
.expect("midnight should be valid"),
chrono::Utc,
);
let aggregate_end_utc = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
aggregate_end_exclusive
.and_hms_opt(0, 0, 0)
.expect("midnight should be valid"),
chrono::Utc,
);
let daily_totals = list_user_dashboard_daily_totals_aggregates(
&pool,
aggregate_start_utc,
aggregate_end_utc,
user_id,
)
.await
.map_err(|err| {
build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
)
})?;
let daily_models = list_user_dashboard_daily_model_aggregates(
&pool,
aggregate_start_utc,
aggregate_end_utc,
user_id,
)
.await
.map_err(|err| {
build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{error_context}: {err:?}"),
false,
)
})?;
let mut by_date =
std::collections::BTreeMap::<chrono::NaiveDate, DashboardDailyAggregate>::new();
let mut model_summary = std::collections::BTreeMap::<String, DashboardModelAggregate>::new();
let mut provider_summary =
std::collections::BTreeMap::<String, DashboardProviderAggregate>::new();
for row in &daily_totals {
dashboard_record_daily_totals_aggregate(&mut by_date, row);
}
for row in &daily_models {
dashboard_record_daily_model_aggregate(&mut by_date, &mut model_summary, row);
}
if aggregate_end_exclusive <= range.end_date {
let raw_range = DashboardDateRange {
start_date: aggregate_end_exclusive,
end_date: range.end_date,
tz_offset_minutes: range.tz_offset_minutes,
};
let raw_rows =
dashboard_daily_breakdown_for_range(state, raw_range, Some(user_id), error_context)
.await?;
dashboard_apply_daily_breakdown_rows(
&raw_rows,
&mut by_date,
&mut model_summary,
&mut provider_summary,
);
}
Ok(Some(dashboard_build_daily_stats_payload(
range,
false,
&by_date,
&model_summary,
&provider_summary,
)))
}
fn dashboard_usage_totals_from_summary(
summary: &StoredUsageDashboardSummary,
) -> DashboardUsageTotals {
@@ -1888,37 +1169,6 @@ pub(super) async fn handle_dashboard_daily_stats_get(
Err(detail) => return dashboard_bad_request_response(detail),
};
let user_filter = (!is_admin).then_some(auth.user.id.as_str());
if is_admin {
match dashboard_admin_daily_stats_aggregate_payload(
state,
range,
"dashboard daily stats lookup failed",
)
.await
{
Ok(Some(payload)) => {
return dashboard_cached_json_response(state, cache_key, cache_ttl, &payload)
}
Ok(None) => {}
Err(response) => return response,
}
} else {
match dashboard_user_daily_stats_aggregate_payload(
state,
range,
auth.user.id.as_str(),
"dashboard daily stats lookup failed",
)
.await
{
Ok(Some(payload)) => {
return dashboard_cached_json_response(state, cache_key, cache_ttl, &payload)
}
Ok(None) => {}
Err(response) => return response,
}
}
let usage = match dashboard_daily_breakdown_for_range(
state,
range,

View File

@@ -8,7 +8,6 @@ use chrono::Utc;
use serde_json::{json, Value};
use crate::handlers::shared::query_param_value;
use crate::query::monitoring as monitoring_query;
use super::{
build_auth_error_response, resolve_authenticated_local_user, AppState,
@@ -112,27 +111,16 @@ pub(super) async fn handle_user_audit_logs(
}
};
let Some(pool) = state.postgres_pool() else {
return build_user_monitoring_audit_logs_payload(
Vec::new(),
0,
let cutoff_time = Utc::now() - chrono::Duration::days(days);
let (items, total) = match state
.list_user_audit_logs(
&auth.user.id,
cutoff_time,
event_type.as_deref(),
limit,
offset,
event_type,
days,
);
};
let cutoff_time = Utc::now() - chrono::Duration::days(days);
let (items, total) = match monitoring_query::list_user_audit_logs(
&pool,
&auth.user.id,
cutoff_time,
event_type.as_deref(),
limit,
offset,
)
.await
)
.await
{
Ok(value) => value,
Err(err) => {

View File

@@ -4,8 +4,8 @@ pub(super) use super::{build_auth_error_response, AppState, GatewayPublicRequest
#[path = "payment/gateway.rs"]
pub(super) mod payment_gateway;
#[path = "payment/postgres.rs"]
mod payment_postgres;
#[path = "payment/repository.rs"]
mod payment_repository;
#[path = "payment/route.rs"]
mod payment_route;
#[path = "payment/shared.rs"]
@@ -14,7 +14,7 @@ mod payment_shared;
#[path = "payment/test_support.rs"]
mod payment_test_support;
use self::payment_postgres::handle_payment_callback_with_postgres;
use self::payment_repository::handle_payment_callback_with_wallet_repository;
use self::payment_shared::NormalizedPaymentCallbackRequest;
const PAYMENT_CALLBACK_STORAGE_UNAVAILABLE_DETAIL: &str = "支付回调存储暂不可用";

View File

@@ -11,14 +11,14 @@ use super::{
GatewayPublicRequestContext,
};
pub(super) async fn handle_payment_callback_with_postgres(
pub(super) async fn handle_payment_callback_with_wallet_repository(
state: &AppState,
payment_method: &str,
request_context: &GatewayPublicRequestContext,
payload: &NormalizedPaymentCallbackRequest,
signature_valid: bool,
) -> Response<Body> {
if state.postgres_pool().is_none() {
if !state.has_database_wallet_data_writer() {
return build_payment_callback_storage_unavailable_response();
}
@@ -128,7 +128,7 @@ pub(super) async fn handle_payment_callback_with_postgres(
#[cfg(test)]
mod tests {
use super::{
handle_payment_callback_with_postgres, AppState, NormalizedPaymentCallbackRequest,
handle_payment_callback_with_wallet_repository, AppState, NormalizedPaymentCallbackRequest,
};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::public::support::support_payment::PAYMENT_CALLBACK_STORAGE_UNAVAILABLE_DETAIL;
@@ -137,10 +137,10 @@ mod tests {
use serde_json::json;
#[tokio::test]
async fn payment_callback_postgres_handler_returns_explicit_503_without_pool() {
async fn payment_callback_repository_handler_returns_explicit_503_without_wallet_writer() {
let state = AppState::new().expect("state should build");
let request_context = GatewayPublicRequestContext::from_request_parts(
"trace-payment-callback-postgres-missing",
"trace-payment-callback-wallet-writer-missing",
&Method::POST,
&"/api/payment/callback/alipay"
.parse::<Uri>()
@@ -159,7 +159,7 @@ mod tests {
payload: json!({ "status": "paid" }),
};
let response = handle_payment_callback_with_postgres(
let response = handle_payment_callback_with_wallet_repository(
&state,
"alipay",
&request_context,

View File

@@ -7,7 +7,7 @@ use super::payment_shared::{
};
use super::{
build_auth_error_response, build_payment_callback_storage_unavailable_response,
handle_payment_callback_with_postgres, AppState, GatewayPublicRequestContext,
handle_payment_callback_with_wallet_repository, AppState, GatewayPublicRequestContext,
};
pub(super) async fn maybe_build_local_payment_callback_route_response(
@@ -104,9 +104,9 @@ pub(super) async fn maybe_build_local_payment_callback_route_response(
}
};
if state.postgres_pool().is_some() {
if state.has_database_wallet_data_writer() {
return Some(
handle_payment_callback_with_postgres(
handle_payment_callback_with_wallet_repository(
state,
&payment_method,
request_context,

View File

@@ -163,7 +163,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
if transport.provider.proxy.is_some()
|| transport.endpoint.proxy.is_some()
|| transport.key.proxy.is_some()
|| crate::provider_transport::resolve_transport_tls_profile(&transport).is_some()
|| crate::provider_transport::resolve_transport_profile(&transport).is_some()
{
return None;
}

View File

@@ -18,9 +18,6 @@ use axum::{
use chrono::Utc;
use serde_json::json;
use crate::query::usage_heatmap::{
list_usage_heatmap_aggregate_rows, read_stats_daily_cutoff_date,
};
use crate::GatewayError;
use super::{
@@ -1204,8 +1201,8 @@ pub(super) async fn handle_users_me_usage_heatmap_get(
async fn build_usage_heatmap_summaries(
state: &AppState,
created_from_unix_secs: u64,
start_date: chrono::NaiveDate,
today: chrono::NaiveDate,
_start_date: chrono::NaiveDate,
_today: chrono::NaiveDate,
user_id: Option<&str>,
) -> Result<Vec<StoredUsageDailySummary>, GatewayError> {
let query = aether_data_contracts::repository::usage::UsageDailyHeatmapQuery {
@@ -1213,43 +1210,7 @@ async fn build_usage_heatmap_summaries(
user_id: user_id.map(ToOwned::to_owned),
admin_mode: user_id.is_none(),
};
let Some(pool) = state.postgres_pool() else {
return state.summarize_usage_daily_heatmap(&query).await;
};
let Some(cutoff_date) = read_stats_daily_cutoff_date(&pool).await? else {
return state.summarize_usage_daily_heatmap(&query).await;
};
let cutoff_day = cutoff_date.date_naive().min(today);
let mut summaries =
list_usage_heatmap_aggregate_rows(&pool, start_date, cutoff_day, user_id).await?;
let raw_start_date = start_date.max(cutoff_day);
if raw_start_date <= today {
let raw_start_of_day = raw_start_date
.and_hms_opt(0, 0, 0)
.expect("heatmap day start should be valid");
let raw_created_from_unix_secs = u64::try_from(
chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
raw_start_of_day,
chrono::Utc,
)
.timestamp(),
)
.unwrap_or_default();
summaries.extend(
state
.summarize_usage_daily_heatmap(
&aether_data_contracts::repository::usage::UsageDailyHeatmapQuery {
created_from_unix_secs: raw_created_from_unix_secs,
user_id: user_id.map(ToOwned::to_owned),
admin_mode: user_id.is_none(),
},
)
.await?,
);
}
let mut summaries = state.summarize_usage_daily_heatmap(&query).await?;
summaries.sort_by(|left, right| left.date.cmp(&right.date));
Ok(summaries)
}

View File

@@ -34,13 +34,11 @@ use self::reads::{
parse_wallet_limit, parse_wallet_offset, wallet_fixed_offset, wallet_today_billing_date_string,
wallet_transaction_payload_from_record,
};
pub(crate) use self::recharge::sanitize_wallet_gateway_response;
use self::recharge::{
handle_wallet_create_recharge, handle_wallet_recharge_detail, handle_wallet_recharge_list,
wallet_recharge_detail_path_matches,
};
pub(crate) use self::recharge::{
sanitize_wallet_gateway_response, wallet_payment_order_payload_from_row,
};
use self::redeem::handle_wallet_redeem;
use self::refunds::{
handle_wallet_create_refund, handle_wallet_refund_detail, handle_wallet_refunds_list,

View File

@@ -5,8 +5,8 @@ use super::{
build_auth_error_response, build_auth_json_response, build_wallet_payload,
build_wallet_recharge_storage_unavailable_response, http, parse_wallet_limit,
parse_wallet_offset, resolve_authenticated_local_user, unix_secs_to_rfc3339,
wallet_normalize_optional_string_field, AppState, Body, GatewayError,
GatewayPublicRequestContext, Response, WALLET_SAFE_GATEWAY_RESPONSE_KEYS,
wallet_normalize_optional_string_field, AppState, Body, GatewayPublicRequestContext, Response,
WALLET_SAFE_GATEWAY_RESPONSE_KEYS,
};
#[cfg(test)]
use super::{
@@ -16,7 +16,6 @@ use super::{
use chrono::Utc;
use serde::Deserialize;
use serde_json::json;
use sqlx::Row;
use uuid::Uuid;
#[derive(Debug, Deserialize)]
@@ -152,65 +151,6 @@ fn build_wallet_payment_order_payload(
})
}
pub(crate) fn wallet_payment_order_payload_from_row(
row: &sqlx::postgres::PgRow,
) -> Result<serde_json::Value, GatewayError> {
let created_at = row
.try_get::<Option<i64>, _>("created_at_unix_ms")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.and_then(|value| u64::try_from(value).ok())
.and_then(unix_secs_to_rfc3339);
let paid_at = row
.try_get::<Option<i64>, _>("paid_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.and_then(|value| u64::try_from(value).ok())
.and_then(unix_secs_to_rfc3339);
let credited_at = row
.try_get::<Option<i64>, _>("credited_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.and_then(|value| u64::try_from(value).ok())
.and_then(unix_secs_to_rfc3339);
let expires_at = row
.try_get::<Option<i64>, _>("expires_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.and_then(|value| u64::try_from(value).ok())
.and_then(unix_secs_to_rfc3339);
Ok(build_wallet_payment_order_payload(
row.try_get::<String, _>("id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get::<String, _>("order_no")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get::<String, _>("wallet_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get::<Option<String>, _>("user_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get::<f64, _>("amount_usd")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get::<Option<f64>, _>("pay_amount")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get::<Option<String>, _>("pay_currency")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get::<Option<f64>, _>("exchange_rate")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get::<f64, _>("refunded_amount_usd")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get::<f64, _>("refundable_amount_usd")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get::<String, _>("payment_method")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get::<Option<String>, _>("gateway_order_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get::<Option<serde_json::Value>, _>("gateway_response")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get::<String, _>("effective_status")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
created_at,
paid_at,
credited_at,
expires_at,
))
}
fn wallet_payment_order_payload_from_record(
record: &aether_data::repository::wallet::StoredAdminPaymentOrder,
) -> serde_json::Value {
@@ -285,7 +225,7 @@ pub(super) async fn handle_wallet_create_recharge(
}
};
if state.postgres_pool().is_none() {
if !state.has_database_wallet_data_writer() {
#[cfg(test)]
{
let Some(wallet) = wallet else {
@@ -485,11 +425,12 @@ pub(super) async fn handle_wallet_recharge_list(
}
};
#[cfg(test)]
let (items, total) = if state.postgres_pool().is_none() && items.is_empty() && total == 0 {
wallet_test_recharge_orders_for_user(&auth.user.id, limit, offset)
} else {
(items, total)
};
let (items, total) =
if !state.has_database_wallet_data_writer() && items.is_empty() && total == 0 {
wallet_test_recharge_orders_for_user(&auth.user.id, limit, offset)
} else {
(items, total)
};
let mut payload = json!({
"items": items,

View File

@@ -2,8 +2,7 @@ use super::{
build_auth_error_response, build_auth_json_response, build_wallet_payload,
build_wallet_refund_storage_unavailable_response, http, parse_wallet_limit,
parse_wallet_offset, resolve_authenticated_local_user, unix_secs_to_rfc3339,
wallet_normalize_optional_string_field, AppState, Body, GatewayError,
GatewayPublicRequestContext, Response,
wallet_normalize_optional_string_field, AppState, Body, GatewayPublicRequestContext, Response,
};
#[cfg(test)]
use super::{
@@ -13,7 +12,6 @@ use super::{
use chrono::Utc;
use serde::Deserialize;
use serde_json::json;
use sqlx::Row;
use uuid::Uuid;
#[derive(Debug, Deserialize)]
@@ -94,51 +92,6 @@ pub(super) fn wallet_refund_detail_path_matches(request_path: &str) -> bool {
wallet_refund_id_from_path(request_path).is_some()
}
fn wallet_refund_payload_from_row(
row: &sqlx::postgres::PgRow,
) -> Result<serde_json::Value, GatewayError> {
let created_at = row
.try_get::<Option<i64>, _>("created_at_unix_ms")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.and_then(|value| u64::try_from(value).ok())
.and_then(unix_secs_to_rfc3339);
let updated_at = row
.try_get::<Option<i64>, _>("updated_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.and_then(|value| u64::try_from(value).ok())
.and_then(unix_secs_to_rfc3339);
let processed_at = row
.try_get::<Option<i64>, _>("processed_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.and_then(|value| u64::try_from(value).ok())
.and_then(unix_secs_to_rfc3339);
let completed_at = row
.try_get::<Option<i64>, _>("completed_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.and_then(|value| u64::try_from(value).ok())
.and_then(unix_secs_to_rfc3339);
Ok(json!({
"id": row.try_get::<String, _>("id").map_err(|err| GatewayError::Internal(err.to_string()))?,
"refund_no": row.try_get::<String, _>("refund_no").map_err(|err| GatewayError::Internal(err.to_string()))?,
"payment_order_id": row.try_get::<Option<String>, _>("payment_order_id").map_err(|err| GatewayError::Internal(err.to_string()))?,
"source_type": row.try_get::<String, _>("source_type").map_err(|err| GatewayError::Internal(err.to_string()))?,
"source_id": row.try_get::<Option<String>, _>("source_id").map_err(|err| GatewayError::Internal(err.to_string()))?,
"refund_mode": row.try_get::<String, _>("refund_mode").map_err(|err| GatewayError::Internal(err.to_string()))?,
"amount_usd": row.try_get::<f64, _>("amount_usd").map_err(|err| GatewayError::Internal(err.to_string()))?,
"status": row.try_get::<String, _>("status").map_err(|err| GatewayError::Internal(err.to_string()))?,
"reason": row.try_get::<Option<String>, _>("reason").map_err(|err| GatewayError::Internal(err.to_string()))?,
"failure_reason": row.try_get::<Option<String>, _>("failure_reason").map_err(|err| GatewayError::Internal(err.to_string()))?,
"gateway_refund_id": row.try_get::<Option<String>, _>("gateway_refund_id").map_err(|err| GatewayError::Internal(err.to_string()))?,
"payout_method": row.try_get::<Option<String>, _>("payout_method").map_err(|err| GatewayError::Internal(err.to_string()))?,
"payout_reference": row.try_get::<Option<String>, _>("payout_reference").map_err(|err| GatewayError::Internal(err.to_string()))?,
"payout_proof": row.try_get::<Option<serde_json::Value>, _>("payout_proof").map_err(|err| GatewayError::Internal(err.to_string()))?,
"created_at": created_at,
"updated_at": updated_at,
"processed_at": processed_at,
"completed_at": completed_at,
}))
}
fn wallet_refund_payload_from_record(
record: &aether_data::repository::wallet::StoredAdminWalletRefund,
) -> serde_json::Value {
@@ -255,18 +208,19 @@ pub(super) async fn handle_wallet_refunds_list(
})
.collect::<Vec<_>>();
#[cfg(test)]
let (items, total) = if state.postgres_pool().is_none() && items.is_empty() && total == 0 {
let all_items = wallet_test_refunds_for_wallet(&wallet.id);
let total = all_items.len() as u64;
let items = all_items
.into_iter()
.skip(offset)
.take(limit)
.collect::<Vec<_>>();
(items, total)
} else {
(items, total)
};
let (items, total) =
if !state.has_database_wallet_data_writer() && items.is_empty() && total == 0 {
let all_items = wallet_test_refunds_for_wallet(&wallet.id);
let total = all_items.len() as u64;
let items = all_items
.into_iter()
.skip(offset)
.take(limit)
.collect::<Vec<_>>();
(items, total)
} else {
(items, total)
};
let mut payload = json!({
"items": items,
@@ -394,7 +348,7 @@ pub(super) async fn handle_wallet_create_refund(
);
};
if state.postgres_pool().is_none() {
if !state.has_database_wallet_data_writer() {
#[cfg(test)]
{
if let Some(idempotency_key) = payload.idempotency_key.as_deref() {

View File

@@ -102,6 +102,29 @@ pub(crate) fn encrypt_catalog_secret_with_fallbacks(
encrypt_python_fernet_plaintext(encryption_key.as_ref(), plaintext).ok()
}
pub(crate) fn take_secret_prefix(value: &str, prefix_chars: usize) -> &str {
let end = value
.char_indices()
.nth(prefix_chars)
.map(|(index, _)| index)
.unwrap_or(value.len());
&value[..end]
}
pub(crate) fn take_secret_suffix(value: &str, suffix_chars: usize) -> &str {
if suffix_chars == 0 {
return &value[value.len()..];
}
let start = value
.char_indices()
.rev()
.nth(suffix_chars - 1)
.map(|(index, _)| index)
.unwrap_or(0);
&value[start..]
}
pub(crate) fn masked_catalog_api_key(state: &AppState, key: &StoredProviderCatalogKey) -> String {
match key.auth_type.trim() {
"service_account" | "vertex_ai" => "[Service Account]".to_string(),
@@ -117,13 +140,13 @@ pub(crate) fn masked_catalog_api_key(state: &AppState, key: &StoredProviderCatal
};
decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext)
.map(|value| {
if value.len() <= 12 {
if value.chars().count() <= 12 {
format!("{value}***")
} else {
format!(
"{}***{}",
&value[..8],
&value[value.len().saturating_sub(4)..]
take_secret_prefix(&value, 8),
take_secret_suffix(&value, 4)
)
}
})
@@ -1621,6 +1644,39 @@ mod tests {
.expect("key transport should build")
}
#[test]
fn masked_catalog_api_key_handles_unicode_plaintext_without_panicking() {
let state = AppState::new().expect("gateway should build");
let encrypted_api_key =
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "测试-密钥-1234567890")
.expect("api key ciphertext should build");
let key = StoredProviderCatalogKey::new(
"key-unicode".to_string(),
"provider-test".to_string(),
"default".to_string(),
"api_key".to_string(),
None,
true,
)
.expect("key should build")
.with_transport_fields(
Some(json!(["openai:chat"])),
encrypted_api_key,
None,
None,
None,
None,
None,
None,
None,
)
.expect("key transport should build");
let masked = masked_catalog_api_key(&state, &key);
assert!(masked.contains("***"));
assert_ne!(masked, "***ERROR***");
}
#[test]
fn provider_key_status_snapshot_payload_backfills_missing_quota_from_upstream_metadata() {
let mut key = sample_catalog_key();

View File

@@ -25,7 +25,7 @@ pub(crate) use self::catalog::{
encrypt_catalog_secret_with_fallbacks, masked_catalog_api_key, parse_catalog_auth_config_json,
provider_catalog_key_supports_format, provider_key_health_summary,
provider_key_status_snapshot_payload, sync_provider_key_oauth_status_snapshot,
sync_provider_key_quota_status_snapshot,
sync_provider_key_quota_status_snapshot, take_secret_prefix, take_secret_suffix,
};
pub(crate) use self::email_templates::{
admin_email_template_definition, admin_email_template_html_key,