From cafdfda8ba586429dcaf0c6c1dcf7e7c5caf70a7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 31 Aug 2026 05:52:17 +0000 Subject: [PATCH 1/3] feat(cli): set ENABLE_TOOL_SEARCH=true for lite claude Claude Code turns tool search off when ANTHROPIC_BASE_URL is a proxy. lite claude, lite up, login --config-claude, and autoroute now force ENABLE_TOOL_SEARCH=true so MCP tools stay deferred through the proxy Co-authored-by: Mateo Wang --- litellm/proxy/client/cli/README.md | 6 +++--- litellm/proxy/client/cli/commands/agents.py | 7 ++++++- .../client/cli/commands/autoroute/settings.py | 3 +++ .../proxy/client/cli/commands/claude_settings.py | 15 +++++++++++---- .../proxy/client/cli/autoroute/test_commands.py | 1 + .../proxy/client/cli/autoroute/test_settings.py | 7 +++++++ .../test_litellm/proxy/client/cli/test_agents.py | 14 ++++++++++++++ .../proxy/client/cli/test_auth_commands.py | 1 + .../proxy/client/cli/test_claude_settings.py | 1 + .../proxy/client/cli/test_up_commands.py | 12 +++++++++++- 10 files changed, 58 insertions(+), 9 deletions(-) diff --git a/litellm/proxy/client/cli/README.md b/litellm/proxy/client/cli/README.md index fe417396317..0da219df700 100644 --- a/litellm/proxy/client/cli/README.md +++ b/litellm/proxy/client/cli/README.md @@ -489,7 +489,7 @@ lite codex exec "summarize the repo" Each command resolves your LiteLLM key (logging in via SSO when none is stored and you are at a terminal; otherwise it expects `LITELLM_PROXY_API_KEY` or `--api-key`), checks the key against the proxy so bad credentials fail immediately instead of deep inside the agent, exports the environment variables the agent reads, then replaces itself with the agent process. -The right variables are picked per agent. Claude Code gets `ANTHROPIC_BASE_URL` (the proxy root, so it appends `/v1/messages`) and `ANTHROPIC_AUTH_TOKEN`, with any stray `ANTHROPIC_API_KEY` cleared so the proxy token wins. Codex and OpenCode get `OPENAI_BASE_URL` (the proxy plus `/v1`) and `OPENAI_API_KEY`. Codex ignores `OPENAI_BASE_URL`, so it is additionally pointed at the proxy through a custom provider passed as `-c` config overrides (HTTP/SSE Responses transport, since the proxy does not speak the Responses WebSocket protocol). +The right variables are picked per agent. Claude Code gets `ANTHROPIC_BASE_URL` (the proxy root, so it appends `/v1/messages`) and `ANTHROPIC_AUTH_TOKEN`, with any stray `ANTHROPIC_API_KEY` cleared so the proxy token wins, and `ENABLE_TOOL_SEARCH=true` so Claude Code keeps tool search on even though the base URL is a proxy rather than a first-party Anthropic host. Codex and OpenCode get `OPENAI_BASE_URL` (the proxy plus `/v1`) and `OPENAI_API_KEY`. Codex ignores `OPENAI_BASE_URL`, so it is additionally pointed at the proxy through a custom provider passed as `-c` config overrides (HTTP/SSE Responses transport, since the proxy does not speak the Responses WebSocket protocol). Options (these belong to the wrapper, so put them before the agent's own flags): @@ -505,7 +505,7 @@ The credential is short-lived by design (default 24h, configurable via `LITELLM_ ### Route Every Claude Code Session Through the Proxy -`lite claude` wraps a single invocation, but `lite up` goes further: it patches `~/.claude/settings.json`, Claude Code's own config file, so that every Claude Code session started afterward -- from any terminal, launched normally with just `claude`, no wrapper needed -- routes through your LiteLLM proxy. It sets `env.ANTHROPIC_BASE_URL` to the proxy URL and `apiKeyHelper` to a `lite auth print-token` invocation, drops any stray static `ANTHROPIC_API_KEY` so the helper-issued token wins, and leaves every other setting in the file untouched. It backs up the original file before patching it. +`lite claude` wraps a single invocation, but `lite up` goes further: it patches `~/.claude/settings.json`, Claude Code's own config file, so that every Claude Code session started afterward -- from any terminal, launched normally with just `claude`, no wrapper needed -- routes through your LiteLLM proxy. It sets `env.ANTHROPIC_BASE_URL` to the proxy URL, `env.ENABLE_TOOL_SEARCH` to `true`, and `apiKeyHelper` to a `lite auth print-token` invocation, drops any stray static `ANTHROPIC_API_KEY` so the helper-issued token wins, and leaves every other setting in the file untouched. It backs up the original file before patching it. Two things need to already be true: you've run `lite login` (or `lite login --pkce`, whose key the helper renews on its own), since the apiKeyHelper depends on that stored token, and the proxy is already reachable, since `lite up` does not start one for you. @@ -529,7 +529,7 @@ Cursor is not supported: it has no equivalent file-based config to hot-patch thi lite --base-url https://your-proxy.example.com login --config-claude ``` -It writes the same two settings `lite up` does, `env.ANTHROPIC_BASE_URL` and `apiKeyHelper`, but persistently: there is no backup, nothing to restore, and no foreground process to keep alive. Every other key in `~/.claude/settings.json` is preserved, the file is created if it does not exist, and it is written atomically with owner-only permissions. Plain `lite login` is unchanged; nothing happens to your Claude Code config unless you pass the flag. +It writes the same settings `lite up` does, `env.ANTHROPIC_BASE_URL`, `env.ENABLE_TOOL_SEARCH`, and `apiKeyHelper`, but persistently: there is no backup, nothing to restore, and no foreground process to keep alive. Every other key in `~/.claude/settings.json` is preserved, the file is created if it does not exist, and it is written atomically with owner-only permissions. Plain `lite login` is unchanged; nothing happens to your Claude Code config unless you pass the flag. Because the credential is reached through `apiKeyHelper` rather than copied into the file, a later `lite login` refreshes it with no further action: Claude Code re-runs the helper on every request and picks up whatever token the most recent login stored. Nothing secret is written to `settings.json`. diff --git a/litellm/proxy/client/cli/commands/agents.py b/litellm/proxy/client/cli/commands/agents.py index e05e85ae483..4d3a441272e 100644 --- a/litellm/proxy/client/cli/commands/agents.py +++ b/litellm/proxy/client/cli/commands/agents.py @@ -13,6 +13,8 @@ from .auth import context_secret_vault, get_stored_api_key, login ANTHROPIC_BASE_URL_ENV: Final = "ANTHROPIC_BASE_URL" ANTHROPIC_AUTH_TOKEN_ENV: Final = "ANTHROPIC_AUTH_TOKEN" ANTHROPIC_API_KEY_ENV: Final = "ANTHROPIC_API_KEY" +ENABLE_TOOL_SEARCH_ENV: Final = "ENABLE_TOOL_SEARCH" +ENABLE_TOOL_SEARCH_VALUE: Final = "true" OPENAI_BASE_URL_ENV: Final = "OPENAI_BASE_URL" OPENAI_API_KEY_ENV: Final = "OPENAI_API_KEY" @@ -61,7 +63,9 @@ def build_agent_env( Anthropic clients (Claude Code) append /v1/messages to ANTHROPIC_BASE_URL, so it stays the bare proxy root; OpenAI clients (Codex, OpenCode) expect the /v1 suffix on OPENAI_BASE_URL. ANTHROPIC_API_KEY is dropped so a stray - Anthropic key cannot win over the bearer token we set. + Anthropic key cannot win over the bearer token we set. ENABLE_TOOL_SEARCH is + forced on because Claude Code turns tool search off when ANTHROPIC_BASE_URL + is not a first-party Anthropic host. """ env: Final = dict(base_env) root: Final = base_url.rstrip("/") @@ -69,6 +73,7 @@ def build_agent_env( env[ANTHROPIC_BASE_URL_ENV] = root env[ANTHROPIC_AUTH_TOKEN_ENV] = api_key env.pop(ANTHROPIC_API_KEY_ENV, None) + env[ENABLE_TOOL_SEARCH_ENV] = ENABLE_TOOL_SEARCH_VALUE if PROFILE_OPENAI in profiles: env[OPENAI_BASE_URL_ENV] = root + "/v1" env[OPENAI_API_KEY_ENV] = api_key diff --git a/litellm/proxy/client/cli/commands/autoroute/settings.py b/litellm/proxy/client/cli/commands/autoroute/settings.py index 9fcb11a585b..8331d41ae78 100644 --- a/litellm/proxy/client/cli/commands/autoroute/settings.py +++ b/litellm/proxy/client/cli/commands/autoroute/settings.py @@ -9,6 +9,8 @@ API_KEY_HELPER_KEY: Final = "apiKeyHelper" ANTHROPIC_API_KEY_KEY: Final = "ANTHROPIC_API_KEY" ANTHROPIC_AUTH_TOKEN_KEY: Final = "ANTHROPIC_AUTH_TOKEN" ANTHROPIC_BASE_URL_KEY: Final = "ANTHROPIC_BASE_URL" +ENABLE_TOOL_SEARCH_KEY: Final = "ENABLE_TOOL_SEARCH" +ENABLE_TOOL_SEARCH_VALUE: Final = "true" # Force every one of Claude Code's own model tiers to request the auto-router by name. # Router's auto-router registry is keyed by the literal requested model string # (litellm/router.py:10711-10717) with no wildcard/pattern resolution, so a bare "*" @@ -37,6 +39,7 @@ def merge_claude_settings_static_token( **base_env, ANTHROPIC_BASE_URL_KEY: base_url.rstrip("/"), ANTHROPIC_AUTH_TOKEN_KEY: auth_token, + ENABLE_TOOL_SEARCH_KEY: ENABLE_TOOL_SEARCH_VALUE, **{key: AUTOROUTER_MODEL_NAME for key in ANTHROPIC_DEFAULT_MODEL_ENV_KEYS}, } env.pop(ANTHROPIC_API_KEY_KEY, None) diff --git a/litellm/proxy/client/cli/commands/claude_settings.py b/litellm/proxy/client/cli/commands/claude_settings.py index e18e5b1b7ee..6d88c69bbd4 100644 --- a/litellm/proxy/client/cli/commands/claude_settings.py +++ b/litellm/proxy/client/cli/commands/claude_settings.py @@ -21,6 +21,8 @@ ENV_KEY: Final = "env" API_KEY_HELPER_KEY: Final = "apiKeyHelper" ANTHROPIC_BASE_URL_KEY: Final = "ANTHROPIC_BASE_URL" ANTHROPIC_API_KEY_KEY: Final = "ANTHROPIC_API_KEY" +ENABLE_TOOL_SEARCH_KEY: Final = "ENABLE_TOOL_SEARCH" +ENABLE_TOOL_SEARCH_VALUE: Final = "true" CLAUDE_SETTINGS_PATH: Final = Path.home() / ".claude" / "settings.json" BACKUP_PATH: Final = Path.home() / ".litellm" / "claude_settings_backup.json" @@ -68,16 +70,19 @@ def merge_claude_settings( ) -> dict[str, JsonValue]: """Return a new settings dict wired to route Claude Code through the proxy. - Only env.ANTHROPIC_BASE_URL and the top-level apiKeyHelper are overridden; a - stray env.ANTHROPIC_API_KEY is dropped so it cannot outrank the helper-issued - token (same reasoning as build_agent_env in agents.py). Every other key is - preserved untouched. + Only env.ANTHROPIC_BASE_URL, env.ENABLE_TOOL_SEARCH, and the top-level + apiKeyHelper are overridden; a stray env.ANTHROPIC_API_KEY is dropped so it + cannot outrank the helper-issued token (same reasoning as build_agent_env + in agents.py). ENABLE_TOOL_SEARCH is forced on because Claude Code turns + tool search off when ANTHROPIC_BASE_URL is not a first-party Anthropic host. + Every other key is preserved untouched. """ raw_env: Final = settings.get(ENV_KEY, {}) base_env: Final = raw_env if isinstance(raw_env, dict) else {} env: Final = { **{key: value for key, value in base_env.items() if key != ANTHROPIC_API_KEY_KEY}, ANTHROPIC_BASE_URL_KEY: base_url.rstrip("/"), + ENABLE_TOOL_SEARCH_KEY: ENABLE_TOOL_SEARCH_VALUE, } return {**settings, ENV_KEY: env, API_KEY_HELPER_KEY: api_key_helper} @@ -141,6 +146,8 @@ __all__ = ( "ANTHROPIC_API_KEY_KEY", "ANTHROPIC_BASE_URL_KEY", "API_KEY_HELPER_KEY", + "ENABLE_TOOL_SEARCH_KEY", + "ENABLE_TOOL_SEARCH_VALUE", "AUTOROUTE_BACKUP_PATH", "BACKUP_PATH", "CLAUDE_SETTINGS_PATH", diff --git a/tests/test_litellm/proxy/client/cli/autoroute/test_commands.py b/tests/test_litellm/proxy/client/cli/autoroute/test_commands.py index 74bf1c95777..4a3b3ef22c4 100644 --- a/tests/test_litellm/proxy/client/cli/autoroute/test_commands.py +++ b/tests/test_litellm/proxy/client/cli/autoroute/test_commands.py @@ -157,6 +157,7 @@ class TestUpCommand: assert captured["settings"]["theme"] == "dark" assert captured["settings"]["env"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:5483" assert captured["settings"]["env"]["ANTHROPIC_AUTH_TOKEN"] == "fixed-master-key" + assert captured["settings"]["env"]["ENABLE_TOOL_SEARCH"] == "true" assert "apiKeyHelper" not in captured["settings"] assert captured["settings_mode"] == 0o600 diff --git a/tests/test_litellm/proxy/client/cli/autoroute/test_settings.py b/tests/test_litellm/proxy/client/cli/autoroute/test_settings.py index 40d3e7f2aee..ded8f0808a0 100644 --- a/tests/test_litellm/proxy/client/cli/autoroute/test_settings.py +++ b/tests/test_litellm/proxy/client/cli/autoroute/test_settings.py @@ -19,6 +19,13 @@ def test_sets_base_url_and_auth_token(): merged = merge_claude_settings_static_token({}, "http://127.0.0.1:4000/", "token-abc") assert merged["env"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:4000" assert merged["env"]["ANTHROPIC_AUTH_TOKEN"] == "token-abc" + assert merged["env"]["ENABLE_TOOL_SEARCH"] == "true" + + +def test_forces_tool_search_on(): + settings = {"env": {"ENABLE_TOOL_SEARCH": "false"}} + merged = merge_claude_settings_static_token(settings, "http://127.0.0.1:4000", "token-abc") + assert merged["env"]["ENABLE_TOOL_SEARCH"] == "true" def test_drops_stray_api_key(): diff --git a/tests/test_litellm/proxy/client/cli/test_agents.py b/tests/test_litellm/proxy/client/cli/test_agents.py index 32dfb8d521d..d03c5d9db2d 100644 --- a/tests/test_litellm/proxy/client/cli/test_agents.py +++ b/tests/test_litellm/proxy/client/cli/test_agents.py @@ -77,9 +77,19 @@ class TestBuildAgentEnv: ) assert env["ANTHROPIC_BASE_URL"] == "http://localhost:4000" assert env["ANTHROPIC_AUTH_TOKEN"] == "sk-key" + assert env["ENABLE_TOOL_SEARCH"] == "true" assert "OPENAI_BASE_URL" not in env assert "OPENAI_API_KEY" not in env + def test_anthropic_profile_forces_tool_search_on(self): + env = build_agent_env( + {"ENABLE_TOOL_SEARCH": "false"}, + "http://localhost:4000", + "sk-key", + frozenset({"anthropic"}), + ) + assert env["ENABLE_TOOL_SEARCH"] == "true" + def test_anthropic_profile_drops_existing_api_key(self): env = build_agent_env( {"ANTHROPIC_API_KEY": "real-key"}, @@ -96,6 +106,7 @@ class TestBuildAgentEnv: assert env["OPENAI_BASE_URL"] == "http://localhost:4000/v1" assert env["OPENAI_API_KEY"] == "sk-key" assert "ANTHROPIC_BASE_URL" not in env + assert "ENABLE_TOOL_SEARCH" not in env def test_both_profiles_set_everything(self): env = build_agent_env( @@ -105,6 +116,7 @@ class TestBuildAgentEnv: assert env["OPENAI_BASE_URL"] == "http://localhost:4000/v1" assert env["ANTHROPIC_AUTH_TOKEN"] == "sk-key" assert env["OPENAI_API_KEY"] == "sk-key" + assert env["ENABLE_TOOL_SEARCH"] == "true" def test_preserves_unrelated_env_and_does_not_mutate_input(self): base = {"PATH": "/usr/bin", "ANTHROPIC_API_KEY": "real-key"} @@ -201,6 +213,7 @@ class TestRunAgent: env = calls["env"] assert env["ANTHROPIC_BASE_URL"] == "http://localhost:4000" assert env["ANTHROPIC_AUTH_TOKEN"] == "sk-key" + assert env["ENABLE_TOOL_SEARCH"] == "true" assert "ANTHROPIC_API_KEY" not in env assert "OPENAI_BASE_URL" not in env @@ -218,6 +231,7 @@ class TestRunAgent: assert calls["env"]["OPENAI_BASE_URL"] == "http://localhost:4000/v1" assert calls["env"]["OPENAI_API_KEY"] == "sk-key" assert "ANTHROPIC_BASE_URL" not in calls["env"] + assert "ENABLE_TOOL_SEARCH" not in calls["env"] def test_codex_injects_proxy_provider_args_before_user_args(self): calls = {} diff --git a/tests/test_litellm/proxy/client/cli/test_auth_commands.py b/tests/test_litellm/proxy/client/cli/test_auth_commands.py index 85a4d90abf9..1d0a99b8e0a 100644 --- a/tests/test_litellm/proxy/client/cli/test_auth_commands.py +++ b/tests/test_litellm/proxy/client/cli/test_auth_commands.py @@ -1373,6 +1373,7 @@ class TestLoginConfigClaude: assert result.exit_code == 0 written = json.loads(settings_path.read_text()) assert written["env"]["ANTHROPIC_BASE_URL"] == "https://test.example.com" + assert written["env"]["ENABLE_TOOL_SEARCH"] == "true" assert written["apiKeyHelper"] == "/usr/local/bin/lite --base-url https://test.example.com auth print-token" assert "Configured Claude Code" in result.output diff --git a/tests/test_litellm/proxy/client/cli/test_claude_settings.py b/tests/test_litellm/proxy/client/cli/test_claude_settings.py index 9010fb4c022..898f9ab1ed7 100644 --- a/tests/test_litellm/proxy/client/cli/test_claude_settings.py +++ b/tests/test_litellm/proxy/client/cli/test_claude_settings.py @@ -48,6 +48,7 @@ class TestWriteClaudeSettings: written = json.loads(settings_path.read_text()) assert written["env"]["ANTHROPIC_BASE_URL"] == "https://proxy.example.com" + assert written["env"]["ENABLE_TOOL_SEARCH"] == "true" assert written["apiKeyHelper"] == "/usr/local/bin/lite --base-url https://proxy.example.com auth print-token" def test_updates_an_existing_file_preserving_unrelated_settings(self, paths, lite_on_path): diff --git a/tests/test_litellm/proxy/client/cli/test_up_commands.py b/tests/test_litellm/proxy/client/cli/test_up_commands.py index 9958286884b..a1aa9eeeb8a 100644 --- a/tests/test_litellm/proxy/client/cli/test_up_commands.py +++ b/tests/test_litellm/proxy/client/cli/test_up_commands.py @@ -55,8 +55,14 @@ class TestMergeClaudeSettings: } merged = merge_claude_settings(settings, "http://localhost:4000/", "new-helper") assert merged["env"]["ANTHROPIC_BASE_URL"] == "http://localhost:4000" + assert merged["env"]["ENABLE_TOOL_SEARCH"] == "true" assert merged["apiKeyHelper"] == "new-helper" + def test_forces_tool_search_on(self): + settings = {"env": {"ENABLE_TOOL_SEARCH": "false"}} + merged = merge_claude_settings(settings, "http://localhost:4000", "helper") + assert merged["env"]["ENABLE_TOOL_SEARCH"] == "true" + def test_drops_stray_api_key(self): settings = {"env": {"ANTHROPIC_API_KEY": "leaked-key"}} merged = merge_claude_settings(settings, "http://localhost:4000", "helper") @@ -64,7 +70,10 @@ class TestMergeClaudeSettings: def test_works_from_empty_settings(self): merged = merge_claude_settings({}, "http://localhost:4000", "helper") - assert merged["env"] == {"ANTHROPIC_BASE_URL": "http://localhost:4000"} + assert merged["env"] == { + "ANTHROPIC_BASE_URL": "http://localhost:4000", + "ENABLE_TOOL_SEARCH": "true", + } assert merged["apiKeyHelper"] == "helper" def test_does_not_mutate_input(self): @@ -486,6 +495,7 @@ class TestUpCommand: assert captured["backup_existed"] is True assert captured["settings"]["theme"] == "dark" assert captured["settings"]["env"]["ANTHROPIC_BASE_URL"] == "http://localhost:4000" + assert captured["settings"]["env"]["ENABLE_TOOL_SEARCH"] == "true" assert captured["settings"]["apiKeyHelper"] == "/usr/local/bin/lite auth print-token" assert json.loads(settings_path.read_text()) == original assert not backup_path.exists() From 096e016baf04fc833ce617025eace2f8937cefe2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 31 Aug 2026 05:58:00 +0000 Subject: [PATCH 2/3] fix(cli): sort claude_settings __all__ for ruff RUF022 Co-authored-by: Mateo Wang --- litellm/proxy/client/cli/commands/claude_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/client/cli/commands/claude_settings.py b/litellm/proxy/client/cli/commands/claude_settings.py index 6d88c69bbd4..41ba25447e1 100644 --- a/litellm/proxy/client/cli/commands/claude_settings.py +++ b/litellm/proxy/client/cli/commands/claude_settings.py @@ -146,11 +146,11 @@ __all__ = ( "ANTHROPIC_API_KEY_KEY", "ANTHROPIC_BASE_URL_KEY", "API_KEY_HELPER_KEY", - "ENABLE_TOOL_SEARCH_KEY", - "ENABLE_TOOL_SEARCH_VALUE", "AUTOROUTE_BACKUP_PATH", "BACKUP_PATH", "CLAUDE_SETTINGS_PATH", + "ENABLE_TOOL_SEARCH_KEY", + "ENABLE_TOOL_SEARCH_VALUE", "ENV_KEY", "SETTINGS_FILE_OWNERS", "ClaudeSettingsError", From 43c64e24715cdd1aa7595544f1d4aba55f7c2512 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 31 Aug 2026 06:00:45 +0000 Subject: [PATCH 3/3] fix(cli): keep an existing ENABLE_TOOL_SEARCH value Default remains true so lite claude turns tool search back on through a proxy. An explicit false or auto in the env or settings is left alone Co-authored-by: Mateo Wang --- litellm/proxy/client/cli/README.md | 4 ++-- litellm/proxy/client/cli/commands/agents.py | 10 ++++++---- .../client/cli/commands/autoroute/settings.py | 2 +- .../proxy/client/cli/commands/claude_settings.py | 14 +++++++------- .../proxy/client/cli/autoroute/test_settings.py | 4 ++-- tests/test_litellm/proxy/client/cli/test_agents.py | 4 ++-- .../proxy/client/cli/test_up_commands.py | 4 ++-- 7 files changed, 22 insertions(+), 20 deletions(-) diff --git a/litellm/proxy/client/cli/README.md b/litellm/proxy/client/cli/README.md index 0da219df700..3ddce35b53d 100644 --- a/litellm/proxy/client/cli/README.md +++ b/litellm/proxy/client/cli/README.md @@ -489,7 +489,7 @@ lite codex exec "summarize the repo" Each command resolves your LiteLLM key (logging in via SSO when none is stored and you are at a terminal; otherwise it expects `LITELLM_PROXY_API_KEY` or `--api-key`), checks the key against the proxy so bad credentials fail immediately instead of deep inside the agent, exports the environment variables the agent reads, then replaces itself with the agent process. -The right variables are picked per agent. Claude Code gets `ANTHROPIC_BASE_URL` (the proxy root, so it appends `/v1/messages`) and `ANTHROPIC_AUTH_TOKEN`, with any stray `ANTHROPIC_API_KEY` cleared so the proxy token wins, and `ENABLE_TOOL_SEARCH=true` so Claude Code keeps tool search on even though the base URL is a proxy rather than a first-party Anthropic host. Codex and OpenCode get `OPENAI_BASE_URL` (the proxy plus `/v1`) and `OPENAI_API_KEY`. Codex ignores `OPENAI_BASE_URL`, so it is additionally pointed at the proxy through a custom provider passed as `-c` config overrides (HTTP/SSE Responses transport, since the proxy does not speak the Responses WebSocket protocol). +The right variables are picked per agent. Claude Code gets `ANTHROPIC_BASE_URL` (the proxy root, so it appends `/v1/messages`) and `ANTHROPIC_AUTH_TOKEN`, with any stray `ANTHROPIC_API_KEY` cleared so the proxy token wins, and `ENABLE_TOOL_SEARCH=true` (unless you already set it) so Claude Code keeps tool search on even though the base URL is a proxy rather than a first-party Anthropic host. Codex and OpenCode get `OPENAI_BASE_URL` (the proxy plus `/v1`) and `OPENAI_API_KEY`. Codex ignores `OPENAI_BASE_URL`, so it is additionally pointed at the proxy through a custom provider passed as `-c` config overrides (HTTP/SSE Responses transport, since the proxy does not speak the Responses WebSocket protocol). Options (these belong to the wrapper, so put them before the agent's own flags): @@ -505,7 +505,7 @@ The credential is short-lived by design (default 24h, configurable via `LITELLM_ ### Route Every Claude Code Session Through the Proxy -`lite claude` wraps a single invocation, but `lite up` goes further: it patches `~/.claude/settings.json`, Claude Code's own config file, so that every Claude Code session started afterward -- from any terminal, launched normally with just `claude`, no wrapper needed -- routes through your LiteLLM proxy. It sets `env.ANTHROPIC_BASE_URL` to the proxy URL, `env.ENABLE_TOOL_SEARCH` to `true`, and `apiKeyHelper` to a `lite auth print-token` invocation, drops any stray static `ANTHROPIC_API_KEY` so the helper-issued token wins, and leaves every other setting in the file untouched. It backs up the original file before patching it. +`lite claude` wraps a single invocation, but `lite up` goes further: it patches `~/.claude/settings.json`, Claude Code's own config file, so that every Claude Code session started afterward -- from any terminal, launched normally with just `claude`, no wrapper needed -- routes through your LiteLLM proxy. It sets `env.ANTHROPIC_BASE_URL` to the proxy URL, `env.ENABLE_TOOL_SEARCH` to `true` when that key is missing, and `apiKeyHelper` to a `lite auth print-token` invocation, drops any stray static `ANTHROPIC_API_KEY` so the helper-issued token wins, and leaves every other setting in the file untouched. It backs up the original file before patching it. Two things need to already be true: you've run `lite login` (or `lite login --pkce`, whose key the helper renews on its own), since the apiKeyHelper depends on that stored token, and the proxy is already reachable, since `lite up` does not start one for you. diff --git a/litellm/proxy/client/cli/commands/agents.py b/litellm/proxy/client/cli/commands/agents.py index 4d3a441272e..45e05d353fb 100644 --- a/litellm/proxy/client/cli/commands/agents.py +++ b/litellm/proxy/client/cli/commands/agents.py @@ -63,9 +63,10 @@ def build_agent_env( Anthropic clients (Claude Code) append /v1/messages to ANTHROPIC_BASE_URL, so it stays the bare proxy root; OpenAI clients (Codex, OpenCode) expect the /v1 suffix on OPENAI_BASE_URL. ANTHROPIC_API_KEY is dropped so a stray - Anthropic key cannot win over the bearer token we set. ENABLE_TOOL_SEARCH is - forced on because Claude Code turns tool search off when ANTHROPIC_BASE_URL - is not a first-party Anthropic host. + Anthropic key cannot win over the bearer token we set. ENABLE_TOOL_SEARCH + defaults to true because Claude Code turns tool search off when + ANTHROPIC_BASE_URL is not a first-party Anthropic host; a value already in + the environment is left alone. """ env: Final = dict(base_env) root: Final = base_url.rstrip("/") @@ -73,7 +74,8 @@ def build_agent_env( env[ANTHROPIC_BASE_URL_ENV] = root env[ANTHROPIC_AUTH_TOKEN_ENV] = api_key env.pop(ANTHROPIC_API_KEY_ENV, None) - env[ENABLE_TOOL_SEARCH_ENV] = ENABLE_TOOL_SEARCH_VALUE + if ENABLE_TOOL_SEARCH_ENV not in env: + env[ENABLE_TOOL_SEARCH_ENV] = ENABLE_TOOL_SEARCH_VALUE if PROFILE_OPENAI in profiles: env[OPENAI_BASE_URL_ENV] = root + "/v1" env[OPENAI_API_KEY_ENV] = api_key diff --git a/litellm/proxy/client/cli/commands/autoroute/settings.py b/litellm/proxy/client/cli/commands/autoroute/settings.py index 8331d41ae78..60729b5410d 100644 --- a/litellm/proxy/client/cli/commands/autoroute/settings.py +++ b/litellm/proxy/client/cli/commands/autoroute/settings.py @@ -36,10 +36,10 @@ def merge_claude_settings_static_token( raw_env: Final = settings.get(ENV_KEY, {}) base_env: Final = raw_env if isinstance(raw_env, dict) else {} env: Final[dict[str, JsonValue]] = { + ENABLE_TOOL_SEARCH_KEY: ENABLE_TOOL_SEARCH_VALUE, **base_env, ANTHROPIC_BASE_URL_KEY: base_url.rstrip("/"), ANTHROPIC_AUTH_TOKEN_KEY: auth_token, - ENABLE_TOOL_SEARCH_KEY: ENABLE_TOOL_SEARCH_VALUE, **{key: AUTOROUTER_MODEL_NAME for key in ANTHROPIC_DEFAULT_MODEL_ENV_KEYS}, } env.pop(ANTHROPIC_API_KEY_KEY, None) diff --git a/litellm/proxy/client/cli/commands/claude_settings.py b/litellm/proxy/client/cli/commands/claude_settings.py index 41ba25447e1..ea1fa019c83 100644 --- a/litellm/proxy/client/cli/commands/claude_settings.py +++ b/litellm/proxy/client/cli/commands/claude_settings.py @@ -70,19 +70,19 @@ def merge_claude_settings( ) -> dict[str, JsonValue]: """Return a new settings dict wired to route Claude Code through the proxy. - Only env.ANTHROPIC_BASE_URL, env.ENABLE_TOOL_SEARCH, and the top-level - apiKeyHelper are overridden; a stray env.ANTHROPIC_API_KEY is dropped so it - cannot outrank the helper-issued token (same reasoning as build_agent_env - in agents.py). ENABLE_TOOL_SEARCH is forced on because Claude Code turns - tool search off when ANTHROPIC_BASE_URL is not a first-party Anthropic host. - Every other key is preserved untouched. + Only env.ANTHROPIC_BASE_URL and the top-level apiKeyHelper are overridden; a + stray env.ANTHROPIC_API_KEY is dropped so it cannot outrank the helper-issued + token (same reasoning as build_agent_env in agents.py). ENABLE_TOOL_SEARCH + defaults to true because Claude Code turns tool search off when + ANTHROPIC_BASE_URL is not a first-party Anthropic host; an existing value is + left alone. Every other key is preserved untouched. """ raw_env: Final = settings.get(ENV_KEY, {}) base_env: Final = raw_env if isinstance(raw_env, dict) else {} env: Final = { + ENABLE_TOOL_SEARCH_KEY: ENABLE_TOOL_SEARCH_VALUE, **{key: value for key, value in base_env.items() if key != ANTHROPIC_API_KEY_KEY}, ANTHROPIC_BASE_URL_KEY: base_url.rstrip("/"), - ENABLE_TOOL_SEARCH_KEY: ENABLE_TOOL_SEARCH_VALUE, } return {**settings, ENV_KEY: env, API_KEY_HELPER_KEY: api_key_helper} diff --git a/tests/test_litellm/proxy/client/cli/autoroute/test_settings.py b/tests/test_litellm/proxy/client/cli/autoroute/test_settings.py index ded8f0808a0..87a33c79a79 100644 --- a/tests/test_litellm/proxy/client/cli/autoroute/test_settings.py +++ b/tests/test_litellm/proxy/client/cli/autoroute/test_settings.py @@ -22,10 +22,10 @@ def test_sets_base_url_and_auth_token(): assert merged["env"]["ENABLE_TOOL_SEARCH"] == "true" -def test_forces_tool_search_on(): +def test_preserves_existing_tool_search(): settings = {"env": {"ENABLE_TOOL_SEARCH": "false"}} merged = merge_claude_settings_static_token(settings, "http://127.0.0.1:4000", "token-abc") - assert merged["env"]["ENABLE_TOOL_SEARCH"] == "true" + assert merged["env"]["ENABLE_TOOL_SEARCH"] == "false" def test_drops_stray_api_key(): diff --git a/tests/test_litellm/proxy/client/cli/test_agents.py b/tests/test_litellm/proxy/client/cli/test_agents.py index d03c5d9db2d..0191dad3d94 100644 --- a/tests/test_litellm/proxy/client/cli/test_agents.py +++ b/tests/test_litellm/proxy/client/cli/test_agents.py @@ -81,14 +81,14 @@ class TestBuildAgentEnv: assert "OPENAI_BASE_URL" not in env assert "OPENAI_API_KEY" not in env - def test_anthropic_profile_forces_tool_search_on(self): + def test_anthropic_profile_preserves_existing_tool_search(self): env = build_agent_env( {"ENABLE_TOOL_SEARCH": "false"}, "http://localhost:4000", "sk-key", frozenset({"anthropic"}), ) - assert env["ENABLE_TOOL_SEARCH"] == "true" + assert env["ENABLE_TOOL_SEARCH"] == "false" def test_anthropic_profile_drops_existing_api_key(self): env = build_agent_env( diff --git a/tests/test_litellm/proxy/client/cli/test_up_commands.py b/tests/test_litellm/proxy/client/cli/test_up_commands.py index a1aa9eeeb8a..053c90c36b4 100644 --- a/tests/test_litellm/proxy/client/cli/test_up_commands.py +++ b/tests/test_litellm/proxy/client/cli/test_up_commands.py @@ -58,10 +58,10 @@ class TestMergeClaudeSettings: assert merged["env"]["ENABLE_TOOL_SEARCH"] == "true" assert merged["apiKeyHelper"] == "new-helper" - def test_forces_tool_search_on(self): + def test_preserves_existing_tool_search(self): settings = {"env": {"ENABLE_TOOL_SEARCH": "false"}} merged = merge_claude_settings(settings, "http://localhost:4000", "helper") - assert merged["env"]["ENABLE_TOOL_SEARCH"] == "true" + assert merged["env"]["ENABLE_TOOL_SEARCH"] == "false" def test_drops_stray_api_key(self): settings = {"env": {"ANTHROPIC_API_KEY": "leaked-key"}}