feat(usage): add Hermes Agent tracking + fix zero-cost bug + perf

Hermes:
- Parse ~/.hermes/state.db sessions (incl. profiles/*/state.db) into
  proxy_request_logs with data_source='hermes_session', WAL-aware
  incremental sync, Hermes-reported cost preferred over model_pricing
  fallback

Zero-cost bug (dashboard showed \$0 totals):
- GPT-5.5 family default pricing (~83% of affected rows used GPT-5.5)
- find_model_pricing_row: ASCII-lowercase normalization so
  "OpenAI/GPT-5.5@HIGH" matches seeded "gpt-5.5"
- Startup cost backfill in async task: scan rows where total_cost <= 0
  but tokens > 0, recompute via model_pricing in a single transaction

Performance:
- Add (app_type, created_at DESC) covering index for dashboard range
  queries
- Add expression index on COALESCE(data_source, 'proxy') so dedup EXISTS
  subqueries use index lookup instead of full scan; drop superseded
  idx_request_logs_dedup_lookup

Refactor:
- row_to_request_log_detail helper (3-way de-dup; fixes cost_multiplier
  \"1\" vs \"1.0\" drift between callers)
- Promote get_sync_state/update_sync_state to shared session_usage
  module (4 copies -> 1)
- run_step helper in lib.rs replaces 9 if-let-Err blocks
- maybe_backfill_log_costs returns bool to skip duplicate total_cost
  parsing in caller
This commit is contained in:
Jason
2026-04-29 22:38:29 +08:00
parent 2ee7cb4101
commit f061b777b7
16 changed files with 1248 additions and 203 deletions
+32 -7
View File
@@ -214,7 +214,7 @@ impl Database {
[],
)
.map_err(|e| AppError::Database(e.to_string()))?;
Self::create_request_logs_dedup_index_if_supported(conn)?;
Self::create_request_logs_usage_indexes_if_supported(conn)?;
// 11. Model Pricing 表
conn.execute(
@@ -1108,7 +1108,7 @@ impl Database {
"data_source",
"TEXT NOT NULL DEFAULT 'proxy'",
)?;
Self::create_request_logs_dedup_index_if_supported(conn)?;
Self::create_request_logs_usage_indexes_if_supported(conn)?;
}
// 2. 创建会话日志同步状态表
@@ -1298,6 +1298,13 @@ impl Database {
"0.30",
"3.75",
),
// GPT-5.5 系列
("gpt-5.5", "GPT-5.5", "5", "30", "0.50", "0"),
("gpt-5.5-low", "GPT-5.5", "5", "30", "0.50", "0"),
("gpt-5.5-medium", "GPT-5.5", "5", "30", "0.50", "0"),
("gpt-5.5-high", "GPT-5.5", "5", "30", "0.50", "0"),
("gpt-5.5-xhigh", "GPT-5.5", "5", "30", "0.50", "0"),
("gpt-5.5-minimal", "GPT-5.5", "5", "30", "0.50", "0"),
// GPT-5.4 系列
("gpt-5.4", "GPT-5.4", "2.50", "15", "0.25", "0"),
("gpt-5.4-mini", "GPT-5.4 Mini", "0.75", "4.50", "0.075", "0"),
@@ -1910,11 +1917,22 @@ impl Database {
Ok(())
}
fn create_request_logs_dedup_index_if_supported(conn: &Connection) -> Result<(), AppError> {
fn create_request_logs_usage_indexes_if_supported(conn: &Connection) -> Result<(), AppError> {
if !Self::table_exists(conn, "proxy_request_logs")? {
return Ok(());
}
let has_app_type = Self::has_column(conn, "proxy_request_logs", "app_type")?;
let has_created_at = Self::has_column(conn, "proxy_request_logs", "created_at")?;
if has_app_type && has_created_at {
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_request_logs_app_created_at
ON proxy_request_logs(app_type, created_at DESC)",
[],
)
.map_err(|e| AppError::Database(format!("创建使用量应用时间索引失败: {e}")))?;
}
let required_columns = [
"app_type",
"data_source",
@@ -1930,13 +1948,20 @@ impl Database {
}
}
conn.execute("DROP INDEX IF EXISTS idx_request_logs_dedup_lookup", [])
.map_err(|e| AppError::Database(format!("删除旧使用量去重索引失败: {e}")))?;
// 查询层为了兼容历史 NULL data_source 行,会使用
// COALESCE(data_source, 'proxy')。普通 data_source 索引无法匹配该表达式,
// 会让跨源去重子查询退化成大量扫描;表达式索引让 SQLite 能按同一表达式查找。
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)",
"CREATE INDEX IF NOT EXISTS idx_request_logs_dedup_lookup_expr
ON proxy_request_logs(app_type, COALESCE(data_source, 'proxy'), input_tokens,
output_tokens, cache_read_tokens, created_at,
cache_creation_tokens)",
[],
)
.map_err(|e| AppError::Database(format!("创建使用量去重索引失败: {e}")))?;
.map_err(|e| AppError::Database(format!("创建使用量去重表达式索引失败: {e}")))?;
Ok(())
}