mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
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:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user