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"