feat: 扩展 cache creation token 细分统计与 effective_input_tokens 计费逻辑

- 新增 cache_creation_ephemeral_5m/1h_input_tokens 字段,区分不同 TTL 的缓存写入 token
- 引入 effective_input_tokens(扣除 cache read 后的有效输入 token),暴露给 usage 接口
- billing 规则生成器支持 5m/1h ephemeral cache 独立定价与分级计费
- usage_mapper 增加 Claude/Anthropic 格式映射,修复 OpenAI responses 格式字段兼容性
- 迁移逻辑增强:支持 checksum 容错、applied/pending 数量日志、逐步执行信息输出
- executor 抽离 LocalExecutionRequestOutcome 类型,统一 sync/stream 路径返回语义
- provider-transport auth 层新增 complete passthrough headers 构建逻辑
- 前端 usage 类型全面补充 effective_input_tokens、cache_creation_tokens、total_input_context 字段
This commit is contained in:
fawney19
2026-04-10 17:44:55 +08:00
parent 5014e2f5fd
commit 010ab127e2
64 changed files with 4217 additions and 477 deletions

View File

@@ -29,7 +29,10 @@ use crate::control::{
should_buffer_request_for_local_auth, trusted_auth_local_rejection, GatewayControlDecision,
GatewayPublicRequestContext,
};
use crate::executor::{maybe_execute_stream_request, maybe_execute_sync_request};
use crate::executor::{
maybe_execute_stream_request, maybe_execute_sync_request,
record_failed_usage_for_exhausted_request, LocalExecutionRequestOutcome,
};
use crate::handlers::shared::{
build_admin_proxy_auth_required_response, build_unhandled_admin_proxy_response,
local_proxy_route_requires_buffered_body, request_enables_control_execute,
@@ -44,7 +47,6 @@ use crate::{
use axum::body::{to_bytes, Body, Bytes};
use axum::extract::{ConnectInfo, Request, State};
use axum::http::{self, header::HeaderName, header::HeaderValue, Response};
use chrono::Utc;
use std::time::Instant;
use tracing::{info, warn};
@@ -645,8 +647,9 @@ pub(crate) async fn proxy_request(
.as_ref()
.expect("execution runtime/control auth gate should have buffered request body");
let stream_request = request_wants_stream(&request_context, buffered_body);
let mut local_execution_exhaustion = None;
if stream_request {
if let Some(execution_runtime_response) = maybe_execute_stream_request(
match maybe_execute_stream_request(
&state,
&parts,
buffered_body,
@@ -655,35 +658,46 @@ pub(crate) async fn proxy_request(
)
.await?
{
LocalExecutionRequestOutcome::Responded(execution_runtime_response) => {
state.clear_local_execution_runtime_miss_diagnostic(&trace_id);
return Ok(finalize_gateway_response_with_context(
&state,
execution_runtime_response,
&remote_addr,
&request_context,
EXECUTION_PATH_EXECUTION_RUNTIME_STREAM,
&started_at,
request_permit.take(),
));
}
LocalExecutionRequestOutcome::Exhausted(outcome) => {
local_execution_exhaustion = Some(outcome);
}
LocalExecutionRequestOutcome::NoPath => {}
}
}
match maybe_execute_sync_request(&state, &parts, buffered_body, &trace_id, control_decision)
.await?
{
LocalExecutionRequestOutcome::Responded(execution_runtime_response) => {
state.clear_local_execution_runtime_miss_diagnostic(&trace_id);
return Ok(finalize_gateway_response_with_context(
&state,
execution_runtime_response,
&remote_addr,
&request_context,
EXECUTION_PATH_EXECUTION_RUNTIME_STREAM,
EXECUTION_PATH_EXECUTION_RUNTIME_SYNC,
&started_at,
request_permit.take(),
));
}
}
if let Some(execution_runtime_response) =
maybe_execute_sync_request(&state, &parts, buffered_body, &trace_id, control_decision)
.await?
{
state.clear_local_execution_runtime_miss_diagnostic(&trace_id);
return Ok(finalize_gateway_response_with_context(
&state,
execution_runtime_response,
&remote_addr,
&request_context,
EXECUTION_PATH_EXECUTION_RUNTIME_SYNC,
&started_at,
request_permit.take(),
));
LocalExecutionRequestOutcome::Exhausted(outcome) => {
local_execution_exhaustion = Some(outcome);
}
LocalExecutionRequestOutcome::NoPath => {}
}
if parts.method != http::Method::POST {
if let Some(execution_runtime_response) = maybe_execute_stream_request(
match maybe_execute_stream_request(
&state,
&parts,
buffered_body,
@@ -692,20 +706,26 @@ pub(crate) async fn proxy_request(
)
.await?
{
state.clear_local_execution_runtime_miss_diagnostic(&trace_id);
return Ok(finalize_gateway_response_with_context(
&state,
execution_runtime_response,
&remote_addr,
&request_context,
EXECUTION_PATH_EXECUTION_RUNTIME_STREAM,
&started_at,
request_permit.take(),
));
LocalExecutionRequestOutcome::Responded(execution_runtime_response) => {
state.clear_local_execution_runtime_miss_diagnostic(&trace_id);
return Ok(finalize_gateway_response_with_context(
&state,
execution_runtime_response,
&remote_addr,
&request_context,
EXECUTION_PATH_EXECUTION_RUNTIME_STREAM,
&started_at,
request_permit.take(),
));
}
LocalExecutionRequestOutcome::Exhausted(outcome) => {
local_execution_exhaustion = Some(outcome);
}
LocalExecutionRequestOutcome::NoPath => {}
}
}
if allow_control_execute_fallback {
if let Some(control_response) = maybe_execute_via_control(
match maybe_execute_via_control(
&state,
&parts,
buffered_body.clone(),
@@ -715,41 +735,47 @@ pub(crate) async fn proxy_request(
)
.await?
{
let reason = GatewayFallbackReason::ControlExecuteEmergency;
let control_execution_path = if stream_request {
EXECUTION_PATH_CONTROL_EXECUTE_STREAM
} else {
EXECUTION_PATH_CONTROL_EXECUTE_SYNC
};
state.record_fallback_metric(
GatewayFallbackMetricKind::ControlExecuteFallback,
control_decision,
None,
Some(control_execution_path),
reason,
);
state.record_fallback_metric(
GatewayFallbackMetricKind::RemoteExecuteEmergency,
control_decision,
None,
Some(control_execution_path),
reason,
);
let mut control_response = control_response;
state.clear_local_execution_runtime_miss_diagnostic(&trace_id);
control_response.headers_mut().insert(
HeaderName::from_static(DEPENDENCY_REASON_HEADER),
HeaderValue::from_static(reason.as_label_value()),
);
return Ok(finalize_gateway_response_with_context(
&state,
control_response,
&remote_addr,
&request_context,
control_execution_path,
&started_at,
request_permit.take(),
));
LocalExecutionRequestOutcome::Responded(control_response) => {
let reason = GatewayFallbackReason::ControlExecuteEmergency;
let control_execution_path = if stream_request {
EXECUTION_PATH_CONTROL_EXECUTE_STREAM
} else {
EXECUTION_PATH_CONTROL_EXECUTE_SYNC
};
state.record_fallback_metric(
GatewayFallbackMetricKind::ControlExecuteFallback,
control_decision,
None,
Some(control_execution_path),
reason,
);
state.record_fallback_metric(
GatewayFallbackMetricKind::RemoteExecuteEmergency,
control_decision,
None,
Some(control_execution_path),
reason,
);
let mut control_response = control_response;
state.clear_local_execution_runtime_miss_diagnostic(&trace_id);
control_response.headers_mut().insert(
HeaderName::from_static(DEPENDENCY_REASON_HEADER),
HeaderValue::from_static(reason.as_label_value()),
);
return Ok(finalize_gateway_response_with_context(
&state,
control_response,
&remote_addr,
&request_context,
control_execution_path,
&started_at,
request_permit.take(),
));
}
LocalExecutionRequestOutcome::Exhausted(outcome) => {
local_execution_exhaustion = Some(outcome);
}
LocalExecutionRequestOutcome::NoPath => {}
}
}
let local_execution_runtime_miss_detail =
@@ -779,6 +805,16 @@ pub(crate) async fn proxy_request(
"gateway local execution runtime miss"
);
}
if let Some(exhaustion) = local_execution_exhaustion {
record_failed_usage_for_exhausted_request(
&state,
exhaustion,
&started_at,
local_execution_runtime_miss_detail,
local_execution_runtime_miss_diagnostic.as_ref(),
)
.await;
}
let mut response = build_local_http_error_response(
&trace_id,
control_decision,

View File

@@ -1,5 +1,6 @@
use std::collections::{BTreeMap, BTreeSet};
use aether_billing::normalize_input_tokens_for_billing;
use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageAuditListQuery};
use axum::{
body::Body,
@@ -88,11 +89,33 @@ fn parse_users_me_usage_ids(query: Option<&str>) -> Option<BTreeSet<String>> {
(!values.is_empty()).then_some(values)
}
fn users_me_usage_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 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)
}
fn users_me_usage_effective_input_tokens(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_read_tokens = i64::try_from(item.cache_read_input_tokens).unwrap_or(i64::MAX);
normalize_input_tokens_for_billing(api_format, input_tokens, cache_read_tokens) as u64
}
fn users_me_usage_effective_unix_secs(item: &StoredRequestUsageAudit) -> u64 {
item.finalized_at_unix_secs
.unwrap_or(item.created_at_unix_ms)
@@ -152,6 +175,7 @@ fn build_users_me_usage_record_payload(
"endpoint_api_format": item.endpoint_api_format,
"has_format_conversion": item.has_format_conversion,
"input_tokens": item.input_tokens,
"effective_input_tokens": users_me_usage_effective_input_tokens(item),
"output_tokens": item.output_tokens,
"total_tokens": item.total_tokens,
"cost": round_to(item.total_cost_usd, 6),
@@ -161,6 +185,8 @@ fn build_users_me_usage_record_payload(
"status": item.status,
"created_at": unix_secs_to_rfc3339(item.created_at_unix_ms),
"cache_creation_input_tokens": item.cache_creation_input_tokens,
"cache_creation_ephemeral_5m_input_tokens": item.cache_creation_ephemeral_5m_input_tokens,
"cache_creation_ephemeral_1h_input_tokens": item.cache_creation_ephemeral_1h_input_tokens,
"cache_read_input_tokens": item.cache_read_input_tokens,
"status_code": item.status_code,
"error_message": item.error_message,
@@ -186,8 +212,11 @@ fn build_users_me_usage_active_payload(item: &StoredRequestUsageAudit) -> serde_
"id": item.id,
"status": item.status,
"input_tokens": item.input_tokens,
"effective_input_tokens": users_me_usage_effective_input_tokens(item),
"output_tokens": item.output_tokens,
"cache_creation_input_tokens": item.cache_creation_input_tokens,
"cache_creation_ephemeral_5m_input_tokens": item.cache_creation_ephemeral_5m_input_tokens,
"cache_creation_ephemeral_1h_input_tokens": item.cache_creation_ephemeral_1h_input_tokens,
"cache_read_input_tokens": item.cache_read_input_tokens,
"cost": round_to(item.total_cost_usd, 6),
"actual_cost": round_to(item.actual_total_cost_usd, 6),
@@ -231,10 +260,13 @@ fn build_users_me_usage_summary_by_model(
"model": item.model,
"requests": 0_u64,
"input_tokens": 0_u64,
"effective_input_tokens": 0_u64,
"output_tokens": 0_u64,
"total_tokens": 0_u64,
"cache_read_tokens": 0_u64,
"cache_creation_tokens": 0_u64,
"cache_creation_ephemeral_5m_tokens": 0_u64,
"cache_creation_ephemeral_1h_tokens": 0_u64,
"total_input_context": 0_u64,
"cache_hit_rate": 0.0,
"total_cost_usd": 0.0,
@@ -245,6 +277,10 @@ fn build_users_me_usage_summary_by_model(
.as_u64()
.unwrap_or(0)
.saturating_add(item.input_tokens));
entry["effective_input_tokens"] = json!(entry["effective_input_tokens"]
.as_u64()
.unwrap_or(0)
.saturating_add(users_me_usage_effective_input_tokens(item)));
entry["output_tokens"] = json!(entry["output_tokens"]
.as_u64()
.unwrap_or(0)
@@ -260,7 +296,17 @@ fn build_users_me_usage_summary_by_model(
entry["cache_creation_tokens"] = json!(entry["cache_creation_tokens"]
.as_u64()
.unwrap_or(0)
.saturating_add(item.cache_creation_input_tokens));
.saturating_add(users_me_usage_cache_creation_tokens(item)));
entry["cache_creation_ephemeral_5m_tokens"] = json!(entry
["cache_creation_ephemeral_5m_tokens"]
.as_u64()
.unwrap_or(0)
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens));
entry["cache_creation_ephemeral_1h_tokens"] = json!(entry
["cache_creation_ephemeral_1h_tokens"]
.as_u64()
.unwrap_or(0)
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens));
entry["total_input_context"] = json!(entry["total_input_context"]
.as_u64()
.unwrap_or(0)
@@ -320,11 +366,14 @@ fn build_users_me_usage_summary_by_provider(
json!({
"provider": item.provider_name,
"requests": 0_u64,
"effective_input_tokens": 0_u64,
"total_tokens": 0_u64,
"total_input_context": 0_u64,
"output_tokens": 0_u64,
"cache_read_tokens": 0_u64,
"cache_creation_tokens": 0_u64,
"cache_creation_ephemeral_5m_tokens": 0_u64,
"cache_creation_ephemeral_1h_tokens": 0_u64,
"cache_hit_rate": 0.0,
"total_cost_usd": 0.0,
"success_rate": 0.0,
@@ -343,6 +392,10 @@ fn build_users_me_usage_summary_by_provider(
.as_u64()
.unwrap_or(0)
.saturating_add(users_me_usage_total_input_context(item)));
entry["effective_input_tokens"] = json!(entry["effective_input_tokens"]
.as_u64()
.unwrap_or(0)
.saturating_add(users_me_usage_effective_input_tokens(item)));
entry["output_tokens"] = json!(entry["output_tokens"]
.as_u64()
.unwrap_or(0)
@@ -354,7 +407,17 @@ fn build_users_me_usage_summary_by_provider(
entry["cache_creation_tokens"] = json!(entry["cache_creation_tokens"]
.as_u64()
.unwrap_or(0)
.saturating_add(item.cache_creation_input_tokens));
.saturating_add(users_me_usage_cache_creation_tokens(item)));
entry["cache_creation_ephemeral_5m_tokens"] = json!(entry
["cache_creation_ephemeral_5m_tokens"]
.as_u64()
.unwrap_or(0)
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens));
entry["cache_creation_ephemeral_1h_tokens"] = json!(entry
["cache_creation_ephemeral_1h_tokens"]
.as_u64()
.unwrap_or(0)
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens));
entry["total_cost_usd"] =
json!(entry["total_cost_usd"].as_f64().unwrap_or(0.0) + item.total_cost_usd);
@@ -445,10 +508,13 @@ fn build_users_me_usage_summary_by_api_format(
"api_format": api_format,
"request_count": 0_u64,
"total_tokens": 0_u64,
"effective_input_tokens": 0_u64,
"total_input_context": 0_u64,
"output_tokens": 0_u64,
"cache_read_tokens": 0_u64,
"cache_creation_tokens": 0_u64,
"cache_creation_ephemeral_5m_tokens": 0_u64,
"cache_creation_ephemeral_1h_tokens": 0_u64,
"cache_hit_rate": 0.0,
"total_cost_usd": 0.0,
"avg_response_time_ms": 0.0,
@@ -468,6 +534,10 @@ fn build_users_me_usage_summary_by_api_format(
.as_u64()
.unwrap_or(0)
.saturating_add(users_me_usage_total_input_context(item)));
entry["effective_input_tokens"] = json!(entry["effective_input_tokens"]
.as_u64()
.unwrap_or(0)
.saturating_add(users_me_usage_effective_input_tokens(item)));
entry["output_tokens"] = json!(entry["output_tokens"]
.as_u64()
.unwrap_or(0)
@@ -479,7 +549,17 @@ fn build_users_me_usage_summary_by_api_format(
entry["cache_creation_tokens"] = json!(entry["cache_creation_tokens"]
.as_u64()
.unwrap_or(0)
.saturating_add(item.cache_creation_input_tokens));
.saturating_add(users_me_usage_cache_creation_tokens(item)));
entry["cache_creation_ephemeral_5m_tokens"] = json!(entry
["cache_creation_ephemeral_5m_tokens"]
.as_u64()
.unwrap_or(0)
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens));
entry["cache_creation_ephemeral_1h_tokens"] = json!(entry
["cache_creation_ephemeral_1h_tokens"]
.as_u64()
.unwrap_or(0)
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens));
entry["total_cost_usd"] =
json!(entry["total_cost_usd"].as_f64().unwrap_or(0.0) + item.total_cost_usd);
if let Some(response_time_ms) = item.response_time_ms {