mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
revert(proxy): drop the 1-hour cache TTL option and TTL-bucketed write accounting
The forced 1-hour cache_control TTL (schema v14) was a mistake. Injected breakpoints return to Anthropic's standard 5-minute TTL, caller-owned markers are preserved verbatim instead of having their TTLs rewritten, and the 5m/1h cache-write buckets are removed from usage parsing and pricing (back to the single aggregate cache-creation rate). The cache TTL selector is removed from the rectifier settings panel along with its i18n keys in all four locales. SCHEMA_VERSION returns to 13: the unreleased cache_creation_1h_tokens column and the v13->v14 migration are removed. The feature never shipped in a release and the introducing commit was never pushed, so no databases were stamped v14 outside this machine (local DB verified at user_version 13).
This commit is contained in:
@@ -661,15 +661,6 @@ pub async fn set_optimizer_config(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
config: crate::proxy::types::OptimizerConfig,
|
||||
) -> Result<bool, String> {
|
||||
// Validate cache_ttl: only allow known values
|
||||
match config.cache_ttl.as_str() {
|
||||
"5m" | "1h" => {}
|
||||
other => {
|
||||
return Err(format!(
|
||||
"Invalid cache_ttl value: '{other}'. Allowed values: '5m', '1h'"
|
||||
))
|
||||
}
|
||||
}
|
||||
state
|
||||
.db
|
||||
.set_optimizer_config(&config)
|
||||
|
||||
@@ -52,7 +52,7 @@ use std::sync::Mutex;
|
||||
|
||||
/// 当前 Schema 版本号
|
||||
/// 每次修改表结构时递增,并在 schema.rs 中添加相应的迁移逻辑
|
||||
pub(crate) const SCHEMA_VERSION: i32 = 14;
|
||||
pub(crate) const SCHEMA_VERSION: i32 = 13;
|
||||
|
||||
/// 安全地序列化 JSON,避免 unwrap panic
|
||||
pub(crate) fn to_json_string<T: Serialize>(value: &T) -> Result<String, AppError> {
|
||||
|
||||
@@ -189,7 +189,6 @@ impl Database {
|
||||
pricing_model TEXT,
|
||||
input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cache_read_tokens INTEGER NOT NULL DEFAULT 0, cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cache_creation_1h_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
input_token_semantics INTEGER NOT NULL DEFAULT 0,
|
||||
input_cost_usd TEXT NOT NULL DEFAULT '0', output_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
cache_read_cost_usd TEXT NOT NULL DEFAULT '0', cache_creation_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
@@ -486,11 +485,6 @@ impl Database {
|
||||
Self::migrate_v12_to_v13(conn)?;
|
||||
Self::set_user_version(conn, 13)?;
|
||||
}
|
||||
13 => {
|
||||
log::info!("迁移数据库从 v13 到 v14(记录 1 小时缓存写入 token)");
|
||||
Self::migrate_v13_to_v14(conn)?;
|
||||
Self::set_user_version(conn, 14)?;
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError::Database(format!(
|
||||
"未知的数据库版本 {version},无法迁移到 {SCHEMA_VERSION}"
|
||||
@@ -1360,22 +1354,6 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// v13 -> v14:持久化 Anthropic 1-hour cache-write bucket。
|
||||
/// 5-minute/unknown writes can be derived from aggregate cache_creation_tokens;
|
||||
/// storing only the premium bucket keeps existing aggregate APIs stable while
|
||||
/// allowing later cost backfills to retain correct TTL pricing.
|
||||
fn migrate_v13_to_v14(conn: &Connection) -> Result<(), AppError> {
|
||||
if Self::table_exists(conn, "proxy_request_logs")? {
|
||||
Self::add_column_if_missing(
|
||||
conn,
|
||||
"proxy_request_logs",
|
||||
"cache_creation_1h_tokens",
|
||||
"INTEGER NOT NULL DEFAULT 0",
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 插入默认模型定价数据
|
||||
/// 格式: (model_id, display_name, input, output, cache_read, cache_creation)
|
||||
/// 注意: model_id 使用短横线格式(如 claude-haiku-4-5),与 API 返回的模型名称标准化后一致
|
||||
@@ -2788,7 +2766,7 @@ mod tests {
|
||||
|
||||
Database::apply_schema_migrations_on_conn(&conn)?;
|
||||
|
||||
assert_eq!(Database::get_user_version(&conn)?, 14);
|
||||
assert_eq!(Database::get_user_version(&conn)?, 13);
|
||||
assert!(Database::has_column(
|
||||
&conn,
|
||||
"proxy_request_logs",
|
||||
@@ -2799,11 +2777,6 @@ mod tests {
|
||||
"usage_daily_rollups",
|
||||
"input_token_semantics"
|
||||
)?);
|
||||
assert!(Database::has_column(
|
||||
&conn,
|
||||
"proxy_request_logs",
|
||||
"cache_creation_1h_tokens"
|
||||
)?);
|
||||
let log_default: i64 = conn.query_row(
|
||||
"SELECT dflt_value = '0' FROM pragma_table_info('proxy_request_logs')
|
||||
WHERE name = 'input_token_semantics'",
|
||||
|
||||
@@ -22,19 +22,9 @@ pub fn inject(body: &mut Value, config: &OptimizerConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
// 升级已有断点的 TTL
|
||||
upgrade_existing_ttl(body, &config.cache_ttl);
|
||||
|
||||
let mut budget = 4_usize.saturating_sub(existing);
|
||||
if budget == 0 {
|
||||
if existing > 0 {
|
||||
log::info!(
|
||||
"[OPT] cache: ttl-upgrade({existing}->{},existing={existing})",
|
||||
config.cache_ttl
|
||||
);
|
||||
} else {
|
||||
log::info!("[OPT] cache: no-op(existing={existing})");
|
||||
}
|
||||
log::info!("[OPT] cache: no-op(existing={existing})");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,10 +36,7 @@ pub fn inject(body: &mut Value, config: &OptimizerConfig) {
|
||||
if let Some(last) = tools.last_mut() {
|
||||
if last.get("cache_control").is_none() {
|
||||
if let Some(o) = last.as_object_mut() {
|
||||
o.insert(
|
||||
"cache_control".to_string(),
|
||||
make_cache_control(&config.cache_ttl),
|
||||
);
|
||||
o.insert("cache_control".to_string(), make_cache_control());
|
||||
}
|
||||
budget -= 1;
|
||||
injected.push("tools");
|
||||
@@ -73,10 +60,7 @@ pub fn inject(body: &mut Value, config: &OptimizerConfig) {
|
||||
if let Some(last) = system.last_mut() {
|
||||
if last.get("cache_control").is_none() {
|
||||
if let Some(o) = last.as_object_mut() {
|
||||
o.insert(
|
||||
"cache_control".to_string(),
|
||||
make_cache_control(&config.cache_ttl),
|
||||
);
|
||||
o.insert("cache_control".to_string(), make_cache_control());
|
||||
}
|
||||
budget -= 1;
|
||||
injected.push("system");
|
||||
@@ -90,7 +74,7 @@ pub fn inject(body: &mut Value, config: &OptimizerConfig) {
|
||||
if budget > 0 {
|
||||
if let Some(messages) = body.get_mut("messages").and_then(|m| m.as_array_mut()) {
|
||||
for message in messages.iter_mut().rev() {
|
||||
if inject_message_breakpoint(message, &config.cache_ttl) {
|
||||
if inject_message_breakpoint(message) {
|
||||
budget -= 1;
|
||||
injected.push("msgs-latest");
|
||||
break;
|
||||
@@ -108,7 +92,7 @@ pub fn inject(body: &mut Value, config: &OptimizerConfig) {
|
||||
}
|
||||
user_count += 1;
|
||||
if user_count == 2 {
|
||||
if inject_message_breakpoint(message, &config.cache_ttl) {
|
||||
if inject_message_breakpoint(message) {
|
||||
injected.push("msgs-prior-user");
|
||||
}
|
||||
break;
|
||||
@@ -122,11 +106,11 @@ pub fn inject(body: &mut Value, config: &OptimizerConfig) {
|
||||
"[OPT] cache: {}bp({},{},pre={existing})",
|
||||
injected.len(),
|
||||
injected.join("+"),
|
||||
config.cache_ttl,
|
||||
"5m",
|
||||
);
|
||||
}
|
||||
|
||||
fn inject_message_breakpoint(message: &mut Value, ttl: &str) -> bool {
|
||||
fn inject_message_breakpoint(message: &mut Value) -> bool {
|
||||
let Some(content) = message.get_mut("content").and_then(Value::as_array_mut) else {
|
||||
return false;
|
||||
};
|
||||
@@ -144,16 +128,12 @@ fn inject_message_breakpoint(message: &mut Value, ttl: &str) -> bool {
|
||||
let Some(object) = block.as_object_mut() else {
|
||||
return false;
|
||||
};
|
||||
object.insert("cache_control".to_string(), make_cache_control(ttl));
|
||||
object.insert("cache_control".to_string(), make_cache_control());
|
||||
true
|
||||
}
|
||||
|
||||
fn make_cache_control(ttl: &str) -> Value {
|
||||
if ttl == "5m" {
|
||||
json!({"type": "ephemeral"})
|
||||
} else {
|
||||
json!({"type": "ephemeral", "ttl": ttl})
|
||||
}
|
||||
fn make_cache_control() -> Value {
|
||||
json!({"type": "ephemeral"})
|
||||
}
|
||||
|
||||
fn count_existing(body: &Value) -> usize {
|
||||
@@ -187,40 +167,6 @@ fn count_existing(body: &Value) -> usize {
|
||||
count
|
||||
}
|
||||
|
||||
fn upgrade_existing_ttl(body: &mut Value, ttl: &str) {
|
||||
let upgrade = |val: &mut Value| {
|
||||
if let Some(cc) = val.get_mut("cache_control").and_then(|c| c.as_object_mut()) {
|
||||
if ttl == "5m" {
|
||||
cc.remove("ttl");
|
||||
} else {
|
||||
cc.insert("ttl".to_string(), json!(ttl));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(tools) = body.get_mut("tools").and_then(|t| t.as_array_mut()) {
|
||||
for tool in tools.iter_mut() {
|
||||
upgrade(tool);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(system) = body.get_mut("system").and_then(|s| s.as_array_mut()) {
|
||||
for block in system.iter_mut() {
|
||||
upgrade(block);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(messages) = body.get_mut("messages").and_then(|m| m.as_array_mut()) {
|
||||
for msg in messages.iter_mut() {
|
||||
if let Some(content) = msg.get_mut("content").and_then(|c| c.as_array_mut()) {
|
||||
for block in content.iter_mut() {
|
||||
upgrade(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -231,7 +177,6 @@ mod tests {
|
||||
enabled: true,
|
||||
thinking_optimizer: true,
|
||||
cache_injection: true,
|
||||
cache_ttl: "1h".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +207,7 @@ mod tests {
|
||||
|
||||
// tools last element
|
||||
assert!(body["tools"][1].get("cache_control").is_some());
|
||||
assert_eq!(body["tools"][1]["cache_control"]["ttl"], "1h");
|
||||
assert!(body["tools"][1]["cache_control"].get("ttl").is_none());
|
||||
// system last element
|
||||
assert!(body["system"][0].get("cache_control").is_some());
|
||||
// assistant last non-thinking block
|
||||
@@ -296,26 +241,26 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_existing_four_breakpoints_only_upgrades_ttl() {
|
||||
fn test_existing_four_breakpoints_preserve_caller_ttl() {
|
||||
let mut body = json!({
|
||||
"model": "test",
|
||||
"tools": [
|
||||
{"name": "t1", "cache_control": {"type": "ephemeral", "ttl": "5m"}},
|
||||
{"name": "t2", "cache_control": {"type": "ephemeral", "ttl": "5m"}}
|
||||
{"name": "t1", "cache_control": {"type": "ephemeral", "ttl": "1h"}},
|
||||
{"name": "t2", "cache_control": {"type": "ephemeral", "ttl": "1h"}}
|
||||
],
|
||||
"system": [
|
||||
{"type": "text", "text": "sys", "cache_control": {"type": "ephemeral", "ttl": "5m"}}
|
||||
{"type": "text", "text": "sys", "cache_control": {"type": "ephemeral", "ttl": "1h"}}
|
||||
],
|
||||
"messages": [
|
||||
{"role": "assistant", "content": [
|
||||
{"type": "text", "text": "ok", "cache_control": {"type": "ephemeral", "ttl": "5m"}}
|
||||
{"type": "text", "text": "ok", "cache_control": {"type": "ephemeral", "ttl": "1h"}}
|
||||
]}
|
||||
]
|
||||
});
|
||||
|
||||
inject(&mut body, &default_config());
|
||||
|
||||
// All TTLs upgraded to 1h, no new breakpoints
|
||||
// Existing markers are caller-owned; only newly injected markers are fixed to 5m.
|
||||
assert_eq!(body["tools"][0]["cache_control"]["ttl"], "1h");
|
||||
assert_eq!(body["tools"][1]["cache_control"]["ttl"], "1h");
|
||||
assert_eq!(body["system"][0]["cache_control"]["ttl"], "1h");
|
||||
@@ -393,18 +338,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ttl_5m_no_ttl_field() {
|
||||
let config = OptimizerConfig {
|
||||
cache_ttl: "5m".to_string(),
|
||||
..default_config()
|
||||
};
|
||||
fn test_standard_five_minute_cache_control_omits_ttl() {
|
||||
let mut body = json!({
|
||||
"model": "test",
|
||||
"tools": [{"name": "tool1"}],
|
||||
"messages": [{"role": "user", "content": [{"type": "text", "text": "hi"}]}]
|
||||
});
|
||||
|
||||
inject(&mut body, &config);
|
||||
inject(&mut body, &default_config());
|
||||
|
||||
let cc = &body["tools"][0]["cache_control"];
|
||||
assert_eq!(cc["type"], "ephemeral");
|
||||
|
||||
@@ -2859,13 +2859,13 @@ fn responses_error_envelope_message(body: &[u8]) -> Option<String> {
|
||||
/// Prompt caching is part of the Codex→Anthropic protocol bridge rather than an
|
||||
/// optional Bedrock optimizer. Codex requests do not contain Anthropic
|
||||
/// `cache_control`, so keep bridge caching on by default while still honoring the
|
||||
/// dedicated cache-injection switch and configured TTL.
|
||||
/// dedicated cache-injection switch. Injected breakpoints always use Anthropic's
|
||||
/// standard 5-minute TTL.
|
||||
fn codex_anthropic_cache_config(config: &OptimizerConfig) -> OptimizerConfig {
|
||||
OptimizerConfig {
|
||||
enabled: true,
|
||||
thinking_optimizer: false,
|
||||
cache_injection: config.cache_injection,
|
||||
cache_ttl: config.cache_ttl.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4233,7 +4233,6 @@ mod tests {
|
||||
let default = codex_anthropic_cache_config(&OptimizerConfig::default());
|
||||
assert!(default.enabled);
|
||||
assert!(default.cache_injection);
|
||||
assert_eq!(default.cache_ttl, "1h");
|
||||
|
||||
let disabled = codex_anthropic_cache_config(&OptimizerConfig {
|
||||
cache_injection: false,
|
||||
|
||||
@@ -191,11 +191,6 @@ pub(crate) fn build_responses_usage_from_anthropic(usage: Option<&Value>) -> Val
|
||||
if cache_creation > 0 {
|
||||
result["cache_creation_input_tokens"] = json!(cache_creation);
|
||||
}
|
||||
if let Some(cache_creation_details) = u.get("cache_creation") {
|
||||
// Preserve Anthropic's TTL buckets as a compatibility extension. The usage
|
||||
// parser consumes these to distinguish 5-minute and 1-hour write pricing.
|
||||
result["cache_creation"] = cache_creation_details.clone();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@@ -2318,11 +2313,7 @@ mod tests {
|
||||
"output_tokens": 5,
|
||||
"output_tokens_details": {"thinking_tokens": 3},
|
||||
"cache_read_input_tokens": 60,
|
||||
"cache_creation_input_tokens": 20,
|
||||
"cache_creation": {
|
||||
"ephemeral_5m_input_tokens": 5,
|
||||
"ephemeral_1h_input_tokens": 15
|
||||
}
|
||||
"cache_creation_input_tokens": 20
|
||||
}
|
||||
});
|
||||
let result = anthropic_response_to_responses(input).unwrap();
|
||||
@@ -2340,12 +2331,8 @@ mod tests {
|
||||
result["usage"]["input_tokens_details"]["cache_write_tokens"],
|
||||
20
|
||||
);
|
||||
// cache_creation is passed through explicitly for downstream billing attribution (counted only once)
|
||||
// Aggregate cache creation is exposed for downstream billing attribution (counted only once).
|
||||
assert_eq!(result["usage"]["cache_creation_input_tokens"], 20);
|
||||
assert_eq!(
|
||||
result["usage"]["cache_creation"]["ephemeral_1h_input_tokens"],
|
||||
15
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -645,14 +645,12 @@ async fn log_usage_internal(
|
||||
let request_id = usage.dedup_request_id();
|
||||
|
||||
log::debug!(
|
||||
"[{app_type}] 记录请求日志: id={request_id}, provider={provider_id}, model={model}, streaming={is_streaming}, status={status_code}, latency_ms={latency_ms}, first_token_ms={first_token_ms:?}, session={}, input={}, output={}, cache_read={}, cache_creation={}, cache_creation_5m={}, cache_creation_1h={}",
|
||||
"[{app_type}] 记录请求日志: id={request_id}, provider={provider_id}, model={model}, streaming={is_streaming}, status={status_code}, latency_ms={latency_ms}, first_token_ms={first_token_ms:?}, session={}, input={}, output={}, cache_read={}, cache_creation={}",
|
||||
session_id.as_deref().unwrap_or("none"),
|
||||
usage.input_tokens,
|
||||
usage.output_tokens,
|
||||
usage.cache_read_tokens,
|
||||
usage.cache_creation_tokens,
|
||||
usage.cache_creation_5m_tokens,
|
||||
usage.cache_creation_1h_tokens
|
||||
usage.cache_creation_tokens
|
||||
);
|
||||
|
||||
if let Err(e) = logger.log_with_calculation(
|
||||
@@ -1006,8 +1004,6 @@ mod tests {
|
||||
output_tokens: 0,
|
||||
cache_read_tokens: 0,
|
||||
cache_creation_tokens: 0,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: None,
|
||||
message_id: None,
|
||||
};
|
||||
@@ -1076,8 +1072,6 @@ mod tests {
|
||||
output_tokens: 0,
|
||||
cache_read_tokens: 0,
|
||||
cache_creation_tokens: 0,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: None,
|
||||
message_id: None,
|
||||
};
|
||||
@@ -1160,8 +1154,6 @@ mod tests {
|
||||
output_tokens: 0,
|
||||
cache_read_tokens: 0,
|
||||
cache_creation_tokens: 0,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: None,
|
||||
message_id: None,
|
||||
};
|
||||
|
||||
@@ -137,7 +137,6 @@ mod tests {
|
||||
enabled: true,
|
||||
thinking_optimizer: true,
|
||||
cache_injection: true,
|
||||
cache_ttl: "1h".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +145,6 @@ mod tests {
|
||||
enabled: true,
|
||||
thinking_optimizer: false,
|
||||
cache_injection: true,
|
||||
cache_ttl: "1h".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -264,13 +264,6 @@ pub struct OptimizerConfig {
|
||||
/// Cache 注入子开关(总开关开启后默认生效)
|
||||
#[serde(default = "default_true")]
|
||||
pub cache_injection: bool,
|
||||
/// Cache TTL: "5m" | "1h"(默认 "1h")
|
||||
#[serde(default = "default_cache_ttl")]
|
||||
pub cache_ttl: String,
|
||||
}
|
||||
|
||||
fn default_cache_ttl() -> String {
|
||||
"1h".to_string()
|
||||
}
|
||||
|
||||
impl Default for OptimizerConfig {
|
||||
@@ -279,7 +272,6 @@ impl Default for OptimizerConfig {
|
||||
enabled: false,
|
||||
thinking_optimizer: true,
|
||||
cache_injection: true,
|
||||
cache_ttl: "1h".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,20 +94,9 @@ impl CostCalculator {
|
||||
Decimal::from(usage.output_tokens) * pricing.output_cost_per_million / million;
|
||||
let cache_read_cost =
|
||||
Decimal::from(usage.cache_read_tokens) * pricing.cache_read_cost_per_million / million;
|
||||
let (cache_creation_unspecified, cache_creation_5m, cache_creation_1h) =
|
||||
usage.normalized_cache_creation_buckets();
|
||||
let cache_creation_5m_price = pricing.cache_creation_cost_per_million;
|
||||
// The stored cache-creation price is the existing 5-minute rate. Anthropic
|
||||
// prices 5m writes at 1.25x input and 1h writes at 2x input, therefore the
|
||||
// corresponding 1h price is 8/5 of the configured 5m price. Unknown providers
|
||||
// without TTL details retain the configured legacy rate.
|
||||
let cache_creation_1h_price =
|
||||
cache_creation_5m_price * Decimal::from(8u32) / Decimal::from(5u32);
|
||||
let cache_creation_cost = (Decimal::from(cache_creation_unspecified)
|
||||
+ Decimal::from(cache_creation_5m))
|
||||
* cache_creation_5m_price
|
||||
/ million
|
||||
+ Decimal::from(cache_creation_1h) * cache_creation_1h_price / million;
|
||||
let cache_creation_cost = Decimal::from(usage.cache_creation_tokens)
|
||||
* pricing.cache_creation_cost_per_million
|
||||
/ million;
|
||||
|
||||
// 总成本 = 各项基础成本之和 × 倍率
|
||||
let base_total = input_cost + output_cost + cache_read_cost + cache_creation_cost;
|
||||
@@ -170,8 +159,6 @@ mod tests {
|
||||
output_tokens: 500,
|
||||
cache_read_tokens: 200,
|
||||
cache_creation_tokens: 100,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: None,
|
||||
message_id: None,
|
||||
};
|
||||
@@ -204,8 +191,6 @@ mod tests {
|
||||
output_tokens: 500,
|
||||
cache_read_tokens: 200,
|
||||
cache_creation_tokens: 100,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: None,
|
||||
message_id: None,
|
||||
};
|
||||
@@ -226,25 +211,6 @@ mod tests {
|
||||
assert_eq!(cost.total_cost, Decimal::from_str("0.010035").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_one_hour_cache_creation_uses_premium_rate() {
|
||||
let usage = TokenUsage {
|
||||
cache_creation_tokens: 1_000_000,
|
||||
cache_creation_5m_tokens: 250_000,
|
||||
cache_creation_1h_tokens: 750_000,
|
||||
..TokenUsage::default()
|
||||
};
|
||||
let pricing = ModelPricing::from_strings("3", "15", "0.3", "3.75").unwrap();
|
||||
|
||||
let cost = CostCalculator::calculate(&usage, &pricing, Decimal::ONE);
|
||||
|
||||
// 250k × $3.75/M + 750k × $6/M = $5.4375.
|
||||
assert_eq!(
|
||||
cost.cache_creation_cost,
|
||||
Decimal::from_str("5.4375").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cost_multiplier() {
|
||||
let usage = TokenUsage {
|
||||
@@ -252,8 +218,6 @@ mod tests {
|
||||
output_tokens: 0,
|
||||
cache_read_tokens: 0,
|
||||
cache_creation_tokens: 0,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: None,
|
||||
message_id: None,
|
||||
};
|
||||
@@ -276,8 +240,6 @@ mod tests {
|
||||
output_tokens: 500,
|
||||
cache_read_tokens: 0,
|
||||
cache_creation_tokens: 0,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: None,
|
||||
message_id: None,
|
||||
};
|
||||
@@ -295,8 +257,6 @@ mod tests {
|
||||
output_tokens: 1,
|
||||
cache_read_tokens: 1,
|
||||
cache_creation_tokens: 1,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: None,
|
||||
message_id: None,
|
||||
};
|
||||
|
||||
@@ -81,12 +81,11 @@ impl<'a> UsageLogger<'a> {
|
||||
"INSERT OR REPLACE INTO proxy_request_logs (
|
||||
request_id, provider_id, app_type, model, request_model, pricing_model,
|
||||
input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens,
|
||||
cache_creation_1h_tokens,
|
||||
input_token_semantics,
|
||||
input_cost_usd, output_cost_usd, cache_read_cost_usd, cache_creation_cost_usd, total_cost_usd,
|
||||
latency_ms, first_token_ms, status_code, error_message, session_id,
|
||||
provider_type, is_streaming, cost_multiplier, created_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26)",
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25)",
|
||||
rusqlite::params![
|
||||
log.request_id,
|
||||
log.provider_id,
|
||||
@@ -98,7 +97,6 @@ impl<'a> UsageLogger<'a> {
|
||||
log.usage.output_tokens,
|
||||
log.usage.cache_read_tokens,
|
||||
log.usage.cache_creation_tokens,
|
||||
log.usage.cache_creation_1h_tokens,
|
||||
input_token_semantics,
|
||||
input_cost,
|
||||
output_cost,
|
||||
@@ -398,8 +396,6 @@ mod tests {
|
||||
output_tokens: 500,
|
||||
cache_read_tokens: 0,
|
||||
cache_creation_tokens: 0,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: None,
|
||||
message_id: None,
|
||||
};
|
||||
|
||||
@@ -27,22 +27,6 @@ fn openai_cache_write_tokens(usage: &Value) -> u32 {
|
||||
.unwrap_or(0) as u32
|
||||
}
|
||||
|
||||
fn cache_creation_ttl_tokens(usage: &Value) -> (u32, u32) {
|
||||
let details = usage
|
||||
.get("cache_creation")
|
||||
.or_else(|| usage.pointer("/input_tokens_details/cache_creation"))
|
||||
.or_else(|| usage.pointer("/prompt_tokens_details/cache_creation"));
|
||||
let five_minutes = details
|
||||
.and_then(|value| value.get("ephemeral_5m_input_tokens"))
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0) as u32;
|
||||
let one_hour = details
|
||||
.and_then(|value| value.get("ephemeral_1h_input_tokens"))
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0) as u32;
|
||||
(five_minutes, one_hour)
|
||||
}
|
||||
|
||||
/// Session 日志 request_id 前缀,与 `session_usage.rs` 中的格式保持一致
|
||||
pub const SESSION_REQUEST_ID_PREFIX: &str = "session:";
|
||||
|
||||
@@ -53,13 +37,6 @@ pub struct TokenUsage {
|
||||
pub output_tokens: u32,
|
||||
pub cache_read_tokens: u32,
|
||||
pub cache_creation_tokens: u32,
|
||||
/// Anthropic cache-write TTL detail. The aggregate above remains the stable
|
||||
/// public/storage metric; these buckets make 1-hour writes billable at 2x input
|
||||
/// instead of the 5-minute 1.25x rate.
|
||||
#[serde(default)]
|
||||
pub cache_creation_5m_tokens: u32,
|
||||
#[serde(default)]
|
||||
pub cache_creation_1h_tokens: u32,
|
||||
/// 从响应中提取的实际模型名称(如果可用)
|
||||
pub model: Option<String>,
|
||||
/// 从响应中提取的消息 ID(用于跨源去重)
|
||||
@@ -70,23 +47,6 @@ pub struct TokenUsage {
|
||||
}
|
||||
|
||||
impl TokenUsage {
|
||||
/// Return mutually exclusive cache-write buckets, clamped to the aggregate.
|
||||
/// Providers that do not expose TTL detail remain in `unspecified` and keep the
|
||||
/// legacy cache-creation price.
|
||||
pub fn normalized_cache_creation_buckets(&self) -> (u32, u32, u32) {
|
||||
let one_hour = self
|
||||
.cache_creation_1h_tokens
|
||||
.min(self.cache_creation_tokens);
|
||||
let five_minutes = self
|
||||
.cache_creation_5m_tokens
|
||||
.min(self.cache_creation_tokens.saturating_sub(one_hour));
|
||||
let unspecified = self
|
||||
.cache_creation_tokens
|
||||
.saturating_sub(one_hour)
|
||||
.saturating_sub(five_minutes);
|
||||
(unspecified, five_minutes, one_hour)
|
||||
}
|
||||
|
||||
/// 生成与 session 日志共享的 request_id,用于跨源去重。
|
||||
/// 有 message_id 时返回 `session:{id}`,否则回退到随机 UUID。
|
||||
pub fn dedup_request_id(&self) -> String {
|
||||
@@ -123,7 +83,6 @@ impl TokenUsage {
|
||||
/// 从 Claude API 非流式响应解析
|
||||
pub fn from_claude_response(body: &Value) -> Option<Self> {
|
||||
let usage = body.get("usage")?;
|
||||
let (cache_creation_5m_tokens, cache_creation_1h_tokens) = cache_creation_ttl_tokens(usage);
|
||||
// 提取响应中的模型名称
|
||||
let model = body
|
||||
.get("model")
|
||||
@@ -145,8 +104,6 @@ impl TokenUsage {
|
||||
.get("cache_creation_input_tokens")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as u32,
|
||||
cache_creation_5m_tokens,
|
||||
cache_creation_1h_tokens,
|
||||
model,
|
||||
message_id,
|
||||
})
|
||||
@@ -177,8 +134,6 @@ impl TokenUsage {
|
||||
}
|
||||
}
|
||||
if let Some(msg_usage) = event.get("message").and_then(|m| m.get("usage")) {
|
||||
let (cache_creation_5m, cache_creation_1h) =
|
||||
cache_creation_ttl_tokens(msg_usage);
|
||||
// 从 message_start 获取 input_tokens(原生 Claude API)
|
||||
if let Some(input) =
|
||||
msg_usage.get("input_tokens").and_then(|v| v.as_u64())
|
||||
@@ -195,14 +150,10 @@ impl TokenUsage {
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0)
|
||||
as u32;
|
||||
usage.cache_creation_5m_tokens = cache_creation_5m;
|
||||
usage.cache_creation_1h_tokens = cache_creation_1h;
|
||||
}
|
||||
}
|
||||
"message_delta" => {
|
||||
if let Some(delta_usage) = event.get("usage") {
|
||||
let (delta_cache_creation_5m, delta_cache_creation_1h) =
|
||||
cache_creation_ttl_tokens(delta_usage);
|
||||
// 从 message_delta 获取 output_tokens
|
||||
if let Some(output) =
|
||||
delta_usage.get("output_tokens").and_then(|v| v.as_u64())
|
||||
@@ -241,14 +192,6 @@ impl TokenUsage {
|
||||
}
|
||||
if let Some(cache_creation) = delta_cache_creation {
|
||||
usage.cache_creation_tokens = cache_creation;
|
||||
if delta_cache_creation_5m > 0
|
||||
|| delta_cache_creation_1h > 0
|
||||
{
|
||||
usage.cache_creation_5m_tokens =
|
||||
delta_cache_creation_5m;
|
||||
usage.cache_creation_1h_tokens =
|
||||
delta_cache_creation_1h;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,10 +206,6 @@ impl TokenUsage {
|
||||
if usage.cache_creation_tokens == 0 {
|
||||
if let Some(cache_creation) = delta_cache_creation {
|
||||
usage.cache_creation_tokens = cache_creation;
|
||||
if delta_cache_creation_5m > 0 || delta_cache_creation_1h > 0 {
|
||||
usage.cache_creation_5m_tokens = delta_cache_creation_5m;
|
||||
usage.cache_creation_1h_tokens = delta_cache_creation_1h;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,8 +237,6 @@ impl TokenUsage {
|
||||
output_tokens: usage.get("completion_tokens")?.as_u64()? as u32,
|
||||
cache_read_tokens: 0,
|
||||
cache_creation_tokens: 0,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: None,
|
||||
message_id: None,
|
||||
})
|
||||
@@ -333,15 +270,12 @@ impl TokenUsage {
|
||||
|
||||
let cached_tokens = openai_cache_read_tokens(usage);
|
||||
let cache_write_tokens = openai_cache_write_tokens(usage);
|
||||
let (cache_creation_5m_tokens, cache_creation_1h_tokens) = cache_creation_ttl_tokens(usage);
|
||||
|
||||
Some(Self {
|
||||
input_tokens: input_tokens? as u32,
|
||||
output_tokens: output_tokens? as u32,
|
||||
cache_read_tokens: cached_tokens,
|
||||
cache_creation_tokens: cache_write_tokens,
|
||||
cache_creation_5m_tokens,
|
||||
cache_creation_1h_tokens,
|
||||
model,
|
||||
message_id: None,
|
||||
})
|
||||
@@ -360,7 +294,6 @@ impl TokenUsage {
|
||||
// 获取 cached_tokens (可能在 cache_read_input_tokens 或 input_tokens_details 中)
|
||||
let cached_tokens = openai_cache_read_tokens(usage);
|
||||
let cache_write_tokens = openai_cache_write_tokens(usage);
|
||||
let (cache_creation_5m_tokens, cache_creation_1h_tokens) = cache_creation_ttl_tokens(usage);
|
||||
|
||||
// 调整 input_tokens: OpenAI total input 同时包含 cache read/write 两桶。
|
||||
let adjusted_input = input_tokens
|
||||
@@ -378,8 +311,6 @@ impl TokenUsage {
|
||||
output_tokens,
|
||||
cache_read_tokens: cached_tokens,
|
||||
cache_creation_tokens: cache_write_tokens,
|
||||
cache_creation_5m_tokens,
|
||||
cache_creation_1h_tokens,
|
||||
model,
|
||||
message_id: None,
|
||||
})
|
||||
@@ -460,7 +391,6 @@ impl TokenUsage {
|
||||
// 获取 cached_tokens (可能在 prompt_tokens_details 中)
|
||||
let cached_tokens = openai_cache_read_tokens(usage);
|
||||
let cache_write_tokens = openai_cache_write_tokens(usage);
|
||||
let (cache_creation_5m_tokens, cache_creation_1h_tokens) = cache_creation_ttl_tokens(usage);
|
||||
|
||||
// 提取响应中的模型名称
|
||||
let model = body
|
||||
@@ -473,8 +403,6 @@ impl TokenUsage {
|
||||
output_tokens: completion_tokens as u32,
|
||||
cache_read_tokens: cached_tokens,
|
||||
cache_creation_tokens: cache_write_tokens,
|
||||
cache_creation_5m_tokens,
|
||||
cache_creation_1h_tokens,
|
||||
model,
|
||||
message_id: None,
|
||||
})
|
||||
@@ -520,8 +448,6 @@ impl TokenUsage {
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as u32,
|
||||
cache_creation_tokens: 0,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model,
|
||||
message_id: None,
|
||||
})
|
||||
@@ -573,8 +499,6 @@ impl TokenUsage {
|
||||
output_tokens: total_output,
|
||||
cache_read_tokens: total_cache_read,
|
||||
cache_creation_tokens: 0,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model,
|
||||
message_id: None,
|
||||
})
|
||||
@@ -597,11 +521,7 @@ mod tests {
|
||||
"input_tokens": 100,
|
||||
"output_tokens": 50,
|
||||
"cache_read_input_tokens": 20,
|
||||
"cache_creation_input_tokens": 10,
|
||||
"cache_creation": {
|
||||
"ephemeral_5m_input_tokens": 4,
|
||||
"ephemeral_1h_input_tokens": 6
|
||||
}
|
||||
"cache_creation_input_tokens": 10
|
||||
}
|
||||
});
|
||||
|
||||
@@ -610,9 +530,6 @@ mod tests {
|
||||
assert_eq!(usage.output_tokens, 50);
|
||||
assert_eq!(usage.cache_read_tokens, 20);
|
||||
assert_eq!(usage.cache_creation_tokens, 10);
|
||||
assert_eq!(usage.cache_creation_5m_tokens, 4);
|
||||
assert_eq!(usage.cache_creation_1h_tokens, 6);
|
||||
assert_eq!(usage.normalized_cache_creation_buckets(), (0, 4, 6));
|
||||
assert_eq!(usage.model, Some("claude-sonnet-4-20250514".to_string()));
|
||||
}
|
||||
|
||||
|
||||
@@ -448,8 +448,6 @@ fn insert_session_log_entry(
|
||||
output_tokens: msg.output_tokens,
|
||||
cache_read_tokens: msg.cache_read_tokens,
|
||||
cache_creation_tokens: msg.cache_creation_tokens,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: Some(msg.model.clone()),
|
||||
message_id: None,
|
||||
};
|
||||
|
||||
@@ -634,8 +634,6 @@ fn insert_codex_session_entry(
|
||||
output_tokens: delta.output,
|
||||
cache_read_tokens: delta.cached_input,
|
||||
cache_creation_tokens: 0,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: Some(model.to_string()),
|
||||
message_id: None,
|
||||
};
|
||||
|
||||
@@ -270,8 +270,6 @@ fn insert_gemini_session_entry(
|
||||
output_tokens,
|
||||
cache_read_tokens: tokens.cached,
|
||||
cache_creation_tokens: 0,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: Some(model.to_string()),
|
||||
message_id: None,
|
||||
};
|
||||
|
||||
@@ -362,8 +362,6 @@ fn insert_opencode_message(
|
||||
output_tokens: output_with_reasoning,
|
||||
cache_read_tokens: msg.cache_read_tokens,
|
||||
cache_creation_tokens: msg.cache_write_tokens,
|
||||
cache_creation_5m_tokens: 0,
|
||||
cache_creation_1h_tokens: 0,
|
||||
model: Some(msg.model_id.clone()),
|
||||
message_id: None,
|
||||
};
|
||||
|
||||
@@ -137,9 +137,6 @@ 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,
|
||||
@@ -162,12 +159,12 @@ pub struct RequestLogDetail {
|
||||
pub pricing_model: Option<String>,
|
||||
}
|
||||
|
||||
/// 把 27 列的查询结果映射为 `RequestLogDetail`。
|
||||
/// 把 26 列的查询结果映射为 `RequestLogDetail`。
|
||||
///
|
||||
/// 调用方的 SELECT **必须**按以下顺序返回 27 列:
|
||||
/// 调用方的 SELECT **必须**按以下顺序返回 26 列:
|
||||
/// `request_id, provider_id, 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_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`
|
||||
@@ -188,22 +185,21 @@ 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,
|
||||
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)?,
|
||||
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)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1534,7 +1530,6 @@ 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,
|
||||
@@ -1579,7 +1574,6 @@ 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,
|
||||
@@ -1736,7 +1730,6 @@ 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,
|
||||
@@ -1843,17 +1836,9 @@ impl Database {
|
||||
let cache_read_cost = rust_decimal::Decimal::from(log.cache_read_tokens as u64)
|
||||
* pricing.cache_read
|
||||
/ million;
|
||||
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)
|
||||
let cache_creation_cost = rust_decimal::Decimal::from(log.cache_creation_tokens as u64)
|
||||
* pricing.cache_creation
|
||||
/ million
|
||||
+ rust_decimal::Decimal::from(cache_creation_1h_tokens) * cache_creation_1h_price
|
||||
/ million;
|
||||
/ million;
|
||||
// 总成本 = 基础成本之和 × 倍率
|
||||
let base_total = input_cost + output_cost + cache_read_cost + cache_creation_cost;
|
||||
let total_cost = base_total * multiplier;
|
||||
@@ -2883,50 +2868,6 @@ 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()?;
|
||||
|
||||
Reference in New Issue
Block a user