mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
fix(usage): prevent double-counting between proxy and session-log sources
Proxy writes and session-log sync wrote to proxy_request_logs with
mismatched request_ids: only Claude on a native Anthropic backend used the
shared `session:{message_id}` key. Codex/Gemini and Claude-through-OpenAI
providers always produced distinct ids, so primary-key dedup never fired
and every real request was recorded twice.
Adds a 7-dim fingerprint dedup (app_type, 4 token counts, 2xx status,
model with case-insensitive match, ±10min window) wired into three layers:
- Write path: should_skip_session_insert() blocks duplicate session rows
before INSERT, unifying the previously-divergent Claude/Codex/Gemini
paths through a single DedupKey-based helper.
- Read path: effective_usage_log_filter() excludes already-covered session
rows from every aggregation query.
- Rollup path: same filter applied so usage_daily_rollups never absorbs
duplicates.
Also adds a covering index (idx_request_logs_dedup_lookup) so the EXISTS
subquery stays index-only, and a transform.rs regression test that pins
openai_to_anthropic id preservation - the missing piece that lets
Claude+OpenAI-compatible providers reuse the session: id scheme.
This commit is contained in:
@@ -214,6 +214,7 @@ impl Database {
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
Self::create_request_logs_dedup_index_if_supported(conn)?;
|
||||
|
||||
// 11. Model Pricing 表
|
||||
conn.execute(
|
||||
@@ -1107,6 +1108,7 @@ impl Database {
|
||||
"data_source",
|
||||
"TEXT NOT NULL DEFAULT 'proxy'",
|
||||
)?;
|
||||
Self::create_request_logs_dedup_index_if_supported(conn)?;
|
||||
}
|
||||
|
||||
// 2. 创建会话日志同步状态表
|
||||
@@ -1908,6 +1910,36 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_request_logs_dedup_index_if_supported(conn: &Connection) -> Result<(), AppError> {
|
||||
if !Self::table_exists(conn, "proxy_request_logs")? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let required_columns = [
|
||||
"app_type",
|
||||
"data_source",
|
||||
"input_tokens",
|
||||
"output_tokens",
|
||||
"cache_read_tokens",
|
||||
"created_at",
|
||||
"cache_creation_tokens",
|
||||
];
|
||||
for column in required_columns {
|
||||
if !Self::has_column(conn, "proxy_request_logs", column)? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_request_logs_dedup_lookup
|
||||
ON proxy_request_logs(app_type, data_source, input_tokens, output_tokens,
|
||||
cache_read_tokens, created_at, cache_creation_tokens)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(format!("创建使用量去重索引失败: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_identifier(s: &str, kind: &str) -> Result<(), AppError> {
|
||||
if s.is_empty() {
|
||||
return Err(AppError::Database(format!("{kind} 不能为空")));
|
||||
|
||||
Reference in New Issue
Block a user