Skip to content

feature(opencode): support opencode sqlite storage#887

Open
unsync wants to merge 9 commits intoryoppippi:mainfrom
unsync:feature/support-opencode-sqlite
Open

feature(opencode): support opencode sqlite storage#887
unsync wants to merge 9 commits intoryoppippi:mainfrom
unsync:feature/support-opencode-sqlite

Conversation

@unsync
Copy link

@unsync unsync commented Mar 10, 2026

Summary

Initial credits to #850
Duplicate: #879

Why this change was made

OpenCode moved its usage data to opencode.db in newer releases, so ccusage was missing recent OpenCode activity and falling back to stale legacy JSON data.

Description

This PR adds SQLite-backed OpenCode loading with a legacy JSON fallback so the OpenCode reports stay accurate across both old and new OpenCode storage formats.

  • Reads OpenCode usage and session metadata from ~/.local/share/opencode/opencode.db when present, while preserving the existing JSON-file path for older installs.
  • Keeps daily and session reports consistent by sharing one combined SQLite load path instead of re-reading usage and session data separately.
  • Falls back to bun:sqlite when better-sqlite3 is unavailable or cannot open under Bun, which fixes local Bun-based CLI runs.
  • Adds focused loader tests for SQLite parsing, deduplication, timestamp fallback, and session metadata defaults.
  • Adds better-sqlite3 as an optional OpenCode dependency and updates workspace build approval/catalog entries for the new native package.

Blast Radius

  • Affects only the OpenCode CLI package under apps/opencode plus the minimal workspace dependency metadata needed to ship the SQLite reader.
  • Impacts users running ccusage against newer OpenCode installs that now store usage in SQLite instead of JSON files.

Practical Consequence

  • After deploy, OpenCode reports should include recent usage again instead of stopping at the last legacy JSON records.
  • Bun-based local runs continue to work even though Bun cannot use better-sqlite3 directly, because the loader now falls back to bun:sqlite instead of dropping to legacy JSON.

Decisions

  • Kept the change scoped to OpenCode data loading and package metadata instead of refactoring unrelated report logic.
  • Made better-sqlite3 optional and externalized it so Node builds can use it while Bun runtime still has a supported fallback.
  • Preserved the legacy JSON loader as a compatibility path for older OpenCode versions and as a recovery path when SQLite reads fail.

Verification

  • Ran pnpm --filter @ccusage/opencode run test successfully.
  • Ran pnpm --filter ccusage run test successfully.
  • Ran pnpm typecheck successfully.
  • Ran pnpm --filter @ccusage/opencode run start daily successfully under Bun and confirmed it now prints a real report instead of falling back after a SQLite open failure.

Summary by CodeRabbit

  • New Features

    • Support for faster OpenCode data loading from local SQLite databases (OpenCode ≥ 1.2.2) with automatic fallback to legacy JSON for compatibility.
  • Chores

    • Enabled optional SQLite runtime support and updated build config to allow bundling the SQLite runtime when present.
  • Tests

    • Added tests and test helpers covering SQLite loading, deduplication, filtering, timestamp handling, and session metadata.

unsync added 2 commits March 10, 2026 08:48
Add support for OpenCode >= 1.2.2 SQLite database format while maintaining backward compatibility with legacy JSON files. Adds better-sqlite3 as optional dependency with Bun SQLite fallback.
@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

SQLite-first loading for OpenCode (>=1.2.2) was added: code now locates and reads a local opencode.db via runtime adapters (better-sqlite3 or bun:sqlite), parses/deduplicates/validates messages and sessions, and falls back to legacy JSON on DB errors; build configs were updated to allow/externalize better-sqlite3.

Changes

Cohort / File(s) Summary
Package & workspace config
apps/opencode/package.json, pnpm-workspace.yaml, apps/opencode/tsdown.config.ts
Adds better-sqlite3 as an optional/runtime dependency, marks it external in tsdown config, updates workspace allowBuilds to include better-sqlite3 and removes sqlite3.
OpenCode data loader
apps/opencode/src/data-loader.ts
Adds SQLite-first loading (getDbPath, openSqliteDb, SqliteAdapter abstraction), implementations to load messages and sessions from SQLite with dedupe/validation/timestamp fallbacks, JSON-based loaders kept for fallback, logging for SQLite fallback, internal types for DB adapters, and test scaffolding including a test SqliteAdapter helper.

Sequence Diagram

sequenceDiagram
    participant Caller as Caller
    participant Loader as loadOpenCodeMessages/Sessions
    participant DbPath as getDbPath
    participant Sqlite as SqliteAdapter
    participant JSON as JSON Loader

    Caller->>Loader: Request OpenCode data
    Loader->>DbPath: Locate opencode.db
    DbPath-->>Loader: DB path or null

    alt DB path found
        Loader->>Sqlite: openSqliteDb & run queries
        Sqlite-->>Loader: rows
        alt parse & validate success
            Loader->>Loader: dedupe, map to LoadedUsage/Metadata
            Loader-->>Caller: return processed data
        else parse/error
            Loader->>Loader: logSqliteFallback
            Loader->>JSON: load from JSON files
            JSON-->>Caller: return JSON-based data
        end
    else no DB
        Loader->>JSON: load from JSON files
        JSON-->>Caller: return JSON-based data
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I nibble rows from opencode.db bright,
parse and dedupe by soft moonlight,
if SQLite trips, I scamper back to JSON,
hop, retry, and fetch — a tidy little mission,
carrots for code and quiet delight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately reflects the main change: adding SQLite storage support for OpenCode data loading alongside existing JSON fallback logic.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@socket-security
Copy link

socket-security bot commented Mar 10, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedbetter-sqlite3@​12.6.210010010088100

View full report

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
apps/opencode/src/commands/session.ts (1)

14-14: Consider migrating daily.ts to the unified API for consistency.

