From 7c7c02960bf5ee7777fa9fc3736ce97ffe044280 Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Mon, 2 Nov 2026 19:59:51 +0530 Subject: [PATCH 1/2] fix(opencode): report $0 for free models and expose cost like other engines When OpenCode reported a step cost of 0, rae estimated it with calculate_cost, which prices any unknown model as Claude Sonnet 4.6. Free models such as the default opencode/big-pickle and local Ollama models were shown as costing Sonnet prices. Estimate only for models with known prices, and resolve short names (sonnet, opus, haiku) through MODEL_MAP before pricing. OpenCode also kept its cost only under usage.cost, which --json usage, /history and /resume do not read. Record it under estimated_cost_usd as well, and map OpenCode's cache_creation_tokens to cache_write_tokens in --json. The README's agent --json table listed usage.total_cost; the field is total_cost_usd. --- CHANGELOG.md | 3 +++ README.md | 2 +- src/reverse_api/cli.py | 2 +- src/reverse_api/opencode_engineer.py | 11 ++++++++--- tests/test_opencode_engineer.py | 1 + 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aa65ad..79d693d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `agent --json` and `engineer --json` report `status: error` (`engine_failure`) when the run ends without writing its client, instead of `ok` with a `script_path` that does not exist. - Playwright MCP prompt: `browser_run_code` is now `browser_run_code_unsafe`, and the nonexistent `browser_scroll` tool is no longer listed. - The Claude SDK message buffer is raised to 32 MB, so a large screenshot no longer aborts a run. +- OpenCode runs on free or local models (`opencode/big-pickle`, Ollama) report a cost of $0. When OpenCode reported no cost, rae priced every unknown model as Claude Sonnet 4.6; it now estimates only for models with known prices, and short names such as `opus` are priced as the model they map to. +- OpenCode runs report their cost and cache writes in `--json` usage (`total_cost_usd`, `cache_write_tokens`) and their cost in `/history`. +- README: the `agent --json` usage fields are `total_cost_usd` and the cache token counts, not `total_cost`. ### Security - Raise security minimums to aiohttp >=3.14.4, certifi >=2026.10.19, multidict >=6.9.1, OpenTelemetry API >=1.45.1, PyJWT >=2.15.0 and urllib3 >=2.8.0, and lock the patched versions. certifi drops the emSign root CAs whose intermediate keys were compromised. diff --git a/README.md b/README.md index dcb951a..e498995 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ Pass `--no-interactive` (and/or `--json`) to skip prompts. With `--json`, stdout | `mode` | `string` \| `null` | `"auto"`, `"chrome-mcp"`, or `"agent-browser"`. | | `har_path` | `string` \| `null` | Captured HAR. | | `script_path` | `string` \| `null` | Generated client; exists whenever `status` is `"ok"`. | -| `usage` | `object` | `{input_tokens, output_tokens, total_cost}`. | +| `usage` | `object` | `{input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_cost_usd}`, plus the SDK's own usage under `raw`. | | `error` | `string` \| `null` | When `status == "error"`. | ### Exit codes diff --git a/src/reverse_api/cli.py b/src/reverse_api/cli.py index 174098c..2026725 100644 --- a/src/reverse_api/cli.py +++ b/src/reverse_api/cli.py @@ -268,7 +268,7 @@ def _select_opencode_pair_for_settings(mode_color: str = THEME_PRIMARY) -> tuple "input_tokens": ("input_tokens",), "output_tokens": ("output_tokens",), "cache_read_tokens": ("cache_read_input_tokens", "cache_read_tokens"), - "cache_write_tokens": ("cache_creation_input_tokens", "cache_write_tokens"), + "cache_write_tokens": ("cache_creation_input_tokens", "cache_write_tokens", "cache_creation_tokens"), "total_cost_usd": ("estimated_cost_usd", "total_cost_usd", "total_cost"), } diff --git a/src/reverse_api/opencode_engineer.py b/src/reverse_api/opencode_engineer.py index 70e48b7..f8f1507 100644 --- a/src/reverse_api/opencode_engineer.py +++ b/src/reverse_api/opencode_engineer.py @@ -736,11 +736,14 @@ async def _handle_part_update(self, properties: dict, seen_parts: set) -> None: cache_read = cache.get("read", 0) cache_write = cache.get("write", 0) - if api_cost == 0 and (input_tokens > 0 or output_tokens > 0): - from .pricing import calculate_cost + # A zero API cost is estimated only for a model with known prices: free models + # (opencode/big-pickle) and local Ollama ones really cost nothing. + from .pricing import calculate_cost, get_model_pricing + model_id = self.MODEL_MAP.get(self.opencode_model, self.opencode_model) + if api_cost == 0 and (input_tokens > 0 or output_tokens > 0) and get_model_pricing(model_id) is not None: calculated_cost = calculate_cost( - model_id=self.opencode_model, + model_id=model_id, input_tokens=input_tokens, output_tokens=output_tokens, cache_creation_tokens=cache_write, @@ -758,6 +761,8 @@ async def _handle_part_update(self, properties: dict, seen_parts: set) -> None: self.usage_metadata["cache_read_tokens"] = self.usage_metadata.get("cache_read_tokens", 0) + cache_read self.usage_metadata["cache_creation_tokens"] = self.usage_metadata.get("cache_creation_tokens", 0) + cache_write self.usage_metadata["cost"] = self.usage_metadata.get("cost", 0) + cost + # The key the other engines use: --json usage, /history and /resume read it. + self.usage_metadata["estimated_cost_usd"] = self.usage_metadata["cost"] else: # Log unhandled part types for debugging diff --git a/tests/test_opencode_engineer.py b/tests/test_opencode_engineer.py index 05681ee..7767b62 100644 --- a/tests/test_opencode_engineer.py +++ b/tests/test_opencode_engineer.py @@ -424,6 +424,7 @@ async def test_step_finish(self, tmp_path): async def test_step_finish_calculates_cost_when_zero(self, tmp_path): """Step finish calculates cost locally when API returns 0.""" eng = self._make_engineer(tmp_path) + eng.opencode_model = "claude-sonnet-4-6" seen_parts = set() properties = { "part": { From 288d4bd38e0c58dce9ebb46d1ed8e5b51c2b6fcd Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Mon, 2 Nov 2026 20:12:47 +0530 Subject: [PATCH 2/2] Address review: note /resume in the changelog and that usage keys are optional --- CHANGELOG.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79d693d..e374ed1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Playwright MCP prompt: `browser_run_code` is now `browser_run_code_unsafe`, and the nonexistent `browser_scroll` tool is no longer listed. - The Claude SDK message buffer is raised to 32 MB, so a large screenshot no longer aborts a run. - OpenCode runs on free or local models (`opencode/big-pickle`, Ollama) report a cost of $0. When OpenCode reported no cost, rae priced every unknown model as Claude Sonnet 4.6; it now estimates only for models with known prices, and short names such as `opus` are priced as the model they map to. -- OpenCode runs report their cost and cache writes in `--json` usage (`total_cost_usd`, `cache_write_tokens`) and their cost in `/history`. +- OpenCode runs report their cost and cache writes in `--json` usage (`total_cost_usd`, `cache_write_tokens`) and their cost in `/history` and `/resume`. - README: the `agent --json` usage fields are `total_cost_usd` and the cache token counts, not `total_cost`. ### Security diff --git a/README.md b/README.md index e498995..1847fb6 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ Pass `--no-interactive` (and/or `--json`) to skip prompts. With `--json`, stdout | `mode` | `string` \| `null` | `"auto"`, `"chrome-mcp"`, or `"agent-browser"`. | | `har_path` | `string` \| `null` | Captured HAR. | | `script_path` | `string` \| `null` | Generated client; exists whenever `status` is `"ok"`. | -| `usage` | `object` | `{input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_cost_usd}`, plus the SDK's own usage under `raw`. | +| `usage` | `object` | `{input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_cost_usd}`, each present only when the SDK reports it, plus the SDK's own usage under `raw`. | | `error` | `string` \| `null` | When `status == "error"`. | ### Exit codes