feat(usage): add OpenCode session usage sync (#3215)

* feat(usage): add OpenCode session usage sync

Add OpenCode as a fourth app type in the usage statistics system.
Reads per-message token data from opencode's local SQLite database
(~/.local/share/opencode/opencode.db) and imports into proxy_request_logs.

- New session_usage_opencode.rs module following Codex/Gemini pattern
- Parses assistant message.data JSON for tokens, cost, model
- Adds "opencode" to AppType union and filter tabs
- Updates dedup filters to include opencode_session data_source
- Adds i18n keys for all 4 locales

* fix(usage): add opencode to UsageHero title themes

* fix: respect XDG_DATA_HOME and platform defaults for OpenCode DB path

- Support OPENCODE_DB env var override (absolute and relative paths)
- Use ~/Library/Application Support/opencode/ on macOS
- Use XDG_DATA_HOME/opencode/ when set
- Fall back to ~/.local/share/opencode/ on Linux
- Rename misleading test to test_parse_message_data_ignores_role

* fix(usage): use ~/.local/share/opencode on all platforms

OpenCode relies on xdg-basedir, which ignores macOS/Windows conventions,
so its DB always lives at ~/.local/share/opencode. The previous macOS
default pointed at ~/Library/Application Support/opencode, which does not
exist, making the sync a silent no-op for macOS users without
XDG_DATA_HOME set.

* fix(usage): include opencode -wal mtime in freshness check

OpenCode runs its SQLite DB in WAL mode; new commits land in the -wal
file and the main DB file's mtime only advances on checkpoint. Keying
the freshness gate solely on opencode.db could skip newly written
sessions until a checkpoint occurred. Take the max of the db and -wal
mtimes instead.

* style(usage): apply cargo fmt to opencode session sync

Fixes Backend Checks cargo fmt --check failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(usage): ignore empty OPENCODE_DB and XDG_DATA_HOME env vars

An empty OPENCODE_DB collapsed the path to the data dir (dropping the opencode.db filename); an empty XDG_DATA_HOME produced a relative "opencode" path. Treat empty strings as unset, per the XDG spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(usage): harden OpenCode session sync error handling and labeling

- Map '_opencode_session' provider_id to 'OpenCode (Session)' display name
- Return accurate inserted flag from insert_opencode_message (was always true)
- Do not advance file/session sync_state when a session errors, so failed
  inserts are retried next run instead of being permanently skipped
- Surface per-message insert failures into the sync result errors
- Add opencode_session data-source icon and i18n labels (zh/zh-TW/en/ja)
- Add provider-stats labeling test for opencode session rows

* fix(usage): only import finalized OpenCode messages

An in-progress assistant message holds partial tokens; OpenCode updates the same message_id with final values later. Since request_id is fixed and the insert uses INSERT OR IGNORE, a partial row could never be corrected. Skip messages without time.completed so each turn is imported once with final usage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(usage): keep OpenCode incomplete sessions retryable

---------

Co-authored-by: Eira Hazel <kip3vx9ma@mozmail.com>
Co-authored-by: Eria hazel <git config --global user.email your@email.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@nothingness0db nothingness0db
2026-06-04 22:26:24 +08:00
committed by GitHub
parent 2a24da517f
commit 0527002cca
13 changed files with 688 additions and 12 deletions
+4 -2
View File
@@ -1374,7 +1374,8 @@
"all": "All",
"claude": "Claude Code",
"codex": "Codex",
"gemini": "Gemini"
"gemini": "Gemini",
"opencode": "OpenCode"
},
"rawInputLabel": "Raw",
"dataSources": "Data Sources",
@@ -1383,7 +1384,8 @@
"session_log": "Session Log",
"codex_db": "Codex DB",
"codex_session": "Codex Session",
"gemini_session": "Gemini Session"
"gemini_session": "Gemini Session",
"opencode_session": "OpenCode Session"
},
"sessionSync": {
"trigger": "Sync session logs",
+4 -2
View File
@@ -1374,7 +1374,8 @@
"all": "すべて",
"claude": "Claude Code",
"codex": "Codex",
"gemini": "Gemini"
"gemini": "Gemini",
"opencode": "OpenCode"
},
"rawInputLabel": "原始",
"dataSources": "データソース",
@@ -1383,7 +1384,8 @@
"session_log": "セッションログ",
"codex_db": "Codex DB",
"codex_session": "Codex セッション",
"gemini_session": "Gemini セッション"
"gemini_session": "Gemini セッション",
"opencode_session": "OpenCode セッション"
},
"sessionSync": {
"trigger": "セッションログを同期",
+4 -2
View File
@@ -1320,7 +1320,8 @@
"all": "全部",
"claude": "Claude Code",
"codex": "Codex",
"gemini": "Gemini"
"gemini": "Gemini",
"opencode": "OpenCode"
},
"rawInputLabel": "原始",
"dataSources": "資料來源",
@@ -1329,7 +1330,8 @@
"session_log": "工作階段日誌",
"codex_db": "Codex 資料庫",
"codex_session": "Codex 工作階段日誌",
"gemini_session": "Gemini 工作階段日誌"
"gemini_session": "Gemini 工作階段日誌",
"opencode_session": "OpenCode 工作階段日誌"
},
"sessionSync": {
"trigger": "同步工作階段日誌",
+4 -2
View File
@@ -1374,7 +1374,8 @@
"all": "全部",
"claude": "Claude Code",
"codex": "Codex",
"gemini": "Gemini"
"gemini": "Gemini",
"opencode": "OpenCode"
},
"rawInputLabel": "原始",
"dataSources": "数据来源",
@@ -1383,7 +1384,8 @@
"session_log": "会话日志",
"codex_db": "Codex 数据库",
"codex_session": "Codex 会话日志",
"gemini_session": "Gemini 会话日志"
"gemini_session": "Gemini 会话日志",
"opencode_session": "OpenCode 会话日志"
},
"sessionSync": {
"trigger": "同步会话日志",