Per the context snippet, apps/opencode/src/commands/daily.ts still imports loadOpenCodeMessages() separately. While this works (the old API is preserved), migrating it to loadOpenCodeData() would provide consistent behavior and avoid redundant DB reads if both commands are invoked in sequence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/opencode/src/commands/session.ts` at line 14, The daily command still
uses the old function loadOpenCodeMessages(), causing inconsistent behavior and
potential redundant DB reads; update apps/opencode/src/commands/daily.ts to
import and call the unified loader loadOpenCodeData() instead (replace any
import of loadOpenCodeMessages with loadOpenCodeData and adapt usage to the
returned shape), and ensure any downstream code in daily.ts that expects the old
structure is adjusted to the new data shape returned by loadOpenCodeData() so
both session.ts and daily.ts use the same unified API.
apps/opencode/src/data-loader.ts (1)

496-512: Standalone loaders wastefully load the entire database.

Both loadOpenCodeSessions() and loadOpenCodeMessages() call loadOpenCodeDataFromSqlite() which loads both messages and sessions, then discards half the result. If both functions are called separately, the DB is read and parsed twice.

Consider either:

  1. Deprecating these in favor of loadOpenCodeData() (already the preferred API)
  2. Creating targeted loadMessagesFromSqlite/loadSessionsFromSqlite wrappers that don't load unnecessary data

This is a minor efficiency concern since loadOpenCodeData() is the recommended API.

Also applies to: 514-530

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/opencode/src/data-loader.ts` around lines 496 - 512, The current loaders
loadOpenCodeSessions and loadOpenCodeMessages both call
loadOpenCodeDataFromSqlite which parses the entire DB twice; change them to
avoid redundant work by either (A) delegating to the unified loadOpenCodeData()
API and returning the appropriate map/message list from that single result, or
(B) add targeted sqlite helpers (e.g., loadSessionsFromSqlite and
loadMessagesFromSqlite) that query only sessions or messages respectively and
call those from loadOpenCodeSessions/loadOpenCodeMessages; update error handling
to still call logSqliteFallback('sessions'/'messages', error) as appropriate and
keep the JSON fallback
(loadOpenCodeSessionsFromJson/loadOpenCodeMessagesFromJson) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/opencode/src/commands/session.ts`:
- Line 14: The daily command still uses the old function loadOpenCodeMessages(),
causing inconsistent behavior and potential redundant DB reads; update
apps/opencode/src/commands/daily.ts to import and call the unified loader
loadOpenCodeData() instead (replace any import of loadOpenCodeMessages with
loadOpenCodeData and adapt usage to the returned shape), and ensure any
downstream code in daily.ts that expects the old structure is adjusted to the
new data shape returned by loadOpenCodeData() so both session.ts and daily.ts
use the same unified API.

In `@apps/opencode/src/data-loader.ts`:
- Around line 496-512: The current loaders loadOpenCodeSessions and
loadOpenCodeMessages both call loadOpenCodeDataFromSqlite which parses the
entire DB twice; change them to avoid redundant work by either (A) delegating to
the unified loadOpenCodeData() API and returning the appropriate map/message
list from that single result, or (B) add targeted sqlite helpers (e.g.,
loadSessionsFromSqlite and loadMessagesFromSqlite) that query only sessions or
messages respectively and call those from
loadOpenCodeSessions/loadOpenCodeMessages; update error handling to still call
logSqliteFallback('sessions'/'messages', error) as appropriate and keep the JSON
fallback (loadOpenCodeSessionsFromJson/loadOpenCodeMessagesFromJson) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9e21053a-ae27-4b7e-bfa3-a30793c65fae

📥 Commits

Reviewing files that changed from the base of the PR and between e75ad4f and c3369a0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • apps/opencode/package.json
  • apps/opencode/src/commands/session.ts
  • apps/opencode/src/data-loader.ts
  • apps/opencode/tsdown.config.ts
  • pnpm-workspace.yaml

@toliner
Copy link

toliner commented Mar 10, 2026

It works on my environment, great job!

toliner:~/project/oss/ccusage/apps/opencode$ gh pr checkout 887
remote: Enumerating objects: 258, done.
remote: Counting objects: 100% (182/182), done.
remote: Compressing objects: 100% (69/69), done.
remote: Total 258 (delta 154), reused 116 (delta 113), pack-reused 76 (from 2)
Receiving objects: 100% (258/258), 196.92 KiB | 2.52 MiB/s, done.
Resolving deltas: 100% (186/186), completed with 4 local objects.
From https://github.com/ryoppippi/ccusage
 * [new ref]         refs/pull/887/head -> feature/support-opencode-sqlite
Switched to branch 'feature/support-opencode-sqlite'
toliner:~/project/oss/ccusage/apps/opencode$ pnpm run start

> @ccusage/opencode@18.0.10 start /home/toliner/project/oss/ccusage/apps/opencode
> bun ./src/index.ts


 WARN  Fetching latest model pricing from LiteLLM...                                                                                                                                                                          @ccusage/opencode 6:02:16 PM

ℹ Loaded pricing for 2696 models                                                                                                                                                                                             @ccusage/opencode 6:02:16 PM

📊 OpenCode Token Usage Report - Daily

┌────────────┬─────────────────────────────────────────────────────────────────┬────────────┬────────────┬───────────────┬─────────────┬───────────────┬─────────────┐
│ Date       │ Models                                                          │      Input │     Output │  Cache Create │  Cache Read │  Total Tokens │  Cost (USD) │
├────────────┼─────────────────────────────────────────────────────────────────┼────────────┼────────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ 2026-01-29 │ - claude-sonnet-4.5                                             │    148,377 │     68,131 │       257,673 │   3,876,125 │     4,350,306 │       $0.47 │
│            │ - gpt-5.1-codex-max                                             │            │            │               │             │               │             │
├────────────┼─────────────────────────────────────────────────────────────────┼────────────┼────────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ 2026-02-02 │ - gpt-5.2-codex                                                 │  2,692,252 │    256,750 │             0 │   7,949,184 │    10,898,186 │       $9.70 │
├────────────┼─────────────────────────────────────────────────────────────────┼────────────┼────────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ 2026-02-03 │ - claude-sonnet-4.5                                             │    271,910 │     29,817 │             0 │   1,594,163 │     1,895,890 │       $0.00 │
├────────────┼─────────────────────────────────────────────────────────────────┼────────────┼────────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ 2026-02-05 │ - gpt-5.2-codex                                                 │    104,009 │     18,973 │             0 │     130,944 │       253,926 │       $0.47 │
├────────────┼─────────────────────────────────────────────────────────────────┼────────────┼────────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ 2026-02-09 │ - gpt-5.2-codex                                                 │    432,350 │    105,855 │             0 │   2,835,712 │     3,373,917 │       $2.73 │
├────────────┼─────────────────────────────────────────────────────────────────┼────────────┼────────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ 2026-02-24 │ - gpt-5.2-codex                                                 │    463,809 │     31,751 │             0 │     378,368 │       873,928 │       $1.32 │
├────────────┼─────────────────────────────────────────────────────────────────┼────────────┼────────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ 2026-02-26 │ - gpt-5.2-codex                                                 │    337,000 │     10,617 │             0 │     199,168 │       546,785 │       $0.77 │
├────────────┼─────────────────────────────────────────────────────────────────┼────────────┼────────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ 2026-03-02 │ - claude-sonnet-4.6                                             │    673,369 │     29,072 │             0 │     912,130 │     1,614,571 │       $2.51 │
│            │ - gpt-5-mini                                                    │            │            │               │             │               │             │
├────────────┼─────────────────────────────────────────────────────────────────┼────────────┼────────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ 2026-03-03 │ - claude-opus-4.6                                               │    363,384 │     15,846 │             0 │     437,865 │       817,095 │       $0.78 │
│            │ - gpt-5.2                                                       │            │            │               │             │               │             │
│            │ - gpt-5.2-codex                                                 │            │            │               │             │               │             │
├────────────┼─────────────────────────────────────────────────────────────────┼────────────┼────────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ 2026-03-04 │ - claude-sonnet-4.6                                             │  1,365,215 │    124,667 │             0 │   1,279,797 │     2,769,679 │       $5.88 │
│            │ - gpt-5-mini                                                    │            │            │               │             │               │             │
│            │ - gpt-5.2-codex                                                 │            │            │               │             │               │             │
├────────────┼─────────────────────────────────────────────────────────────────┼────────────┼────────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ 2026-03-05 │ - claude-opus-4.6                                               │  1,324,364 │    377,139 │             0 │   6,995,205 │     8,696,708 │       $6.76 │
│            │ - claude-sonnet-4.6                                             │            │            │               │             │               │             │
│            │ - gpt-5-mini                                                    │            │            │               │             │               │             │
│            │ - gpt-5.2-codex                                                 │            │            │               │             │               │             │
├────────────┼─────────────────────────────────────────────────────────────────┼────────────┼────────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ Total      │                                                                 │  8,176,039 │  1,068,618 │       257,673 │  26,588,661 │    36,090,991 │      $31.40 │
└────────────┴─────────────────────────────────────────────────────────────────┴────────────┴────────────┴───────────────┴─────────────┴───────────────┴─────────────┘

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
apps/opencode/src/data-loader.ts (3)

198-201: Export LoadedOpenCodeData type for consumer use.

This type is the return type of the exported loadOpenCodeData() function. Exporting it allows consumers to properly type their variables without relying on type inference.

♻️ Proposed fix
-type LoadedOpenCodeData = {
+export type LoadedOpenCodeData = {
 	entries: LoadedUsageEntry[];
 	sessionMetadataMap: Map<string, LoadedSessionMetadata>;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/opencode/src/data-loader.ts` around lines 198 - 201, The type
