diff --git a/src-tauri/src/usage.rs b/src-tauri/src/usage.rs index 72259ee..48d3565 100644 --- a/src-tauri/src/usage.rs +++ b/src-tauri/src/usage.rs @@ -732,7 +732,6 @@ fn persist_queue_items( let records = items .into_iter() .filter(Value::is_object) - .filter(is_generated_usage_event) .map(|item| normalize_usage_record(item, config)) .collect::, _>>()?; if records.is_empty() { @@ -823,34 +822,6 @@ fn insert_usage_records_in_transaction( Ok(inserted) } -fn is_generated_usage_event(value: &Value) -> bool { - if value.get("generate").and_then(Value::as_bool) == Some(false) { - return false; - } - let legacy_websocket_prewarm = value - .get("executor_type") - .and_then(Value::as_str) - .is_some_and(|value| value.trim() == "CodexWebsocketsExecutor") - && value - .get("tokens") - .and_then(Value::as_object) - .is_some_and(|tokens| { - [ - "input_tokens", - "output_tokens", - "reasoning_tokens", - "cached_tokens", - "cache_tokens", - "cache_read_tokens", - "cache_creation_tokens", - "total_tokens", - ] - .into_iter() - .all(|key| u64_field(tokens, key) == 0) - }); - !legacy_websocket_prewarm -} - fn normalize_usage_record(value: Value, config: &GuiConfigFile) -> Result { let object = value .as_object() @@ -2263,6 +2234,71 @@ mod tests { assert!(!record.api_key_hash.is_empty()); } + #[test] + fn persists_successful_health_checks_and_zero_token_websocket_events() { + let root = test_root("queue-events-without-generation"); + initialize_usage_storage_at(&root).unwrap(); + let config = GuiConfigFile::default(); + let inserted = persist_queue_items( + &root, + vec![ + serde_json::json!({ + "timestamp": "2026-07-29T10:00:00+08:00", + "request_id": "cherry-health-check", + "generate": false, + "failed": false, + "provider": "openai", + "model": "gpt-test", + "endpoint": "POST /v1/chat/completions", + "tokens": { + "input_tokens": 1, + "output_tokens": 1, + "total_tokens": 2 + } + }), + serde_json::json!({ + "timestamp": "2026-07-29T10:00:01+08:00", + "request_id": "websocket-zero-token", + "failed": false, + "provider": "codex", + "model": "gpt-test", + "executor_type": "CodexWebsocketsExecutor", + "tokens": { + "input_tokens": 0, + "output_tokens": 0, + "total_tokens": 0 + } + }), + ], + &config, + ) + .unwrap(); + let connection = open_usage_database_at(&root).unwrap(); + let events = load_usage_events( + &connection, + &UsageQuery { + failed: Some(false), + ..UsageQuery::default() + }, + &config, + ) + .unwrap(); + + assert_eq!(inserted, 2); + assert_eq!(events.total, 2); + assert!(events.items.iter().all(|event| !event.failed)); + assert!(events + .items + .iter() + .any(|event| event.request_id == "cherry-health-check")); + assert!(events + .items + .iter() + .any(|event| event.request_id == "websocket-zero-token")); + drop(connection); + fs::remove_dir_all(root).unwrap(); + } + #[test] fn sqlite_storage_uses_wal_and_reference_busy_timeout() { let root = test_root("sqlite-pragmas"); diff --git a/src/styles.css b/src/styles.css index dcb6dd2..f0242cc 100644 --- a/src/styles.css +++ b/src/styles.css @@ -5782,6 +5782,8 @@ input:focus-visible { padding-right: 2px; } +.usage-events-table th:nth-child(3), +.usage-events-table td:nth-child(3), .usage-events-table th:nth-child(4), .usage-events-table td:nth-child(4) { overflow: hidden;