mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(grok): read opencode session usage for cost capture
Confirmed by inspecting a local opencode run: opencode persists per-session usage in SQLite at ~/.local/share/opencode/opencode.db — the `session` table carries cost + tokens_input/output/reasoning/cache_read/cache_write. xAI's response usage object (prompt_tokens, completion_tokens, prompt_tokens_details.cached_tokens, completion_tokens_details.reasoning_tokens) maps directly onto those columns. Add opencode_usage.read_session_usage / cost_for_session: read the opencode DB and price the tokens via roboco.billing.pricing (our cost stays authoritative; opencode's own `cost` column is kept for reference). Tested against a fixture DB mirroring the real schema (single session, summed sessions, missing/empty DB). Remaining wiring (for the live spawn): mount the opencode data dir on grok spawn + call cost_for_session at reap to record the usage rollup.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
"""Tests for opencode usage capture (reading the opencode SQLite session table).
|
||||
|
||||
The fixture DB mirrors the real opencode v1.x ``session`` table columns observed
|
||||
from a local run (cost + tokens_input/output/reasoning/cache_read/cache_write).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from roboco.llm.providers.opencode_usage import (
|
||||
cost_for_session,
|
||||
read_session_usage,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
_M = 1_000_000
|
||||
_TOL = 1e-4
|
||||
_ZERO_COST = 0.0
|
||||
|
||||
# Single-session fixture: input, output, reasoning, cache_read, cache_write.
|
||||
_IN, _OUT, _REASON, _CREAD, _CWRITE = 100, 50, 10, 20, 5
|
||||
# Second session for the summation test.
|
||||
_S2_IN, _S2_OUT, _S2_CREAD = 200, 70, 10
|
||||
# grok-build-0.1: 1M input ($1.00) + 1M output ($2.00) = $3.00.
|
||||
_GROK_COST_1M_1M = 3.00
|
||||
|
||||
|
||||
def _make_db(
|
||||
path: Path, rows: list[tuple[str, int, int, int, int, int, float]]
|
||||
) -> None:
|
||||
con = sqlite3.connect(path)
|
||||
con.execute(
|
||||
"""
|
||||
CREATE TABLE session (
|
||||
id text PRIMARY KEY,
|
||||
tokens_input integer DEFAULT 0 NOT NULL,
|
||||
tokens_output integer DEFAULT 0 NOT NULL,
|
||||
tokens_reasoning integer DEFAULT 0 NOT NULL,
|
||||
tokens_cache_read integer DEFAULT 0 NOT NULL,
|
||||
tokens_cache_write integer DEFAULT 0 NOT NULL,
|
||||
cost real DEFAULT 0 NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
con.executemany(
|
||||
"INSERT INTO session "
|
||||
"(id, tokens_input, tokens_output, tokens_reasoning, "
|
||||
"tokens_cache_read, tokens_cache_write, cost) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
rows,
|
||||
)
|
||||
con.commit()
|
||||
con.close()
|
||||
|
||||
|
||||
def test_read_missing_db_returns_none(tmp_path: Path) -> None:
|
||||
assert read_session_usage(tmp_path / "nope.db") is None
|
||||
|
||||
|
||||
def test_read_single_session(tmp_path: Path) -> None:
|
||||
db = tmp_path / "opencode.db"
|
||||
# (id, input, output, reasoning, cache_read, cache_write, cost)
|
||||
_make_db(db, [("s1", _IN, _OUT, _REASON, _CREAD, _CWRITE, 0.0007)])
|
||||
usage = read_session_usage(db, session_id="s1")
|
||||
assert usage is not None
|
||||
assert usage.tokens_input == _IN
|
||||
assert usage.tokens_output == _OUT
|
||||
assert usage.tokens_cache_read == _CREAD
|
||||
assert usage.tokens_cache_write == _CWRITE
|
||||
assert usage.tokens_reasoning == _REASON
|
||||
|
||||
|
||||
def test_read_sums_all_sessions_when_no_id(tmp_path: Path) -> None:
|
||||
db = tmp_path / "opencode.db"
|
||||
_make_db(
|
||||
db,
|
||||
[
|
||||
("s1", _IN, _OUT, 0, 0, 0, 0.0),
|
||||
("s2", _S2_IN, _S2_OUT, 0, _S2_CREAD, 0, 0.0),
|
||||
],
|
||||
)
|
||||
usage = read_session_usage(db)
|
||||
assert usage is not None
|
||||
assert usage.tokens_input == _IN + _S2_IN
|
||||
assert usage.tokens_output == _OUT + _S2_OUT
|
||||
assert usage.tokens_cache_read == _S2_CREAD
|
||||
|
||||
|
||||
def test_read_empty_table_returns_none(tmp_path: Path) -> None:
|
||||
db = tmp_path / "opencode.db"
|
||||
_make_db(db, [])
|
||||
assert read_session_usage(db) is None
|
||||
|
||||
|
||||
def test_cost_for_session_uses_roboco_pricing(tmp_path: Path) -> None:
|
||||
db = tmp_path / "opencode.db"
|
||||
# 1M input + 1M output for grok-build-0.1 → our $3.00, not opencode's 99.0.
|
||||
_make_db(db, [("s1", _M, _M, 0, 0, 0, 99.0)])
|
||||
usage, cost = cost_for_session("grok-build-0.1", db, session_id="s1")
|
||||
assert usage is not None
|
||||
assert abs(cost - _GROK_COST_1M_1M) < _TOL
|
||||
|
||||
|
||||
def test_cost_for_session_missing_db(tmp_path: Path) -> None:
|
||||
usage, cost = cost_for_session("grok-build-0.1", tmp_path / "nope.db")
|
||||
assert usage is None
|
||||
assert cost == _ZERO_COST
|
||||
Reference in New Issue
Block a user