Skip to content

feat(provider-opencode-go): add OpenCode Go Chat Completions provider worker - #690

Open
faramirezs wants to merge 7 commits into
iii-hq:mainfrom
faramirezs:feat/provider-opencode-go
Open

feat(provider-opencode-go): add OpenCode Go Chat Completions provider worker#690
faramirezs wants to merge 7 commits into
iii-hq:mainfrom
faramirezs:feat/provider-opencode-go

Conversation

@faramirezs

@faramirezs faramirezs commented Aug 4, 2026

Copy link
Copy Markdown

What

New Rust worker provider-opencode-go: an LLM provider worker behind
llm-router speaking the OpenCode Go API — Chat Completions
(https://opencode.ai/zen/go/v1/chat/completions), SSE streaming, live model
discovery, auth/error taxonomy, reasoning-effort mapping, tool calling, and
structured output. Registers provider::opencode_go::stream / refresh_models
/ abort with the router, binds identity via registration token (state scope
provider-opencode-go), and reads OPENCODE_GO_API_KEY as 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-openai worker ports directly — same protocol, same relay/pump
scaffold, same error taxonomy, with the OpenAI-only surfaces (embeddings,
Responses API) dropped as dead code.

How it works

  • Credentials: resolved per request via router::provider::resolve
    (config slice → OPENCODE_GO_API_KEY env on the router → none); sent as
    Authorization: Bearer.
  • Wire / streaming: Chat Completions SSE → AssistantMessageEvent frames
    into a router-owned channel; ping ≥ every 30s of silence; a failed channel
    write (router::abort / caller gone) drops the SSE receiver and aborts the
    in-flight HTTP request. Stream-path tracing::debug for provider-side
    observability.
  • Catalog / discovery: live GET /v1/models supplies bare ids; each is
    enriched from a hardcoded curated metadata table (src/curated.rs) prepared
    from 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 the
    table keep conservative defaults (128K, no thinking, tools on) — same
    pattern as provider-openai's curated.rs.
  • Reasoning: thinking_level maps to the upstream reasoning_effort only
    when the model's curated effort list accepts the level (e.g. grok-4.5
    accepts low/medium/high; deepseek-v4-flash and glm-5.2 accept
    high/max; hy3 accepts none/low/high); toggle-only models and
    unknown ids stream without the field.
  • Registration: self-declares via router::provider::register with
    backoff until acked, re-declares on router::ready; registration_token
    persisted in iii-state (scope provider-opencode-go).

Scope / caveat

  • Port of provider-openai: mechanical renames only in most files; the
    OpenAI-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.
  • Not a replacement or rewrite of the Node opencode/ worker —
    different role (CLI wrapper vs provider), no overlap; both install side by
    side.
  • hy3-preview is listed by the live GET /v1/models but the chat endpoint
    currently returns ModelNotFound — an upstream inconsistency; the curated
    row keeps conservative defaults and the provider surfaces the upstream error
    cleanly.
  • External contributor: no Linear ticket — no-ticket label applied.

Repo wiring

  • Root README.md Modules row added for provider-opencode-go
    (alphabetical, between provider-openai and provider-xai)
  • Release wiring: create-tag.yml, release.yml, alpha-release.yml, and
    .github/scripts/discover_changed_workers.py
  • llm-router/README.md reference note (same structure as provider-openai)

Verification

  • cargo fmt --check and cargo clippy --all-targets --all-features -- -D warnings clean
  • cargo test --all-features: 68 pass — 58 lib unit + 2 bin unit + 4
    schema/golden + 4 integration
  • Harness prompt sweep (harness/tests/prompts.rs, 5 tests) passes: the
    identity prompt is aligned with the MOT-4335 rewrite — teaches the live
    surface (engine::register_trigger, harness::spawn,
    orchestrator: true) and prescribes no orchestration process
  • CodeRabbit review addressed (9 comments): abort denied in
    iii-permissions.yaml; delta.reasoning_content relayed as thinking
    blocks; SSE data: parsed per spec (optional space, multi-line join);
    per-model max_output_tokens from models.dev; Minimalminimal/none
    effort fallback; router::ready registration failure logged; root README
    table row repaired
  • Integration tests run against the real engine (v0.22.0) + llm-router with a
    stubbed upstream: chat stream end-to-end (incl. cache_read usage),
    401 → auth_expired error frame, refresh_models → catalog from the
    curated table, re-declare on router::ready
  • cargo build --release OK
  • Live smoke (real engine + llm-router 1.4.0 + OpenCode Go subscription):
    refresh_models → 25 models; router::models::list → 25 curated entries
    with metadata; router::complete served on glm-5, deepseek-v4-flash
    (incl. thinking_level: highreasoning_effort), and hy3; thinking
    blocks relayed for reasoning models

Test plan

  • cargo fmt --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo test — 67 pass
  • III_ENGINE_BIN=$(which iii) cargo test --test integration -- --test-threads=1
  • Live: iii worker add provider-opencode-gorefresh_models
    router::models::listrouter::complete

Summary by CodeRabbit

  • New Features

    • Added OpenCode Go provider support for streaming chat completions, tool calls, structured output, reasoning controls, and live model discovery.
    • Added curated metadata for supported models, including capabilities and context limits.
    • Added credential handling, configuration validation, error reporting, and registration lifecycle support.
    • Enabled OpenCode Go provider selection in release workflows.
  • Documentation

    • Documented installation, configuration, supported features, model metadata, and usage.
  • Tests

    • Added comprehensive unit, integration, streaming, discovery, and schema validation coverage.

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.
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

@faramirezs is attempting to deploy a commit to the motia Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a complete provider-opencode-go Rust worker with router registration, credential resolution, live model discovery, reasoning support, Chat Completions SSE streaming, wire serialization, typed schemas, integration tests, and release workflow integration.

