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
+1
View File
@@ -17,6 +17,7 @@ const DATA_SOURCE_ICONS: Record<string, React.ReactNode> = {
codex_db: <Database className="h-3.5 w-3.5" />,
codex_session: <FileText className="h-3.5 w-3.5" />,
gemini_session: <FileText className="h-3.5 w-3.5" />,
hermes_session: <Database className="h-3.5 w-3.5" />,
};
export function DataSourceBar({ refreshIntervalMs }: DataSourceBarProps) {
+1
View File
@@ -141,6 +141,7 @@ export function RequestLogTable({
<SelectItem value="claude">Claude</SelectItem>
<SelectItem value="codex">Codex</SelectItem>
<SelectItem value="gemini">Gemini</SelectItem>
<SelectItem value="hermes">Hermes</SelectItem>
</SelectContent>
</Select>
+1
View File
@@ -35,6 +35,7 @@ const APP_FILTER_OPTIONS: AppTypeFilter[] = [
"claude",
"codex",
"gemini",
"hermes",
];
export function UsageDashboard() {
+4 -2
View File
@@ -1120,7 +1120,8 @@
"all": "All",
"claude": "Claude Code",
"codex": "Codex",
"gemini": "Gemini"
"gemini": "Gemini",
"hermes": "Hermes"
},
"dataSources": "Data Sources",
"dataSource": {
@@ -1128,7 +1129,8 @@
"session_log": "Session Log",
"codex_db": "Codex DB",
"codex_session": "Codex Session",
"gemini_session": "Gemini Session"
"gemini_session": "Gemini Session",
"hermes_session": "Hermes Session"
},
"sessionSync": {
"trigger": "Sync session logs",
+4 -2
View File
@@ -1120,7 +1120,8 @@
"all": "すべて",
"claude": "Claude Code",
"codex": "Codex",
"gemini": "Gemini"
"gemini": "Gemini",
"hermes": "Hermes"
},
"dataSources": "データソース",
"dataSource": {
@@ -1128,7 +1129,8 @@
"session_log": "セッションログ",
"codex_db": "Codex DB",
"codex_session": "Codex セッション",
"gemini_session": "Gemini セッション"
"gemini_session": "Gemini セッション",
"hermes_session": "Hermes セッション"
},
"sessionSync": {
"trigger": "セッションログを同期",
+4 -2
View File
@@ -1120,7 +1120,8 @@
"all": "全部",
"claude": "Claude Code",
"codex": "Codex",
"gemini": "Gemini"
"gemini": "Gemini",
"hermes": "Hermes"
},
"dataSources": "数据来源",
"dataSource": {
@@ -1128,7 +1129,8 @@
"session_log": "会话日志",
"codex_db": "Codex 数据库",
"codex_session": "Codex 会话日志",
"gemini_session": "Gemini 会话日志"
"gemini_session": "Gemini 会话日志",
"hermes_session": "Hermes 会话日志"
},
"sessionSync": {
"trigger": "同步会话日志",
+1 -1
View File
@@ -129,7 +129,7 @@ export interface UsageRangeSelection {
customEndDate?: number;
}
export type AppTypeFilter = "all" | "claude" | "codex" | "gemini";
export type AppTypeFilter = "all" | "claude" | "codex" | "gemini" | "hermes";
export interface StatsFilters {
timeRange: UsageRangePreset;