Skip to content

Add OpenCode Go provider - #54

Open
mulfyx wants to merge 3 commits into
raine:mainfrom
mulfyx:feat/opencode-go-provider-pr
Open

Add OpenCode Go provider#54
mulfyx wants to merge 3 commits into
raine:mainfrom
mulfyx:feat/opencode-go-provider-pr

Conversation

@mulfyx

@mulfyx mulfyx commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Add OpenCode Go provider

Closes #46.

Summary

  • Add OpenCode Go as an API-key provider behind the existing Anthropic-compatible server.
  • Follow OpenCode's documented endpoint table: 11 models use Chat Completions, GPT 5.6 Luna uses Responses, and 6 models use Messages.
  • Support the provider-qualified opencode-go/<model-id> form for every model and bare IDs where they do not collide with an existing provider.
  • Support OpenCode Go models through /v1/messages and the opt-in /v1/chat/completions and /v1/responses routes.
  • Add configuration, model registration, CLI output, provider documentation, and protocol-level regression coverage.
  • Preserve images returned inside Anthropic tool_result blocks when translating to OpenAI-compatible Chat Completions.
  • Keep parallel tool responses contiguous before the follow-up user vision message.
  • Namespace downstream Chat Completions tool IDs per response so nested agents cannot collide when an upstream model reuses IDs such as Agent_0.

Routing and authentication

The implementation registers the 18 mappings in OpenCode Go's published endpoint table instead of guessing a protocol from /models or using models.dev.

Upstream protocol Authentication Registered models
OpenAI-compatible Chat Completions Authorization: Bearer ... grok-4.5, glm-5.2, glm-5.1, kimi-k3, kimi-k2.7-code, kimi-k2.6, deepseek-v4-pro, deepseek-v4-flash, mimo-v2.5, mimo-v2.5-pro, hy3
OpenAI Responses Authorization: Bearer ... gpt-5.6-luna
Anthropic-compatible Messages x-api-key + anthropic-version minimax-m3, minimax-m2.7, minimax-m2.5, qwen3.7-max, qwen3.7-plus, qwen3.6-plus

The API key is resolved in this order:

  1. CCP_OPENCODE_API_KEY
  2. OPENCODE_API_KEY
  3. opencode.apiKey in config.json

CCP_OPENCODE_BASE_URL or opencode.baseUrl can override the default https://opencode.ai/zen/go/v1 base URL.

Bare IDs already owned by Codex, Grok, or Kimi keep their existing routing. For example, kimi-k2.6 remains a Kimi model while opencode-go/kimi-k2.6 selects OpenCode Go. The same rule applies to gpt-5.6-luna, grok-4.5, and kimi-k3.

Protocol handling

  • Chat Completions has a provider-local request translator and stateful SSE reducer for reasoning, text, fragmented tool calls, usage, finish reasons, and OpenCode's cost metadata trailer.
  • Assistant reasoning is replayed in reasoning_content; DeepSeek assistant turns include the field even when it is empty, matching OpenCode's own transform.
  • tool_choice and disable_parallel_tool_use map to tool_choice and parallel_tool_calls without changing other providers.
  • Anthropic image blocks inside tool_result are converted to image_url parts. All matching role: tool messages are emitted first, followed by one role: user vision message, matching OpenCode's OpenAI-compatible adapter.
  • Chat Completions tool-call IDs are generated from CCP's unique response message ID and the upstream tool index. Fragmented deltas keep one stable ID, while independent parent and child completions receive different IDs even when upstream restarts from Agent_0.
  • Messages models retain their native Anthropic request and event shapes while model names and authentication are rewritten for OpenCode Go. Usage parsing remains correct across arbitrary HTTP chunk boundaries.
  • GPT 5.6 Luna reuses the generic Responses request/event translation without Codex-only service-tier, lane, compaction, or effort overrides.
  • Streaming responses remain live through the provider contract used by Anthropic Messages and both model-routed OpenAI-compatible surfaces. Buffered responses are bounded to 8 MiB.
  • Missing credentials return HTTP 401. Permission failures, rate limits, invalid requests, and upstream failures preserve their distinct error classes; 429 keeps Retry-After.
  • /v1/messages/count_tokens remains a local heuristic and does not send a request to OpenCode Go.

The catalog is intentionally static. OpenCode's live /models endpoint can expose IDs before the documentation assigns a wire protocol; those IDs remain unroutable until an endpoint mapping is published.