Changes

OpenCode Go provider

Layer / File(s) Summary
Worker package and deployment contract
.github/..., README.md, harness/..., llm-router/README.md, provider-opencode-go/Cargo.toml, provider-opencode-go/*.yaml, provider-opencode-go/src/{lib,main,manifest}.rs, provider-opencode-go/prompts/*
Adds the worker package, binary, manifest, runtime entry point, permissions, configuration policy, identity prompt, documentation, and release workflow integration.
Configuration and model discovery
provider-opencode-go/src/{config,curated,discovery,errors,reasoning}.rs
Adds credential and endpoint resolution, error classification, curated model metadata, live model refresh, and reasoning-effort selection.
Request, wire, and streaming pipeline
provider-opencode-go/src/{request,sse,upstream}.rs, provider-opencode-go/src/wire/*
Builds authenticated Chat Completions requests, converts messages and tools, decodes SSE responses, and emits assistant stream events.
Router registration and provider runtime
provider-opencode-go/src/{register,router_client,state,stream_fn,surface}.rs
Adds provider registration, token persistence, router-ready handling, function surfaces, retries, cancellation, and abort-aware streaming.
Function schemas and end-to-end validation
provider-opencode-go/tests/*
Adds typed schema goldens, schema validation helpers, and engine-backed tests for streaming, authentication failures, model reconciliation, and router restart recovery.

Estimated code review effort: 5 (Critical) | ~90 minutes

Possibly related PRs

  • iii-hq/workers#382: Adds a structurally parallel Rust llm-router provider.
  • iii-hq/workers#525: Introduces shared provider scaffold components used by this worker.
  • iii-hq/workers#691: Adds a structurally parallel provider worker with similar integration and validation.

Suggested labels: no-ticket

Poem

A rabbit hops through streams of light,
Models bloom in catalogues bright.
Tools and thoughts join every call,
SSE events answer all.
Tags and tests now land just right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 86.90% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the addition of the OpenCode Go Chat Completions provider worker.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (3)
provider-opencode-go/src/router_client.rs (1)

1-3: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the module doc with register.

The doc states that every call binds PROVIDER_ID and carries the registration token. register at Line 41 forwards only the declaration value. The provider id and token travel inside that payload, built in register::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 lift

Add 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() publishes provider::opencode_go::abort, and stream_fn.rs couples the abort guard to pump_abortable and to the pre-spawn is_fired() check at Line 147 of provider-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 reports aborted.

🤖 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 win

Parse SSE data fields per the spec.

data_line matches only the exact prefix "data: " and keeps only the last matching line. Two valid frame shapes are lost. A frame written as data:{...} without the space is ignored. A frame with several data: 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 data lines 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 decode closure then compares data.as_str() against "[DONE]" and passes &data to serde_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

📥 Commits

Reviewing files that changed from the base of the PR and between df0102e and 20201db.

⛔ Files ignored due to path filters (1)
  • provider-opencode-go/Cargo.lock is 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.yml
  • README.md
  • harness/iii.worker.yaml
  • llm-router/README.md
  • provider-opencode-go/.gitignore
  • provider-opencode-go/Cargo.toml
  • provider-opencode-go/README.md
  • provider-opencode-go/build.rs
  • provider-opencode-go/config.yaml
  • provider-opencode-go/iii-permissions.yaml
  • provider-opencode-go/iii.worker.yaml
  • provider-opencode-go/prompts/identity.txt
  • provider-opencode-go/src/config.rs
  • provider-opencode-go/src/curated.rs
  • provider-opencode-go/src/discovery.rs
  • provider-opencode-go/src/errors.rs
  • provider-opencode-go/src/lib.rs
  • provider-opencode-go/src/main.rs
  • provider-opencode-go/src/manifest.rs
  • provider-opencode-go/src/reasoning.rs
  • provider-opencode-go/src/register.rs
  • provider-opencode-go/src/request.rs
  • provider-opencode-go/src/router_client.rs
  • provider-opencode-go/src/sse.rs
  • provider-opencode-go/src/state.rs
  • provider-opencode-go/src/stream_fn.rs
  • provider-opencode-go/src/surface.rs
  • provider-opencode-go/src/upstream.rs
  • provider-opencode-go/src/wire/messages.rs
  • provider-opencode-go/src/wire/mod.rs
  • provider-opencode-go/src/wire/names.rs
  • provider-opencode-go/src/wire/tools.rs
  • provider-opencode-go/tests/golden/schemas/provider.opencode_go.abort.json
  • provider-opencode-go/tests/golden/schemas/provider.opencode_go.on_router_ready.json
  • provider-opencode-go/tests/golden/schemas/provider.opencode_go.refresh_models.json
  • provider-opencode-go/tests/golden/schemas/provider.opencode_go.stream.json
  • provider-opencode-go/tests/integration.rs
  • provider-opencode-go/tests/schemas.rs
  • provider-opencode-go/tests/support/mod.rs

Comment thread provider-opencode-go/iii-permissions.yaml
Comment thread provider-opencode-go/src/curated.rs Outdated
Comment thread provider-opencode-go/src/discovery.rs
Comment thread provider-opencode-go/src/main.rs
Comment thread provider-opencode-go/src/reasoning.rs Outdated
Comment thread provider-opencode-go/src/register.rs Outdated
Comment thread provider-opencode-go/src/sse.rs
Comment thread provider-opencode-go/src/sse.rs
Comment thread README.md Outdated
- 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
@rohitg00

rohitg00 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Hey @faramirezs, Thanks for the opencode-go-provider.

I have assigned @ytallo and @andersonleal as reviewers, they'll get back to you soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants