Merge upstream/main into feat/356-usage-record-columns

This commit is contained in:
RWDai
2026-05-18 20:33:05 +08:00
95 changed files with 14199 additions and 2756 deletions

View File

@@ -516,7 +516,9 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response(
.summarize_usage_audits(&UsageAuditSummaryQuery {
created_from_unix_secs,
created_until_unix_secs,
..Default::default()
user_id: query_param_value(query, "user_id"),
provider_name: query_param_value(query, "provider"),
model: query_param_value(query, "model"),
})
.await?;
return Ok(Some(build_admin_usage_summary_stats_response_from_summary(

View File

@@ -13,12 +13,18 @@ pub(super) fn key_api_formats_without_entry(
}
pub(super) fn endpoint_key_counts_by_format(
provider_type: &str,
endpoints: &[StoredProviderCatalogEndpoint],
keys: &[StoredProviderCatalogKey],
) -> (
std::collections::BTreeMap<String, usize>,
std::collections::BTreeMap<String, usize>,
) {
admin_provider_endpoints_pure::endpoint_key_counts_by_format(keys)
admin_provider_endpoints_pure::endpoint_key_counts_by_format(provider_type, endpoints, keys)
}
pub(super) fn normalize_endpoint_api_format(api_format: &str) -> String {
admin_provider_endpoints_pure::normalize_endpoint_api_format(api_format)
}
pub(super) fn build_admin_provider_endpoint_response(

View File

@@ -4,7 +4,10 @@ use aether_data_contracts::repository::provider_catalog::{
};
use std::time::{SystemTime, UNIX_EPOCH};
use super::payloads::{build_admin_provider_endpoint_response, endpoint_key_counts_by_format};
use super::payloads::{
build_admin_provider_endpoint_response, endpoint_key_counts_by_format,
normalize_endpoint_api_format,
};
pub(crate) async fn build_admin_provider_endpoints_payload(
state: &AdminAppState<'_>,
@@ -38,7 +41,8 @@ pub(crate) async fn build_admin_provider_endpoints_payload(
.await
.ok()
.unwrap_or_default();
let (total_keys_by_format, active_keys_by_format) = endpoint_key_counts_by_format(&keys);
let (total_keys_by_format, active_keys_by_format) =
endpoint_key_counts_by_format(&provider.provider_type, &endpoints, &keys);
let now_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
@@ -51,15 +55,16 @@ pub(crate) async fn build_admin_provider_endpoints_payload(
.skip(skip)
.take(limit)
.map(|endpoint| {
let endpoint_api_format = normalize_endpoint_api_format(&endpoint.api_format);
build_admin_provider_endpoint_response(
&endpoint,
&provider.name,
total_keys_by_format
.get(endpoint.api_format.as_str())
.get(endpoint_api_format.as_str())
.copied()
.unwrap_or(0),
active_keys_by_format
.get(endpoint.api_format.as_str())
.get(endpoint_api_format.as_str())
.copied()
.unwrap_or(0),
now_unix_secs,
@@ -92,22 +97,27 @@ pub(crate) async fn build_admin_endpoint_payload(
.await
.ok()
.unwrap_or_default();
let (total_keys_by_format, active_keys_by_format) = endpoint_key_counts_by_format(&keys);
let (total_keys_by_format, active_keys_by_format) = endpoint_key_counts_by_format(
&provider.provider_type,
std::slice::from_ref(&endpoint),
&keys,
);
let now_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs())
.unwrap_or(0);
let endpoint_api_format = normalize_endpoint_api_format(&endpoint.api_format);
Some(build_admin_provider_endpoint_response(
&endpoint,
&provider.name,
total_keys_by_format
.get(endpoint.api_format.as_str())
.get(endpoint_api_format.as_str())
.copied()
.unwrap_or(0),
active_keys_by_format
.get(endpoint.api_format.as_str())
.get(endpoint_api_format.as_str())
.copied()
.unwrap_or(0),
now_unix_secs,

View File

@@ -1,7 +1,7 @@
use super::extractors::admin_endpoint_id;
use super::payloads::{
build_admin_provider_endpoint_response, endpoint_key_counts_by_format,
AdminProviderEndpointUpdatePatch,
normalize_endpoint_api_format, AdminProviderEndpointUpdatePatch,
};
use super::support::build_admin_endpoints_data_unavailable_response;
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
@@ -147,18 +147,23 @@ pub(super) async fn maybe_handle(
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
.await
.unwrap_or_default();
let (total_keys_by_format, active_keys_by_format) = endpoint_key_counts_by_format(&keys);
let (total_keys_by_format, active_keys_by_format) = endpoint_key_counts_by_format(
&provider.provider_type,
std::slice::from_ref(&updated),
&keys,
);
let updated_api_format = normalize_endpoint_api_format(&updated.api_format);
Ok(Some(
Json(build_admin_provider_endpoint_response(
&updated,
&provider.name,
total_keys_by_format
.get(updated.api_format.as_str())
.get(updated_api_format.as_str())
.copied()
.unwrap_or(0),
active_keys_by_format
.get(updated.api_format.as_str())
.get(updated_api_format.as_str())
.copied()
.unwrap_or(0),
now_unix_secs,

View File

@@ -30,6 +30,25 @@ use axum::response::IntoResponse;
use axum::Json;
use serde_json::json;
async fn apply_supplied_auth_context(
state: &AppState,
decision: &mut GatewayControlDecision,
auth_context: Option<crate::control::GatewayControlAuthContext>,
) -> Result<bool, GatewayError> {
let Some(auth_context) = auth_context else {
return Ok(false);
};
let refreshed = crate::control::refresh_execution_runtime_auth_context(
state,
auth_context,
decision.auth_endpoint_signature.as_deref(),
)
.await?;
decision.local_auth_rejection = refreshed.local_rejection.clone();
decision.auth_context = Some(refreshed);
Ok(true)
}
pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
state: &AppState,
request_context: &GatewayPublicRequestContext,
@@ -206,11 +225,8 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
Json(build_internal_gateway_fallback_plan_payload(None)).into_response(),
));
};
let provided_auth_context = payload.auth_context.is_some();
if let Some(auth_context) = payload.auth_context {
resolved.auth_context = Some(auth_context);
resolved.local_auth_rejection = None;
}
let provided_auth_context =
apply_supplied_auth_context(state, &mut resolved, payload.auth_context).await?;
let auth_context = resolved.auth_context.as_ref();
if auth_context
.map(|value| !value.access_allowed)
@@ -308,11 +324,8 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
Json(build_internal_gateway_fallback_plan_payload(None)).into_response(),
));
};
let provided_auth_context = payload.auth_context.is_some();
if let Some(auth_context) = payload.auth_context {
resolved.auth_context = Some(auth_context);
resolved.local_auth_rejection = None;
}
let provided_auth_context =
apply_supplied_auth_context(state, &mut resolved, payload.auth_context).await?;
let auth_context = resolved.auth_context.as_ref();
if auth_context
.map(|value| !value.access_allowed)
@@ -407,11 +420,8 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
else {
return Ok(Some(build_internal_gateway_proxy_public_response()));
};
let provided_auth_context = payload.auth_context.is_some();
if let Some(auth_context) = payload.auth_context {
resolved.auth_context = Some(auth_context);
resolved.local_auth_rejection = None;
}
let provided_auth_context =
apply_supplied_auth_context(state, &mut resolved, payload.auth_context).await?;
if let Some(mut planned) = api::maybe_build_sync_plan_payload(
state,
&parts,
@@ -474,11 +484,8 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
else {
return Ok(Some(build_internal_gateway_proxy_public_response()));
};
let provided_auth_context = payload.auth_context.is_some();
if let Some(auth_context) = payload.auth_context {
resolved.auth_context = Some(auth_context);
resolved.local_auth_rejection = None;
}
let provided_auth_context =
apply_supplied_auth_context(state, &mut resolved, payload.auth_context).await?;
if let Some(mut planned) = api::maybe_build_stream_plan_payload(
state,
&parts,
@@ -546,10 +553,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
else {
return Ok(None);
};
if let Some(auth_context) = payload.auth_context {
resolved.auth_context = Some(auth_context);
resolved.local_auth_rejection = None;
}
apply_supplied_auth_context(state, &mut resolved, payload.auth_context).await?;
if let Some(plan_payload) = api::maybe_build_sync_plan_payload(
state,
&parts,
@@ -630,10 +634,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
else {
return Ok(None);
};
if let Some(auth_context) = payload.auth_context {
resolved.auth_context = Some(auth_context);
resolved.local_auth_rejection = None;
}
apply_supplied_auth_context(state, &mut resolved, payload.auth_context).await?;
if let Some(plan_payload) = api::maybe_build_stream_plan_payload(
state,
&parts,

View File

@@ -30,10 +30,10 @@ mod refunds;
use self::flow::handle_wallet_flow;
pub(in crate::handlers::public::support) use self::reads::build_wallet_balance_payload_for_user;
use self::reads::{
build_wallet_daily_usage_payload, build_wallet_payload, build_wallet_zero_today_entry,
handle_wallet_balance, handle_wallet_today_cost, handle_wallet_transactions,
parse_wallet_limit, parse_wallet_offset, wallet_fixed_offset, wallet_today_billing_date_string,
wallet_transaction_payload_from_record,
build_wallet_daily_usage_payload, build_wallet_live_today_usage_payload_for_user,
build_wallet_payload, build_wallet_zero_today_entry, handle_wallet_balance,
handle_wallet_today_cost, handle_wallet_transactions, parse_wallet_limit, parse_wallet_offset,
wallet_fixed_offset, wallet_transaction_payload_from_record,
};
pub(crate) use self::recharge::sanitize_wallet_gateway_response;
use self::recharge::{

View File

@@ -1,9 +1,10 @@
use super::{
build_auth_error_response, build_auth_json_response, build_wallet_daily_usage_payload,
build_wallet_payload, build_wallet_zero_today_entry, http, parse_wallet_limit,
parse_wallet_offset, resolve_authenticated_local_user, unix_secs_to_rfc3339,
wallet_fixed_offset, wallet_today_billing_date_string, wallet_transaction_payload_from_record,
AppState, Body, GatewayPublicRequestContext, Response, WALLET_LEGACY_TIMEZONE,
build_wallet_live_today_usage_payload_for_user, build_wallet_payload,
build_wallet_zero_today_entry, http, parse_wallet_limit, parse_wallet_offset,
resolve_authenticated_local_user, unix_secs_to_rfc3339, wallet_fixed_offset,
wallet_transaction_payload_from_record, AppState, Body, GatewayPublicRequestContext, Response,
WALLET_LEGACY_TIMEZONE,
};
use serde_json::json;
@@ -98,32 +99,43 @@ pub(super) async fn handle_wallet_flow(
return build_auth_json_response(http::StatusCode::OK, payload, None);
};
let mut today_entry = build_wallet_zero_today_entry();
if let Ok(Some(today_usage)) = state
.find_wallet_today_usage(&wallet.id, WALLET_LEGACY_TIMEZONE)
.await
let mut today_entry =
match build_wallet_live_today_usage_payload_for_user(state, &auth.user.id).await {
Ok(Some(today_usage)) => today_usage,
_ => build_wallet_zero_today_entry(),
};
if today_entry
.get("total_requests")
.and_then(serde_json::Value::as_u64)
.unwrap_or_default()
== 0
{
today_entry = build_wallet_daily_usage_payload(
today_usage.id,
today_usage.billing_date,
today_usage.billing_timezone,
today_usage.total_cost_usd,
today_usage.total_requests,
today_usage.input_tokens,
today_usage.output_tokens,
today_usage.cache_creation_tokens,
today_usage.cache_read_tokens,
today_usage
.first_finalized_at_unix_secs
.and_then(unix_secs_to_rfc3339),
today_usage
.last_finalized_at_unix_secs
.and_then(unix_secs_to_rfc3339),
today_usage
.aggregated_at_unix_secs
.and_then(unix_secs_to_rfc3339),
true,
);
if let Ok(Some(today_usage)) = state
.find_wallet_today_usage(&wallet.id, WALLET_LEGACY_TIMEZONE)
.await
{
today_entry = build_wallet_daily_usage_payload(
today_usage.id,
today_usage.billing_date,
today_usage.billing_timezone,
today_usage.total_cost_usd,
today_usage.total_requests,
today_usage.input_tokens,
today_usage.output_tokens,
today_usage.cache_creation_tokens,
today_usage.cache_read_tokens,
today_usage
.first_finalized_at_unix_secs
.and_then(unix_secs_to_rfc3339),
today_usage
.last_finalized_at_unix_secs
.and_then(unix_secs_to_rfc3339),
today_usage
.aggregated_at_unix_secs
.and_then(unix_secs_to_rfc3339),
true,
);
}
}
let fetch_size = offset.saturating_add(limit).min(5200);

View File

@@ -1,11 +1,11 @@
use super::{
build_auth_error_response, build_auth_json_response, build_auth_wallet_summary_payload, http,
query_param_value, resolve_authenticated_local_user, unix_secs_to_rfc3339, AppState, Body,
GatewayError, GatewayPublicRequestContext, Response, WALLET_LEGACY_TIMEZONE,
GatewayPublicRequestContext, Response, WALLET_LEGACY_TIMEZONE,
};
use crate::handlers::shared::round_to;
use aether_data_contracts::repository::usage::UsageSettledCostSummaryQuery;
use chrono::Utc;
use chrono::{TimeZone, Utc};
use serde_json::json;
const WALLET_TODAY_COST_UNAVAILABLE_DETAIL: &str = "钱包今日费用数据暂不可用";
@@ -143,6 +143,26 @@ pub(super) fn wallet_today_billing_date_string() -> String {
.to_string()
}
fn wallet_today_usage_window() -> Result<(String, String, u64, u64), String> {
let offset = wallet_fixed_offset();
let today = Utc::now().with_timezone(&offset).date_naive();
let Some(local_start_naive) = today.and_hms_opt(0, 0, 0) else {
return Err("wallet today start is invalid".to_string());
};
let Some(local_start) = offset.from_local_datetime(&local_start_naive).single() else {
return Err("wallet today local start is ambiguous".to_string());
};
let local_end = local_start + chrono::Duration::days(1);
let start_unix_secs = local_start.timestamp().max(0) as u64;
let end_unix_secs = local_end.timestamp().max(0) as u64;
Ok((
today.to_string(),
WALLET_LEGACY_TIMEZONE.to_string(),
start_unix_secs,
end_unix_secs,
))
}
pub(super) fn build_wallet_daily_usage_payload(
id: Option<String>,
date: String,
@@ -193,6 +213,43 @@ pub(super) fn build_wallet_zero_today_entry() -> serde_json::Value {
)
}
pub(super) async fn build_wallet_live_today_usage_payload_for_user(
state: &AppState,
user_id: &str,
) -> Result<Option<serde_json::Value>, String> {
if !state.has_usage_data_reader() {
return Ok(None);
}
let (date, timezone, start_unix_secs, end_unix_secs) = wallet_today_usage_window()?;
let summary = state
.summarize_usage_settled_cost(&UsageSettledCostSummaryQuery {
created_from_unix_secs: start_unix_secs,
created_until_unix_secs: end_unix_secs,
user_id: Some(user_id.to_string()),
})
.await
.map_err(|err| format!("wallet today cost lookup failed: {err:?}"))?;
Ok(Some(build_wallet_daily_usage_payload(
None,
date,
timezone,
summary.total_cost_usd,
summary.total_requests,
summary.input_tokens,
summary.output_tokens,
summary.cache_creation_tokens,
summary.cache_read_tokens,
summary
.first_finalized_at_unix_secs
.and_then(unix_secs_to_rfc3339),
summary
.last_finalized_at_unix_secs
.and_then(unix_secs_to_rfc3339),
Some(Utc::now().to_rfc3339()),
true,
)))
}
pub(super) fn wallet_transaction_payload_from_record(
record: &aether_data::repository::wallet::StoredAdminWalletTransaction,
) -> serde_json::Value {
@@ -253,65 +310,17 @@ pub(super) async fn handle_wallet_today_cost(
Ok(value) => value,
Err(response) => return response,
};
let today = Utc::now().date_naive();
let Some(start_of_day) = today.and_hms_opt(0, 0, 0) else {
return build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
"wallet today start is invalid",
match build_wallet_live_today_usage_payload_for_user(state, &auth.user.id).await {
Ok(Some(payload)) => build_auth_json_response(http::StatusCode::OK, payload, None),
Ok(None) => build_auth_error_response(
http::StatusCode::SERVICE_UNAVAILABLE,
WALLET_TODAY_COST_UNAVAILABLE_DETAIL,
false,
);
};
let start_unix_secs = u64::try_from(
chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(start_of_day, chrono::Utc)
.timestamp(),
)
.unwrap_or_default();
let end_unix_secs = start_unix_secs.saturating_add(24 * 3600);
let summary = match state
.summarize_usage_settled_cost(&UsageSettledCostSummaryQuery {
created_from_unix_secs: start_unix_secs,
created_until_unix_secs: end_unix_secs,
user_id: Some(auth.user.id.clone()),
})
.await
{
Ok(value) => value,
Err(err) => {
return build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("wallet today cost lookup failed: {err:?}"),
false,
)
),
Err(detail) => {
build_auth_error_response(http::StatusCode::INTERNAL_SERVER_ERROR, detail, false)
}
};
let first_finalized_at = summary
.first_finalized_at_unix_secs
.and_then(unix_secs_to_rfc3339);
let last_finalized_at = summary
.last_finalized_at_unix_secs
.and_then(unix_secs_to_rfc3339);
build_auth_json_response(
http::StatusCode::OK,
json!({
"id": serde_json::Value::Null,
"date": today.to_string(),
"timezone": "UTC",
"total_cost": round_to(summary.total_cost_usd, 6),
"total_requests": summary.total_requests,
"input_tokens": summary.input_tokens,
"output_tokens": summary.output_tokens,
"cache_creation_tokens": summary.cache_creation_tokens,
"cache_read_tokens": summary.cache_read_tokens,
"first_finalized_at": first_finalized_at,
"last_finalized_at": last_finalized_at,
"aggregated_at": Utc::now().to_rfc3339(),
"is_today": true,
}),
None,
)
}
}
pub(super) async fn handle_wallet_transactions(