Skip to content

Commit c5daf76

Browse files
committed
fix(opencode): wire databricks-openai provider so GPT-5/GPT-5.6/Codex reach OpenCode
The OSS branch (fix/1m-context-claude-gpt-pi) rebuilt opencode around a databricks-oss bucket but dropped the databricks-openai (GPT/Codex) wiring that only lived on fix/opencode-codex-provider. Neither branch had both, so on the latest work GPT-5.x/5.6 was unreachable from OpenCode. Reconcile the two efforts across all three layers: - databricks.build_opencode_base_urls: add the 'openai' -> /ai-gateway/codex/v1 base URL - cli.configure_shared_state: discover codex models for opencode and populate opencode_models['openai'] - agents.opencode: emit the databricks-openai provider (per-model useResponsesApi=true) alongside oss Now OpenCode gets anthropic + google + openai + oss together. Ports #97 wiring onto the OSS/1M-context work. Adds TestOpenAIProvider + base-url coverage. Co-authored-by: fix/opencode-codex-provider
1 parent b7c7a28 commit c5daf76

5 files changed

Lines changed: 135 additions & 3 deletions

File tree

src/ucode/agents/opencode.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
PROVIDER_KEYS: list[list[str]] = [
4343
["provider", "databricks-anthropic"],
4444
["provider", "databricks-google"],
45+
["provider", "databricks-openai"],
4546
["provider", "databricks-oss"],
4647
]
4748

@@ -52,7 +53,14 @@ def is_update_available() -> tuple[str, str] | None:
5253

5354
def _resolve_model_selector(model: str, opencode_models: dict[str, list[str]]) -> str:
5455
"""Return an OpenCode model selector in provider/model form when possible."""
55-
if model.startswith(("databricks-anthropic/", "databricks-google/", "databricks-oss/")):
56+
if model.startswith(
57+
(
58+
"databricks-anthropic/",
59+
"databricks-google/",
60+
"databricks-openai/",
61+
"databricks-oss/",
62+
)
63+
):
5664
return model
5765

5866
anthropic_models = opencode_models.get("anthropic") or []
@@ -63,6 +71,10 @@ def _resolve_model_selector(model: str, opencode_models: dict[str, list[str]]) -
6371
if model in gemini_models:
6472
return f"databricks-google/{model}"
6573