LoadedOpenCodeData is used as the return type of the exported function
loadOpenCodeData but is currently not exported; export the type so downstream
consumers can import it. Update the type declaration for LoadedOpenCodeData to
be exported (e.g., add the export modifier) and ensure any related types
(LoadedUsageEntry, LoadedSessionMetadata) are also exported or available to
consumers as needed so consumers can properly type variables using
loadOpenCodeData().

485-485: Simplify deduplication key.

The template literal is unnecessary here since message.id is already a string.

♻️ Proposed fix
-		const dedupeKey = `${message.id}`;
+		const dedupeKey = message.id;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/opencode/src/data-loader.ts` at line 485, Replace the unnecessary
template literal when building the deduplication key: change the assignment to
use message.id directly (e.g., set dedupeKey = message.id) in the code that
defines dedupeKey so you avoid creating a needless string interpolation; if
there's any type uncertainty, use String(message.id) to ensure a string.

532-566: Inefficient: loading all data when only one part is needed.

Both loadOpenCodeSessions() and loadOpenCodeMessages() call loadOpenCodeDataFromSqlite() which loads both messages and sessions from the database, then discards one part. This wastes I/O and memory, especially for large databases.

Consider either:

  1. Creating separate loadMessagesOnlyFromSqlite and loadSessionsOnlyFromSqlite wrappers, or
  2. Accepting the inefficiency if these functions are rarely called independently (the daily.ts command only uses loadOpenCodeMessages, while session.ts uses loadOpenCodeData)
♻️ Proposed fix: separate SQLite loading paths
+function loadMessagesOnlyFromSqlite(dbPath: string): LoadedUsageEntry[] {
+	const db = openSqliteDb(dbPath);
+	try {
+		return loadMessagesFromSqlite(db);
+	} finally {
+		db.close();
+	}
+}
+
+function loadSessionsOnlyFromSqlite(dbPath: string): Map<string, LoadedSessionMetadata> {
+	const db = openSqliteDb(dbPath);
+	try {
+		return loadSessionsFromSqlite(db);
+	} finally {
+		db.close();
+	}
+}
+
 export async function loadOpenCodeSessions(): Promise<Map<string, LoadedSessionMetadata>> {
 	const openCodePath = getOpenCodePath();
 	if (openCodePath == null) {
 		return new Map();
 	}

 	const dbPath = getDbPath(openCodePath);
 	if (dbPath != null) {
 		try {
-			return loadOpenCodeDataFromSqlite(dbPath).sessionMetadataMap;
+			return loadSessionsOnlyFromSqlite(dbPath);
 		} catch (error) {
 			logSqliteFallback('sessions', error);
 		}
 	}

 	return loadOpenCodeSessionsFromJson(openCodePath);
 }

 export async function loadOpenCodeMessages(): Promise<LoadedUsageEntry[]> {
 	const openCodePath = getOpenCodePath();
 	if (openCodePath == null) {
 		return [];
 	}

 	const dbPath = getDbPath(openCodePath);
 	if (dbPath != null) {
 		try {
-			return loadOpenCodeDataFromSqlite(dbPath).entries;
+			return loadMessagesOnlyFromSqlite(dbPath);
 		} catch (error) {
 			logSqliteFallback('messages', error);
 		}
 	}

 	return loadOpenCodeMessagesFromJson(openCodePath);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/opencode/src/data-loader.ts` around lines 532 - 566, Both loaders call
