Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions backend/druks/harnesses/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ def normalize_token_usage(metadata: dict[str, Any] | None) -> dict[str, int] | N
# Codex's output_tokens already include its reasoning tokens.
output = _int(metadata.get("output_tokens"))
reasoning = _int(metadata.get("reasoning_output_tokens"))
elif provider in ("opencode", "pi"):
# Both CLIs report input without its cache tokens. opencode also
# reports output without its reasoning tokens; pi includes them.
cached_input = _int(metadata.get("cached_input_tokens"))
cache_creation = _int(metadata.get("cache_creation_tokens"))
input_total = _int(metadata.get("input_tokens")) + cached_input + cache_creation
reasoning = _int(metadata.get("reasoning_tokens"))
output = _int(metadata.get("output_tokens"))
if provider == "opencode":
output += reasoning
else:
# Unknown provider — try the fields generously so a future
# adapter that writes the canonical names just works.
Expand Down
43 changes: 43 additions & 0 deletions backend/tests/test_cost_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,49 @@ def test_normalize_token_usage_openai_keeps_input_and_output_inclusive():
}


def test_normalize_token_usage_opencode_adds_cache_to_input_and_reasoning_to_output():
metadata = {
"provider": "opencode",
"input_tokens": 8_000,
"cached_input_tokens": 90_000,
"cache_creation_tokens": 2_000,
"output_tokens": 900,
"reasoning_tokens": 600,
}

canonical = normalize_token_usage(metadata)

assert canonical == {
"input_tokens": 100_000,
"output_tokens": 1_500,
"cached_input_tokens": 90_000,
"cache_creation_tokens": 2_000,
"reasoning_tokens": 600,
"total_tokens": 101_500,
}


def test_normalize_token_usage_pi_adds_cache_to_input():
metadata = {
"provider": "pi",
"input_tokens": 8_000,
"cached_input_tokens": 90_000,
"cache_creation_tokens": 2_000,
"output_tokens": 1_500,
}

canonical = normalize_token_usage(metadata)

assert canonical == {
"input_tokens": 100_000,
"output_tokens": 1_500,
"cached_input_tokens": 90_000,
"cache_creation_tokens": 2_000,
"reasoning_tokens": 0,
"total_tokens": 101_500,
}


def test_normalize_token_usage_returns_none_for_empty_metadata():
assert normalize_token_usage({"provider": "anthropic"}) is None
assert normalize_token_usage({}) is None
Expand Down
Loading