mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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.
112 lines
3.4 KiB
Python
112 lines
3.4 KiB
Python
"""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
|