mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
Merge remote-tracking branch 'origin/aether-rust-pioneer' into pr-375-session-scope
This commit is contained in:
@@ -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<'_>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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_{}_{}",
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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:*"))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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}"))?;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 = "支付回调存储暂不可用";
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user