fix(opencode): accept an API base with or without /v1 on the chat endpoint

The messages and responses endpoints already add /v1 when a configured OPENCODE_*_API_BASE leaves it off, but chat appended /chat/completions directly. A base like https://opencode.ai/zen therefore worked for two endpoints and sent chat to a path that does not exist

Chat now builds its URL the same way, so one base value serves every endpoint. A full /v1/chat/completions URL is still used as given
This commit is contained in:
Sean Murphy 2026-09-12 19:35:30 -07:00
parent 379af61b4f
commit f92bef07f9
2 changed files with 57 additions and 11 deletions

View file

@ -76,8 +76,12 @@ class OpenCodeConfig(OpenAIGPTConfig):
stream: bool | None = None,
) -> str:
"""Return {api_base}/v1/chat/completions."""
base: Final = resolve_opencode_api_base(self.surface, api_base) or self._base_url()
return f"{base.rstrip('/')}/chat/completions"
base: Final = (resolve_opencode_api_base(self.surface, api_base) or self._base_url()).rstrip("/")
if base.endswith("/v1/chat/completions"):
return base
if base.endswith("/v1"):
return f"{base}/chat/completions"
return f"{base}/v1/chat/completions"
def get_error_class(
self,

View file

@ -84,15 +84,20 @@ class TestOpenCodeConfig:
url = cfg.get_complete_url(None, None, "gpt-5.1", {}, {})
assert url == "https://opencode.ai/zen/go/v1/chat/completions"
def test_get_complete_url_api_base_override(self):
@pytest.mark.parametrize(
"api_base",
[
"http://localhost:4000",
"http://localhost:4000/",
"http://localhost:4000/v1",
"http://localhost:4000/v1/",
"http://localhost:4000/v1/chat/completions",
],
)
def test_get_complete_url_api_base_with_or_without_v1(self, api_base):
cfg = OpenCodeConfig(surface="zen")
url = cfg.get_complete_url("http://localhost:4000", None, "gpt-5.1", {}, {})
assert url == "http://localhost:4000/chat/completions"
def test_get_complete_url_api_base_trailing_slash(self):
cfg = OpenCodeConfig(surface="zen")
url = cfg.get_complete_url("http://localhost:4000/", None, "gpt-5.1", {}, {})
assert url == "http://localhost:4000/chat/completions"
url = cfg.get_complete_url(api_base, None, "gpt-5.1", {}, {})
assert url == "http://localhost:4000/v1/chat/completions"
def test_error_class(self):
cfg = OpenCodeConfig(surface="zen")
@ -416,7 +421,7 @@ class TestMockedCompletion:
def test_api_base_override(self, respx_mock, monkeypatch):
"""Explicit api_base overrides the default gateway URL."""
respx_mock.post("http://localhost:4000/chat/completions").mock(
respx_mock.post("http://localhost:4000/v1/chat/completions").mock(
return_value=Response(200, json=_make_response("grok-4.5", "local"))
)
@ -432,6 +437,43 @@ class TestMockedCompletion:
assert result.choices[0].message.content == "local"
assert len(respx_mock.calls) > 0
def test_api_base_without_v1_serves_chat_and_messages(self, respx_mock, monkeypatch):
"""One OPENCODE_ZEN_API_BASE value without /v1 reaches both the chat and the messages endpoint."""
chat_endpoint = respx_mock.post("https://gateway.example.com/zen/v1/chat/completions").mock(
return_value=Response(200, json=_make_response("grok-4.5", "chat ok"))
)
messages_endpoint = respx_mock.post("https://gateway.example.com/zen/v1/messages").mock(
return_value=Response(
200,
json={
"id": "msg_123",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4",
"content": [{"type": "text", "text": "messages ok"}],
"stop_reason": "end_turn",
"usage": {"input_tokens": 1, "output_tokens": 1},
},
)
)
monkeypatch.setenv("OPENCODE_ZEN_API_BASE", "https://gateway.example.com/zen")
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
litellm.completion(
model="opencode_zen/grok-4.5",
messages=[{"role": "user", "content": "hi"}],
api_key="sk-fake",
)
litellm.completion(
model="opencode_zen/claude-sonnet-4",
messages=[{"role": "user", "content": "hi"}],
api_key="sk-fake",
max_tokens=16,
)
assert chat_endpoint.call_count == 1
assert messages_endpoint.call_count == 1
def test_bearer_auth_from_surface_key(self, respx_mock, monkeypatch):
"""Surface-specific env var provides the Bearer token."""
monkeypatch.setenv("OPENCODE_ZEN_API_KEY", "sk-surface-key")