|
|
|
@@ -137,6 +137,9 @@ pub struct RequestLogDetail {
|
|
|
|
|
pub output_tokens: u32,
|
|
|
|
|
pub cache_read_tokens: u32,
|
|
|
|
|
pub cache_creation_tokens: u32,
|
|
|
|
|
/// Internal TTL pricing bucket; aggregate UI metrics remain unchanged.
|
|
|
|
|
#[serde(skip)]
|
|
|
|
|
pub cache_creation_1h_tokens: u32,
|
|
|
|
|
/// Internal storage semantics; omitted from the UI/API payload.
|
|
|
|
|
#[serde(skip)]
|
|
|
|
|
pub input_token_semantics: i64,
|
|
|
|
@@ -159,12 +162,12 @@ pub struct RequestLogDetail {
|
|
|
|
|
pub pricing_model: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 把 26 列的查询结果映射为 `RequestLogDetail`。
|
|
|
|
|
/// 把 27 列的查询结果映射为 `RequestLogDetail`。
|
|
|
|
|
///
|
|
|
|
|
/// 调用方的 SELECT **必须**按以下顺序返回 26 列:
|
|
|
|
|
/// 调用方的 SELECT **必须**按以下顺序返回 27 列:
|
|
|
|
|
/// `request_id, provider_id, provider_name, app_type, model, request_model,
|
|
|
|
|
/// cost_multiplier, input_tokens, output_tokens, cache_read_tokens,
|
|
|
|
|
/// cache_creation_tokens, input_cost_usd, output_cost_usd, cache_read_cost_usd,
|
|
|
|
|
/// cache_creation_tokens, cache_creation_1h_tokens, input_cost_usd, output_cost_usd, cache_read_cost_usd,
|
|
|
|
|
/// cache_creation_cost_usd, total_cost_usd, is_streaming, latency_ms,
|
|
|
|
|
/// first_token_ms, duration_ms, status_code, error_message, created_at,
|
|
|
|
|
/// data_source, pricing_model, input_token_semantics`
|
|
|
|
@@ -185,21 +188,22 @@ fn row_to_request_log_detail(row: &rusqlite::Row<'_>) -> rusqlite::Result<Reques
|
|
|
|
|
output_tokens: row.get::<_, i64>(8)? as u32,
|
|
|
|
|
cache_read_tokens: row.get::<_, i64>(9)? as u32,
|
|
|
|
|
cache_creation_tokens: row.get::<_, i64>(10)? as u32,
|
|
|
|
|
input_cost_usd: row.get(11)?,
|
|
|
|
|
output_cost_usd: row.get(12)?,
|
|
|
|
|
cache_read_cost_usd: row.get(13)?,
|
|
|
|
|
cache_creation_cost_usd: row.get(14)?,
|
|
|
|
|
total_cost_usd: row.get(15)?,
|
|
|
|
|
is_streaming: row.get::<_, i64>(16)? != 0,
|
|
|
|
|
latency_ms: row.get::<_, i64>(17)? as u64,
|
|
|
|
|
first_token_ms: row.get::<_, Option<i64>>(18)?.map(|v| v as u64),
|
|
|
|
|
duration_ms: row.get::<_, Option<i64>>(19)?.map(|v| v as u64),
|
|
|
|
|
status_code: row.get::<_, i64>(20)? as u16,
|
|
|
|
|
error_message: row.get(21)?,
|
|
|
|
|
created_at: row.get(22)?,
|
|
|
|
|
data_source: row.get(23)?,
|
|
|
|
|
pricing_model: row.get(24)?,
|
|
|
|
|
input_token_semantics: row.get::<_, i64>(25)?,
|
|
|
|
|
cache_creation_1h_tokens: row.get::<_, i64>(11)? as u32,
|
|
|
|
|
input_cost_usd: row.get(12)?,
|
|
|
|
|
output_cost_usd: row.get(13)?,
|
|
|
|
|
cache_read_cost_usd: row.get(14)?,
|
|
|
|
|
cache_creation_cost_usd: row.get(15)?,
|
|
|
|
|
total_cost_usd: row.get(16)?,
|
|
|
|
|
is_streaming: row.get::<_, i64>(17)? != 0,
|
|
|
|
|
latency_ms: row.get::<_, i64>(18)? as u64,
|
|
|
|
|
first_token_ms: row.get::<_, Option<i64>>(19)?.map(|v| v as u64),
|
|
|
|
|
duration_ms: row.get::<_, Option<i64>>(20)?.map(|v| v as u64),
|
|
|
|
|
status_code: row.get::<_, i64>(21)? as u16,
|
|
|
|
|
error_message: row.get(22)?,
|
|
|
|
|
created_at: row.get(23)?,
|
|
|
|
|
data_source: row.get(24)?,
|
|
|
|
|
pricing_model: row.get(25)?,
|
|
|
|
|
input_token_semantics: row.get::<_, i64>(26)?,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1530,6 +1534,7 @@ impl Database {
|
|
|
|
|
"SELECT l.request_id, l.provider_id, {logs_pname} as provider_name, l.app_type, l.model,
|
|
|
|
|
l.request_model, l.cost_multiplier,
|
|
|
|
|
l.input_tokens, l.output_tokens, l.cache_read_tokens, l.cache_creation_tokens,
|
|
|
|
|
l.cache_creation_1h_tokens,
|
|
|
|
|
l.input_cost_usd, l.output_cost_usd, l.cache_read_cost_usd, l.cache_creation_cost_usd, l.total_cost_usd,
|
|
|
|
|
l.is_streaming, l.latency_ms, l.first_token_ms, l.duration_ms,
|
|
|
|
|
l.status_code, l.error_message, l.created_at, l.data_source, l.pricing_model,
|
|
|
|
@@ -1574,6 +1579,7 @@ impl Database {
|
|
|
|
|
"SELECT l.request_id, l.provider_id, {detail_pname} as provider_name, l.app_type, l.model,
|
|
|
|
|
l.request_model, l.cost_multiplier,
|
|
|
|
|
input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens,
|
|
|
|
|
cache_creation_1h_tokens,
|
|
|
|
|
input_cost_usd, output_cost_usd, cache_read_cost_usd, cache_creation_cost_usd, total_cost_usd,
|
|
|
|
|
is_streaming, latency_ms, first_token_ms, duration_ms,
|
|
|
|
|
status_code, error_message, created_at, l.data_source, l.pricing_model,
|
|
|
|
@@ -1730,6 +1736,7 @@ impl Database {
|
|
|
|
|
"SELECT request_id, provider_id, NULL AS provider_name, app_type, model, request_model,
|
|
|
|
|
cost_multiplier,
|
|
|
|
|
input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens,
|
|
|
|
|
cache_creation_1h_tokens,
|
|
|
|
|
input_cost_usd, output_cost_usd, cache_read_cost_usd,
|
|
|
|
|
cache_creation_cost_usd, total_cost_usd, is_streaming, latency_ms,
|
|
|
|
|
first_token_ms, duration_ms, status_code, error_message, created_at,
|
|
|
|
@@ -1836,9 +1843,17 @@ impl Database {
|
|
|
|
|
let cache_read_cost = rust_decimal::Decimal::from(log.cache_read_tokens as u64)
|
|
|
|
|
* pricing.cache_read
|
|
|
|
|
/ million;
|
|
|
|
|
let cache_creation_cost = rust_decimal::Decimal::from(log.cache_creation_tokens as u64)
|
|
|
|
|
let cache_creation_1h_tokens =
|
|
|
|
|
log.cache_creation_1h_tokens.min(log.cache_creation_tokens) as u64;
|
|
|
|
|
let cache_creation_standard_tokens =
|
|
|
|
|
(log.cache_creation_tokens as u64).saturating_sub(cache_creation_1h_tokens);
|
|
|
|
|
let cache_creation_1h_price = pricing.cache_creation * rust_decimal::Decimal::from(8u32)
|
|
|
|
|
/ rust_decimal::Decimal::from(5u32);
|
|
|
|
|
let cache_creation_cost = rust_decimal::Decimal::from(cache_creation_standard_tokens)
|
|
|
|
|
* pricing.cache_creation
|
|
|
|
|
/ million;
|
|
|
|
|
/ million
|
|
|
|
|
+ rust_decimal::Decimal::from(cache_creation_1h_tokens) * cache_creation_1h_price
|
|
|
|
|
/ million;
|
|
|
|
|
// 总成本 = 基础成本之和 × 倍率
|
|
|
|
|
let base_total = input_cost + output_cost + cache_read_cost + cache_creation_cost;
|
|
|
|
|
let total_cost = base_total * multiplier;
|
|
|
|
@@ -2868,6 +2883,50 @@ mod tests {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_backfill_preserves_one_hour_cache_write_pricing() -> Result<(), AppError> {
|
|
|
|
|
let db = Database::memory()?;
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let conn = lock_conn!(db.conn);
|
|
|
|
|
insert_usage_log(
|
|
|
|
|
&conn,
|
|
|
|
|
"claude-cache-one-hour",
|
|
|
|
|
"claude",
|
|
|
|
|
"p1",
|
|
|
|
|
"claude-haiku-4-5",
|
|
|
|
|
"proxy",
|
|
|
|
|
1000,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
1_000_000,
|
|
|
|
|
200,
|
|
|
|
|
"0",
|
|
|
|
|
)?;
|
|
|
|
|
conn.execute(
|
|
|
|
|
"UPDATE proxy_request_logs
|
|
|
|
|
SET cache_creation_1h_tokens = 1000000
|
|
|
|
|
WHERE request_id = 'claude-cache-one-hour'",
|
|
|
|
|
[],
|
|
|
|
|
)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert_eq!(db.backfill_missing_usage_costs()?, 1);
|
|
|
|
|
|
|
|
|
|
let conn = lock_conn!(db.conn);
|
|
|
|
|
let (cache_creation_cost, total_cost): (String, String) = conn.query_row(
|
|
|
|
|
"SELECT cache_creation_cost_usd, total_cost_usd
|
|
|
|
|
FROM proxy_request_logs WHERE request_id = 'claude-cache-one-hour'",
|
|
|
|
|
[],
|
|
|
|
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
|
|
|
|
)?;
|
|
|
|
|
assert_eq!(cache_creation_cost, "2.000000");
|
|
|
|
|
assert_eq!(total_cost, "2.000000");
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_get_usage_summary() -> Result<(), AppError> {
|
|
|
|
|
let db = Database::memory()?;
|
|
|
|
|