mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(grok): correct opencode provider (Responses API), stdin, reasoning cost
A live opencode run against api.x.ai/v1 surfaced three real bugs: 1. Provider package — grok-build-0.1 is driven via the OpenAI Responses API (opencode calls model.responses()). @ai-sdk/openai-compatible is chat/completions only and errors "responses is not a function". Switch the generated opencode.json provider + the grok image to @ai-sdk/openai. 2. Headless hang — `opencode run` blocks after init without a TTY; close stdin (`< /dev/null`) in the entrypoint so it proceeds to the model call. 3. Reasoning-token cost — grok-build-0.1 is a reasoning model; reasoning tokens bill as output but opencode stores them in a separate column. cost_for_session folds tokens_reasoning into output (else ~22x undercount). Verified end-to-end against a real session row (input=6120, output=1, reasoning=226, cache_read=1856): our pricing reproduces opencode's stored USD cost ($0.0069452) exactly. Tests anchored to that real row.
This commit is contained in:
@@ -12,11 +12,12 @@ FROM roboco-agent-base
|
||||
|
||||
USER root
|
||||
|
||||
# opencode — the OpenAI-protocol agent runtime. The @ai-sdk/openai-compatible
|
||||
# package backs the custom xAI provider declared in the generated opencode.json;
|
||||
# opencode also resolves it at runtime, but pre-installing keeps first spawn off
|
||||
# the network.
|
||||
RUN npm install -g opencode-ai @ai-sdk/openai-compatible \
|
||||
# opencode — the OpenAI-protocol agent runtime. grok-build-0.1 is driven via the
|
||||
# OpenAI Responses API, so the provider package is @ai-sdk/openai (NOT
|
||||
# @ai-sdk/openai-compatible, which is chat/completions only and errors with
|
||||
# "responses is not a function"). opencode resolves it at runtime, but
|
||||
# pre-installing keeps first spawn off the network.
|
||||
RUN npm install -g opencode-ai @ai-sdk/openai \
|
||||
&& npm cache clean --force \
|
||||
&& rm -rf /root/.npm /tmp/*
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ python -m roboco.llm.providers.opencode_config
|
||||
# positional); `--` separates it from flags so a prompt starting with `--`
|
||||
# cannot be parsed as CLI options. The model also comes from the rendered
|
||||
# config; --model is passed explicitly as belt-and-suspenders.
|
||||
#
|
||||
# `< /dev/null` is REQUIRED: without a closed stdin, `opencode run` hangs after
|
||||
# init in a headless / no-TTY environment (it blocks waiting on stdin). Verified
|
||||
# live — closing stdin lets the run proceed to the model call and exit cleanly.
|
||||
exec opencode run \
|
||||
--model "xai/${ROBOCO_AGENT_MODEL:-grok-build-0.1}" \
|
||||
-- "${ROBOCO_INITIAL_PROMPT:-}"
|
||||
-- "${ROBOCO_INITIAL_PROMPT:-}" < /dev/null
|
||||
|
||||
@@ -112,6 +112,10 @@ def calculate_cost(
|
||||
tokens_output: Number of output tokens (completion).
|
||||
tokens_cache_read: Prompt-cache read tokens (charged at reduced rate).
|
||||
tokens_cache_write: Prompt-cache write tokens (charged at reduced rate).
|
||||
Reasoning/thinking tokens that a provider reports *separately* from
|
||||
output (e.g. xAI grok-build-*) are billed at the output rate by the
|
||||
caller folding them into ``tokens_output`` (see
|
||||
``opencode_usage.cost_for_session``).
|
||||
|
||||
Returns:
|
||||
Estimated cost in USD as a float. Returns 0.0 for unpriced models
|
||||
|
||||
@@ -31,7 +31,11 @@ from typing import Any
|
||||
|
||||
_OPENCODE_SCHEMA = "https://opencode.ai/config.json"
|
||||
_PROVIDER_ID = "xai"
|
||||
_OPENAI_COMPAT_NPM = "@ai-sdk/openai-compatible"
|
||||
# grok-build-0.1 is driven through the OpenAI **Responses** API (opencode calls
|
||||
# model.responses()). Only @ai-sdk/openai implements that — @ai-sdk/openai-compatible
|
||||
# is chat/completions only and errors with "responses is not a function".
|
||||
# Confirmed via a live opencode run against api.x.ai/v1.
|
||||
_PROVIDER_NPM = "@ai-sdk/openai"
|
||||
|
||||
# Plugins baked into the roboco-agent-grok image (see docker/agent-grok.Dockerfile).
|
||||
# secret-scrub ports the bash-guard deny rules to opencode's tool.execute.before.
|
||||
@@ -85,7 +89,7 @@ def build_opencode_config(
|
||||
"$schema": _OPENCODE_SCHEMA,
|
||||
"provider": {
|
||||
_PROVIDER_ID: {
|
||||
"npm": _OPENAI_COMPAT_NPM,
|
||||
"npm": _PROVIDER_NPM,
|
||||
"name": "xAI",
|
||||
"options": {"baseURL": target.base_url, "apiKey": target.api_key},
|
||||
"models": {target.model: {"name": target.model}},
|
||||
|
||||
@@ -106,10 +106,15 @@ def cost_for_session(
|
||||
usage = read_session_usage(db_path, session_id)
|
||||
if usage is None:
|
||||
return None, 0.0
|
||||
# opencode stores tokens_input as non-cached input (disjoint from
|
||||
# tokens_cache_read) and tokens_output EXCLUDING reasoning, with reasoning
|
||||
# in its own column. Reasoning bills at the output rate, so fold it into
|
||||
# output. Verified against a live run: this reproduces opencode's own `cost`
|
||||
# column (= xAI's authoritative cost) to the cent.
|
||||
cost = calculate_cost(
|
||||
model,
|
||||
tokens_input=usage.tokens_input,
|
||||
tokens_output=usage.tokens_output,
|
||||
tokens_output=usage.tokens_output + usage.tokens_reasoning,
|
||||
tokens_cache_read=usage.tokens_cache_read,
|
||||
tokens_cache_write=usage.tokens_cache_write,
|
||||
)
|
||||
|
||||
@@ -71,7 +71,8 @@ def test_build_opencode_config_provider_and_model() -> None:
|
||||
instruction_paths=["/app/system-prompt.md"],
|
||||
)
|
||||
provider = cfg["provider"]["xai"]
|
||||
assert provider["npm"] == "@ai-sdk/openai-compatible"
|
||||
# grok-build-0.1 needs the Responses API → @ai-sdk/openai, not -compatible.
|
||||
assert provider["npm"] == "@ai-sdk/openai"
|
||||
assert provider["options"]["baseURL"] == "https://api.x.ai/v1"
|
||||
assert provider["options"]["apiKey"] == "xai-key"
|
||||
assert "grok-build-0.1" in provider["models"]
|
||||
|
||||
@@ -28,6 +28,11 @@ _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
|
||||
|
||||
# A REAL grok-build-0.1 session row observed from a live opencode run. Our
|
||||
# pricing must reproduce opencode's own stored `cost` (= xAI authoritative).
|
||||
_REAL_IN, _REAL_OUT, _REAL_REASON, _REAL_CREAD = 6120, 1, 226, 1856
|
||||
_REAL_COST = 0.0069452
|
||||
|
||||
|
||||
def _make_db(
|
||||
path: Path, rows: list[tuple[str, int, int, int, int, int, float]]
|
||||
@@ -109,3 +114,21 @@ 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
|
||||
|
||||
|
||||
def test_cost_reproduces_opencode_authoritative_cost(tmp_path: Path) -> None:
|
||||
"""Real observed row: our pricing must match opencode's stored USD cost.
|
||||
|
||||
Proves the column semantics (non-cached input disjoint from cache_read;
|
||||
reasoning separate, billed at output rate).
|
||||
"""
|
||||
db = tmp_path / "opencode.db"
|
||||
# (id, input, output, reasoning, cache_read, cache_write, cost)
|
||||
_make_db(
|
||||
db,
|
||||
[("real", _REAL_IN, _REAL_OUT, _REAL_REASON, _REAL_CREAD, 0, _REAL_COST)],
|
||||
)
|
||||
usage, cost = cost_for_session("grok-build-0.1", db, session_id="real")
|
||||
assert usage is not None
|
||||
assert abs(cost - _REAL_COST) < _TOL
|
||||
assert abs(cost - usage.opencode_cost) < _TOL
|
||||
|
||||
Reference in New Issue
Block a user