feat(provider-opencode-go): add OpenCode Go Chat Completions provider worker - #690
feat(provider-opencode-go): add OpenCode Go Chat Completions provider worker#690faramirezs wants to merge 7 commits into
Conversation
OpenCode Go Chat Completions provider behind llm-router. Implements the provider protocol: stream (SSE chunks to AssistantMessageEvent frames), abort, refresh_models (live GET /v1/models enriched with models.dev metadata: context window, reasoning efforts, tool/structured-output capability), and re-declaration on router::ready. Chat Completions wire format only, max_completion_tokens, strict json_schema structured output, reasoning_effort low/medium/high for deepseek-/kimi-k2.7- families. Wired into create-tag/release workflows and the harness worker deps.
README to the provider family structure (Behavior/Tests/Running), manifest tags+description to the canonical form, release wiring in alpha-release.yml and discover_changed_workers.py, llm-router README reference note.
Upstream MOT-4335 rewrote all provider identity prompts to teach the live
surface (register_trigger, harness::spawn, orchestrator: true) and stripped
orchestration-process doctrine. The fork-PR merge with the new main runs the
harness prompts sweep over every shipped prompt, which failed on our pre-
rewrite copy ("delegation is one-way" etc.). Absorb the rewritten prompt,
update the register.rs identity assertions, and apply the iii-state -> state
rename.
|
@faramirezs is attempting to deploy a commit to the motia Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds a complete ChangesOpenCode Go provider
Estimated code review effort: 5 (Critical) | ~90 minutes Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (3)
provider-opencode-go/src/router_client.rs (1)
1-3: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAlign the module doc with
register.The doc states that every call binds
PROVIDER_IDand carries the registration token.registerat Line 41 forwards only the declaration value. The provider id and token travel inside that payload, built inregister::declare_once. Narrow the doc claim to the resolve/reconcile/models_get wrappers.♻️ Proposed doc correction
//! Provider-scoped shims over the shared router-protocol client -//! (`llm_router::provider_scaffold::router_client`): every call binds this -//! crate's `PROVIDER_ID` and carries the registration token. +//! (`llm_router::provider_scaffold::router_client`): the resolve, reconcile, +//! and models_get wrappers bind this crate's `PROVIDER_ID` and carry the +//! registration token. `register` forwards a declaration payload that already +//! carries both (see `register::declare_once`).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@provider-opencode-go/src/router_client.rs` around lines 1 - 3, Update the module-level documentation above the router client wrappers to limit the “binds PROVIDER_ID and carries the registration token” claim to the resolve, reconcile, and models_get wrappers; describe register as forwarding only the declaration payload, which already contains those values via register::declare_once.provider-opencode-go/tests/integration.rs (1)
287-429: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftAdd integration coverage for
provider::opencode_go::abort.The suite covers streaming, upstream 401, catalog reconciliation, and re-declaration. It does not exercise the abort path.
surface::catalog()publishesprovider::opencode_go::abort, andstream_fn.rscouples the abort guard topump_abortableand to the pre-spawnis_fired()check at Line 147 ofprovider-opencode-go/src/stream_fn.rs. A regression in that coupling would leave billed upstream generation running and no current test would fail.Add a test that starts a stream against a slow stub, calls the abort function with the
resolution_key, and asserts that the terminal frame reportsaborted.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@provider-opencode-go/tests/integration.rs` around lines 287 - 429, Add an integration test alongside the existing streaming tests that uses a slow upstream stub, starts `router::chat`, captures its `resolution_key`, invokes `provider::opencode_go::abort`, and waits for completion. Assert the terminal stream frame reports `aborted`, covering the abort guard and pre-spawn cancellation path in `stream_fn.rs`.provider-opencode-go/src/upstream.rs (1)
43-49: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winParse SSE
datafields per the spec.
data_linematches only the exact prefix"data: "and keeps only the last matching line. Two valid frame shapes are lost. A frame written asdata:{...}without the space is ignored. A frame with severaldata:lines is truncated to its last line, which then fails JSON parsing and is discarded. Both cases drop assistant output with no error frame.Strip the prefix without requiring the space, and join all
datalines in the block with\n.🔧 Proposed fix
-/// Last `data: ` payload in an SSE block, if any. -fn data_line(block: &str) -> Option<&str> { - block - .lines() - .filter_map(|l| l.strip_prefix("data: ")) - .next_back() -} +/// All `data` field values in an SSE block, joined with `\n` per the SSE +/// spec. The optional single space after the colon is stripped. +fn data_line(block: &str) -> Option<String> { + let mut parts = block.lines().filter_map(|l| { + l.strip_prefix("data:") + .map(|v| v.strip_prefix(' ').unwrap_or(v)) + }); + let first = parts.next()?; + let mut out = first.to_string(); + for p in parts { + out.push('\n'); + out.push_str(p); + } + Some(out) +}The
decodeclosure then comparesdata.as_str()against"[DONE]"and passes&datatoserde_json::from_str.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@provider-opencode-go/src/upstream.rs` around lines 43 - 49, Update data_line to recognize both “data:” and “data: ” SSE fields by stripping the prefix without requiring a space, then collect and join every matching data payload in block order with newline separators. Preserve the existing Option return behavior so decode can continue comparing the resulting string with “[DONE]” and parsing it as JSON.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@provider-opencode-go/iii-permissions.yaml`:
- Around line 7-9: Add !provider::opencode_go::abort to the deny list in
iii-permissions.yaml alongside the existing provider::opencode_go entries,
preventing agents from invoking the provider-owned abort operation.
In `@provider-opencode-go/src/curated.rs`:
- Around line 219-224: Update the OpenCode Go model metadata and the `Some(m) =>
Model` construction in the curation flow to source `max_output_tokens` from each
known model’s `ModelMeta`; retain 4096 only as the fallback for unknown model
IDs. Ensure the curated `Model.max_output_tokens` reflects the catalog value
used by routing and downstream configuration.
In `@provider-opencode-go/src/discovery.rs`:
- Around line 21-27: Update models_url to derive the models endpoint only from
the configured api_url origin, returning no URL when the path does not end with
/chat/completions instead of falling back to opencode.ai. Adjust
fetch_live_models/refresh_models to skip discovery on that None result while
preserving the existing models slice, and update the related test to verify the
configured origin is retained.
In `@provider-opencode-go/src/main.rs`:
- Line 22: Update the `#[arg]` configuration for the `--url` option in `main.rs`
to use the documented `III_WS_URL` environment variable instead of `III_URL`,
preserving the existing default WebSocket URL.
In `@provider-opencode-go/src/reasoning.rs`:
- Around line 30-52: Update level_str and reasoning_effort_for so each
ThinkingLevel maps to an ordered list of accepted effort aliases, with Minimal
preferring "minimal" then "none"; return the first alias contained in
supported_efforts(model), preserving None when no alias is supported. Add
coverage verifying Minimal returns Some("none") for gpt-5.6-luna and None for
grok-4.5.
In `@provider-opencode-go/src/register.rs`:
- Around line 167-172: Handle the Result returned by register_trigger in
register_provider for the router::ready trigger instead of discarding it with
let _. Log registration failures or propagate the error through
register_provider, ensuring failures remain visible and the existing successful
registration behavior is preserved.
In `@provider-opencode-go/src/sse.rs`:
- Around line 252-257: In the tool-call handling loop around
`state.function_calls` in the SSE delta processing, reject entries whose parsed
`index` exceeds a small fixed bound before the capacity-growing `while` loop.
Skip those oversized tool-call entries and preserve existing behavior for valid
indices.
- Around line 235-251: Update ProviderOpencodeGo’s streaming path so thinking
support is consistent: if the Chat Completions stream provides
delta.reasoning_content, handle it in handle_chunk by tracking state.thinking
and emitting the appropriate thinking events, adding an OpenBlock::Thinking
variant as needed; otherwise remove the unused thinking state/branch and narrow
supports_thinking in the curated metadata.
In `@README.md`:
- Around line 71-73: Repair the Markdown table entries for provider-openai and
provider-opencode-go so each is a complete three-column row with properly placed
pipe delimiters. Keep the existing descriptions intact, ensuring the
provider-openai description remains on its own row and the provider-opencode-go
row remains separate.
---
Nitpick comments:
In `@provider-opencode-go/src/router_client.rs`:
- Around line 1-3: Update the module-level documentation above the router client
wrappers to limit the “binds PROVIDER_ID and carries the registration token”
claim to the resolve, reconcile, and models_get wrappers; describe register as
forwarding only the declaration payload, which already contains those values via
register::declare_once.
In `@provider-opencode-go/src/upstream.rs`:
- Around line 43-49: Update data_line to recognize both “data:” and “data: ” SSE
fields by stripping the prefix without requiring a space, then collect and join
every matching data payload in block order with newline separators. Preserve the
existing Option return behavior so decode can continue comparing the resulting
string with “[DONE]” and parsing it as JSON.
In `@provider-opencode-go/tests/integration.rs`:
- Around line 287-429: Add an integration test alongside the existing streaming
tests that uses a slow upstream stub, starts `router::chat`, captures its
`resolution_key`, invokes `provider::opencode_go::abort`, and waits for
completion. Assert the terminal stream frame reports `aborted`, covering the
abort guard and pre-spawn cancellation path in `stream_fn.rs`.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 7baecbd2-7991-4e64-bf13-d4d1868d28e7
⛔ Files ignored due to path filters (1)
provider-opencode-go/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (42)
.github/scripts/discover_changed_workers.py.github/workflows/alpha-release.yml.github/workflows/create-tag.yml.github/workflows/release.ymlREADME.mdharness/iii.worker.yamlllm-router/README.mdprovider-opencode-go/.gitignoreprovider-opencode-go/Cargo.tomlprovider-opencode-go/README.mdprovider-opencode-go/build.rsprovider-opencode-go/config.yamlprovider-opencode-go/iii-permissions.yamlprovider-opencode-go/iii.worker.yamlprovider-opencode-go/prompts/identity.txtprovider-opencode-go/src/config.rsprovider-opencode-go/src/curated.rsprovider-opencode-go/src/discovery.rsprovider-opencode-go/src/errors.rsprovider-opencode-go/src/lib.rsprovider-opencode-go/src/main.rsprovider-opencode-go/src/manifest.rsprovider-opencode-go/src/reasoning.rsprovider-opencode-go/src/register.rsprovider-opencode-go/src/request.rsprovider-opencode-go/src/router_client.rsprovider-opencode-go/src/sse.rsprovider-opencode-go/src/state.rsprovider-opencode-go/src/stream_fn.rsprovider-opencode-go/src/surface.rsprovider-opencode-go/src/upstream.rsprovider-opencode-go/src/wire/messages.rsprovider-opencode-go/src/wire/mod.rsprovider-opencode-go/src/wire/names.rsprovider-opencode-go/src/wire/tools.rsprovider-opencode-go/tests/golden/schemas/provider.opencode_go.abort.jsonprovider-opencode-go/tests/golden/schemas/provider.opencode_go.on_router_ready.jsonprovider-opencode-go/tests/golden/schemas/provider.opencode_go.refresh_models.jsonprovider-opencode-go/tests/golden/schemas/provider.opencode_go.stream.jsonprovider-opencode-go/tests/integration.rsprovider-opencode-go/tests/schemas.rsprovider-opencode-go/tests/support/mod.rs
- iii-permissions.yaml: deny provider::opencode_go::abort (agents must not cancel router-owned streams; matches provider-claude-code) - sse.rs: relay delta.reasoning_content as thinking blocks (the OpenCode Go wire emits it, live-verified); bound tool-call index to 64 (malformed upstream could grow the vec unboundedly) - upstream.rs: data_line per SSE spec — accept data: without a space and join repeated data: lines instead of silently dropping output - curated.rs: per-model max_output_tokens from models.dev limit.output; 4096 stays the unknown-id fallback - reasoning.rs: Minimal maps to minimal then none (gpt-5.6-luna floor) - register.rs: log router::ready trigger registration failures - router_client.rs: narrow module doc claim - README: repair split provider-openai table row; III_WS_URL -> III_URL (code + engine convention); thinking-delta relay note
|
Hey @faramirezs, Thanks for the opencode-go-provider. I have assigned @ytallo and @andersonleal as reviewers, they'll get back to you soon. |
What
New Rust worker
provider-opencode-go: an LLM provider worker behindllm-routerspeaking the OpenCode Go API — Chat Completions(
https://opencode.ai/zen/go/v1/chat/completions), SSE streaming, live modeldiscovery, auth/error taxonomy, reasoning-effort mapping, tool calling, and
structured output. Registers
provider::opencode_go::stream/refresh_models/
abortwith the router, binds identity via registration token (state scopeprovider-opencode-go), and readsOPENCODE_GO_API_KEYas credential.Why
The stack has no way to route chat completions to an OpenCode Go subscription.
The OpenCode Go API is Chat Completions compatible, so the existing
provider-openaiworker ports directly — same protocol, same relay/pumpscaffold, same error taxonomy, with the OpenAI-only surfaces (embeddings,
Responses API) dropped as dead code.
How it works
router::provider::resolve(config slice →
OPENCODE_GO_API_KEYenv on the router → none); sent asAuthorization: Bearer.AssistantMessageEventframesinto a router-owned channel;
ping≥ every 30s of silence; a failed channelwrite (
router::abort/ caller gone) drops the SSE receiver and aborts thein-flight HTTP request. Stream-path
tracing::debugfor provider-sideobservability.
GET /v1/modelssupplies bare ids; each isenriched from a hardcoded curated metadata table (
src/curated.rs) preparedfrom models.dev (2026-08-03) — context window, reasoning support/effort
levels, tool-call and structured-output capability for the maintainer's
curated model set (24 models.dev entries +
hy3-preview). Ids outside thetable keep conservative defaults (128K, no thinking, tools on) — same
pattern as provider-openai's
curated.rs.thinking_levelmaps to the upstreamreasoning_effortonlywhen the model's curated effort list accepts the level (e.g.
grok-4.5accepts
low/medium/high;deepseek-v4-flashandglm-5.2accepthigh/max;hy3acceptsnone/low/high); toggle-only models andunknown ids stream without the field.
router::provider::registerwithbackoff until acked, re-declares on
router::ready;registration_tokenpersisted in iii-state (scope
provider-opencode-go).Scope / caveat
provider-openai: mechanical renames only in most files; theOpenAI-only surface (ApiMode, embeddings, Responses-API event handlers and
thinking deltas, curated reasoning-fallback ladder, luna guard) is dropped —
OpenCode Go has no such surface.
opencode/worker —different role (CLI wrapper vs provider), no overlap; both install side by
side.
hy3-previewis listed by the liveGET /v1/modelsbut the chat endpointcurrently returns
ModelNotFound— an upstream inconsistency; the curatedrow keeps conservative defaults and the provider surfaces the upstream error
cleanly.
no-ticketlabel applied.Repo wiring
README.mdModules row added forprovider-opencode-go(alphabetical, between
provider-openaiandprovider-xai)create-tag.yml,release.yml,alpha-release.yml, and.github/scripts/discover_changed_workers.pyllm-router/README.mdreference note (same structure as provider-openai)Verification
cargo fmt --checkandcargo clippy --all-targets --all-features -- -D warningscleancargo test --all-features: 68 pass — 58 lib unit + 2 bin unit + 4schema/golden + 4 integration
identity prompt is aligned with the MOT-4335 rewrite — teaches the live
surface (
engine::register_trigger,harness::spawn,orchestrator: true) and prescribes no orchestration processabortdenied iniii-permissions.yaml;delta.reasoning_contentrelayed as thinkingblocks; SSE
data:parsed per spec (optional space, multi-line join);per-model
max_output_tokensfrom models.dev;Minimal→minimal/noneeffort fallback;
router::readyregistration failure logged; root READMEtable row repaired
stubbed upstream: chat stream end-to-end (incl.
cache_readusage),401 →
auth_expirederror frame,refresh_models→ catalog from thecurated table, re-declare on
router::readycargo build --releaseOKrefresh_models→ 25 models;router::models::list→ 25 curated entrieswith metadata;
router::completeserved onglm-5,deepseek-v4-flash(incl.
thinking_level: high→reasoning_effort), andhy3; thinkingblocks relayed for reasoning models
Test plan
cargo fmt --checkcargo clippy --all-targets --all-features -- -D warningscargo test— 67 passIII_ENGINE_BIN=$(which iii) cargo test --test integration -- --test-threads=1iii worker add provider-opencode-go→refresh_models→router::models::list→router::completeSummary by CodeRabbit
New Features
Documentation
Tests