mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(usage): 修复缓存创建 token 展示并在总量中计入缓存组件
- gateway/admin 用量载荷在 cache_creation_input_tokens 为 0 时回填 ephemeral_5m/1h 合计 - 标准化用量写入时将 cache_creation/cache_read token 计入 total_tokens - 前端列表与轮询同步新增分类字段,修复缓存 token 列显示
This commit is contained in:
@@ -200,6 +200,7 @@ fn build_users_me_usage_record_payload(
|
|||||||
let output_price_per_1m = item.settlement_output_price_per_1m();
|
let output_price_per_1m = item.settlement_output_price_per_1m();
|
||||||
let cache_creation_price_per_1m = item.settlement_cache_creation_price_per_1m();
|
let cache_creation_price_per_1m = item.settlement_cache_creation_price_per_1m();
|
||||||
let cache_read_price_per_1m = item.settlement_cache_read_price_per_1m();
|
let cache_read_price_per_1m = item.settlement_cache_read_price_per_1m();
|
||||||
|
let cache_creation_input_tokens = users_me_usage_cache_creation_tokens(item);
|
||||||
let rate_multiplier = item.settlement_rate_multiplier();
|
let rate_multiplier = item.settlement_rate_multiplier();
|
||||||
let mut payload = json!({
|
let mut payload = json!({
|
||||||
"id": item.id,
|
"id": item.id,
|
||||||
@@ -219,7 +220,7 @@ fn build_users_me_usage_record_payload(
|
|||||||
"status": item.status,
|
"status": item.status,
|
||||||
"has_fallback": item.has_fallback(),
|
"has_fallback": item.has_fallback(),
|
||||||
"created_at": unix_secs_to_rfc3339(item.created_at_unix_ms),
|
"created_at": unix_secs_to_rfc3339(item.created_at_unix_ms),
|
||||||
"cache_creation_input_tokens": item.cache_creation_input_tokens,
|
"cache_creation_input_tokens": cache_creation_input_tokens,
|
||||||
"cache_creation_ephemeral_5m_input_tokens": item.cache_creation_ephemeral_5m_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_creation_ephemeral_1h_input_tokens": item.cache_creation_ephemeral_1h_input_tokens,
|
||||||
"cache_read_input_tokens": item.cache_read_input_tokens,
|
"cache_read_input_tokens": item.cache_read_input_tokens,
|
||||||
@@ -247,13 +248,14 @@ fn build_users_me_usage_record_payload(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_users_me_usage_active_payload(item: &StoredRequestUsageAudit) -> serde_json::Value {
|
fn build_users_me_usage_active_payload(item: &StoredRequestUsageAudit) -> serde_json::Value {
|
||||||
|
let cache_creation_input_tokens = users_me_usage_cache_creation_tokens(item);
|
||||||
let mut payload = json!({
|
let mut payload = json!({
|
||||||
"id": item.id,
|
"id": item.id,
|
||||||
"status": item.status,
|
"status": item.status,
|
||||||
"input_tokens": item.input_tokens,
|
"input_tokens": item.input_tokens,
|
||||||
"effective_input_tokens": users_me_usage_effective_input_tokens(item),
|
"effective_input_tokens": users_me_usage_effective_input_tokens(item),
|
||||||
"output_tokens": item.output_tokens,
|
"output_tokens": item.output_tokens,
|
||||||
"cache_creation_input_tokens": item.cache_creation_input_tokens,
|
"cache_creation_input_tokens": cache_creation_input_tokens,
|
||||||
"cache_creation_ephemeral_5m_input_tokens": item.cache_creation_ephemeral_5m_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_creation_ephemeral_1h_input_tokens": item.cache_creation_ephemeral_1h_input_tokens,
|
||||||
"cache_read_input_tokens": item.cache_read_input_tokens,
|
"cache_read_input_tokens": item.cache_read_input_tokens,
|
||||||
@@ -1022,3 +1024,86 @@ pub(super) async fn handle_users_me_usage_heatmap_get(
|
|||||||
}))
|
}))
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use aether_data_contracts::repository::usage::StoredRequestUsageAudit;
|
||||||
|
|
||||||
|
use super::{build_users_me_usage_active_payload, build_users_me_usage_record_payload};
|
||||||
|
|
||||||
|
fn sample_usage(status: &str) -> StoredRequestUsageAudit {
|
||||||
|
StoredRequestUsageAudit::new(
|
||||||
|
"usage-1".to_string(),
|
||||||
|
"req-1".to_string(),
|
||||||
|
Some("user-1".to_string()),
|
||||||
|
Some("api-key-1".to_string()),
|
||||||
|
Some("alice".to_string()),
|
||||||
|
Some("default".to_string()),
|
||||||
|
"OpenAI".to_string(),
|
||||||
|
"gpt-5".to_string(),
|
||||||
|
None,
|
||||||
|
Some("provider-1".to_string()),
|
||||||
|
Some("endpoint-1".to_string()),
|
||||||
|
Some("provider-key-1".to_string()),
|
||||||
|
Some("chat".to_string()),
|
||||||
|
Some("openai:chat".to_string()),
|
||||||
|
Some("openai".to_string()),
|
||||||
|
Some("chat".to_string()),
|
||||||
|
Some("openai:chat".to_string()),
|
||||||
|
Some("openai".to_string()),
|
||||||
|
Some("chat".to_string()),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
30,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
Some(200),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(120),
|
||||||
|
None,
|
||||||
|
status.to_string(),
|
||||||
|
"settled".to_string(),
|
||||||
|
100,
|
||||||
|
101,
|
||||||
|
Some(102),
|
||||||
|
)
|
||||||
|
.expect("usage should build")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_usage_record_payload_rehydrates_cache_creation_total_from_classified_fields() {
|
||||||
|
let item = StoredRequestUsageAudit {
|
||||||
|
cache_creation_input_tokens: 0,
|
||||||
|
cache_creation_ephemeral_5m_input_tokens: 9,
|
||||||
|
cache_creation_ephemeral_1h_input_tokens: 11,
|
||||||
|
..sample_usage("completed")
|
||||||
|
};
|
||||||
|
|
||||||
|
let payload = build_users_me_usage_record_payload(&item, false, &BTreeMap::new(), false);
|
||||||
|
|
||||||
|
assert_eq!(payload["cache_creation_input_tokens"], 20);
|
||||||
|
assert_eq!(payload["cache_creation_ephemeral_5m_input_tokens"], 9);
|
||||||
|
assert_eq!(payload["cache_creation_ephemeral_1h_input_tokens"], 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_usage_active_payload_rehydrates_cache_creation_total_from_classified_fields() {
|
||||||
|
let item = StoredRequestUsageAudit {
|
||||||
|
cache_creation_input_tokens: 0,
|
||||||
|
cache_creation_ephemeral_5m_input_tokens: 4,
|
||||||
|
cache_creation_ephemeral_1h_input_tokens: 6,
|
||||||
|
..sample_usage("streaming")
|
||||||
|
};
|
||||||
|
|
||||||
|
let payload = build_users_me_usage_active_payload(&item);
|
||||||
|
|
||||||
|
assert_eq!(payload["cache_creation_input_tokens"], 10);
|
||||||
|
assert_eq!(payload["cache_creation_ephemeral_5m_input_tokens"], 4);
|
||||||
|
assert_eq!(payload["cache_creation_ephemeral_1h_input_tokens"], 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -457,6 +457,7 @@ pub fn admin_usage_record_json(
|
|||||||
let output_price_per_1m = item.settlement_output_price_per_1m();
|
let output_price_per_1m = item.settlement_output_price_per_1m();
|
||||||
let cache_creation_price_per_1m = item.settlement_cache_creation_price_per_1m();
|
let cache_creation_price_per_1m = item.settlement_cache_creation_price_per_1m();
|
||||||
let cache_read_price_per_1m = item.settlement_cache_read_price_per_1m();
|
let cache_read_price_per_1m = item.settlement_cache_read_price_per_1m();
|
||||||
|
let cache_creation_input_tokens = admin_usage_cache_creation_tokens(item);
|
||||||
let is_free_tier = item.settlement_is_free_tier();
|
let is_free_tier = item.settlement_is_free_tier();
|
||||||
let user = item
|
let user = item
|
||||||
.user_id
|
.user_id
|
||||||
@@ -485,7 +486,7 @@ pub fn admin_usage_record_json(
|
|||||||
"input_tokens": item.input_tokens,
|
"input_tokens": item.input_tokens,
|
||||||
"effective_input_tokens": admin_usage_effective_input_tokens(item),
|
"effective_input_tokens": admin_usage_effective_input_tokens(item),
|
||||||
"output_tokens": item.output_tokens,
|
"output_tokens": item.output_tokens,
|
||||||
"cache_creation_input_tokens": item.cache_creation_input_tokens,
|
"cache_creation_input_tokens": cache_creation_input_tokens,
|
||||||
"cache_creation_ephemeral_5m_input_tokens": item.cache_creation_ephemeral_5m_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_creation_ephemeral_1h_input_tokens": item.cache_creation_ephemeral_1h_input_tokens,
|
||||||
"cache_read_input_tokens": item.cache_read_input_tokens,
|
"cache_read_input_tokens": item.cache_read_input_tokens,
|
||||||
@@ -1418,13 +1419,14 @@ pub fn build_admin_usage_active_requests_response(
|
|||||||
let provider_key_name = admin_usage_provider_key_name(item, provider_key_names);
|
let provider_key_name = admin_usage_provider_key_name(item, provider_key_names);
|
||||||
let api_key_name =
|
let api_key_name =
|
||||||
admin_usage_api_key_name(item, api_key_names, auth_api_key_reader_available);
|
admin_usage_api_key_name(item, api_key_names, auth_api_key_reader_available);
|
||||||
|
let cache_creation_input_tokens = admin_usage_cache_creation_tokens(item);
|
||||||
let mut value = json!({
|
let mut value = json!({
|
||||||
"id": item.id,
|
"id": item.id,
|
||||||
"status": item.status,
|
"status": item.status,
|
||||||
"input_tokens": item.input_tokens,
|
"input_tokens": item.input_tokens,
|
||||||
"effective_input_tokens": admin_usage_effective_input_tokens(item),
|
"effective_input_tokens": admin_usage_effective_input_tokens(item),
|
||||||
"output_tokens": item.output_tokens,
|
"output_tokens": item.output_tokens,
|
||||||
"cache_creation_input_tokens": item.cache_creation_input_tokens,
|
"cache_creation_input_tokens": cache_creation_input_tokens,
|
||||||
"cache_creation_ephemeral_5m_input_tokens": item.cache_creation_ephemeral_5m_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_creation_ephemeral_1h_input_tokens": item.cache_creation_ephemeral_1h_input_tokens,
|
||||||
"cache_read_input_tokens": item.cache_read_input_tokens,
|
"cache_read_input_tokens": item.cache_read_input_tokens,
|
||||||
@@ -1923,6 +1925,30 @@ mod tests {
|
|||||||
assert_eq!(payload["cache_read_price_per_1m"], 0.30);
|
assert_eq!(payload["cache_read_price_per_1m"], 0.30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn admin_usage_record_rehydrates_cache_creation_total_from_classified_fields() {
|
||||||
|
let item = StoredRequestUsageAudit {
|
||||||
|
cache_creation_input_tokens: 0,
|
||||||
|
cache_creation_ephemeral_5m_input_tokens: 12,
|
||||||
|
cache_creation_ephemeral_1h_input_tokens: 8,
|
||||||
|
..sample_usage("completed", Some(200), None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let payload = admin_usage_record_json(
|
||||||
|
&item,
|
||||||
|
&BTreeMap::new(),
|
||||||
|
&BTreeMap::new(),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
Some("primary"),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(payload["cache_creation_input_tokens"], 20);
|
||||||
|
assert_eq!(payload["cache_creation_ephemeral_5m_input_tokens"], 12);
|
||||||
|
assert_eq!(payload["cache_creation_ephemeral_1h_input_tokens"], 8);
|
||||||
|
assert_eq!(payload["total_tokens"], 50);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detail_payload_exposes_typed_settlement_section() {
|
fn detail_payload_exposes_typed_settlement_section() {
|
||||||
let item = StoredRequestUsageAudit {
|
let item = StoredRequestUsageAudit {
|
||||||
|
|||||||
@@ -1366,10 +1366,7 @@ fn apply_standardized_usage(
|
|||||||
if usage.cache_read_tokens > 0 {
|
if usage.cache_read_tokens > 0 {
|
||||||
data.cache_read_input_tokens = Some(usage.cache_read_tokens as u64);
|
data.cache_read_input_tokens = Some(usage.cache_read_tokens as u64);
|
||||||
}
|
}
|
||||||
let total_tokens = usage
|
let total_tokens = standardized_usage_total_tokens(&usage);
|
||||||
.input_tokens
|
|
||||||
.saturating_add(usage.output_tokens)
|
|
||||||
.max(0) as u64;
|
|
||||||
if total_tokens > 0 {
|
if total_tokens > 0 {
|
||||||
data.total_tokens = Some(total_tokens);
|
data.total_tokens = Some(total_tokens);
|
||||||
}
|
}
|
||||||
@@ -1396,15 +1393,23 @@ fn apply_standardized_usage_seed(usage: &StandardizedUsage, data: &mut UsageEven
|
|||||||
if usage.cache_read_tokens > 0 {
|
if usage.cache_read_tokens > 0 {
|
||||||
data.cache_read_input_tokens = Some(usage.cache_read_tokens as u64);
|
data.cache_read_input_tokens = Some(usage.cache_read_tokens as u64);
|
||||||
}
|
}
|
||||||
let total_tokens = usage
|
let total_tokens = standardized_usage_total_tokens(usage);
|
||||||
.input_tokens
|
|
||||||
.saturating_add(usage.output_tokens)
|
|
||||||
.max(0) as u64;
|
|
||||||
if total_tokens > 0 {
|
if total_tokens > 0 {
|
||||||
data.total_tokens = Some(total_tokens);
|
data.total_tokens = Some(total_tokens);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn standardized_usage_total_tokens(usage: &StandardizedUsage) -> u64 {
|
||||||
|
positive_usage_component(usage.input_tokens)
|
||||||
|
.saturating_add(positive_usage_component(usage.output_tokens))
|
||||||
|
.saturating_add(positive_usage_component(usage.cache_creation_tokens))
|
||||||
|
.saturating_add(positive_usage_component(usage.cache_read_tokens))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn positive_usage_component(value: i64) -> u64 {
|
||||||
|
value.max(0) as u64
|
||||||
|
}
|
||||||
|
|
||||||
fn headers_to_json(headers: &BTreeMap<String, String>) -> Value {
|
fn headers_to_json(headers: &BTreeMap<String, String>) -> Value {
|
||||||
Value::Object(Map::from_iter(headers.iter().map(|(key, value)| {
|
Value::Object(Map::from_iter(headers.iter().map(|(key, value)| {
|
||||||
(key.clone(), Value::String(mask_header_value(key, value)))
|
(key.clone(), Value::String(mask_header_value(key, value)))
|
||||||
@@ -1661,10 +1666,20 @@ fn extract_token_counts_from_json(value: &Value) -> Option<(u64, u64, u64)> {
|
|||||||
.or_else(|| usage.get("completion_tokens"))
|
.or_else(|| usage.get("completion_tokens"))
|
||||||
.and_then(Value::as_u64)
|
.and_then(Value::as_u64)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let total = usage
|
let raw_total = usage
|
||||||
.get("total_tokens")
|
.get("total_tokens")
|
||||||
.and_then(Value::as_u64)
|
.and_then(Value::as_u64)
|
||||||
.unwrap_or(input + output);
|
.unwrap_or(input + output);
|
||||||
|
let cache_creation = extract_cache_creation_tokens_from_usage_object(usage);
|
||||||
|
let cache_read = extract_cache_read_tokens_from_usage_object(usage);
|
||||||
|
let total = if cache_creation > 0 || cache_read > 0 {
|
||||||
|
input
|
||||||
|
.saturating_add(output)
|
||||||
|
.saturating_add(cache_creation)
|
||||||
|
.saturating_add(cache_read)
|
||||||
|
} else {
|
||||||
|
raw_total
|
||||||
|
};
|
||||||
return Some((input, output, total));
|
return Some((input, output, total));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1677,10 +1692,19 @@ fn extract_token_counts_from_json(value: &Value) -> Option<(u64, u64, u64)> {
|
|||||||
.get("candidatesTokenCount")
|
.get("candidatesTokenCount")
|
||||||
.and_then(Value::as_u64)
|
.and_then(Value::as_u64)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let total = usage
|
let raw_total = usage
|
||||||
.get("totalTokenCount")
|
.get("totalTokenCount")
|
||||||
.and_then(Value::as_u64)
|
.and_then(Value::as_u64)
|
||||||
.unwrap_or(input + output);
|
.unwrap_or(input + output);
|
||||||
|
let cache_read = usage
|
||||||
|
.get("cachedContentTokenCount")
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let total = if cache_read > 0 {
|
||||||
|
input.saturating_add(output).saturating_add(cache_read)
|
||||||
|
} else {
|
||||||
|
raw_total
|
||||||
|
};
|
||||||
return Some((input, output, total));
|
return Some((input, output, total));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1703,6 +1727,59 @@ fn extract_token_counts_from_json(value: &Value) -> Option<(u64, u64, u64)> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_cache_creation_tokens_from_usage_object(usage: &serde_json::Map<String, Value>) -> u64 {
|
||||||
|
let direct = usage
|
||||||
|
.get("cache_creation_input_tokens")
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.unwrap_or_default();
|
||||||
|
if direct > 0 {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
|
||||||
|
let breakdown = usage
|
||||||
|
.get("cache_creation")
|
||||||
|
.and_then(Value::as_object)
|
||||||
|
.map(|cache_creation| {
|
||||||
|
cache_creation
|
||||||
|
.get("ephemeral_5m_input_tokens")
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.saturating_add(
|
||||||
|
cache_creation
|
||||||
|
.get("ephemeral_1h_input_tokens")
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
if breakdown > 0 {
|
||||||
|
return breakdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
usage
|
||||||
|
.get("input_tokens_details")
|
||||||
|
.or_else(|| usage.get("prompt_tokens_details"))
|
||||||
|
.and_then(Value::as_object)
|
||||||
|
.and_then(|details| details.get("cached_creation_tokens"))
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_cache_read_tokens_from_usage_object(usage: &serde_json::Map<String, Value>) -> u64 {
|
||||||
|
usage
|
||||||
|
.get("cache_read_input_tokens")
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.or_else(|| {
|
||||||
|
usage
|
||||||
|
.get("input_tokens_details")
|
||||||
|
.or_else(|| usage.get("prompt_tokens_details"))
|
||||||
|
.and_then(Value::as_object)
|
||||||
|
.and_then(|details| details.get("cached_tokens"))
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
fn empty_to_none(value: Option<String>) -> Option<String> {
|
fn empty_to_none(value: Option<String>) -> Option<String> {
|
||||||
value
|
value
|
||||||
.map(|value| value.trim().to_string())
|
.map(|value| value.trim().to_string())
|
||||||
@@ -1746,6 +1823,39 @@ mod tests {
|
|||||||
assert_eq!(tokens, (3, 5, 8));
|
assert_eq!(tokens, (3, 5, 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extracts_openai_usage_tokens_with_cache_components() {
|
||||||
|
let tokens = extract_token_counts_from_json(&json!({
|
||||||
|
"usage": {
|
||||||
|
"input_tokens": 3,
|
||||||
|
"output_tokens": 5,
|
||||||
|
"total_tokens": 8,
|
||||||
|
"input_tokens_details": {
|
||||||
|
"cached_tokens": 2,
|
||||||
|
"cached_creation_tokens": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.expect("tokens should exist");
|
||||||
|
|
||||||
|
assert_eq!(tokens, (3, 5, 11));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extracts_claude_usage_tokens_with_cache_components() {
|
||||||
|
let tokens = extract_token_counts_from_json(&json!({
|
||||||
|
"usage": {
|
||||||
|
"input_tokens": 6,
|
||||||
|
"output_tokens": 20,
|
||||||
|
"cache_creation_input_tokens": 41857,
|
||||||
|
"cache_read_input_tokens": 0
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.expect("tokens should exist");
|
||||||
|
|
||||||
|
assert_eq!(tokens, (6, 20, 41883));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builds_upsert_record_from_terminal_event() {
|
fn builds_upsert_record_from_terminal_event() {
|
||||||
let record = build_upsert_usage_record_from_event(&UsageEvent {
|
let record = build_upsert_usage_record_from_event(&UsageEvent {
|
||||||
@@ -2117,7 +2227,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(event.data.input_tokens, Some(3));
|
assert_eq!(event.data.input_tokens, Some(3));
|
||||||
assert_eq!(event.data.output_tokens, Some(5));
|
assert_eq!(event.data.output_tokens, Some(5));
|
||||||
assert_eq!(event.data.total_tokens, Some(8));
|
assert_eq!(event.data.total_tokens, Some(11));
|
||||||
assert_eq!(event.data.cache_creation_input_tokens, Some(1));
|
assert_eq!(event.data.cache_creation_input_tokens, Some(1));
|
||||||
assert_eq!(event.data.cache_read_input_tokens, Some(2));
|
assert_eq!(event.data.cache_read_input_tokens, Some(2));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2531,7 +2641,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(event.data.input_tokens, Some(3));
|
assert_eq!(event.data.input_tokens, Some(3));
|
||||||
assert_eq!(event.data.output_tokens, Some(5));
|
assert_eq!(event.data.output_tokens, Some(5));
|
||||||
assert_eq!(event.data.total_tokens, Some(8));
|
assert_eq!(event.data.total_tokens, Some(11));
|
||||||
assert_eq!(event.data.cache_creation_input_tokens, Some(1));
|
assert_eq!(event.data.cache_creation_input_tokens, Some(1));
|
||||||
assert_eq!(event.data.cache_read_input_tokens, Some(2));
|
assert_eq!(event.data.cache_read_input_tokens, Some(2));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ export interface UsageRecordDetail {
|
|||||||
is_stream: boolean
|
is_stream: boolean
|
||||||
created_at: string
|
created_at: string
|
||||||
cache_creation_input_tokens?: number
|
cache_creation_input_tokens?: number
|
||||||
|
cache_creation_ephemeral_5m_input_tokens?: number
|
||||||
|
cache_creation_ephemeral_1h_input_tokens?: number
|
||||||
cache_read_input_tokens?: number
|
cache_read_input_tokens?: number
|
||||||
status_code: number
|
status_code: number
|
||||||
error_message?: string
|
error_message?: string
|
||||||
@@ -284,8 +286,11 @@ export const meApi = {
|
|||||||
id: string
|
id: string
|
||||||
status: 'pending' | 'streaming' | 'completed' | 'failed' | 'cancelled'
|
status: 'pending' | 'streaming' | 'completed' | 'failed' | 'cancelled'
|
||||||
input_tokens: number
|
input_tokens: number
|
||||||
|
effective_input_tokens?: number | null
|
||||||
output_tokens: number
|
output_tokens: number
|
||||||
cache_creation_input_tokens?: number | null
|
cache_creation_input_tokens?: number | null
|
||||||
|
cache_creation_ephemeral_5m_input_tokens?: number | null
|
||||||
|
cache_creation_ephemeral_1h_input_tokens?: number | null
|
||||||
cache_read_input_tokens?: number | null
|
cache_read_input_tokens?: number | null
|
||||||
cost: number
|
cost: number
|
||||||
actual_cost?: number | null
|
actual_cost?: number | null
|
||||||
@@ -296,6 +301,7 @@ export const meApi = {
|
|||||||
endpoint_api_format?: string | null
|
endpoint_api_format?: string | null
|
||||||
has_format_conversion?: boolean | null
|
has_format_conversion?: boolean | null
|
||||||
has_fallback?: boolean | null
|
has_fallback?: boolean | null
|
||||||
|
target_model?: string | null
|
||||||
}>
|
}>
|
||||||
}> {
|
}> {
|
||||||
const params = ids ? { ids } : {}
|
const params = ids ? { ids } : {}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export interface UsageRecord {
|
|||||||
effective_input_tokens?: number
|
effective_input_tokens?: number
|
||||||
output_tokens: number
|
output_tokens: number
|
||||||
cache_creation_input_tokens?: number
|
cache_creation_input_tokens?: number
|
||||||
|
cache_creation_ephemeral_5m_input_tokens?: number
|
||||||
|
cache_creation_ephemeral_1h_input_tokens?: number
|
||||||
cache_read_input_tokens?: number
|
cache_read_input_tokens?: number
|
||||||
total_tokens: number
|
total_tokens: number
|
||||||
cost?: number
|
cost?: number
|
||||||
@@ -295,6 +297,8 @@ export const usageApi = {
|
|||||||
effective_input_tokens?: number | null
|
effective_input_tokens?: number | null
|
||||||
output_tokens: number
|
output_tokens: number
|
||||||
cache_creation_input_tokens?: number | null
|
cache_creation_input_tokens?: number | null
|
||||||
|
cache_creation_ephemeral_5m_input_tokens?: number | null
|
||||||
|
cache_creation_ephemeral_1h_input_tokens?: number | null
|
||||||
cache_read_input_tokens?: number | null
|
cache_read_input_tokens?: number | null
|
||||||
cost: number
|
cost: number
|
||||||
actual_cost?: number | null
|
actual_cost?: number | null
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { getCacheCreationTokens, getEffectiveInputTokens } from '../token-normalization'
|
||||||
|
|
||||||
|
describe('usage token normalization', () => {
|
||||||
|
it('prefers explicit cache creation totals when present', () => {
|
||||||
|
expect(getCacheCreationTokens({
|
||||||
|
cache_creation_input_tokens: 18,
|
||||||
|
cache_creation_ephemeral_5m_input_tokens: 5,
|
||||||
|
cache_creation_ephemeral_1h_input_tokens: 7,
|
||||||
|
})).toBe(18)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rehydrates cache creation totals from classified fields when legacy total is zero', () => {
|
||||||
|
expect(getCacheCreationTokens({
|
||||||
|
cache_creation_input_tokens: 0,
|
||||||
|
cache_creation_ephemeral_5m_input_tokens: 12,
|
||||||
|
cache_creation_ephemeral_1h_input_tokens: 8,
|
||||||
|
})).toBe(20)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps effective input token normalization unchanged', () => {
|
||||||
|
expect(getEffectiveInputTokens({
|
||||||
|
input_tokens: 100,
|
||||||
|
cache_read_input_tokens: 20,
|
||||||
|
api_format: 'openai:chat',
|
||||||
|
})).toBe(80)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -557,9 +557,9 @@
|
|||||||
<span>{{ formatTokens(record.output_tokens || 0) }}</span>
|
<span>{{ formatTokens(record.output_tokens || 0) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 text-muted-foreground">
|
<div class="flex items-center gap-1 text-muted-foreground">
|
||||||
<span :class="record.cache_creation_input_tokens ? 'text-foreground/70' : ''">{{ record.cache_creation_input_tokens ? formatTokens(record.cache_creation_input_tokens) : '-' }}</span>
|
<span :class="hasPositiveTokens(getRecordCacheCreationTokens(record)) ? 'text-foreground/70' : ''">{{ formatOptionalTokens(getRecordCacheCreationTokens(record)) }}</span>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<span :class="record.cache_read_input_tokens ? 'text-foreground/70' : ''">{{ record.cache_read_input_tokens ? formatTokens(record.cache_read_input_tokens) : '-' }}</span>
|
<span :class="hasPositiveTokens(record.cache_read_input_tokens) ? 'text-foreground/70' : ''">{{ formatOptionalTokens(record.cache_read_input_tokens) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -677,7 +677,7 @@ import {
|
|||||||
import { RefreshCcw, Search } from 'lucide-vue-next'
|
import { RefreshCcw, Search } from 'lucide-vue-next'
|
||||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||||
import { formatDateTime } from '../composables'
|
import { formatDateTime } from '../composables'
|
||||||
import { getEffectiveInputTokens } from '../token-normalization'
|
import { getCacheCreationTokens, getEffectiveInputTokens } from '../token-normalization'
|
||||||
import { isUsageRecordFailed, resolveDisplayRequestStatus } from '../utils/status'
|
import { isUsageRecordFailed, resolveDisplayRequestStatus } from '../utils/status'
|
||||||
import { useRowClick } from '@/composables/useRowClick'
|
import { useRowClick } from '@/composables/useRowClick'
|
||||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
@@ -787,6 +787,18 @@ function getRecordEffectiveInputTokens(record: UsageRecord): number {
|
|||||||
return getEffectiveInputTokens(record)
|
return getEffectiveInputTokens(record)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRecordCacheCreationTokens(record: UsageRecord): number {
|
||||||
|
return getCacheCreationTokens(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPositiveTokens(value: number | null | undefined): boolean {
|
||||||
|
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatOptionalTokens(value: number | null | undefined): string {
|
||||||
|
return hasPositiveTokens(value) ? formatTokens(value) : '-'
|
||||||
|
}
|
||||||
|
|
||||||
// useDebounceFn 自动处理清理,无需 onUnmounted
|
// useDebounceFn 自动处理清理,无需 onUnmounted
|
||||||
|
|
||||||
// 判断是否应该显示格式转换信息
|
// 判断是否应该显示格式转换信息
|
||||||
|
|||||||
@@ -393,6 +393,12 @@ export function useUsageData(options: UseUsageDataOptions) {
|
|||||||
effective_input_tokens: existing.effective_input_tokens ?? record.effective_input_tokens,
|
effective_input_tokens: existing.effective_input_tokens ?? record.effective_input_tokens,
|
||||||
output_tokens: existing.output_tokens || record.output_tokens,
|
output_tokens: existing.output_tokens || record.output_tokens,
|
||||||
cache_creation_input_tokens: existing.cache_creation_input_tokens ?? record.cache_creation_input_tokens,
|
cache_creation_input_tokens: existing.cache_creation_input_tokens ?? record.cache_creation_input_tokens,
|
||||||
|
cache_creation_ephemeral_5m_input_tokens:
|
||||||
|
existing.cache_creation_ephemeral_5m_input_tokens
|
||||||
|
?? record.cache_creation_ephemeral_5m_input_tokens,
|
||||||
|
cache_creation_ephemeral_1h_input_tokens:
|
||||||
|
existing.cache_creation_ephemeral_1h_input_tokens
|
||||||
|
?? record.cache_creation_ephemeral_1h_input_tokens,
|
||||||
cache_read_input_tokens: existing.cache_read_input_tokens ?? record.cache_read_input_tokens,
|
cache_read_input_tokens: existing.cache_read_input_tokens ?? record.cache_read_input_tokens,
|
||||||
cost: existing.cost || record.cost,
|
cost: existing.cost || record.cost,
|
||||||
actual_cost: existing.actual_cost ?? record.actual_cost,
|
actual_cost: existing.actual_cost ?? record.actual_cost,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
type UsageTokenLike = {
|
type UsageTokenLike = {
|
||||||
effective_input_tokens?: number | null
|
effective_input_tokens?: number | null
|
||||||
input_tokens?: number | null
|
input_tokens?: number | null
|
||||||
|
cache_creation_input_tokens?: number | null
|
||||||
|
cache_creation_ephemeral_5m_input_tokens?: number | null
|
||||||
|
cache_creation_ephemeral_1h_input_tokens?: number | null
|
||||||
cache_read_input_tokens?: number | null
|
cache_read_input_tokens?: number | null
|
||||||
api_format?: string | null
|
api_format?: string | null
|
||||||
endpoint_api_format?: string | null
|
endpoint_api_format?: string | null
|
||||||
@@ -17,6 +20,16 @@ function apiFamily(apiFormat: string | null | undefined): string {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCacheCreationTokens(usage: UsageTokenLike): number {
|
||||||
|
const explicit = toNonNegativeNumber(usage.cache_creation_input_tokens)
|
||||||
|
const classified = toNonNegativeNumber(usage.cache_creation_ephemeral_5m_input_tokens)
|
||||||
|
+ toNonNegativeNumber(usage.cache_creation_ephemeral_1h_input_tokens)
|
||||||
|
if (explicit === 0 && classified > 0) {
|
||||||
|
return classified
|
||||||
|
}
|
||||||
|
return explicit
|
||||||
|
}
|
||||||
|
|
||||||
export function getEffectiveInputTokens(usage: UsageTokenLike): number {
|
export function getEffectiveInputTokens(usage: UsageTokenLike): number {
|
||||||
const explicit = toNonNegativeNumber(usage.effective_input_tokens)
|
const explicit = toNonNegativeNumber(usage.effective_input_tokens)
|
||||||
if (explicit > 0) {
|
if (explicit > 0) {
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ export interface UsageRecord {
|
|||||||
effective_input_tokens?: number
|
effective_input_tokens?: number
|
||||||
output_tokens: number
|
output_tokens: number
|
||||||
cache_creation_input_tokens?: number
|
cache_creation_input_tokens?: number
|
||||||
|
cache_creation_ephemeral_5m_input_tokens?: number
|
||||||
|
cache_creation_ephemeral_1h_input_tokens?: number
|
||||||
cache_read_input_tokens?: number
|
cache_read_input_tokens?: number
|
||||||
total_tokens: number
|
total_tokens: number
|
||||||
cost: number
|
cost: number
|
||||||
|
|||||||
@@ -391,6 +391,10 @@ async function pollActiveRequests() {
|
|||||||
record.effective_input_tokens = update.effective_input_tokens ?? record.effective_input_tokens
|
record.effective_input_tokens = update.effective_input_tokens ?? record.effective_input_tokens
|
||||||
record.output_tokens = update.output_tokens
|
record.output_tokens = update.output_tokens
|
||||||
record.cache_creation_input_tokens = update.cache_creation_input_tokens ?? undefined
|
record.cache_creation_input_tokens = update.cache_creation_input_tokens ?? undefined
|
||||||
|
record.cache_creation_ephemeral_5m_input_tokens =
|
||||||
|
update.cache_creation_ephemeral_5m_input_tokens ?? undefined
|
||||||
|
record.cache_creation_ephemeral_1h_input_tokens =
|
||||||
|
update.cache_creation_ephemeral_1h_input_tokens ?? undefined
|
||||||
record.cache_read_input_tokens = update.cache_read_input_tokens ?? undefined
|
record.cache_read_input_tokens = update.cache_read_input_tokens ?? undefined
|
||||||
record.cost = update.cost
|
record.cost = update.cost
|
||||||
record.actual_cost = update.actual_cost ?? undefined
|
record.actual_cost = update.actual_cost ?? undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user