diff --git a/headroom/providers/opencode/config.py b/headroom/providers/opencode/config.py index 6c64544fe..08a6f5748 100644 --- a/headroom/providers/opencode/config.py +++ b/headroom/providers/opencode/config.py @@ -31,24 +31,18 @@ _MCP_BLOCK_RE = re.compile( ) HEADROOM_OPENCODE_PLUGIN = "headroom-opencode" -# Models exposed by the injected `headroom` provider. OpenCode only resolves -# `headroom/` for ids listed in the provider's `models` map, so an empty -# map means every documented `headroom/*` model fails with "Model not found". -# Keep in sync with DEFAULT_MODELS in plugins/opencode/src/provider.ts and the -# table in plugins/opencode/README.md. +# Models exposed by the injected `headroom` provider. This provider uses +# ``@ai-sdk/openai-compatible`` and the proxy's ``/v1/chat/completions`` path, +# which is routed to the configured OpenAI upstream. Do not advertise Claude +# models here: OpenCode would send them through the OpenAI endpoint and report +# an ``invalid_api_key`` error instead of reaching Anthropic. Claude models are +# available through OpenCode's native ``anthropic`` provider, whose base URL is +# also redirected to Headroom by ``build_opencode_config_content``. +# +# OpenCode only resolves ``headroom/`` for ids listed in this map, so an +# empty map means every documented ``headroom/*`` model fails with "Model not +# found". HEADROOM_OPENCODE_MODELS: dict[str, Any] = { - "claude-sonnet-4-6": { - "name": "Claude Sonnet 4.6", - "limit": {"context": 200000, "output": 16384}, - }, - "claude-opus-4-6": { - "name": "Claude Opus 4.6", - "limit": {"context": 200000, "output": 16384}, - }, - "claude-haiku-4-5-20251001": { - "name": "Claude Haiku 4.5", - "limit": {"context": 200000, "output": 8192}, - }, "gpt-4o": { "name": "GPT-4o", "limit": {"context": 128000, "output": 16384}, diff --git a/tests/test_providers_opencode_config.py b/tests/test_providers_opencode_config.py index 658ba1ea4..aec387013 100644 --- a/tests/test_providers_opencode_config.py +++ b/tests/test_providers_opencode_config.py @@ -199,7 +199,10 @@ def test_inject_provider_config_creates_file( assert config["provider"]["headroom"]["npm"] == "@ai-sdk/openai-compatible" # Bare model ids: OpenCode resolves them as "headroom/" (#1657). models = config["provider"]["headroom"]["models"] - assert "claude-sonnet-4-6" in models + assert set(models) == {"gpt-4o", "gpt-4.1"} + # The injected provider is OpenAI-compatible. Claude models must remain on + # OpenCode's native Anthropic provider so they are not sent to OpenAI. + assert not any(model_id.startswith("claude-") for model_id in models) assert all(not model_id.startswith("headroom/") for model_id in models) assert "mcp" not in config assert "model" not in config # headroom provider is a transparent pass-through @@ -456,10 +459,12 @@ def test_build_opencode_config_content_without_mcp( providers = config["provider"] assert providers["anthropic"]["options"]["baseURL"] == "http://127.0.0.1:8787/v1" assert providers["openai"]["options"]["baseURL"] == "http://127.0.0.1:8787/v1" - # The headroom provider exposes explicit models so "headroom/" resolves (#1657). + # The headroom provider exposes only models supported by its + # OpenAI-compatible endpoint so "headroom/" resolves safely (#1657). assert providers["headroom"]["options"]["baseURL"] == "http://127.0.0.1:8787/v1" models = providers["headroom"]["models"] - assert "claude-sonnet-4-6" in models + assert set(models) == {"gpt-4o", "gpt-4.1"} + assert not any(model_id.startswith("claude-") for model_id in models) assert all(not model_id.startswith("headroom/") for model_id in models) # The transport plugin is injected by absolute path (opencode loads it directly). assert config["plugin"] == [str(plugin)]