mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
Merge pull request #289 from AAEE86/rust
fix(usage): 统一使用记录与仪表盘的缓存命中率计算口径
This commit is contained in:
@@ -4,8 +4,9 @@ use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::usage::{
|
||||
admin_usage_bad_request_response, admin_usage_data_unavailable_response,
|
||||
admin_usage_matches_optional_id, admin_usage_parse_recent_hours,
|
||||
admin_usage_bad_request_response, admin_usage_cache_creation_tokens,
|
||||
admin_usage_data_unavailable_response, admin_usage_matches_optional_id,
|
||||
admin_usage_parse_recent_hours, admin_usage_total_input_context,
|
||||
ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use axum::{
|
||||
@@ -50,10 +51,9 @@ pub(super) async fn build_admin_usage_cache_affinity_hit_analysis_response(
|
||||
.iter()
|
||||
.map(|item| item.cache_read_input_tokens)
|
||||
.sum();
|
||||
let total_cache_creation_tokens: u64 = filtered
|
||||
.iter()
|
||||
.map(|item| item.cache_creation_input_tokens)
|
||||
.sum();
|
||||
let total_cache_creation_tokens: u64 =
|
||||
filtered.iter().map(admin_usage_cache_creation_tokens).sum();
|
||||
let total_input_context: u64 = filtered.iter().map(admin_usage_total_input_context).sum();
|
||||
let total_cache_read_cost: f64 = filtered.iter().map(|item| item.cache_read_cost_usd).sum();
|
||||
let total_cache_creation_cost: f64 = filtered
|
||||
.iter()
|
||||
@@ -63,12 +63,11 @@ pub(super) async fn build_admin_usage_cache_affinity_hit_analysis_response(
|
||||
.iter()
|
||||
.filter(|item| item.cache_read_input_tokens > 0)
|
||||
.count();
|
||||
let total_context_tokens = total_input_tokens.saturating_add(total_cache_read_tokens);
|
||||
let token_cache_hit_rate = if total_context_tokens == 0 {
|
||||
let token_cache_hit_rate = if total_input_context == 0 {
|
||||
0.0
|
||||
} else {
|
||||
round_to(
|
||||
total_cache_read_tokens as f64 / total_context_tokens as f64 * 100.0,
|
||||
total_cache_read_tokens as f64 / total_input_context as f64 * 100.0,
|
||||
2,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -16,12 +16,12 @@ use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
};
|
||||
use aether_model_fetch::{
|
||||
aggregate_models_for_cache, build_models_fetch_execution_plan,
|
||||
endpoint_supports_rust_models_fetch, extract_error_message, parse_models_response,
|
||||
aggregate_models_for_cache, fetch_models_from_transports, json_string_list,
|
||||
preset_models_for_provider, selected_models_fetch_endpoints,
|
||||
};
|
||||
use axum::{body::Body, http::Response, response::IntoResponse, Json};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
pub(crate) const ADMIN_PROVIDER_QUERY_LOCAL_TEST_MODEL_MESSAGE: &str =
|
||||
"Rust local provider-query model test is not configured";
|
||||
@@ -31,22 +31,15 @@ const ADMIN_PROVIDER_QUERY_NO_ACTIVE_ENDPOINT_DETAIL: &str =
|
||||
"No active endpoints found for this provider";
|
||||
const ADMIN_PROVIDER_QUERY_NO_MODELS_FROM_ENDPOINT_DETAIL: &str =
|
||||
"No models returned from any endpoint";
|
||||
const PROVIDER_QUERY_FETCH_FORMAT_PRIORITY: &[&[&str]] = &[
|
||||
&[
|
||||
"openai:chat",
|
||||
"openai:responses",
|
||||
"openai:cli",
|
||||
"openai:compact",
|
||||
],
|
||||
&["claude:chat", "claude:cli"],
|
||||
&["gemini:chat", "gemini:cli"],
|
||||
];
|
||||
const ADMIN_PROVIDER_QUERY_NO_MODELS_FROM_KEY_DETAIL: &str = "No models returned from any key";
|
||||
const ANTIGRAVITY_PROVIDER_CACHE_KEY_PREFIX: &str = "upstream_models_provider:";
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ProviderQueryKeyFetchResult {
|
||||
models: Vec<Value>,
|
||||
error: Option<String>,
|
||||
from_cache: bool,
|
||||
has_success: bool,
|
||||
}
|
||||
|
||||
fn provider_query_provider_payload(provider: &StoredProviderCatalogProvider) -> Value {
|
||||
@@ -66,64 +59,6 @@ fn provider_query_key_display_name(key: &StoredProviderCatalogKey) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_query_normalize_api_format(value: &str) -> String {
|
||||
value.trim().to_ascii_lowercase()
|
||||
}
|
||||
|
||||
fn provider_query_selected_fetch_endpoints(
|
||||
endpoints: &[StoredProviderCatalogEndpoint],
|
||||
key: &StoredProviderCatalogKey,
|
||||
) -> Vec<StoredProviderCatalogEndpoint> {
|
||||
let allowed_api_formats = key
|
||||
.api_formats
|
||||
.as_ref()
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(provider_query_normalize_api_format)
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<BTreeSet<_>>()
|
||||
})
|
||||
.filter(|items| !items.is_empty());
|
||||
let mut by_format = BTreeMap::<String, StoredProviderCatalogEndpoint>::new();
|
||||
for endpoint in endpoints.iter().filter(|endpoint| endpoint.is_active) {
|
||||
let api_format = provider_query_normalize_api_format(&endpoint.api_format);
|
||||
if api_format.is_empty() || !endpoint_supports_rust_models_fetch(&api_format) {
|
||||
continue;
|
||||
}
|
||||
if allowed_api_formats
|
||||
.as_ref()
|
||||
.is_some_and(|formats| !formats.contains(&api_format))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
by_format.insert(api_format, endpoint.clone());
|
||||
}
|
||||
|
||||
// 与 Python 版本保持一致:同族优先使用 chat 端点,其次才回退到其他抓取格式。
|
||||
let covered_formats = PROVIDER_QUERY_FETCH_FORMAT_PRIORITY
|
||||
.iter()
|
||||
.flat_map(|items| items.iter().copied())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let mut selected = PROVIDER_QUERY_FETCH_FORMAT_PRIORITY
|
||||
.iter()
|
||||
.filter_map(|candidates| {
|
||||
candidates
|
||||
.iter()
|
||||
.find_map(|api_format| by_format.remove(*api_format))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
selected.extend(
|
||||
by_format
|
||||
.into_iter()
|
||||
.filter(|(api_format, _)| !covered_formats.contains(api_format.as_str()))
|
||||
.map(|(_, endpoint)| endpoint),
|
||||
);
|
||||
selected
|
||||
}
|
||||
|
||||
async fn provider_query_read_cached_models(
|
||||
state: &AdminAppState<'_>,
|
||||
provider_id: &str,
|
||||
@@ -147,38 +82,97 @@ async fn provider_query_read_cached_models(
|
||||
Some(aggregate_models_for_cache(&parsed))
|
||||
}
|
||||
|
||||
async fn provider_query_fetch_models_from_transport(
|
||||
async fn provider_query_read_provider_cached_models(
|
||||
state: &AdminAppState<'_>,
|
||||
transport: &crate::provider_transport::GatewayProviderTransportSnapshot,
|
||||
) -> Result<Vec<Value>, String> {
|
||||
let plan = build_models_fetch_execution_plan(state.app(), transport).await?;
|
||||
let result = execution_runtime::execute_execution_runtime_sync_plan(state.app(), None, &plan)
|
||||
provider_id: &str,
|
||||
) -> Option<Vec<Value>> {
|
||||
let runner = state.app().redis_kv_runner()?;
|
||||
let cache_key = runner.keyspace().key(&format!(
|
||||
"{ANTIGRAVITY_PROVIDER_CACHE_KEY_PREFIX}{provider_id}"
|
||||
));
|
||||
let mut connection = runner
|
||||
.client()
|
||||
.get_multiplexed_async_connection()
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
.ok()?;
|
||||
let raw = redis::cmd("GET")
|
||||
.arg(&cache_key)
|
||||
.query_async::<Option<String>>(&mut connection)
|
||||
.await
|
||||
.ok()??;
|
||||
let parsed = serde_json::from_str::<Vec<Value>>(&raw).ok()?;
|
||||
Some(aggregate_models_for_cache(&parsed))
|
||||
}
|
||||
|
||||
if result.status_code != 200 {
|
||||
let message = result
|
||||
.body
|
||||
.as_ref()
|
||||
.and_then(|body| body.json_body.as_ref())
|
||||
.and_then(extract_error_message)
|
||||
.or_else(|| {
|
||||
result.error.as_ref().and_then(|error| {
|
||||
let message = error.message.trim();
|
||||
(!message.is_empty()).then_some(message.to_string())
|
||||
async fn provider_query_write_provider_cached_models(
|
||||
state: &AdminAppState<'_>,
|
||||
provider_id: &str,
|
||||
models: &[Value],
|
||||
) {
|
||||
let Some(runner) = state.app().redis_kv_runner() else {
|
||||
return;
|
||||
};
|
||||
let Ok(serialized) = serde_json::to_string(&aggregate_models_for_cache(models)) else {
|
||||
return;
|
||||
};
|
||||
let cache_key = format!("{ANTIGRAVITY_PROVIDER_CACHE_KEY_PREFIX}{provider_id}");
|
||||
let _ = runner
|
||||
.setex(
|
||||
&cache_key,
|
||||
&serialized,
|
||||
Some(aether_model_fetch::model_fetch_interval_minutes().saturating_mul(60)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn provider_query_antigravity_tier_weight(raw_auth_config: Option<&str>) -> i32 {
|
||||
raw_auth_config
|
||||
.and_then(|value| serde_json::from_str::<Value>(value).ok())
|
||||
.and_then(|value| value.get("tier").cloned())
|
||||
.and_then(|value| value.as_str().map(ToOwned::to_owned))
|
||||
.map(|tier| match tier.trim().to_ascii_lowercase().as_str() {
|
||||
"ultra" => 3,
|
||||
"pro" => 2,
|
||||
"free" => 1,
|
||||
_ => 0,
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
async fn provider_query_sort_antigravity_keys(
|
||||
state: &AdminAppState<'_>,
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
endpoints: &[StoredProviderCatalogEndpoint],
|
||||
keys: Vec<StoredProviderCatalogKey>,
|
||||
) -> Result<Vec<StoredProviderCatalogKey>, GatewayError> {
|
||||
let mut ranked = Vec::new();
|
||||
for key in keys {
|
||||
let availability = if key.oauth_invalid_at_unix_secs.is_some() {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let tier_weight = if let Some(endpoint) = selected_models_fetch_endpoints(endpoints, &key)
|
||||
.into_iter()
|
||||
.next()
|
||||
{
|
||||
state
|
||||
.app()
|
||||
.read_provider_transport_snapshot(&provider.id, &endpoint.id, &key.id)
|
||||
.await?
|
||||
.map(|transport| {
|
||||
provider_query_antigravity_tier_weight(
|
||||
transport.key.decrypted_auth_config.as_deref(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| format!("upstream returned status {}", result.status_code));
|
||||
return Err(message);
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
ranked.push(((availability, tier_weight), key));
|
||||
}
|
||||
|
||||
let body_json = result
|
||||
.body
|
||||
.as_ref()
|
||||
.and_then(|body| body.json_body.as_ref())
|
||||
.ok_or_else(|| "models fetch response body is missing JSON payload".to_string())?;
|
||||
let parsed = parse_models_response(&transport.endpoint.api_format, body_json)?;
|
||||
Ok(parsed.cached_models)
|
||||
ranked.sort_by(|left, right| right.0.cmp(&left.0));
|
||||
Ok(ranked.into_iter().map(|(_, key)| key).collect())
|
||||
}
|
||||
|
||||
async fn provider_query_fetch_models_for_key(
|
||||
@@ -196,20 +190,30 @@ async fn provider_query_fetch_models_for_key(
|
||||
models: cached_models,
|
||||
error: None,
|
||||
from_cache: true,
|
||||
has_success: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let selected_endpoints = provider_query_selected_fetch_endpoints(endpoints, key);
|
||||
let selected_endpoints = selected_models_fetch_endpoints(endpoints, key);
|
||||
if selected_endpoints.is_empty() {
|
||||
if let Some(models) = preset_models_for_provider(&provider.provider_type) {
|
||||
return Ok(ProviderQueryKeyFetchResult {
|
||||
models: aggregate_models_for_cache(&models),
|
||||
error: None,
|
||||
from_cache: false,
|
||||
has_success: true,
|
||||
});
|
||||
}
|
||||
return Ok(ProviderQueryKeyFetchResult {
|
||||
models: Vec::new(),
|
||||
error: Some(ADMIN_PROVIDER_QUERY_NO_ACTIVE_ENDPOINT_DETAIL.to_string()),
|
||||
from_cache: false,
|
||||
has_success: false,
|
||||
});
|
||||
}
|
||||
|
||||
let mut all_models = Vec::new();
|
||||
let mut transports = Vec::new();
|
||||
let mut all_errors = Vec::new();
|
||||
for endpoint in selected_endpoints {
|
||||
let Some(transport) = state
|
||||
@@ -223,14 +227,34 @@ async fn provider_query_fetch_models_for_key(
|
||||
));
|
||||
continue;
|
||||
};
|
||||
match provider_query_fetch_models_from_transport(state, &transport).await {
|
||||
Ok(models) => all_models.extend(models),
|
||||
Err(err) => all_errors.push(err),
|
||||
}
|
||||
transports.push(transport);
|
||||
}
|
||||
|
||||
let unique_models = aggregate_models_for_cache(&all_models);
|
||||
if !unique_models.is_empty() {
|
||||
if transports.is_empty() {
|
||||
return Ok(ProviderQueryKeyFetchResult {
|
||||
models: Vec::new(),
|
||||
error: Some(all_errors.join("; ")),
|
||||
from_cache: false,
|
||||
has_success: false,
|
||||
});
|
||||
}
|
||||
|
||||
let outcome = match fetch_models_from_transports(state.app(), &transports).await {
|
||||
Ok(outcome) => outcome,
|
||||
Err(err) => {
|
||||
all_errors.push(err);
|
||||
return Ok(ProviderQueryKeyFetchResult {
|
||||
models: Vec::new(),
|
||||
error: Some(all_errors.join("; ")),
|
||||
from_cache: false,
|
||||
has_success: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
all_errors.extend(outcome.errors);
|
||||
let unique_models = aggregate_models_for_cache(&outcome.cached_models);
|
||||
if outcome.has_success && !unique_models.is_empty() {
|
||||
<AppState as ModelFetchRuntimeState>::write_upstream_models_cache(
|
||||
state.app(),
|
||||
&provider.id,
|
||||
@@ -253,6 +277,7 @@ async fn provider_query_fetch_models_for_key(
|
||||
models: unique_models,
|
||||
error,
|
||||
from_cache: false,
|
||||
has_success: outcome.has_success,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -317,7 +342,10 @@ pub(crate) async fn build_admin_provider_query_models_response(
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let active_keys = keys.iter().filter(|key| key.is_active).collect::<Vec<_>>();
|
||||
let active_keys = keys
|
||||
.into_iter()
|
||||
.filter(|key| key.is_active)
|
||||
.collect::<Vec<_>>();
|
||||
if active_keys.is_empty() {
|
||||
return Ok(build_admin_provider_query_bad_request_response(
|
||||
ADMIN_PROVIDER_QUERY_NO_ACTIVE_API_KEY_DETAIL,
|
||||
@@ -325,11 +353,45 @@ pub(crate) async fn build_admin_provider_query_models_response(
|
||||
}
|
||||
let active_key_count = active_keys.len();
|
||||
|
||||
if provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("antigravity")
|
||||
&& !force_refresh
|
||||
{
|
||||
if let Some(models) = provider_query_read_provider_cached_models(state, &provider.id).await
|
||||
{
|
||||
return Ok(Json(json!({
|
||||
"success": !models.is_empty(),
|
||||
"data": {
|
||||
"models": models,
|
||||
"error": serde_json::Value::Null,
|
||||
"from_cache": true,
|
||||
"keys_total": active_key_count,
|
||||
"keys_cached": active_key_count,
|
||||
"keys_fetched": 0,
|
||||
},
|
||||
"provider": provider_query_provider_payload(&provider),
|
||||
}))
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
|
||||
let ordered_keys = if provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("antigravity")
|
||||
{
|
||||
provider_query_sort_antigravity_keys(state, &provider, &endpoints, active_keys).await?
|
||||
} else {
|
||||
active_keys
|
||||
};
|
||||
|
||||
let mut all_models = Vec::new();
|
||||
let mut all_errors = Vec::new();
|
||||
let mut cache_hit_count = 0usize;
|
||||
let mut fetch_count = 0usize;
|
||||
for key in active_keys {
|
||||
for key in &ordered_keys {
|
||||
let result =
|
||||
provider_query_fetch_models_for_key(state, &provider, &endpoints, key, force_refresh)
|
||||
.await?;
|
||||
@@ -346,9 +408,25 @@ pub(crate) async fn build_admin_provider_query_models_response(
|
||||
} else {
|
||||
fetch_count += 1;
|
||||
}
|
||||
if provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("antigravity")
|
||||
&& result.has_success
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let models = aggregate_models_for_cache(&all_models);
|
||||
if provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("antigravity")
|
||||
&& !models.is_empty()
|
||||
{
|
||||
provider_query_write_provider_cached_models(state, &provider.id, &models).await;
|
||||
}
|
||||
let success = !models.is_empty();
|
||||
let mut error = if all_errors.is_empty() {
|
||||
None
|
||||
@@ -356,7 +434,7 @@ pub(crate) async fn build_admin_provider_query_models_response(
|
||||
Some(all_errors.join("; "))
|
||||
};
|
||||
if !success && error.is_none() {
|
||||
error = Some("No models returned from any key".to_string());
|
||||
error = Some(ADMIN_PROVIDER_QUERY_NO_MODELS_FROM_KEY_DETAIL.to_string());
|
||||
}
|
||||
|
||||
Ok(Json(json!({
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::{
|
||||
build_auth_error_response, query_param_value, resolve_authenticated_local_user, AppState,
|
||||
GatewayError, GatewayPublicRequestContext,
|
||||
};
|
||||
use aether_billing::normalize_total_input_context_for_cache_hit_rate;
|
||||
use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageAuditListQuery};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -28,6 +29,7 @@ struct DashboardUsageTotals {
|
||||
total_tokens: u64,
|
||||
cache_creation_tokens: u64,
|
||||
cache_read_tokens: u64,
|
||||
cache_hit_total_input_context: u64,
|
||||
cache_creation_cost_usd: f64,
|
||||
cache_read_cost_usd: f64,
|
||||
total_cost_usd: f64,
|
||||
@@ -69,12 +71,14 @@ pub(super) fn decision_route_kind(request_context: &GatewayPublicRequestContext)
|
||||
|
||||
impl DashboardUsageTotals {
|
||||
fn record(&mut self, item: &StoredRequestUsageAudit) {
|
||||
let cache_creation_tokens = dashboard_cache_creation_tokens(item);
|
||||
self.requests += 1;
|
||||
self.input_tokens += item.input_tokens;
|
||||
self.output_tokens += item.output_tokens;
|
||||
self.total_tokens += item.total_tokens;
|
||||
self.cache_creation_tokens += item.cache_creation_input_tokens;
|
||||
self.cache_creation_tokens += cache_creation_tokens;
|
||||
self.cache_read_tokens += item.cache_read_input_tokens;
|
||||
self.cache_hit_total_input_context += dashboard_total_input_context(item);
|
||||
self.cache_creation_cost_usd += item.cache_creation_cost_usd;
|
||||
self.cache_read_cost_usd += item.cache_read_cost_usd;
|
||||
self.total_cost_usd += item.total_cost_usd;
|
||||
@@ -102,15 +106,50 @@ impl DashboardUsageTotals {
|
||||
}
|
||||
|
||||
fn cache_hit_rate(&self) -> f64 {
|
||||
let total_cache_tokens = self.cache_creation_tokens + self.cache_read_tokens;
|
||||
if total_cache_tokens == 0 {
|
||||
if self.cache_hit_total_input_context == 0 {
|
||||
0.0
|
||||
} else {
|
||||
dashboard_round_f64(self.cache_read_tokens as f64 / total_cache_tokens as f64, 4)
|
||||
dashboard_round_f64(
|
||||
self.cache_read_tokens as f64 / self.cache_hit_total_input_context as f64 * 100.0,
|
||||
2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dashboard_usage_should_count_in_summary(item: &StoredRequestUsageAudit) -> bool {
|
||||
!matches!(item.status.as_str(), "pending" | "streaming")
|
||||
&& !matches!(item.provider_name.as_str(), "unknown" | "pending")
|
||||
}
|
||||
|
||||
fn dashboard_cache_creation_tokens(item: &StoredRequestUsageAudit) -> u64 {
|
||||
let classified = item
|
||||
.cache_creation_ephemeral_5m_input_tokens
|
||||
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
|
||||
if item.cache_creation_input_tokens == 0 && classified > 0 {
|
||||
classified
|
||||
} else {
|
||||
item.cache_creation_input_tokens
|
||||
}
|
||||
}
|
||||
|
||||
fn dashboard_total_input_context(item: &StoredRequestUsageAudit) -> u64 {
|
||||
let api_format = item
|
||||
.endpoint_api_format
|
||||
.as_deref()
|
||||
.or(item.api_format.as_deref());
|
||||
let input_tokens = i64::try_from(item.input_tokens).unwrap_or(i64::MAX);
|
||||
let cache_creation_tokens =
|
||||
i64::try_from(dashboard_cache_creation_tokens(item)).unwrap_or(i64::MAX);
|
||||
let cache_read_tokens = i64::try_from(item.cache_read_input_tokens).unwrap_or(i64::MAX);
|
||||
normalize_total_input_context_for_cache_hit_rate(
|
||||
api_format,
|
||||
input_tokens,
|
||||
cache_creation_tokens,
|
||||
cache_read_tokens,
|
||||
) as u64
|
||||
}
|
||||
|
||||
fn dashboard_round_f64(value: f64, decimals: u32) -> f64 {
|
||||
let factor = 10_f64.powi(i32::try_from(decimals).unwrap_or_default());
|
||||
(value * factor).round() / factor
|
||||
@@ -128,6 +167,39 @@ fn dashboard_format_integer(value: u64) -> String {
|
||||
formatted.chars().rev().collect()
|
||||
}
|
||||
|
||||
fn dashboard_trimmed_decimal(value: f64, decimals: usize) -> String {
|
||||
let mut formatted = format!("{value:.decimals$}");
|
||||
while formatted.contains('.') && formatted.ends_with('0') {
|
||||
formatted.pop();
|
||||
}
|
||||
if formatted.ends_with('.') {
|
||||
formatted.pop();
|
||||
}
|
||||
formatted
|
||||
}
|
||||
|
||||
fn dashboard_format_token_compact(value: u64) -> String {
|
||||
if value < 1_000 {
|
||||
return dashboard_format_integer(value);
|
||||
}
|
||||
|
||||
if value < 1_000_000 {
|
||||
let thousands = value as f64 / 1_000.0;
|
||||
if thousands >= 100.0 {
|
||||
return format!("{}K", thousands.round() as u64);
|
||||
}
|
||||
let decimals = if thousands >= 10.0 { 1 } else { 2 };
|
||||
return format!("{}K", dashboard_trimmed_decimal(thousands, decimals));
|
||||
}
|
||||
|
||||
let millions = value as f64 / 1_000_000.0;
|
||||
if millions >= 100.0 {
|
||||
return format!("{}M", millions.round() as u64);
|
||||
}
|
||||
let decimals = if millions >= 10.0 { 1 } else { 2 };
|
||||
format!("{}M", dashboard_trimmed_decimal(millions, decimals))
|
||||
}
|
||||
|
||||
fn dashboard_format_usd(value: f64) -> String {
|
||||
format!("${:.2}", dashboard_round_f64(value, 2))
|
||||
}
|
||||
@@ -144,6 +216,16 @@ fn dashboard_format_token_subvalue(totals: &DashboardUsageTotals) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn dashboard_format_today_token_subvalue(totals: &DashboardUsageTotals) -> String {
|
||||
format!(
|
||||
"输入 {} / 输出 {} · 写缓存 {} / 读缓存 {}",
|
||||
dashboard_format_token_compact(totals.input_tokens),
|
||||
dashboard_format_token_compact(totals.output_tokens),
|
||||
dashboard_format_token_compact(totals.cache_creation_tokens),
|
||||
dashboard_format_token_compact(totals.cache_read_tokens)
|
||||
)
|
||||
}
|
||||
|
||||
fn dashboard_parse_tz_offset_minutes(query: Option<&str>) -> Result<i32, String> {
|
||||
query_param_value(query, "tz_offset_minutes")
|
||||
.map(|value| {
|
||||
@@ -392,7 +474,10 @@ async fn dashboard_list_usage_for_range(
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(value) => Ok(value),
|
||||
Ok(mut value) => {
|
||||
value.retain(dashboard_usage_should_count_in_summary);
|
||||
Ok(value)
|
||||
}
|
||||
Err(err) => Err(build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("{error_context}: {err:?}"),
|
||||
@@ -578,8 +663,8 @@ pub(super) async fn handle_dashboard_stats_get(
|
||||
},
|
||||
{
|
||||
"name": "今日 Token",
|
||||
"value": dashboard_format_integer(today_totals.total_tokens),
|
||||
"subValue": dashboard_format_token_subvalue(&today_totals),
|
||||
"value": dashboard_format_token_compact(today_totals.total_tokens),
|
||||
"subValue": dashboard_format_today_token_subvalue(&today_totals),
|
||||
"icon": "Zap",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use aether_billing::normalize_input_tokens_for_billing;
|
||||
use aether_billing::{
|
||||
normalize_input_tokens_for_billing, normalize_total_input_context_for_cache_hit_rate,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageAuditListQuery};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -101,9 +103,20 @@ fn users_me_usage_cache_creation_tokens(item: &StoredRequestUsageAudit) -> u64 {
|
||||
}
|
||||
|
||||
fn users_me_usage_total_input_context(item: &StoredRequestUsageAudit) -> u64 {
|
||||
item.input_tokens
|
||||
.saturating_add(users_me_usage_cache_creation_tokens(item))
|
||||
.saturating_add(item.cache_read_input_tokens)
|
||||
let api_format = item
|
||||
.endpoint_api_format
|
||||
.as_deref()
|
||||
.or(item.api_format.as_deref());
|
||||
let input_tokens = i64::try_from(item.input_tokens).unwrap_or(i64::MAX);
|
||||
let cache_creation_tokens =
|
||||
i64::try_from(users_me_usage_cache_creation_tokens(item)).unwrap_or(i64::MAX);
|
||||
let cache_read_tokens = i64::try_from(item.cache_read_input_tokens).unwrap_or(i64::MAX);
|
||||
normalize_total_input_context_for_cache_hit_rate(
|
||||
api_format,
|
||||
input_tokens,
|
||||
cache_creation_tokens,
|
||||
cache_read_tokens,
|
||||
) as u64
|
||||
}
|
||||
|
||||
fn users_me_usage_effective_input_tokens(item: &StoredRequestUsageAudit) -> u64 {
|
||||
|
||||
Reference in New Issue
Block a user