Verification

  • cargo fmt --all -- --check
  • cargo clippy --offline --all-targets -- -D warnings
  • cargo test --offline --all-targets: 860 passed, 0 failed
  • documentation build: 22 pages built
  • deterministic protocol tests cover every byte split, incomplete and malformed SSE frames, live-first streaming, fragmented tool arguments, usage after finish reasons, native Messages usage, Responses completion, auth headers, status mapping, and provider-contract streaming
  • regression tests cover base64 and URL image tool results, image ordering, parallel tool-result ordering, fragmented tool-call IDs, parent/child ID isolation, and request round trips
  • live streaming and buffered checks passed through Chat Completions, Responses, and Messages, including both OpenAI-compatible proxy routes
  • a live GLM tool round trip produced thinking plus a forced tool_use, accepted the matching tool_result, and completed normally
  • Claude Code CLI 2.1.218 completed both a subagent run and a one-agent Workflow through opencode-go/glm-5.2
  • a live Kimi K3 turn issued Read, Read(image), and Bash in parallel; upstream received assistant,tool,tool,tool,user(image_url) and returned HTTP 200 with PARALLEL_IMAGE_OK:circle:blue:7
  • a live Kimi K2.7 Code nested run completed main → subagent → sub-subagent with distinct response-scoped tool IDs and returned NESTED_OK
  • all 18 documented model routes were exercised: 17 returned successful model responses; deepseek-v4-flash reached OpenCode and returned its account-level HTTP 403 requiring explicit China-hosting opt-in

References

@mulfyx
mulfyx force-pushed the feat/opencode-go-provider-pr branch from 1071d2f to 8ba593c Compare July 29, 2026 13:39
@mulfyx
mulfyx force-pushed the feat/opencode-go-provider-pr branch from 8ba593c to b283b25 Compare August 1, 2026 11:00
@raine

raine commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Thanks! Review found the points below. Could you please check whether they are valid, especially against the live OpenCode Go stream shapes you observed?

https://gist.github.com/raine/10bb94843909186b8ef670989c827981

@mulfyx

mulfyx commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — I verified all three findings against the PR head.

1. Tool call followed by text/reasoning

Confirmed and fixed in 16bf3d6.

The reducer used one started flag for two different facts: whether a tool block had ever been emitted and whether it was currently open. Closing an emitted block cleared the flag, so finalize() later misclassified it as a tool that never received an ID/name.

The tool lifecycle is now explicit: Pending -> Open -> Closed. finalize() rejects only Pending, while close_tools() moves Open to Closed without losing the fact that the block was emitted. A continuation for the same upstream tool index after its Anthropic block has closed now fails explicitly instead of silently dropping arguments or emitting a duplicate block. A new tool index after text remains valid.

Regression coverage now includes:

  • complete tool -> text -> finish_reason: tool_calls -> [DONE];
  • complete tool -> reasoning -> finish;
  • every byte split for both stream shapes;
  • exactly one tool start/stop, with the stop before the following text/thinking block;
  • missing ID/name still failing at finalization;
  • same-index continuation after close failing explicitly;
  • a new tool index starting successfully after text.

The original mock reproduced HTTP 502 before the fix. With the same upstream sequence, buffered and streaming HTTP routes now return 200. The streaming order is:

tool start -> tool delta -> tool stop -> text start -> text delta -> text stop -> message stop

I also checked the available live captures: 330 upstream SSE bodies in total, 154 with non-empty tool-call arrays across 10 models, and none contained non-empty text or reasoning after the first tool delta. So this was a reachable state-machine bug, but that ordering was not observed in the current live matrix.

2. OpenAI ingress loses parallel_tool_calls: false

Confirmed. Chat models omit the field upstream, while OpenCode Luna receives parallel_tool_calls: true.

The loss happens in the shared openai_compat ingress before the request reaches an OpenCode translator, and that parser is unchanged from the PR base. Native Codex /v1/responses is not affected because it uses its own passthrough path.

I agree this should be tracked as a shared follow-up rather than worked around inside OpenCode or used as a merge blocker for this PR. The shared fix needs explicit precedence against Anthropic tool_choice.disable_parallel_tool_use plus coverage for both OpenAI ingress surfaces and the affected non-Codex providers.

3. OpenCode CLI status implementation

Confirmed as an implementation detail, not a user-facing regression. The current CLI intentionally exposes no OpenCode auth command, matching the documentation and the v1 scope in issue #46. The provider still has to return a CliHandlers implementation because that is required by the current Provider trait.

I left the CLI surface unchanged. A minimal opencode status command or a cleaner no-auth provider abstraction can be handled separately.

Verification after the fix

  • cargo fmt --all -- --check
  • cargo clippy --offline --all-targets -- -D warnings
  • cargo test --offline --all-targets: 864 passed, 0 failed
  • isolated buffered mock HTTP reproduction: 502 before, 200 after
  • isolated streaming mock HTTP reproduction: 200 with ordered tool stop before text start

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.

FR: add OpenCode Go provider

2 participants