Add OpenCode Go subscription gateway provider (#505)

## Summary

Adds support for the **OpenCode Go** subscription gateway at
`opencode.ai/zen/go/v1`, as requested in #504.

OpenCode Go exposes the same OpenAI-compatible Chat Completions API as
OpenCode Zen, so the implementation reuses `OpenCodeProvider` with a
configurable `provider_name` parameter — avoiding code duplication.

### Changes
- **Provider**: `OpenCodeProvider` now accepts `provider_name` (defaults
to `"OPENCODE"` for backward compatibility)
- **Catalog**: New `opencode_go` descriptor with correct base URL,
credential, and capabilities
- **Registry**: `_create_opencode_go` factory that passes
`provider_name="OPENCODE_GO"`
- **Settings**: `opencode_go_api_key` and `opencode_go_proxy` fields
- **Admin UI**: OpenCode Go API key, proxy, and smoke model config
fields
- **API services**: `opencode_go` added to OpenAI Chat Completions
upstream IDs
- **Smoke config**: Default smoke model `opencode_go/gpt-5.3-codex`
- **Tests**: New test for base URL, provider name, and API key; existing
tests updated

## Test plan
- [x] `test_opencode_go_provider_config_uses_correct_base_url_and_name`
— passes
- [x] `test_create_provider_instantiates_each_builtin` — covers
opencode_go
- [x]
`test_provider_and_platform_registries_include_advertised_builtins` —
covers opencode_go
- [x] `uv run ruff format`, `ruff check`, `ty check`, `pytest` — pass
locally on Python 3.14

Closes #504.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Alishahryar1 <alishahryar2@gmail.com>
This commit is contained in:
George Levis
2026-05-21 07:22:20 +03:00
committed by GitHub
parent d5715a607e
commit f5e49ea78d
16 changed files with 81 additions and 5 deletions
+7 -1
View File
@@ -22,6 +22,10 @@ WAFER_API_KEY=""
OPENCODE_API_KEY=""
# OpenCode Go Config (OpenAI-compatible Chat Completions at opencode.ai/zen/go/v1)
OPENCODE_GO_API_KEY=""
# Z.ai Config (Anthropic-compatible Messages at api.z.ai/api/anthropic)
ZAI_API_KEY=""
@@ -44,7 +48,7 @@ OLLAMA_BASE_URL="http://localhost:11434"
# All Claude model requests are mapped to these models, plain model is fallback
# Format: provider_type/model/name
# Valid providers: "nvidia_nim" | "open_router" | "deepseek" | "lmstudio" | "llamacpp" | "ollama" | "kimi" | "wafer" | "opencode" | "zai" | "fireworks"
# Valid providers: "nvidia_nim" | "open_router" | "deepseek" | "lmstudio" | "llamacpp" | "ollama" | "kimi" | "wafer" | "opencode" | "opencode_go" | "zai" | "fireworks"
MODEL_OPUS=
MODEL_SONNET=
MODEL_HAIKU=
@@ -62,6 +66,7 @@ FCC_SMOKE_MODEL_OLLAMA=
FCC_SMOKE_MODEL_KIMI=
FCC_SMOKE_MODEL_WAFER=
FCC_SMOKE_MODEL_OPENCODE=
FCC_SMOKE_MODEL_OPENCODE_GO=
FCC_SMOKE_MODEL_ZAI=
FCC_SMOKE_MODEL_FIREWORKS=
FCC_SMOKE_NIM_MODELS=
@@ -88,6 +93,7 @@ LLAMACPP_PROXY=""
KIMI_PROXY=""
WAFER_PROXY=""
OPENCODE_PROXY=""
OPENCODE_GO_PROXY=""
ZAI_PROXY=""
FIREWORKS_PROXY=""
+24
View File
@@ -167,6 +167,15 @@ FIELDS: tuple[ConfigFieldSpec, ...] = (
secret=True,
description="OpenCode Zen curated model gateway at opencode.ai.",
),
ConfigFieldSpec(
"OPENCODE_GO_API_KEY",
"OpenCode Go API Key",
"providers",
"secret",
settings_attr="opencode_go_api_key",
secret=True,
description="OpenCode Go subscription gateway at opencode.ai.",
),
ConfigFieldSpec(
"ZAI_API_KEY",
"Z.ai API Key",
@@ -269,6 +278,15 @@ FIELDS: tuple[ConfigFieldSpec, ...] = (
secret=True,
advanced=True,
),
ConfigFieldSpec(
"OPENCODE_GO_PROXY",
"OpenCode Go Proxy",
"providers",
"secret",
settings_attr="opencode_go_proxy",
secret=True,
advanced=True,
),
ConfigFieldSpec(
"ZAI_PROXY",
"Z.ai Proxy",
@@ -729,6 +747,12 @@ FIELDS: tuple[ConfigFieldSpec, ...] = (
"smoke",
advanced=True,
),
ConfigFieldSpec(
"FCC_SMOKE_MODEL_OPENCODE_GO",
"Smoke OpenCode Go Model",
"smoke",
advanced=True,
),
ConfigFieldSpec(
"FCC_SMOKE_MODEL_ZAI",
"Smoke Z.ai Model",
+1
View File
@@ -68,6 +68,7 @@ function providerName(providerId) {
kimi: "Kimi",
wafer: "Wafer",
opencode: "OpenCode Zen",
opencode_go: "OpenCode Go",
zai: "Z.ai",
};
if (names[providerId]) return names[providerId];
+1 -1
View File
@@ -34,7 +34,7 @@ TokenCounter = Callable[[list[Any], str | list[Any] | None, list[Any] | None], i
ProviderGetter = Callable[[str], BaseProvider]
# Providers that use ``/chat/completions`` + Anthropic-to-OpenAI conversion (not native Messages).
_OPENAI_CHAT_UPSTREAM_IDS = frozenset({"nvidia_nim", "opencode", "zai"})
_OPENAI_CHAT_UPSTREAM_IDS = frozenset({"nvidia_nim", "opencode", "opencode_go", "zai"})
def anthropic_sse_streaming_response(
+11
View File
@@ -25,6 +25,7 @@ LMSTUDIO_DEFAULT_BASE = "http://localhost:1234/v1"
LLAMACPP_DEFAULT_BASE = "http://localhost:8080/v1"
OLLAMA_DEFAULT_BASE = "http://localhost:11434"
OPENCODE_DEFAULT_BASE = "https://opencode.ai/zen/v1"
OPENCODE_GO_DEFAULT_BASE = "https://opencode.ai/zen/go/v1"
ZAI_DEFAULT_BASE = "https://api.z.ai/api/coding/paas/v4"
@@ -137,6 +138,16 @@ PROVIDER_CATALOG: dict[str, ProviderDescriptor] = {
proxy_attr="opencode_proxy",
capabilities=("chat", "streaming", "tools", "thinking", "rate_limit"),
),
"opencode_go": ProviderDescriptor(
provider_id="opencode_go",
transport_type="openai_chat",
credential_env="OPENCODE_GO_API_KEY",
credential_url="https://opencode.ai/auth",
credential_attr="opencode_go_api_key",
default_base_url=OPENCODE_GO_DEFAULT_BASE,
proxy_attr="opencode_go_proxy",
capabilities=("chat", "streaming", "tools", "thinking", "rate_limit"),
),
"zai": ProviderDescriptor(
provider_id="zai",
transport_type="openai_chat",
+4
View File
@@ -122,6 +122,9 @@ class Settings(BaseSettings):
# ==================== OpenCode Zen Config ====================
opencode_api_key: str = Field(default="", validation_alias="OPENCODE_API_KEY")
# ==================== OpenCode Go Config ====================
opencode_go_api_key: str = Field(default="", validation_alias="OPENCODE_GO_API_KEY")
# ==================== Z.ai Config ====================
zai_api_key: str = Field(default="", validation_alias="ZAI_API_KEY")
@@ -180,6 +183,7 @@ class Settings(BaseSettings):
kimi_proxy: str = Field(default="", validation_alias="KIMI_PROXY")
wafer_proxy: str = Field(default="", validation_alias="WAFER_PROXY")
opencode_proxy: str = Field(default="", validation_alias="OPENCODE_PROXY")
opencode_go_proxy: str = Field(default="", validation_alias="OPENCODE_GO_PROXY")
zai_proxy: str = Field(default="", validation_alias="ZAI_PROXY")
fireworks_proxy: str = Field(default="", validation_alias="FIREWORKS_PROXY")
+2
View File
@@ -9,6 +9,7 @@ from config.provider_catalog import (
NVIDIA_NIM_DEFAULT_BASE,
OLLAMA_DEFAULT_BASE,
OPENCODE_DEFAULT_BASE,
OPENCODE_GO_DEFAULT_BASE,
OPENROUTER_DEFAULT_BASE,
WAFER_DEFAULT_BASE,
ZAI_DEFAULT_BASE,
@@ -23,6 +24,7 @@ __all__ = (
"NVIDIA_NIM_DEFAULT_BASE",
"OLLAMA_DEFAULT_BASE",
"OPENCODE_DEFAULT_BASE",
"OPENCODE_GO_DEFAULT_BASE",
"OPENROUTER_DEFAULT_BASE",
"WAFER_DEFAULT_BASE",
"ZAI_DEFAULT_BASE",
+2 -1
View File
@@ -1,10 +1,11 @@
"""OpenCode Zen provider exports."""
from providers.defaults import OPENCODE_DEFAULT_BASE
from providers.defaults import OPENCODE_DEFAULT_BASE, OPENCODE_GO_DEFAULT_BASE
from .client import OpenCodeProvider
__all__ = [
"OPENCODE_DEFAULT_BASE",
"OPENCODE_GO_DEFAULT_BASE",
"OpenCodeProvider",
]
+2 -2
View File
@@ -14,10 +14,10 @@ from .request import build_request_body
class OpenCodeProvider(OpenAIChatTransport):
"""OpenCode Zen provider using ``https://opencode.ai/zen/v1/chat/completions``."""
def __init__(self, config: ProviderConfig):
def __init__(self, config: ProviderConfig, provider_name: str = "OPENCODE"):
super().__init__(
config,
provider_name="OPENCODE",
provider_name=provider_name,
base_url=config.base_url or OPENCODE_DEFAULT_BASE,
api_key=config.api_key,
)
+7
View File
@@ -86,6 +86,12 @@ def _create_opencode(config: ProviderConfig, _settings: Settings) -> BaseProvide
return OpenCodeProvider(config)
def _create_opencode_go(config: ProviderConfig, _settings: Settings) -> BaseProvider:
from providers.opencode import OpenCodeProvider
return OpenCodeProvider(config, provider_name="OPENCODE_GO")
def _create_zai(config: ProviderConfig, _settings: Settings) -> BaseProvider:
from providers.zai import ZaiProvider
@@ -108,6 +114,7 @@ PROVIDER_FACTORIES: dict[str, ProviderFactory] = {
"kimi": _create_kimi,
"wafer": _create_wafer,
"opencode": _create_opencode,
"opencode_go": _create_opencode_go,
"zai": _create_zai,
"fireworks": _create_fireworks,
}
+1
View File
@@ -50,6 +50,7 @@ PROVIDER_SMOKE_DEFAULT_MODELS: dict[str, str] = {
"ollama": "ollama/llama3.1",
"wafer": "wafer/DeepSeek-V4-Pro",
"opencode": "opencode/gpt-5.3-codex",
"opencode_go": "opencode_go/gpt-5.3-codex",
"zai": "zai/glm-5.1",
}
+2
View File
@@ -38,6 +38,7 @@ def _make_mock_settings(**overrides):
mock.deepseek_api_key = "test_deepseek_key"
mock.wafer_api_key = "test_wafer_key"
mock.opencode_api_key = "test_opencode_key"
mock.opencode_go_api_key = "test_opencode_go_key"
mock.zai_api_key = "test_zai_key"
mock.lm_studio_base_url = "http://localhost:1234/v1"
mock.ollama_base_url = "http://localhost:11434"
@@ -46,6 +47,7 @@ def _make_mock_settings(**overrides):
mock.kimi_proxy = ""
mock.wafer_proxy = ""
mock.opencode_proxy = ""
mock.opencode_go_proxy = ""
mock.zai_proxy = ""
mock.nim = NimSettings()
mock.http_read_timeout = 300.0
+1
View File
@@ -78,6 +78,7 @@ def test_provider_and_platform_registries_include_advertised_builtins() -> None:
"ollama": OllamaProvider,
"wafer": WaferProvider,
"opencode": OpenCodeProvider,
"opencode_go": OpenCodeProvider,
"zai": ZaiProvider,
}
for provider_class in provider_classes.values():
+1
View File
@@ -28,6 +28,7 @@ def _settings(**overrides):
"deepseek_api_key": "",
"wafer_api_key": "",
"opencode_api_key": "",
"opencode_go_api_key": "",
"zai_api_key": "",
"lm_studio_base_url": "",
"llamacpp_base_url": "",
+2
View File
@@ -34,6 +34,7 @@ def _settings(
deepseek_api_key: str = "",
wafer_api_key: str = "",
opencode_api_key: str = "",
opencode_go_api_key: str = "",
zai_api_key: str = "",
) -> Settings:
return Settings.model_construct(
@@ -46,6 +47,7 @@ def _settings(
deepseek_api_key=deepseek_api_key,
wafer_api_key=wafer_api_key,
opencode_api_key=opencode_api_key,
opencode_go_api_key=opencode_go_api_key,
zai_api_key=zai_api_key,
log_api_error_tracebacks=False,
)
+13
View File
@@ -34,6 +34,7 @@ def _make_settings(**overrides):
mock.deepseek_api_key = "test_deepseek_key"
mock.wafer_api_key = "test_wafer_key"
mock.opencode_api_key = "test_opencode_key"
mock.opencode_go_api_key = "test_opencode_go_key"
mock.zai_api_key = "test_zai_key"
mock.lm_studio_base_url = "http://localhost:1234/v1"
mock.llamacpp_base_url = "http://localhost:8080/v1"
@@ -45,6 +46,7 @@ def _make_settings(**overrides):
mock.kimi_proxy = ""
mock.wafer_proxy = ""
mock.opencode_proxy = ""
mock.opencode_go_proxy = ""
mock.zai_proxy = ""
mock.provider_rate_limit = 40
mock.provider_rate_window = 60
@@ -109,6 +111,16 @@ def test_zai_provider_config_ignores_stale_base_url_setting():
assert config.base_url == ZAI_DEFAULT_BASE
def test_opencode_go_provider_config_uses_correct_base_url_and_name():
with patch("httpx.AsyncClient"):
provider = create_provider("opencode_go", _make_settings())
assert isinstance(provider, OpenCodeProvider)
assert provider._base_url == "https://opencode.ai/zen/go/v1"
assert provider._provider_name == "OPENCODE_GO"
assert provider._api_key == "test_opencode_go_key"
def test_create_provider_uses_native_openrouter_by_default():
with patch("httpx.AsyncClient"):
provider = create_provider("open_router", _make_settings())
@@ -126,6 +138,7 @@ def test_create_provider_instantiates_each_builtin():
"ollama": OllamaProvider,
"wafer": WaferProvider,
"opencode": OpenCodeProvider,
"opencode_go": OpenCodeProvider,
"zai": ZaiProvider,
}