Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/ucode/agents/claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,21 @@ def is_update_available() -> tuple[str, str] | None:
def _resolve_web_search_model(state: dict) -> str | None:
"""Pick the model the web_search MCP server should call. Prefers an
explicit override in state, otherwise the first endpoint discovered as
Responses-API-capable. Returns None if no GPT endpoint is available —
callers should skip the MCP wiring in that case."""
Responses-API-capable. Returns None if no GPT endpoint is available, or
if `web_search_model` is explicitly set to `False` to opt out — callers
should skip the MCP wiring in that case.

The `False` opt-out exists because Responses-API-shaped discovery
(`codex_models`) can only tell that an endpoint speaks the wire protocol,
not that it has real OpenAI backing for server-side tools like
`web_search`. A self-hosted model served through Databricks' Open
Responses API wrapper (e.g. gpt-oss) matches the wire-protocol filter but
has no such backing, so calling it 400s with "No OpenAI Response API
backing." When that's the only candidate, set `web_search_model: false`
in ucode's state to stop auto-discovery from picking it."""
override = state.get("web_search_model")
if override is False:
return None
if isinstance(override, str) and override.strip():
return override.strip()
codex_models = state.get("codex_models") or []
Expand Down Expand Up @@ -335,6 +347,11 @@ def write_tool_config(

if web_search_model:
_register_web_search_mcp(state["workspace"], web_search_model, state.get("profile"))
else:
# Clean up a stale registration from a prior run — e.g. the workspace's
# only codex model was removed, or `web_search_model` was just set to
# `False` to opt out of a previously-registered broken endpoint.
_unregister_web_search_mcp()

state = mark_tool_managed(state, "claude", managed_keys)
save_state(state)
Expand Down
38 changes: 37 additions & 1 deletion tests/test_agent_claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,17 @@ def test_override_wins_over_codex_models(self):
state = {"web_search_model": "winner", "codex_models": ["loser"]}
assert claude._resolve_web_search_model(state) == "winner"

def test_explicit_false_disables_regardless_of_codex_models(self):
# A self-hosted model (e.g. gpt-oss) can be the only discovered codex
# model yet have no real OpenAI backing for the web_search tool. An
# explicit `False` opts out instead of silently picking it.
state = {"web_search_model": False, "codex_models": ["mirion_ai_bronze"]}
assert claude._resolve_web_search_model(state) is None

def test_explicit_false_beats_string_codex_model(self):
state = {"web_search_model": False, "codex_models": ["m1"]}
assert claude._resolve_web_search_model(state) is None


class TestClaudeDefaultModel:
def test_prefers_opus(self):
Expand Down Expand Up @@ -308,6 +319,11 @@ def _common_patches(self, monkeypatch, calls):
"_register_web_search_mcp",
lambda ws, model, profile=None: calls.append(("register", ws, model)),
)
monkeypatch.setattr(
claude,
"_unregister_web_search_mcp",
lambda: calls.append(("unregister",)),
)

def test_registers_mcp_when_codex_model_available(self, monkeypatch):
calls: list = []
Expand All @@ -321,7 +337,10 @@ def test_skips_registration_without_codex_model(self, monkeypatch):
self._common_patches(monkeypatch, calls)
state = {"workspace": WS, "codex_models": []}
claude.write_tool_config(state, "databricks-claude-sonnet-4")
assert calls == []
# No candidate model exists, so any stale registration from a prior
# run (e.g. the workspace's only GPT endpoint was decommissioned)
# gets cleaned up rather than left dangling.
assert calls == [("unregister",)]

def test_explicit_override_used_over_codex_models(self, monkeypatch):
calls: list = []
Expand All @@ -334,6 +353,23 @@ def test_explicit_override_used_over_codex_models(self, monkeypatch):
claude.write_tool_config(state, "databricks-claude-sonnet-4")
assert calls == [("register", WS, "explicit-model")]

def test_explicit_false_unregisters_instead_of_registering(self, monkeypatch):
# Regression: a workspace whose only Responses-API-shaped endpoint is
# a self-hosted model (e.g. gpt-oss served through Databricks' Open
# Responses API wrapper) has no real OpenAI backing for the
# web_search tool, so calling it 400s with "No OpenAI Response API
# backing." `web_search_model: false` opts out and any previously
# registered (broken) entry is removed.
calls: list = []
self._common_patches(monkeypatch, calls)
state = {
"workspace": WS,
"web_search_model": False,
"codex_models": ["mirion_ai_bronze"],
}
claude.write_tool_config(state, "databricks-claude-sonnet-4")
assert calls == [("unregister",)]


class TestRegisterWebSearchMcp:
def test_clears_existing_then_adds(self, monkeypatch):
Expand Down