loadOpenCodeDataFromSqlite() which reads both messages and sessions then
discards one half, causing unnecessary I/O/memory; change the SQLite path so
each loader only reads what it needs: add two targeted helpers (e.g.
loadMessagesOnlyFromSqlite(dbPath) and loadSessionsOnlyFromSqlite(dbPath)) or
extend loadOpenCodeDataFromSqlite to accept a mode flag ('messages' |
'sessions'), implement the SQL queries to return only the needed rows, then
update loadOpenCodeMessages() to call the messages-only helper and
loadOpenCodeSessions() to call the sessions-only helper (keeping the same error
fallback to logSqliteFallback and JSON fallbacks).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/opencode/src/data-loader.ts`:
- Around line 234-255: The openSqliteDb function currently unconditionally
requires 'bun:sqlite' which will throw in Node.js when better-sqlite3 is
unavailable; update openSqliteDb to detect Bun before loading Bun's SQLite
adapter (e.g., check typeof globalThis.Bun !== 'undefined' or
process?.versions?.bun) and only call require('bun:sqlite') when running under
Bun; if not running under Bun, avoid the require and return null or propagate a
controlled error so the caller can fall back to the JSON path; adjust the return
type/handling accordingly and keep references to requireBetterSqlite3,
createBetterSqliteAdapter and createBunSqliteAdapter intact.

---

Nitpick comments:
In `@apps/opencode/src/data-loader.ts`:
- Around line 198-201: The type LoadedOpenCodeData is used as the return type of
the exported function loadOpenCodeData but is currently not exported; export the
type so downstream consumers can import it. Update the type declaration for
LoadedOpenCodeData to be exported (e.g., add the export modifier) and ensure any
related types (LoadedUsageEntry, LoadedSessionMetadata) are also exported or
available to consumers as needed so consumers can properly type variables using
loadOpenCodeData().
- Line 485: Replace the unnecessary template literal when building the
deduplication key: change the assignment to use message.id directly (e.g., set
dedupeKey = message.id) in the code that defines dedupeKey so you avoid creating
a needless string interpolation; if there's any type uncertainty, use
String(message.id) to ensure a string.
- Around line 532-566: Both loaders call loadOpenCodeDataFromSqlite() which
reads both messages and sessions then discards one half, causing unnecessary
I/O/memory; change the SQLite path so each loader only reads what it needs: add
two targeted helpers (e.g. loadMessagesOnlyFromSqlite(dbPath) and
loadSessionsOnlyFromSqlite(dbPath)) or extend loadOpenCodeDataFromSqlite to
accept a mode flag ('messages' | 'sessions'), implement the SQL queries to
return only the needed rows, then update loadOpenCodeMessages() to call the
messages-only helper and loadOpenCodeSessions() to call the sessions-only helper
(keeping the same error fallback to logSqliteFallback and JSON fallbacks).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0eb2a4d-87d3-42b9-91ea-376502905421

📥 Commits

Reviewing files that changed from the base of the PR and between c3369a0 and 0cf867b.

📒 Files selected for processing (1)
  • apps/opencode/src/data-loader.ts

unsync added 2 commits March 10, 2026 10:08
Replaces unified loadOpenCodeData() with separate loadOpenCodeMessages() and loadOpenCodeSessions() functions, allowing parallel loading of data sources.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
apps/opencode/src/data-loader.ts (1)

234-255: ⚠️ Potential issue | 🟠 Major

Guard the Bun-only fallback before requiring bun:sqlite.

If better-sqlite3 is unavailable under Node.js, Line 251 throws before any SQLite data can load. On installs that only have opencode.db, the later JSON fallback returns empty results. This same Node/Bun gap was raised earlier and is still present.

🛡️ Proposed fix
 	if (BetterSqlite3 != null) {
 		try {
 			return createBetterSqliteAdapter(new BetterSqlite3(dbPath, { readonly: true }));
 		} catch {
 			// Fall back to Bun's SQLite adapter when better-sqlite3 cannot open in Bun.
 		}
 	}
 
+	if (typeof globalThis.Bun === "undefined" && process.versions.bun == null) {
+		throw new Error("SQLite adapter unavailable: better-sqlite3 is not installed and bun:sqlite is not available");
+	}
+
 	const { Database } = require("bun:sqlite") as {
 		Database: new (path: string, opts?: { readonly?: boolean }) => BunSqliteDatabase;
 	};

Run this to confirm the missing Bun runtime guard. Expected result: require("bun:sqlite") is present in openSqliteDb, and there is no globalThis.Bun/process.versions.bun check in that function.

#!/bin/bash
set -euo pipefail

sed -n '234,255p' apps/opencode/src/data-loader.ts
rg -n "bun:sqlite|globalThis\.Bun|process\.versions\.bun" apps/opencode/src/data-loader.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/opencode/src/data-loader.ts` around lines 234 - 255, The openSqliteDb
function currently unconditionally requires 'bun:sqlite', which will throw under
Node when Bun is not present; modify openSqliteDb to first detect the Bun
runtime (check globalThis.Bun or process.versions?.bun) before calling
require('bun:sqlite'), so only require Bun's Database when running in Bun; keep
the existing requireBetterSqlite3()/createBetterSqliteAdapter flow and on
non-Bun runtimes fall back to the JSON/other fallback instead of attempting
require('bun:sqlite'); ensure references to require('bun:sqlite'), Database,
createBunSqliteAdapter, and requireBetterSqlite3 are guarded by the Bun runtime
check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/opencode/src/data-loader.ts`:
- Around line 521-555: Both loaders independently call
loadOpenCodeDataFromSqlite(dbPath) causing duplicate reads; refactor to a single
shared loader that returns the full snapshot (sessions + entries) and have
loadOpenCodeSessions and loadOpenCodeMessages delegate to it. Add a new function
(e.g. loadOpenCodeData or loadOpenCodeSnapshot) that: resolves
openCodePath/getDbPath, tries loadOpenCodeDataFromSqlite(dbPath) once (logging
via logSqliteFallback on error) and falls back to calling
loadOpenCodeSessionsFromJson/loadOpenCodeMessagesFromJson to build the combined
result; then update loadOpenCodeSessions and loadOpenCodeMessages to call that
shared loader and return only the sessionMetadataMap or entries. Optionally
memoize the snapshot by openCodePath/dbPath to avoid repeated reads within the
same process.
- Around line 294-296: The current filter in data-loader.ts skips rows when
data.tokens is null or tokens.input and tokens.output are zero, which drops rows
that only have cache usage; update the check around data.tokens (the block using
data.tokens.input and data.tokens.output) to treat a row as empty only if all
four fields are absent or zero — tokens.input, tokens.output, tokens.cache.read,
and tokens.cache.write — so rows with cache.read or cache.write > 0 are
preserved; ensure you reference and safely access data.tokens.cache.read and
data.tokens.cache.write when performing the emptiness check.
- Around line 270-286: The dedupe logic currently marks row IDs in dedupeSet
before JSON.parse and v.safeParse, causing malformed or filtered rows to prevent
later valid duplicates from being processed; update the loop so you first
attempt JSON.parse(row.data) and validate with
v.safeParse(sqliteMessageDataSchema), and only after result.success do you check
dedupeSet.has(row.id) and then add row.id to dedupeSet (ensuring you continue
appropriately on parse/validation failure).

---

Duplicate comments:
In `@apps/opencode/src/data-loader.ts`:
- Around line 234-255: The openSqliteDb function currently unconditionally
requires 'bun:sqlite', which will throw under Node when Bun is not present;
modify openSqliteDb to first detect the Bun runtime (check globalThis.Bun or
process.versions?.bun) before calling require('bun:sqlite'), so only require
Bun's Database when running in Bun; keep the existing
requireBetterSqlite3()/createBetterSqliteAdapter flow and on non-Bun runtimes
fall back to the JSON/other fallback instead of attempting
require('bun:sqlite'); ensure references to require('bun:sqlite'), Database,
createBunSqliteAdapter, and requireBetterSqlite3 are guarded by the Bun runtime
check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e1d40500-d01c-480e-bb1e-38f763eb569b

📥 Commits

Reviewing files that changed from the base of the PR and between 0cf867b and f667097.

📒 Files selected for processing (1)
  • apps/opencode/src/data-loader.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/opencode/src/data-loader.ts (1)

502-505: ⚠️ Potential issue | 🟠 Major

Same cache-only filtering issue in JSON loader.

Consistent with the SQLite path, this filter also drops rows that only have cache tokens but zero input/output tokens.

🧮 Proposed fix
 		// Skip messages with no tokens
-		if (message.tokens == null || (message.tokens.input === 0 && message.tokens.output === 0)) {
+		const hasUsage =
+			message.tokens != null &&
+			((message.tokens.input ?? 0) > 0 ||
+				(message.tokens.output ?? 0) > 0 ||
+				(message.tokens.cache?.read ?? 0) > 0 ||
+				(message.tokens.cache?.write ?? 0) > 0);
+		if (!hasUsage) {
 			continue;
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/opencode/src/data-loader.ts` around lines 502 - 505, The current filter
in the JSON loader skips messages when message.tokens.input and
message.tokens.output are both zero even if message.tokens.cache exists; update
the condition around message.tokens in the loop to only skip when tokens are
null or all token counts are zero (input, output and cache). Concretely, modify
the if that references message.tokens so it continues only when message.tokens
== null || (message.tokens.input === 0 && message.tokens.output === 0 &&
(message.tokens.cache == null || message.tokens.cache === 0)), leaving the rest
of the loader logic unchanged.
♻️ Duplicate comments (2)
apps/opencode/src/data-loader.ts (2)