74+
openai_models = opencode_models.get("openai") or []
75+
if model in openai_models:
76+
return f"databricks-openai/{model}"
77+
6678
oss_models = opencode_models.get("oss") or []
6779
if model in oss_models:
6880
return f"databricks-oss/{model}"
@@ -102,6 +114,7 @@ def render_overlay(
102114

103115
anthropic_models = opencode_models.get("anthropic") or []
104116
gemini_models = opencode_models.get("gemini") or []
117+
openai_models = opencode_models.get("openai") or []
105118
oss_models = opencode_models.get("oss") or []
106119

107120
providers: dict = {}
@@ -137,6 +150,28 @@ def render_overlay(
137150
"models": {m: {"headers": ua_header} for m in gemini_models},
138151
}
139152
keys.append(["provider", "databricks-google"])
153+
if openai_models:
154+
# @ai-sdk/openai supports both the Responses API and the legacy
155+
# chat-completions API. Databricks GPT-5 / GPT-5.6 / Codex models are
156+
# Responses-only on /ai-gateway/codex/v1, so the per-model flag
157+
# `useResponsesApi: true` lives in models.<m>.options where opencode
158+
# reads it (provider-level options is read by the SDK only).
159+
providers["databricks-openai"] = {
160+
"npm": "@ai-sdk/openai",
161+
"options": {
162+
"baseURL": opencode_base_urls["openai"],
163+
"apiKey": token,
164+
"headers": auth_headers,
165+
},
166+
"models": {
167+
m: {
168+
"headers": ua_header,
169+
"options": {"useResponsesApi": True},
170+
}
171+
for m in openai_models
172+
},
173+
}
174+
keys.append(["provider", "databricks-openai"])
140175
if oss_models:
141176
providers["databricks-oss"] = {
142177
"npm": "@ai-sdk/openai",
@@ -233,6 +268,9 @@ def default_model(state: dict) -> str | None:
233268
anthropic = opencode_models.get("anthropic") or []
234269
if anthropic:
235270
return anthropic[0]
271+
openai = opencode_models.get("openai") or []
272+
if openai:
273+
return openai[0]
236274
gemini = opencode_models.get("gemini") or []
237275
if gemini:
238276
return gemini[0]

src/ucode/cli.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888

8989
_DISCOVERY_CONSUMERS: dict[str, tuple[str, ...]] = {
9090
"claude": ("claude", "opencode", "copilot", "pi"),
91-
"codex": ("codex", "copilot", "pi"),
91+
"codex": ("codex", "copilot", "opencode", "pi"),
9292
"gemini": ("gemini", "opencode", "pi"),
9393
"oss": ("opencode",),
9494
}
@@ -336,7 +336,9 @@ def configure_shared_state(
336336
fetch_all or "claude" in tools or "opencode" in tools or "copilot" in tools or "pi" in tools
337337
)
338338
want_gemini = fetch_all or "gemini" in tools or "opencode" in tools or "pi" in tools
339-
want_codex = fetch_all or "codex" in tools or "copilot" in tools or "pi" in tools
339+
want_codex = (
340+
fetch_all or "codex" in tools or "copilot" in tools or "opencode" in tools or "pi" in tools
341+
)
340342
want_oss = fetch_all or "opencode" in tools or "pi" in tools
341343

342344
claude_reason: str | None = None
@@ -393,6 +395,8 @@ def configure_shared_state(
393395
opencode_models["anthropic"] = list(claude_models.values())
394396
if gemini_models:
395397
opencode_models["gemini"] = gemini_models
398+
if codex_models:
399+
opencode_models["openai"] = codex_models
396400
if oss_models:
397401
opencode_models["oss"] = oss_models
398402

src/ucode/databricks.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2253,6 +2253,9 @@ def build_opencode_base_urls(workspace: str) -> dict[str, str]:
22532253
return {
22542254
"anthropic": build_tool_base_url("claude", workspace) + "/v1",
22552255
"gemini": build_tool_base_url("gemini", workspace) + "/v1beta",
2256+
# @ai-sdk/openai appends "/responses" (or "/chat/completions") to baseURL,
2257+
# so stop just before that — matches the Pi adapter's build_pi_base_urls.
2258+
"openai": build_tool_base_url("codex", workspace),
22562259
"oss": f"{workspace}/ai-gateway/mlflow/v1",
22572260
}
22582261

tests/test_agent_opencode.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def _base_urls() -> dict[str, str]:
1414
return {
1515
"anthropic": f"{WS}/ai-gateway/anthropic/v1",
1616
"gemini": f"{WS}/ai-gateway/gemini/v1beta",
17+
"openai": f"{WS}/ai-gateway/codex/v1",
1718
"oss": f"{WS}/ai-gateway/mlflow/v1",
1819
}
1920

@@ -211,6 +212,76 @@ def test_prefixes_oss_model_with_provider_id(self):
211212
assert overlay["model"] == "databricks-oss/system.ai.kimi-k2-7-code"
212213

213214

215+
class TestOpenAIProvider:
216+
"""OpenCode reaches Databricks GPT-5 / GPT-5.6 / Codex models through the
217+
databricks-openai provider (@ai-sdk/openai against /ai-gateway/codex/v1).
218+
Without this wiring the codex/openai family is unreachable from OpenCode."""
219+
220+
def test_openai_provider_added_when_codex_models_present(self):
221+
models = {"openai": ["databricks-gpt-5-6-sol"]}
222+
overlay, _ = opencode.render_overlay("databricks-gpt-5-6-sol", "tok", _base_urls(), models)
223+
assert "databricks-openai" in overlay["provider"]
224+
225+
def test_openai_provider_uses_ai_sdk_openai_npm(self):
226+
models = {"openai": ["databricks-gpt-5-6-sol"]}
227+
overlay, _ = opencode.render_overlay("databricks-gpt-5-6-sol", "tok", _base_urls(), models)
228+
assert overlay["provider"]["databricks-openai"]["npm"] == "@ai-sdk/openai"
229+
230+
def test_openai_base_url_points_at_codex_gateway(self):
231+
models = {"openai": ["databricks-gpt-5-6-sol"]}
232+
overlay, _ = opencode.render_overlay("databricks-gpt-5-6-sol", "tok", _base_urls(), models)
233+
options = overlay["provider"]["databricks-openai"]["options"]
234+
assert options["baseURL"] == f"{WS}/ai-gateway/codex/v1"
235+
236+
def test_use_responses_api_set_on_every_codex_model(self):
237+
models = {"openai": ["databricks-gpt-5-6-sol", "databricks-gpt-codex"]}
238+
overlay, _ = opencode.render_overlay("databricks-gpt-5-6-sol", "tok", _base_urls(), models)
239+
provider_models = overlay["provider"]["databricks-openai"]["models"]
240+
for m in ("databricks-gpt-5-6-sol", "databricks-gpt-codex"):
241+
assert provider_models[m]["options"]["useResponsesApi"] is True
242+
243+
def test_openai_authorization_header(self):
244+
models = {"openai": ["databricks-gpt-5-6-sol"]}
245+
overlay, _ = opencode.render_overlay("databricks-gpt-5-6-sol", "tok", _base_urls(), models)
246+
headers = overlay["provider"]["databricks-openai"]["options"]["headers"]
247+
assert headers["Authorization"] == "Bearer tok"
248+
249+
def test_managed_keys_include_openai_provider(self):
250+
models = {"openai": ["databricks-gpt-5-6-sol"]}
251+
_, keys = opencode.render_overlay("databricks-gpt-5-6-sol", "tok", _base_urls(), models)
252+
assert ["provider", "databricks-openai"] in keys
253+
254+
def test_prefixes_openai_model_with_provider_id(self):
255+
models = {"openai": ["databricks-gpt-5-6-sol"]}
256+
overlay, _ = opencode.render_overlay("databricks-gpt-5-6-sol", "tok", _base_urls(), models)
257+
assert overlay["model"] == "databricks-openai/databricks-gpt-5-6-sol"
258+
259+
def test_already_prefixed_openai_model_is_preserved(self):
260+
models = {"openai": ["databricks-gpt-5-6-sol"]}
261+
overlay, _ = opencode.render_overlay(
262+
"databricks-openai/databricks-gpt-5-6-sol", "tok", _base_urls(), models
263+
)
264+
assert overlay["model"] == "databricks-openai/databricks-gpt-5-6-sol"
265+
266+
def test_all_four_providers_when_all_present(self):
267+
models = {
268+
"anthropic": ["claude-sonnet"],
269+
"gemini": ["gemini-2"],
270+
"openai": ["databricks-gpt-5-6-sol"],
271+
"oss": ["system.ai.kimi-k2-7-code"],
272+
}
273+
overlay, _ = opencode.render_overlay("claude-sonnet", "tok", _base_urls(), models)
274+
assert set(overlay["provider"].keys()) == {
275+
"databricks-anthropic",
276+
"databricks-google",
277+
"databricks-openai",
278+
"databricks-oss",
279+
}
280+
281+
def test_provider_keys_listed_in_module(self):
282+
assert ["provider", "databricks-openai"] in opencode.PROVIDER_KEYS
283+
284+
214285
class TestMcpServerConfig:
215286
def test_builds_remote_server_entry_with_oauth_token_env_header(self):
216287
entry = opencode.build_mcp_server_entry(f"{WS}/api/2.0/mcp/external/github")
@@ -323,6 +394,16 @@ def test_prefers_anthropic(self):
323394
state = {"opencode_models": {"anthropic": ["claude-sonnet"], "gemini": ["gemini-2"]}}
324395
assert opencode.default_model(state) == "claude-sonnet"
325396

397+
def test_falls_back_to_openai_before_gemini(self):
398+
state = {
399+
"opencode_models": {
400+
"anthropic": [],
401+
"openai": ["databricks-gpt-5-6-sol"],
402+
"gemini": ["gemini-2"],
403+
}
404+
}
405+
assert opencode.default_model(state) == "databricks-gpt-5-6-sol"
406+
326407
def test_falls_back_to_gemini(self):
327408
state = {"opencode_models": {"anthropic": [], "gemini": ["gemini-2"]}}
328409
assert opencode.default_model(state) == "gemini-2"

tests/test_databricks.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ def test_returns_anthropic_gemini_and_oss(self):
100100
assert urls["gemini"] == f"{WS}/ai-gateway/gemini/v1beta"
101101
assert urls["oss"] == f"{WS}/ai-gateway/mlflow/v1"
102102

103+
def test_returns_openai_codex_gateway(self):
104+
# @ai-sdk/openai appends /responses (Responses API) or /chat/completions
105+
# to baseURL, so stop just before that suffix. Mirrors build_pi_base_urls.
106+
urls = build_opencode_base_urls(WS)
107+
assert urls["openai"] == f"{WS}/ai-gateway/codex/v1"
108+
103109

104110
class TestBuildSharedBaseUrls:
105111
def test_contains_all_tools(self):

0 commit comments

Comments
 (0)