@@ -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 }
1819
1920
@@ -152,6 +153,101 @@ def test_prefixes_gemini_model_with_provider_id(self):
152153 assert overlay ["model" ] == "databricks-google/gemini-2"
153154
154155
156+ class TestRenderOverlayCodex :
157+ """Regression coverage for #97: the GPT-5 / Codex (Responses) family must
158+ land in opencode.json alongside Anthropic and Gemini. Before the fix, only
159+ two providers were written and `databricks-gpt-5-5` could not be reached
160+ from OpenCode at all."""
161+
162+ def test_openai_provider_added_when_codex_models_present (self ):
163+ models = {"openai" : ["databricks-gpt-5-5" ]}
164+ overlay , _ = opencode .render_overlay ("databricks-gpt-5-5" , "tok" , _base_urls (), models )
165+ assert "databricks-openai" in overlay ["provider" ]
166+
167+ def test_openai_provider_uses_ai_sdk_openai_npm (self ):
168+ models = {"openai" : ["databricks-gpt-5-5" ]}
169+ overlay , _ = opencode .render_overlay ("databricks-gpt-5-5" , "tok" , _base_urls (), models )
170+ assert overlay ["provider" ]["databricks-openai" ]["npm" ] == "@ai-sdk/openai"
171+
172+ def test_openai_base_url_points_at_codex_gateway (self ):
173+ models = {"openai" : ["databricks-gpt-5-5" ]}
174+ overlay , _ = opencode .render_overlay ("databricks-gpt-5-5" , "tok" , _base_urls (), models )
175+ options = overlay ["provider" ]["databricks-openai" ]["options" ]
176+ assert options ["baseURL" ] == f"{ WS } /ai-gateway/codex/v1"
177+
178+ def test_use_responses_api_flag_set_per_model (self ):
179+ # Databricks GPT-5 / Codex models are Responses-only on
180+ # /ai-gateway/codex/v1. The per-model `useResponsesApi: true` lives in
181+ # `models.<m>.options` where opencode reads it.
182+ models = {"openai" : ["databricks-gpt-5-5" ]}
183+ overlay , _ = opencode .render_overlay ("databricks-gpt-5-5" , "tok" , _base_urls (), models )
184+ model_entry = overlay ["provider" ]["databricks-openai" ]["models" ]["databricks-gpt-5-5" ]
185+ assert model_entry ["options" ]["useResponsesApi" ] is True
186+
187+ def test_use_responses_api_set_on_every_codex_model (self ):
188+ models = {"openai" : ["databricks-gpt-5-5" , "databricks-gpt-codex" ]}
189+ overlay , _ = opencode .render_overlay ("databricks-gpt-5-5" , "tok" , _base_urls (), models )
190+ provider_models = overlay ["provider" ]["databricks-openai" ]["models" ]
191+ for m in ("databricks-gpt-5-5" , "databricks-gpt-codex" ):
192+ assert provider_models [m ]["options" ]["useResponsesApi" ] is True
193+
194+ def test_openai_token_in_api_key (self ):
195+ models = {"openai" : ["databricks-gpt-5-5" ]}
196+ overlay , _ = opencode .render_overlay ("databricks-gpt-5-5" , "mytoken" , _base_urls (), models )
197+ assert overlay ["provider" ]["databricks-openai" ]["options" ]["apiKey" ] == "mytoken"
198+
199+ def test_openai_authorization_header (self ):
200+ models = {"openai" : ["databricks-gpt-5-5" ]}
201+ overlay , _ = opencode .render_overlay ("databricks-gpt-5-5" , "tok" , _base_urls (), models )
202+ headers = overlay ["provider" ]["databricks-openai" ]["options" ]["headers" ]
203+ assert headers ["Authorization" ] == "Bearer tok"
204+
205+ def test_user_agent_header_codex (self , monkeypatch ):
206+ monkeypatch .setattr (opencode , "ucode_version" , lambda : "0.1.0" )
207+ monkeypatch .setattr (opencode , "agent_version" , lambda binary : "0.74.0" )
208+ models = {"openai" : ["databricks-gpt-5-5" ]}
209+ overlay , _ = opencode .render_overlay ("databricks-gpt-5-5" , "tok" , _base_urls (), models )
210+ model_headers = overlay ["provider" ]["databricks-openai" ]["models" ]["databricks-gpt-5-5" ][
211+ "headers"
212+ ]
213+ assert model_headers ["User-Agent" ] == "ucode/0.1.0 opencode/0.74.0"
214+
215+ def test_managed_keys_include_openai_provider (self ):
216+ models = {"openai" : ["databricks-gpt-5-5" ]}
217+ _ , keys = opencode .render_overlay ("databricks-gpt-5-5" , "tok" , _base_urls (), models )
218+ assert ["provider" , "databricks-openai" ] in keys
219+
220+ def test_prefixes_openai_model_with_provider_id (self ):
221+ models = {"openai" : ["databricks-gpt-5-5" ]}
222+ overlay , _ = opencode .render_overlay ("databricks-gpt-5-5" , "tok" , _base_urls (), models )
223+ assert overlay ["model" ] == "databricks-openai/databricks-gpt-5-5"
224+
225+ def test_already_prefixed_codex_model_is_preserved (self ):
226+ models = {"openai" : ["databricks-gpt-5-5" ]}
227+ overlay , _ = opencode .render_overlay (
228+ "databricks-openai/databricks-gpt-5-5" , "tok" , _base_urls (), models
229+ )
230+ assert overlay ["model" ] == "databricks-openai/databricks-gpt-5-5"
231+
232+ def test_all_three_providers_when_all_present (self ):
233+ models = {
234+ "anthropic" : ["claude-sonnet" ],
235+ "gemini" : ["gemini-2" ],
236+ "openai" : ["databricks-gpt-5-5" ],
237+ }
238+ overlay , _ = opencode .render_overlay ("claude-sonnet" , "tok" , _base_urls (), models )
239+ assert set (overlay ["provider" ].keys ()) == {
240+ "databricks-anthropic" ,
241+ "databricks-google" ,
242+ "databricks-openai" ,
243+ }
244+
245+ def test_provider_keys_listed_in_module (self ):
246+ # `PROVIDER_KEYS` drives the stale-config cleanup. The codex provider
247+ # must be in this list or stale entries would leak across configures.
248+ assert ["provider" , "databricks-openai" ] in opencode .PROVIDER_KEYS
249+
250+
155251class TestMcpServerConfig :
156252 def test_builds_remote_server_entry_with_oauth_token_env_header (self ):
157253 entry = opencode .build_mcp_server_entry (f"{ WS } /api/2.0/mcp/external/github" )
@@ -264,6 +360,18 @@ def test_prefers_anthropic(self):
264360 state = {"opencode_models" : {"anthropic" : ["claude-sonnet" ], "gemini" : ["gemini-2" ]}}
265361 assert opencode .default_model (state ) == "claude-sonnet"
266362
363+ def test_falls_back_to_openai_before_gemini (self ):
364+ # Codex/GPT-5 ranks above Gemini in the fallback order — these are the
365+ # primary code models on Databricks once the codex provider is wired.
366+ state = {
367+ "opencode_models" : {
368+ "anthropic" : [],
369+ "openai" : ["databricks-gpt-5-5" ],
370+ "gemini" : ["gemini-2" ],
371+ }
372+ }
373+ assert opencode .default_model (state ) == "databricks-gpt-5-5"
374+
267375 def test_falls_back_to_gemini (self ):
268376 state = {"opencode_models" : {"anthropic" : [], "gemini" : ["gemini-2" ]}}
269377 assert opencode .default_model (state ) == "gemini-2"
0 commit comments