304-306: ⚠️ Potential issue | 🟠 Major

Cache-only usage rows are incorrectly filtered out.

This condition drops rows that have cache.read or cache.write tokens but zero input/output tokens, causing cache-only usage to be undercounted.

As per coding guidelines, "Message structure must include tokens.input, tokens.output, tokens.cache.read, and tokens.cache.write fields".

🧮 Proposed fix: include cache tokens in emptiness check
-		if (data.tokens == null || (data.tokens.input === 0 && data.tokens.output === 0)) {
+		const hasUsage =
+			data.tokens != null &&
+			((data.tokens.input ?? 0) > 0 ||
+				(data.tokens.output ?? 0) > 0 ||
+				(data.tokens.cache?.read ?? 0) > 0 ||
+				(data.tokens.cache?.write ?? 0) > 0);
+		if (!hasUsage) {
 			continue;
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/opencode/src/data-loader.ts` around lines 304 - 306, The current filter
wrongly skips rows when tokens.input and tokens.output are zero even though
cache usage exists; update the emptiness check around data.tokens to treat a row
as empty only if all four fields are zero (tokens.input, tokens.output,
tokens.cache.read, tokens.cache.write) or tokens is null/undefined. Locate the
branch that uses data.tokens (the if checking data.tokens == null ||
(data.tokens.input === 0 && data.tokens.output === 0)) and modify it to include
tokens.cache.read and tokens.cache.write in the emptiness test so cache-only
rows are kept.

280-284: ⚠️ Potential issue | 🟠 Major

Dedupe check should occur after parsing succeeds.

The deduplication currently marks an ID as seen before JSON parsing and validation. If the first row with a given ID is malformed or fails validation, any later valid duplicate with the same ID will be incorrectly skipped.

🐛 Proposed fix: move dedupe after validation
 	for (const row of rows) {
-		if (dedupeSet.has(row.id)) {
-			continue;
-		}
-		dedupeSet.add(row.id);
-
 		let parsed: unknown;
 		try {
 			parsed = JSON.parse(row.data);
 		} catch {
 			continue;
 		}
 
 		const result = v.safeParse(sqliteMessageDataSchema, parsed);
 		if (!result.success) {
 			continue;
 		}
 
 		const data = result.output;
 
 		if (data.role !== 'assistant') {
 			continue;
 		}
 
-		if (data.tokens == null || (data.tokens.input === 0 && data.tokens.output === 0)) {
+		const hasUsage =
+			data.tokens != null &&
+			((data.tokens.input ?? 0) > 0 ||
+				(data.tokens.output ?? 0) > 0 ||
+				(data.tokens.cache?.read ?? 0) > 0 ||
+				(data.tokens.cache?.write ?? 0) > 0);
+		if (!hasUsage) {
 			continue;
 		}
 
 		if (data.providerID == null || data.modelID == null) {
 			continue;
 		}
 
+		if (dedupeSet.has(row.id)) {
+			continue;
+		}
+		dedupeSet.add(row.id);
+
 		const createdMs = data.time?.created ?? row.time_created;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/opencode/src/data-loader.ts` around lines 280 - 284, The dedupeSet check
is happening before JSON parsing/validation so a malformed first row can wrongly
block later valid duplicates; move the dedupeSet.has(row.id) check and
dedupeSet.add(row.id) to occur only after the row has been successfully parsed
and validated (i.e., after your parsing/validation logic for each row succeeds),
and ensure any parse/validation errors skip adding the id so later valid rows
with the same row.id are processed normally.
🧹 Nitpick comments (1)
apps/opencode/src/data-loader.ts (1)

594-624: Consider using valid LiteLLM model names in tests.

Lines 599 and 623 use 'claude-sonnet-4-5' which doesn't match LiteLLM's pricing database entries. While this test only validates pass-through behavior, using consistent model names like 'claude-sonnet-4-20250514' would align with the coding guidelines.

♻️ Suggested fix
-				modelID: 'claude-sonnet-4-5' as v.InferOutput<typeof modelNameSchema>,
+				modelID: 'claude-sonnet-4-20250514' as v.InferOutput<typeof modelNameSchema>,
 ...
-			expect(entry.model).toBe('claude-sonnet-4-5');
+			expect(entry.model).toBe('claude-sonnet-4-20250514');

As per coding guidelines, "All test files must use current Claude 4 models (claude-sonnet-4-20250514, claude-opus-4-20250514), not outdated Claude 3 models".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/opencode/src/data-loader.ts` around lines 594 - 624, The test data uses
an outdated model name 'claude-sonnet-4-5'; update the test to use a current
LiteLLM Claude 4 model name (e.g. 'claude-sonnet-4-20250514') so the
convertOpenCodeMessageToUsageEntry test uses a valid model string—replace the
modelID in the test message object passed to convertOpenCodeMessageToUsageEntry
and update the final expect that checks entry.model accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/opencode/src/data-loader.ts`:
- Around line 502-505: The current filter in the JSON loader skips messages when
message.tokens.input and message.tokens.output are both zero even if
message.tokens.cache exists; update the condition around message.tokens in the
loop to only skip when tokens are null or all token counts are zero (input,
output and cache). Concretely, modify the if that references message.tokens so
it continues only when message.tokens == null || (message.tokens.input === 0 &&
message.tokens.output === 0 && (message.tokens.cache == null ||
message.tokens.cache === 0)), leaving the rest of the loader logic unchanged.

---

Duplicate comments:
In `@apps/opencode/src/data-loader.ts`:
- Around line 304-306: The current filter wrongly skips rows when tokens.input
and tokens.output are zero even though cache usage exists; update the emptiness
check around data.tokens to treat a row as empty only if all four fields are
zero (tokens.input, tokens.output, tokens.cache.read, tokens.cache.write) or
tokens is null/undefined. Locate the branch that uses data.tokens (the if
checking data.tokens == null || (data.tokens.input === 0 && data.tokens.output
=== 0)) and modify it to include tokens.cache.read and tokens.cache.write in the
emptiness test so cache-only rows are kept.
- Around line 280-284: The dedupeSet check is happening before JSON
parsing/validation so a malformed first row can wrongly block later valid
duplicates; move the dedupeSet.has(row.id) check and dedupeSet.add(row.id) to
occur only after the row has been successfully parsed and validated (i.e., after
your parsing/validation logic for each row succeeds), and ensure any
parse/validation errors skip adding the id so later valid rows with the same
row.id are processed normally.

---

Nitpick comments:
In `@apps/opencode/src/data-loader.ts`:
- Around line 594-624: The test data uses an outdated model name
'claude-sonnet-4-5'; update the test to use a current LiteLLM Claude 4 model
name (e.g. 'claude-sonnet-4-20250514') so the convertOpenCodeMessageToUsageEntry
test uses a valid model string—replace the modelID in the test message object
passed to convertOpenCodeMessageToUsageEntry and update the final expect that
checks entry.model accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3437e751-1291-4966-958d-7ef28d73eb68

📥 Commits

Reviewing files that changed from the base of the PR and between f667097 and fe0892b.

📒 Files selected for processing (1)
  • apps/opencode/src/data-loader.ts

Replaces object return type with function overloads that return messages or sessions based on mode parameter, enabling selective data loading.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/opencode/src/data-loader.ts (1)

509-511: ⚠️ Potential issue | 🟡 Minor

Apply the same cache-only usage fix to the JSON loader.

For consistency with the SQLite loader fix, this filter should also consider cache.read and cache.write fields to avoid undercounting cached-token usage.

♻️ Proposed fix
 		// Skip messages with no tokens
-		if (message.tokens == null || (message.tokens.input === 0 && message.tokens.output === 0)) {
+		const hasUsage =
+			message.tokens != null &&
+			((message.tokens.input ?? 0) > 0 ||
+				(message.tokens.output ?? 0) > 0 ||
+				(message.tokens.cache?.read ?? 0) > 0 ||
+				(message.tokens.cache?.write ?? 0) > 0);
+		if (!hasUsage) {
 			continue;
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/opencode/src/data-loader.ts` around lines 509 - 511, The JSON loader
currently skips messages when message.tokens is null or both input/output tokens
are zero, which undercounts cached usage; modify the filter to also check
message.cache.read and message.cache.write so you only continue when tokens are
null or both tokens.input and tokens.output are zero AND (message.cache.read ||
message.cache.write) are falsy/zero. Update the conditional around the "message"
check (the block using message.tokens) to include these cache fields
(message.cache.read, message.cache.write) so cached-only messages aren't
skipped—mirror the same logic used in the SQLite loader fix.
♻️ Duplicate comments (1)
apps/opencode/src/data-loader.ts (1)

277-293: ⚠️ Potential issue | 🟠 Major

Deduplicate only after a row survives parsing and validation.

The dedupe check at lines 278-281 marks an ID as seen before JSON parsing and validation. If the first occurrence of an ID is malformed or fails validation, a later valid duplicate will be incorrectly skipped.

♻️ Proposed fix
 	for (const row of rows) {
-		if (dedupeSet.has(row.id)) {
-			continue;
-		}
-		dedupeSet.add(row.id);
-
 		let parsed: unknown;
 		try {
 			parsed = JSON.parse(row.data);
 		} catch {
 			continue;
 		}

 		const result = v.safeParse(sqliteMessageDataSchema, parsed);
 		if (!result.success) {
 			continue;
 		}

 		const data = result.output;

 		if (data.role !== 'assistant') {
 			continue;
 		}

-		if (data.tokens == null || (data.tokens.input === 0 && data.tokens.output === 0)) {
+		const hasUsage =
+			data.tokens != null &&
+			((data.tokens.input ?? 0) > 0 ||
+				(data.tokens.output ?? 0) > 0 ||
+				(data.tokens.cache?.read ?? 0) > 0 ||
+				(data.tokens.cache?.write ?? 0) > 0);
+		if (!hasUsage) {
 			continue;
 		}

 		if (data.providerID == null || data.modelID == null) {
 			continue;
 		}

+		if (dedupeSet.has(row.id)) {
+			continue;
+		}
+		dedupeSet.add(row.id);
+
 		const createdMs = data.time?.created ?? row.time_created;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/opencode/src/data-loader.ts` around lines 277 - 293, The dedupeSet check
currently runs before parsing/validation so malformed or invalid first-seen rows
can cause later valid duplicates to be skipped; update the loop in data-loader
(the for (const row of rows) block) to perform JSON.parse and
v.safeParse(sqliteMessageDataSchema, parsed) first, and only after
result.success add row.id to dedupeSet and skip duplicates using
dedupeSet.has(row.id); ensure you still continue on parse or validation failures
and only mark IDs as seen when validation passes.
🧹 Nitpick comments (1)
apps/opencode/src/data-loader.ts (1)

655-748: Test doesn't cover the dedupe edge case with invalid-then-valid duplicates.

The test verifies that duplicate IDs are deduplicated when both rows are valid (the second is dropped). However, it doesn't cover the scenario where the first occurrence fails validation but a later duplicate with the same ID is valid—which would incorrectly be skipped with the current dedupe logic.

Consider adding a test case where the first row for an ID has invalid data (e.g., missing required fields or role='user') and a second row with the same ID is valid, then verify the valid row is kept.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/opencode/src/data-loader.ts` around lines 655 - 748, Add a new unit test
for loadMessagesFromSqlite that verifies the dedupe logic handles an
invalid-first then valid-second duplicate: use createSqliteAdapter to seed
messageRows where one ID appears twice with the first row containing invalid
JSON or role='user' (so it would be filtered/invalid) and the second row valid
(assistant with required fields); call loadMessagesFromSqlite and assert the
output includes the valid record (and not dropped) — reference
loadMessagesFromSqlite and createSqliteAdapter when locating where to add the
test and name the test something like "keeps valid duplicate when earlier
duplicate is invalid".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/opencode/src/data-loader.ts`:
- Around line 509-511: The JSON loader currently skips messages when
message.tokens is null or both input/output tokens are zero, which undercounts
cached usage; modify the filter to also check message.cache.read and
message.cache.write so you only continue when tokens are null or both
tokens.input and tokens.output are zero AND (message.cache.read ||
message.cache.write) are falsy/zero. Update the conditional around the "message"
check (the block using message.tokens) to include these cache fields
(message.cache.read, message.cache.write) so cached-only messages aren't
skipped—mirror the same logic used in the SQLite loader fix.

---

Duplicate comments:
In `@apps/opencode/src/data-loader.ts`:
- Around line 277-293: The dedupeSet check currently runs before
parsing/validation so malformed or invalid first-seen rows can cause later valid
duplicates to be skipped; update the loop in data-loader (the for (const row of
rows) block) to perform JSON.parse and v.safeParse(sqliteMessageDataSchema,
parsed) first, and only after result.success add row.id to dedupeSet and skip
duplicates using dedupeSet.has(row.id); ensure you still continue on parse or
validation failures and only mark IDs as seen when validation passes.

---

Nitpick comments:
In `@apps/opencode/src/data-loader.ts`:
- Around line 655-748: Add a new unit test for loadMessagesFromSqlite that
verifies the dedupe logic handles an invalid-first then valid-second duplicate:
use createSqliteAdapter to seed messageRows where one ID appears twice with the
first row containing invalid JSON or role='user' (so it would be
filtered/invalid) and the second row valid (assistant with required fields);
call loadMessagesFromSqlite and assert the output includes the valid record (and
not dropped) — reference loadMessagesFromSqlite and createSqliteAdapter when
locating where to add the test and name the test something like "keeps valid
duplicate when earlier duplicate is invalid".

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bb60e287-57f7-4e0b-94c1-8f8b8ef6dafd

📥 Commits

Reviewing files that changed from the base of the PR and between fe0892b and ef7db3c.

📒 Files selected for processing (1)
  • apps/opencode/src/data-loader.ts

Extracts token validation into reusable function that checks cache tokens. Moves deduplication after validation to avoid processing invalid entries twice.
@unsync
Copy link
Author

unsync commented Mar 10, 2026

@ryoppippi this is an intentional duplicate of #850 so solve the same issue, but with:

  • rebased on main
  • added tests
  • adressed coderabbit comments

AI Disclosure: I threw gpt-5.4 at the first PR and adressed coderabbit + ran it locally.

Initial credit goes to @dobbymaniac and kudos to @toliner for the added manual tests 🙏

@toliner
Copy link

toliner commented Mar 10, 2026

Hi, I found bug for this PR.

If a session uses multiple model, cost calculation doesn't work correctly.

I ran pnpm run start session and result is like:

├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤
│ ツールシステム実装                                                                                                           │ - claude-opus-4.6              │ 2,732,4… │   76,871 │    195,355 │ 4,170,908 │  7,175,629 │     $0.00 │
│                                                                                                                              │ - minimax-m2.5-free            │          │          │            │           │            │           │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤
│   ↳ Explore existing tool and item implementations (@explore subagent)                                                       │ - grok-code-fast-1             │   47,945 │    4,421 │          0 │    21,504 │     73,870 │     $0.02 │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤
│   ↳ Explore energy system and event handlers (@explore subagent)                                                             │ - grok-code-fast-1             │   33,923 │    3,841 │          0 │    30,784 │     68,548 │     $0.01 │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤
│   ↳ Research Forge 1.16.5 custom tool and NBT capability patterns (@librarian subagent)                                      │ - gemini-3-flash-preview       │   54,451 │    1,841 │          0 │   131,607 │    187,899 │     $0.04 │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤
│   ↳ Research Forge 1.16.5 TileEntity container GUI patterns (@librarian subagent)                                            │ - gemini-3-flash-preview       │   78,828 │    2,187 │          0 │   242,505 │    323,520 │     $0.06 │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤
│   ↳ Create comprehensive implementation plan for Tool System (@plan subagent)                                                │ - claude-opus-4.6              │  234,449 │   18,002 │     61,404 │ 2,424,221 │  2,738,076 │     $0.00 │
│                                                                                                                              │ - minimax-m2.5-free            │          │          │            │           │            │           │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤

In this session, I use claude-opus-4.6 for main model and minimax-m2.5-free for compaction.
Cost of sessions with minimax-m2.5-free are calculated as $0

Converts dotted Claude model names (e.g. claude-opus-4.5) to dash format (claude-opus-4-5) before pricing lookup to ensure consistent model name resolution.
@unsync
Copy link
Author

unsync commented Mar 10, 2026

@toliner it seem it could have have been a naming issue when pulling claude models names from LiteLLM, could you pull the branch and try again ? 👀

@toliner
Copy link

toliner commented Mar 10, 2026

@unsync Fixed, thank you!

├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤
│ ツールシステム実装                                                                                                           │ - claude-opus-4.6              │ 3,554,9… │  106,056 │    195,355 │ 5,445,460 │  9,301,825 │    $23.05 │
│                                                                                                                              │ - minimax-m2.5-free            │          │          │            │           │            │           │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤
│   ↳ Explore existing tool and item implementations (@explore subagent)                                                       │ - grok-code-fast-1             │   47,945 │    4,421 │          0 │    21,504 │     73,870 │     $0.02 │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤
│   ↳ Explore energy system and event handlers (@explore subagent)                                                             │ - grok-code-fast-1             │   33,923 │    3,841 │          0 │    30,784 │     68,548 │     $0.01 │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤
│   ↳ Research Forge 1.16.5 custom tool and NBT capability patterns (@librarian subagent)                                      │ - gemini-3-flash-preview       │   54,451 │    1,841 │          0 │   131,607 │    187,899 │     $0.04 │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤
│   ↳ Research Forge 1.16.5 TileEntity container GUI patterns (@librarian subagent)                                            │ - gemini-3-flash-preview       │   78,828 │    2,187 │          0 │   242,505 │    323,520 │     $0.06 │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤
│   ↳ Create comprehensive implementation plan for Tool System (@plan subagent)                                                │ - claude-opus-4.6              │  234,449 │   18,002 │     61,404 │ 2,424,221 │  2,738,076 │     $2.78 │
│                                                                                                                              │ - minimax-m2.5-free            │          │          │            │           │            │           │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────┼──────────┼────────────┼───────────┼────────────┼───────────┤

@unsync unsync changed the title feature/support opencode sqlite storage feature(opencode): support opencode sqlite storage Mar 10, 2026
@unsync
Copy link
Author

unsync commented Mar 10, 2026

@yukukotani i see you made #879

For the sake of reducing duplicates, do you have a take on both implementations ?
Here is what my gpt5.4 agent said (must be biased but captured the main differences):

Short version: for ccusage as it exists today, I think your current branch is better.

Why I’d choose the current branch
- It preserves backward compatibility for legacy OpenCode JSON data, while PR #879 explicitly breaks OpenCode <1.2.0.
- It keeps the existing Node 20 floor in apps/opencode/package.json:34 and apps/opencode/tsdown.config.ts:10, instead of forcing Node 22 just to use node:sqlite.
- It has already absorbed several real-world fixes that PR #879 did not cover:
  - Bun fallback guard in Node in apps/opencode/src/data-loader.ts
  - cache-only token accounting in apps/opencode/src/data-loader.ts
  - safer SQLite dedupe behavior in apps/opencode/src/data-loader.ts
  - dotted Claude model normalization for pricing in apps/opencode/src/cost-utils.ts
- It is more aligned with what users actually reported after testing, not just the initial migration.

Where PR #879 is better
- It is conceptually cleaner: one storage model, one DB-first path, fewer compatibility branches.
- It avoids introducing better-sqlite3; using node:sqlite + bun:sqlite is a nice runtime story if you are willing to require Node 22.
- If the product decision were “support only modern OpenCode and simplify aggressively,” I’d prefer its direction.

Where PR #879 is weaker
- It chose a breaking migration up front.
- It bumped runtime requirements more aggressively than necessary.
- It still had at least one implementation regression caught in review (path-type removal while still importing isDirectorySync), which suggests it was cleaner in theory than in shipped shape.
- From the PR summary/review, it did not address the later correctness issues you’ve now found in the current branch.

My take
- Best repo-ready implementation: your current branch
- Cleanest greenfield implementation: PR #879, if you intentionally drop old OpenCode support and accept Node 22+
- ```

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants