From 5e53b8aa0a720b5e6c4974b4b77b734da90ecde7 Mon Sep 17 00:00:00 2001 From: Suliman Abdulrazzaq <144490671+SulimanAbdulrazzaq@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:46:03 +0300 Subject: [PATCH] fix(opencode): keep Claude models off OpenAI provider ## Description The injected `headroom` OpenCode provider uses `@ai-sdk/openai-compatible` and the proxy's `/v1/chat/completions` route. It currently advertises Claude model IDs in that provider, so OpenCode sends Claude requests to the OpenAI upstream and receives `invalid_api_key` errors. Keep Claude on OpenCode's native `anthropic` provider, which Headroom already redirects to the proxy. Closes #2911 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (bug fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Remove Claude IDs from the injected OpenAI-compatible provider model map. - Keep GPT models available through the `headroom/` namespace. - Add regression assertions that generated config never advertises Claude models on this endpoint. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text python -m pytest tests/test_providers_opencode_config.py -q -k "not build_launch_env_with_project" 40 passed, 1 deselected python -m ruff check headroom/providers/opencode/config.py tests/test_providers_opencode_config.py All checks passed! python -m compileall -q headroom/providers/opencode/config.py tests/test_providers_opencode_config.py (pass) ``` The full config test module also exposes an unrelated pre-existing Windows path assertion failure in `test_build_launch_env_with_project`; the failure is caused by comparing a native `Path` string with JSON-escaped backslashes and is outside this change. ## Real Behavior Proof - Environment: Windows 11, Python 3.11; no external API credentials used. - Exact command / steps: `python -c "from headroom.providers.opencode.config import headroom_provider_entry; print(sorted(headroom_provider_entry(8787)['models']))"` - Observed result: `['gpt-4.1', 'gpt-4o']`; the generated OpenAI-compatible provider no longer advertises any `claude-*` IDs. - Not tested: live OpenCode request routing or a vendor API call, because they require external credentials. The regression suite verifies the generated configuration consumed by OpenCode. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas (not needed; the provider routing rationale is documented inline) - [ ] I have made corresponding changes to the documentation (the generated provider behavior is documented in code; existing docs describe the separate npm provider) - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective - [x] New and existing relevant unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` ## Additional Notes The native `anthropic` and `openai` provider entries both continue to point at the Headroom proxy, so this change only removes an invalid duplicate Claude route and does not affect native Claude traffic. --- headroom/providers/opencode/config.py | 28 ++++++++++--------------- tests/test_providers_opencode_config.py | 11 +++++++--- 2 files changed, 19 insertions(+), 20 deletions(-) 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)]