fix(claude): stop forcing tool search on Foundry (#2477)
## Description Foundry sessions launched through `headroom wrap claude` currently receive Headroom's generic `ENABLE_TOOL_SEARCH=true` default when the user did not choose a tool-search mode. That can push Claude Code into a deferred-tool request shape that Azure Foundry rejects with `API Error: 400 ... Some tools are not available`. This narrows the default-only path so Foundry sessions stop forcing deferred-tool mode when the user did not ask for it, while explicit overrides and the existing non-Foundry custom-host behavior stay unchanged. Closes #2464 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - add a Foundry-specific default for the no-override tool-search branch - preserve explicit `--tool-search` values and pre-set `ENABLE_TOOL_SEARCH` values exactly - keep the generic non-Foundry default as `true` - add focused helper-level regression coverage for Foundry defaulting and adjacent negative space ## Testing - [x] Focused unit tests pass (`uv run pytest tests/test_cli/test_wrap_claude.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py tests/test_issue_746_tool_search.py -q`) - [x] Edited-file linting passes (`uv run ruff check headroom/cli/wrap.py headroom/providers/claude/runtime.py tests/test_cli/test_wrap_claude.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text Command: uv run pytest tests/test_cli/test_wrap_claude.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py tests/test_issue_746_tool_search.py -q 61 passed Command: uv run ruff check headroom/cli/wrap.py headroom/providers/claude/runtime.py tests/test_cli/test_wrap_claude.py All checks passed! ``` ## Real Behavior Proof - Environment: Windows, Python via `uv`, Foundry mode modeled through the wrap helper inputs - Exact command / steps: run the focused helper regression and edited-file lint commands above - Observed result: `61 passed`; `All checks passed!`; Foundry mode without an override writes `ENABLE_TOOL_SEARCH=false`, while explicit overrides, existing values, blank handling, and the non-Foundry default remain covered - Not tested: live Azure Foundry tenant run ## 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 - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes `CHANGELOG.md` stays untouched because Headroom generates release notes from conventional commits. Live Foundry proof is intentionally left to a real tenant run; the code and focused tests only claim the launch-mode change inside Headroom.
This commit is contained in:
+14
-8
@@ -87,6 +87,7 @@ from headroom.providers.claude import (
|
||||
from headroom.providers.claude import (
|
||||
proxy_base_url as _claude_proxy_base_url,
|
||||
)
|
||||
from headroom.providers.claude.runtime import TOOL_SEARCH_FOUNDRY_DEFAULT
|
||||
from headroom.providers.codex import build_launch_env as _build_codex_launch_env
|
||||
from headroom.providers.codex.install import codex_uses_chatgpt_auth
|
||||
from headroom.providers.codex.threads import retag_to_headroom, retag_to_native
|
||||
@@ -269,13 +270,14 @@ _WRAP_PROXY_TIMEOUT_ML_MODULES = ("torch", "sentence_transformers", "spacy")
|
||||
# Issue #746: Claude Code disables on-demand tool loading (deferral) when
|
||||
# ANTHROPIC_BASE_URL is a custom host and ENABLE_TOOL_SEARCH is unset, which
|
||||
# inflates the local context window by tens of K tokens. Setting the env var
|
||||
# when we launch Claude Code keeps deferral on. Default to "true" — defer the
|
||||
# MCP/system tools for maximum context savings, matching native first-party
|
||||
# behaviour (core built-ins like Read/Edit/Bash are never deferred by Claude
|
||||
# Code, so the agent loop is unaffected). The key/default are shared with
|
||||
# `init` and `install` via the Claude provider package to prevent drift.
|
||||
# when we launch Claude Code keeps deferral on. The generic default stays
|
||||
# "true" for non-Foundry sessions, while Foundry uses a dedicated compatibility
|
||||
# default of "false" because its upstream does not support the deferred-tool
|
||||
# shape. The key/defaults are shared with `init` and `install` via the Claude
|
||||
# provider package to prevent drift.
|
||||
_TOOL_SEARCH_ENV = TOOL_SEARCH_ENV
|
||||
_TOOL_SEARCH_DEFAULT = TOOL_SEARCH_DEFAULT
|
||||
_TOOL_SEARCH_FOUNDRY_DEFAULT = TOOL_SEARCH_FOUNDRY_DEFAULT
|
||||
_AGENT_SAVINGS_WRAP_AGENTS = {"claude", "codex", "cursor", "grok", "grok_build"}
|
||||
|
||||
# 1M context window for `wrap claude` (#1158). Claude Code only sends the
|
||||
@@ -358,7 +360,8 @@ def _configure_tool_search_env(env: dict[str, str], flag_value: str | None) -> s
|
||||
1. explicit ``--tool-search`` flag — wins (the user asked for it on the CLI),
|
||||
2. a pre-existing ``ENABLE_TOOL_SEARCH`` in the environment — respected and
|
||||
left untouched (the user's own Claude Code knob),
|
||||
3. the built-in default (``true``).
|
||||
3. the built-in mode-specific default (``true`` normally, ``false`` on
|
||||
Foundry).
|
||||
|
||||
Returns the value written, or ``None`` when an existing environment value
|
||||
was deliberately left in place.
|
||||
@@ -373,8 +376,11 @@ def _configure_tool_search_env(env: dict[str, str], flag_value: str | None) -> s
|
||||
existing = env.get(_TOOL_SEARCH_ENV)
|
||||
if existing is not None and existing.strip():
|
||||
return None
|
||||
env[_TOOL_SEARCH_ENV] = _TOOL_SEARCH_DEFAULT
|
||||
return _TOOL_SEARCH_DEFAULT
|
||||
default = (
|
||||
_TOOL_SEARCH_FOUNDRY_DEFAULT if env.get("CLAUDE_CODE_USE_FOUNDRY") else _TOOL_SEARCH_DEFAULT
|
||||
)
|
||||
env[_TOOL_SEARCH_ENV] = default
|
||||
return default
|
||||
|
||||
|
||||
# ENABLE_TOOL_SEARCH modes that turn deferral OFF. Everything else Claude Code
|
||||
|
||||
@@ -15,6 +15,7 @@ DEFAULT_API_URL = "https://api.anthropic.com"
|
||||
# single source of truth shared by `wrap`, `init`, and `install`.
|
||||
TOOL_SEARCH_ENV = "ENABLE_TOOL_SEARCH"
|
||||
TOOL_SEARCH_DEFAULT = "true"
|
||||
TOOL_SEARCH_FOUNDRY_DEFAULT = "false"
|
||||
REMOTE_CONTROL_BASE_URL_ENV = "ANTHROPIC_BASE_URL"
|
||||
REMOTE_CONTROL_FEATURE = "Remote Control"
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Focused tests for Claude wrap tool-search defaults."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from headroom.cli.wrap import (
|
||||
_TOOL_SEARCH_DEFAULT,
|
||||
_TOOL_SEARCH_ENV,
|
||||
_configure_tool_search_env,
|
||||
)
|
||||
|
||||
|
||||
def test_foundry_without_override_disables_tool_search() -> None:
|
||||
env = {"CLAUDE_CODE_USE_FOUNDRY": "1"}
|
||||
|
||||
result = _configure_tool_search_env(env, None)
|
||||
|
||||
assert result == "false"
|
||||
assert env[_TOOL_SEARCH_ENV] == "false"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("flag_value", ["auto", "false"])
|
||||
def test_explicit_flag_wins_in_foundry(flag_value: str) -> None:
|
||||
env = {"CLAUDE_CODE_USE_FOUNDRY": "1"}
|
||||
|
||||
result = _configure_tool_search_env(env, flag_value)
|
||||
|
||||
assert result == flag_value
|
||||
assert env[_TOOL_SEARCH_ENV] == flag_value
|
||||
|
||||
|
||||
def test_existing_environment_value_wins_in_foundry() -> None:
|
||||
env = {"CLAUDE_CODE_USE_FOUNDRY": "1", _TOOL_SEARCH_ENV: "auto:30"}
|
||||
|
||||
result = _configure_tool_search_env(env, None)
|
||||
|
||||
assert result is None
|
||||
assert env[_TOOL_SEARCH_ENV] == "auto:30"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("blank", ["", " ", "\t"])
|
||||
def test_blank_environment_uses_mode_default(blank: str) -> None:
|
||||
foundry_env = {"CLAUDE_CODE_USE_FOUNDRY": "1", _TOOL_SEARCH_ENV: blank}
|
||||
generic_env = {_TOOL_SEARCH_ENV: blank}
|
||||
|
||||
assert _configure_tool_search_env(foundry_env, None) == "false"
|
||||
assert foundry_env[_TOOL_SEARCH_ENV] == "false"
|
||||
assert _configure_tool_search_env(generic_env, None) == _TOOL_SEARCH_DEFAULT
|
||||
assert generic_env[_TOOL_SEARCH_ENV] == _TOOL_SEARCH_DEFAULT
|
||||
|
||||
|
||||
def test_non_foundry_without_override_keeps_generic_default() -> None:
|
||||
env: dict[str, str] = {}
|
||||
|
||||
result = _configure_tool_search_env(env, None)
|
||||
|
||||
assert result == _TOOL_SEARCH_DEFAULT
|
||||
assert env[_TOOL_SEARCH_ENV] == _TOOL_SEARCH_DEFAULT
|
||||
Reference in New Issue
Block a user