From 6d989de33636138f7aef5e86dc8d3e605565c6c7 Mon Sep 17 00:00:00 2001
From: chengyongru <2755839590@qq.com>
Date: Wed, 24 Jun 2026 00:46:53 +0800
Subject: [PATCH] docs: document OpenCode provider setup
Maintainer edit: document OpenCode Zen and Go configuration, keep their registry entries with gateway providers, and add focused provider registration tests.
---
docs/configuration.md | 69 +++++++++++++++
docs/provider-cookbook.md | 74 ++++++++++++++++
docs/providers.md | 57 ++++++++++++
nanobot/providers/registry.py | 46 +++++-----
tests/providers/test_opencode_provider.py | 102 ++++++++++++++++++++++
5 files changed, 328 insertions(+), 20 deletions(-)
create mode 100644 tests/providers/test_opencode_provider.py
diff --git a/docs/configuration.md b/docs/configuration.md
index b370905e..071dd620 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -199,6 +199,7 @@ Tracing covers the providers that go through nanobot's OpenAI-compatible client
> - **MiniMax (Mainland China)**: If your API key is from MiniMax's mainland China platform (minimaxi.com), set `"apiBase": "https://api.minimaxi.com/v1"` in your minimax provider config.
> - **MiniMax thinking mode**: `providers.minimaxAnthropic` is the config block for `reasoningEffort` / thinking mode. MiniMax exposes that capability through its Anthropic-compatible endpoint, so nanobot keeps it as a separate provider instead of guessing MiniMax-specific thinking parameters on the generic OpenAI-compatible `minimax` endpoint. It uses the same `MINIMAX_API_KEY`. Default Anthropic-compatible base URL: `https://api.minimax.io/anthropic`; for mainland China use `https://api.minimaxi.com/anthropic`.
> - **VolcEngine / BytePlus Coding Plan**: Subscription endpoints are configured through dedicated providers `volcengineCodingPlan` or `byteplusCodingPlan`, separate from the pay-per-use `volcengine` / `byteplus` providers.
+> - **OpenCode Zen / Go**: `providers.opencodeZen` and `providers.opencodeGo` use the same `OPENCODE_API_KEY`, but route to different OpenCode gateways. These providers use OpenCode's OpenAI-compatible `chat/completions` endpoints; choose model IDs from that endpoint family.
> - **Zhipu Coding Plan**: If you're on Zhipu's coding plan, set `"apiBase": "https://open.bigmodel.cn/api/coding/paas/v4"` in your zhipu provider config.
> - **Alibaba Cloud BaiLian**: If you're using Alibaba Cloud BaiLian's OpenAI-compatible endpoint, set `"apiBase": "https://dashscope.aliyuncs.com/compatible-mode/v1"` in your dashscope provider config.
> - **StepFun Step Plan**: If you're on StepFun's Step Plan subscription, set `"apiBase": "https://api.stepfun.ai/step_plan/v1"` in your stepfun provider config. Supported models include `step-3.5-flash`, `step-3.5-flash-2603`, and `step-router-v1`.
@@ -211,6 +212,8 @@ Tracing covers the providers that go through nanobot's OpenAI-compatible client
|----------|---------|-------------|
| `custom` | Any OpenAI-compatible endpoint | — |
| `openrouter` | LLM gateway for hosted model families + Voice transcription (STT models) | [openrouter.ai](https://openrouter.ai) |
+| `opencode_zen` | LLM gateway (OpenCode Zen coding-agent models) | [opencode.ai/docs/zen](https://opencode.ai/docs/zen/) |
+| `opencode_go` | LLM gateway (OpenCode Go low-cost coding models) | [opencode.ai/docs/go](https://opencode.ai/docs/go/) |
| `huggingface` | LLM (Hugging Face Inference Providers) | [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) |
| `skywork` | LLM (Skywork / APIFree API gateway) | [apifree.ai](https://www.apifree.ai) |
| `volcengine` | LLM (VolcEngine, pay-per-use) | [Coding Plan](https://www.volcengine.com/activity/codingplan?utm_campaign=nanobot&utm_content=nanobot&utm_medium=devrel&utm_source=OWO&utm_term=nanobot) · [volcengine.com](https://www.volcengine.com) |
@@ -677,6 +680,72 @@ nanobot agent -c ~/.nanobot-telegram/config.json -w /tmp/nanobot-telegram-test -
+
+OpenCode Zen / Go
+
+OpenCode Zen and OpenCode Go are available through nanobot's built-in
+OpenAI-compatible provider flow. They share the `OPENCODE_API_KEY` environment
+variable, but use separate provider keys and default base URLs:
+
+| Provider | Default API base | Model prefix accepted by nanobot |
+|----------|------------------|-----------------------------------|
+| `opencode_zen` | `https://opencode.ai/zen/v1` | `opencode/` |
+| `opencode_go` | `https://opencode.ai/zen/go/v1` | `opencode-go/` |
+
+OpenCode Zen:
+
+```json
+{
+ "providers": {
+ "opencodeZen": {
+ "apiKey": "${OPENCODE_API_KEY}"
+ }
+ },
+ "modelPresets": {
+ "opencodeZen": {
+ "provider": "opencode_zen",
+ "model": "opencode/deepseek-v4-pro"
+ }
+ },
+ "agents": {
+ "defaults": {
+ "modelPreset": "opencodeZen"
+ }
+ }
+}
+```
+
+OpenCode Go:
+
+```json
+{
+ "providers": {
+ "opencodeGo": {
+ "apiKey": "${OPENCODE_API_KEY}"
+ }
+ },
+ "modelPresets": {
+ "opencodeGo": {
+ "provider": "opencode_go",
+ "model": "opencode-go/deepseek-v4-flash"
+ }
+ },
+ "agents": {
+ "defaults": {
+ "modelPreset": "opencodeGo"
+ }
+ }
+}
+```
+
+OpenCode's own docs list models across `responses`, `messages`,
+provider-specific model endpoints, and `chat/completions`. nanobot's OpenCode
+providers use the OpenAI-compatible `chat/completions` path, so pick model IDs
+from that endpoint family. The `opencode/...` and `opencode-go/...` prefixes are
+accepted for config readability and stripped before sending the request.
+
+
+
LongCat (OpenAI-compatible)
diff --git a/docs/provider-cookbook.md b/docs/provider-cookbook.md
index d78ae458..0d9d1598 100644
--- a/docs/provider-cookbook.md
+++ b/docs/provider-cookbook.md
@@ -15,6 +15,7 @@ Match the recipe to the credential or endpoint you already have:
| What you have | Recipe | Must match |
|---|---|---|
| A gateway key and model IDs that include a model family path, such as `provider/model-name` | [OpenRouter Gateway](#recipe-openrouter-gateway) | API key, provider config key, preset provider, and gateway model ID |
+| An OpenCode Zen or Go key | [OpenCode Zen or Go](#recipe-opencode-zen-or-go) | `OPENCODE_API_KEY`, the Zen/Go provider key, and a model ID from the matching OpenCode endpoint |
| An OpenAI platform API key and OpenAI model ID | [OpenAI Direct](#recipe-openai-direct) | `OPENAI_API_KEY`, `provider: "openai"`, and an OpenAI model available to that account |
| An Anthropic API key and Anthropic model ID | [Anthropic Direct](#recipe-anthropic-direct) | `ANTHROPIC_API_KEY`, `provider: "anthropic"`, and a non-gateway model ID |
| An OpenAI-compatible `/v1` endpoint that is not a named nanobot provider | [Custom OpenAI-Compatible Provider](#recipe-custom-openai-compatible-provider) | `apiBase`, optional API key, and the model ID served by that endpoint |
@@ -94,6 +95,79 @@ nanobot agent -m "Hello!"
If this fails with `401` or `unauthorized`, check that `OPENROUTER_API_KEY` is visible in the same terminal or service that starts nanobot. If it fails with `model not found`, choose a model ID that OpenRouter lists for your account.
+## Recipe: OpenCode Zen or Go
+
+This recipe applies when your credential comes from OpenCode Zen or OpenCode Go.
+Both providers use `OPENCODE_API_KEY`; pick the provider block that matches the
+subscription or balance you want to use.
+
+OpenCode Zen:
+
+```json
+{
+ "providers": {
+ "opencodeZen": {
+ "apiKey": "${OPENCODE_API_KEY}"
+ }
+ },
+ "modelPresets": {
+ "primary": {
+ "label": "OpenCode Zen",
+ "provider": "opencode_zen",
+ "model": "opencode/deepseek-v4-pro",
+ "maxTokens": 4096,
+ "contextWindowTokens": 65536,
+ "temperature": 0.1
+ }
+ },
+ "agents": {
+ "defaults": {
+ "modelPreset": "primary"
+ }
+ }
+}
+```
+
+OpenCode Go:
+
+```json
+{
+ "providers": {
+ "opencodeGo": {
+ "apiKey": "${OPENCODE_API_KEY}"
+ }
+ },
+ "modelPresets": {
+ "primary": {
+ "label": "OpenCode Go",
+ "provider": "opencode_go",
+ "model": "opencode-go/deepseek-v4-flash",
+ "maxTokens": 4096,
+ "contextWindowTokens": 65536,
+ "temperature": 0.1
+ }
+ },
+ "agents": {
+ "defaults": {
+ "modelPreset": "primary"
+ }
+ }
+}
+```
+
+Verify:
+
+```bash
+nanobot status
+nanobot agent -m "Hello!"
+```
+
+OpenCode's docs list models across multiple endpoint types. The `opencode_zen`
+and `opencode_go` providers in nanobot use the OpenAI-compatible
+`chat/completions` path. If a model fails with `model not found` or an endpoint
+shape error, choose a model that OpenCode lists under `chat/completions` for the
+matching Zen or Go endpoint.
+
## Recipe: OpenAI Direct
This recipe applies when you have an OpenAI API key and want to call OpenAI directly instead of through a gateway.
diff --git a/docs/providers.md b/docs/providers.md
index f6d6dda6..41b78849 100644
--- a/docs/providers.md
+++ b/docs/providers.md
@@ -17,6 +17,7 @@ The docs show concrete provider names so the JSON is copyable, not because nanob
| If you have... | Configure... |
|---|---|
| An API key from a hosted provider or gateway | That provider's `providers..apiKey`, then a preset with that provider name and a model ID from that service. |
+| An OpenCode Zen or Go key | `providers.opencodeZen.apiKey` or `providers.opencodeGo.apiKey`, then a preset with `provider: "opencode_zen"` or `provider: "opencode_go"`. |
| A company proxy or regional endpoint | The matching provider block plus `apiBase` if the proxy gives you a URL. |
| A local OpenAI-compatible server | A local provider block such as `ollama`, `vllm`, `lmStudio`, or `custom`, usually with `apiBase`. |
| An OAuth-based account | Run the matching `nanobot provider login ...` command, then select that provider explicitly in a preset. |
@@ -94,6 +95,62 @@ Gateway-style setup for model IDs served through OpenRouter.
Use the model ID exactly as OpenRouter lists it.
+### OpenCode Zen and Go
+
+OpenCode Zen and OpenCode Go are OpenCode-managed gateways for coding-agent models.
+They share `OPENCODE_API_KEY`, but use separate provider config keys and default base
+URLs in nanobot.
+
+```json
+{
+ "providers": {
+ "opencodeZen": {
+ "apiKey": "${OPENCODE_API_KEY}"
+ }
+ },
+ "modelPresets": {
+ "primary": {
+ "provider": "opencode_zen",
+ "model": "opencode/deepseek-v4-pro",
+ "maxTokens": 8192,
+ "contextWindowTokens": 65536
+ }
+ },
+ "agents": {
+ "defaults": {
+ "modelPreset": "primary"
+ }
+ }
+}
+```
+
+For OpenCode Go, switch the provider block and preset:
+
+```json
+{
+ "providers": {
+ "opencodeGo": {
+ "apiKey": "${OPENCODE_API_KEY}"
+ }
+ },
+ "modelPresets": {
+ "primary": {
+ "provider": "opencode_go",
+ "model": "opencode-go/deepseek-v4-flash",
+ "maxTokens": 8192,
+ "contextWindowTokens": 65536
+ }
+ }
+}
+```
+
+OpenCode documents model IDs with `opencode/` for Zen and
+`opencode-go/` for Go. nanobot accepts those prefixes and strips them
+before sending the request to OpenCode. Use model IDs that OpenCode lists under
+the `chat/completions` endpoint; models listed only under `responses`,
+`messages`, or provider-specific endpoints are not handled by this
+OpenAI-compatible provider path.
+
### Anthropic Direct
```json
diff --git a/nanobot/providers/registry.py b/nanobot/providers/registry.py
index 88bf76f4..d9e2ad37 100644
--- a/nanobot/providers/registry.py
+++ b/nanobot/providers/registry.py
@@ -176,6 +176,32 @@ PROVIDERS: tuple[ProviderSpec, ...] = (
supports_prompt_caching=True,
gateway_reasoning_style="reasoning_effort",
),
+ # OpenCode Zen: OpenAI-compatible chat-completions gateway for coding models.
+ # OpenCode's own config uses "opencode/"; send the bare model upstream.
+ ProviderSpec(
+ name="opencode_zen",
+ keywords=("opencode/", "opencode_zen", "opencode-zen"),
+ env_key="OPENCODE_API_KEY",
+ display_name="OpenCode Zen",
+ backend="openai_compat",
+ is_gateway=True,
+ detect_by_base_keyword="opencode.ai/zen",
+ default_api_base="https://opencode.ai/zen/v1",
+ strip_model_prefixes=("opencode", "opencode_zen", "opencode-zen"),
+ ),
+ # OpenCode Go: OpenAI-compatible chat-completions gateway for low-cost models.
+ # OpenCode's own config uses "opencode-go/"; send the bare model upstream.
+ ProviderSpec(
+ name="opencode_go",
+ keywords=("opencode-go", "opencode_go"),
+ env_key="OPENCODE_API_KEY",
+ display_name="OpenCode Go",
+ backend="openai_compat",
+ is_gateway=True,
+ detect_by_base_keyword="opencode.ai/zen/go",
+ default_api_base="https://opencode.ai/zen/go/v1",
+ strip_model_prefixes=("opencode-go", "opencode_go"),
+ ),
# Hugging Face Inference Providers: OpenAI-compatible router for chat models.
ProviderSpec(
name="huggingface",
@@ -543,26 +569,6 @@ PROVIDERS: tuple[ProviderSpec, ...] = (
detect_by_base_keyword="nvidia.com",
default_api_base="https://integrate.api.nvidia.com/v1",
),
- # OpenCode Zen: curated reliable models for coding agents
- ProviderSpec(
- name="opencode_zen",
- keywords=("opencode_zen", "opencode-zen", "zen"),
- env_key="OPENCODE_API_KEY",
- display_name="OpenCode Zen",
- backend="openai_compat",
- is_gateway=True,
- default_api_base="https://opencode.ai/zen/v1",
- ),
- # OpenCode Go: low-cost coding models
- ProviderSpec(
- name="opencode_go",
- keywords=("opencode_go", "opencode-go"),
- env_key="OPENCODE_API_KEY",
- display_name="OpenCode Go",
- backend="openai_compat",
- is_gateway=True,
- default_api_base="https://opencode.ai/zen/go/v1",
- ),
# === Auxiliary (not a primary LLM provider) ============================
# Groq: mainly used for Whisper voice transcription, also usable for LLM
ProviderSpec(
diff --git a/tests/providers/test_opencode_provider.py b/tests/providers/test_opencode_provider.py
new file mode 100644
index 00000000..d6372761
--- /dev/null
+++ b/tests/providers/test_opencode_provider.py
@@ -0,0 +1,102 @@
+"""Tests for the OpenCode Zen and OpenCode Go provider registrations."""
+
+from nanobot.config.schema import Config, ProvidersConfig
+from nanobot.providers.openai_compat_provider import OpenAICompatProvider
+from nanobot.providers.registry import PROVIDERS, find_by_name
+
+
+def test_opencode_config_fields_exist() -> None:
+ config = ProvidersConfig()
+
+ assert hasattr(config, "opencode_zen")
+ assert hasattr(config, "opencode_go")
+
+
+def test_opencode_specs_use_openai_compatible_gateways() -> None:
+ specs = {spec.name: spec for spec in PROVIDERS}
+
+ zen = specs["opencode_zen"]
+ assert zen.backend == "openai_compat"
+ assert zen.env_key == "OPENCODE_API_KEY"
+ assert zen.display_name == "OpenCode Zen"
+ assert zen.is_gateway is True
+ assert zen.detect_by_base_keyword == "opencode.ai/zen"
+ assert zen.default_api_base == "https://opencode.ai/zen/v1"
+ assert "opencode" in zen.strip_model_prefixes
+
+ go = specs["opencode_go"]
+ assert go.backend == "openai_compat"
+ assert go.env_key == "OPENCODE_API_KEY"
+ assert go.display_name == "OpenCode Go"
+ assert go.is_gateway is True
+ assert go.detect_by_base_keyword == "opencode.ai/zen/go"
+ assert go.default_api_base == "https://opencode.ai/zen/go/v1"
+ assert "opencode-go" in go.strip_model_prefixes
+
+
+def test_find_by_name_opencode_providers() -> None:
+ zen = find_by_name("opencode_zen")
+ assert zen is not None
+ assert zen.name == "opencode_zen"
+
+ go = find_by_name("opencode-go")
+ assert go is not None
+ assert go.name == "opencode_go"
+
+
+def test_opencode_forced_providers_use_default_api_base() -> None:
+ zen_config = Config.model_validate(
+ {
+ "providers": {"opencodeZen": {"apiKey": "opencode-key"}},
+ "agents": {"defaults": {"provider": "opencode_zen", "model": "opencode/o3"}},
+ }
+ )
+
+ assert zen_config.get_provider_name() == "opencode_zen"
+ assert zen_config.get_api_key() == "opencode-key"
+ assert zen_config.get_api_base() == "https://opencode.ai/zen/v1"
+
+ go_config = Config.model_validate(
+ {
+ "providers": {"opencodeGo": {"apiKey": "opencode-key"}},
+ "agents": {"defaults": {"provider": "opencode_go", "model": "opencode-go/o3"}},
+ }
+ )
+
+ assert go_config.get_provider_name() == "opencode_go"
+ assert go_config.get_api_key() == "opencode-key"
+ assert go_config.get_api_base() == "https://opencode.ai/zen/go/v1"
+
+
+def test_opencode_prefixes_are_stripped_before_request() -> None:
+ zen_provider = OpenAICompatProvider(
+ api_key=None,
+ default_model="opencode/o3",
+ spec=find_by_name("opencode_zen"),
+ )
+ zen_kwargs = zen_provider._build_kwargs(
+ messages=[{"role": "user", "content": "hi"}],
+ tools=None,
+ model="opencode/o3",
+ max_tokens=1024,
+ temperature=0.7,
+ reasoning_effort=None,
+ tool_choice=None,
+ )
+ assert zen_kwargs["model"] == "o3"
+
+ go_provider = OpenAICompatProvider(
+ api_key=None,
+ default_model="opencode-go/o3",
+ spec=find_by_name("opencode_go"),
+ )
+ go_kwargs = go_provider._build_kwargs(
+ messages=[{"role": "user", "content": "hi"}],
+ tools=None,
+ model="opencode-go/o3",
+ max_tokens=1024,
+ temperature=0.7,
+ reasoning_effort=None,
+ tool_choice=None,
+ )
+ assert go_kwargs["model"] == "o3"