From 083990f6d7065e456f450901e02d3aa251c3218d Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 2 Nov 2026 13:05:16 -0800 Subject: [PATCH] fix(anthropic): run the advisor loop for an anthropic deployment on a Foundry host The advisor interceptor treated every anthropic deployment as native, so a Foundry-hosted one received the advisor_20260301 tool and Foundry answered 400. Interceptors now get the deployment's beta row from resolve_beta_provider, so that deployment runs litellm's advisor loop the same way an azure_ai deployment does --- .../llms/anthropic/messages/native/handler.py | 9 ++++- .../messages/native/interceptors/README.md | 4 +- .../native/test_advisor_integration.py | 40 +++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/litellm/llms/anthropic/messages/native/handler.py b/litellm/llms/anthropic/messages/native/handler.py index 922de386fe4..37417806b54 100644 --- a/litellm/llms/anthropic/messages/native/handler.py +++ b/litellm/llms/anthropic/messages/native/handler.py @@ -15,7 +15,7 @@ from typing import Final, cast from pydantic import TypeAdapter import litellm -from litellm.anthropic_beta_headers_manager import aload_beta_headers_config +from litellm.anthropic_beta_headers_manager import aload_beta_headers_config, resolve_beta_provider from litellm.constants import PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO from litellm.integrations.anthropic_cache_control_hook import AnthropicCacheControlHook from litellm.litellm_core_utils.core_helpers import as_str_mapping @@ -540,11 +540,16 @@ async def anthropic_messages( if short_circuit_response is not None: return short_circuit_response + serving_provider: Final = ( + resolve_beta_provider(custom_llm_provider, AnthropicModelInfo.get_api_base(api_base)) + if custom_llm_provider + else None + ) # Run registered MessagesInterceptors (e.g. advisor orchestration loop). # Named params on `anthropic_messages` are bound to locals, not `**kwargs`, # so forward them explicitly — otherwise interceptor sub-calls drop them. for interceptor in get_messages_interceptors(): - if interceptor.can_handle(tools, custom_llm_provider): + if interceptor.can_handle(tools, serving_provider): return await interceptor.handle( model=model, messages=messages, diff --git a/litellm/llms/anthropic/messages/native/interceptors/README.md b/litellm/llms/anthropic/messages/native/interceptors/README.md index b6df1edc854..f62e03ed43d 100644 --- a/litellm/llms/anthropic/messages/native/interceptors/README.md +++ b/litellm/llms/anthropic/messages/native/interceptors/README.md @@ -51,9 +51,9 @@ _interceptors = [ ### `AdvisorOrchestrationHandler` -Handles `advisor_20260301` tool for providers that don't support it natively (all non-Anthropic providers for now). +Handles `advisor_20260301` tool for providers that don't support it natively (every provider but Anthropic itself, including an `anthropic/` deployment on an Azure AI Foundry host). -**Triggers when:** `advisor_20260301` is in `tools` AND `custom_llm_provider` is not in `ADVISOR_NATIVE_PROVIDERS`. +**Triggers when:** `advisor_20260301` is in `tools` AND the deployment's beta row (`resolve_beta_provider`, which maps an `anthropic` deployment on a Foundry host to `azure_ai`) is not in `ADVISOR_NATIVE_PROVIDERS`. **What it does:** - Translates the advisor tool to a regular function tool the provider understands. diff --git a/tests/unit/llms/anthropic/messages/native/test_advisor_integration.py b/tests/unit/llms/anthropic/messages/native/test_advisor_integration.py index 8bd36c6564a..b3b8624d472 100644 --- a/tests/unit/llms/anthropic/messages/native/test_advisor_integration.py +++ b/tests/unit/llms/anthropic/messages/native/test_advisor_integration.py @@ -190,6 +190,46 @@ async def test_anthropic_provider_bypasses_interceptor(): assert any("Native anthropic" in b.get("text", "") for b in text_blocks) +@pytest.mark.asyncio +@pytest.mark.parametrize( + "api_base", + ["https://res.services.ai.azure.com/anthropic", "https://res.cognitiveservices.azure.com/anthropic"], +) +async def test_anthropic_deployment_on_foundry_runs_the_advisor_loop(api_base): + from litellm.llms.anthropic.messages.native.handler import ( + anthropic_messages, + ) + + sent_tool_types = [] + + async def mock_handler(model, messages, tools, stream, max_tokens, **kwargs): + sent_tool_types.append([t.get("type") for t in tools or []]) + if len(sent_tool_types) == 1: + return _advisor_call_resp() + if len(sent_tool_types) == 2: + return _text_resp("Use trial division.", model="claude-opus-4-6") + return _text_resp("def is_prime(n): ...") + + with patch( + "litellm.llms.anthropic.messages.native.interceptors.advisor._call_messages_handler", + side_effect=mock_handler, + ): + result = await anthropic_messages( + model="claude-sonnet-4-6", + messages=MESSAGES, + tools=[ADVISOR_TOOL], + stream=False, + max_tokens=512, + custom_llm_provider="anthropic", + api_base=api_base, + ) + + assert len(sent_tool_types) == 3 + assert all("advisor_20260301" not in types for types in sent_tool_types) + assert isinstance(result, dict) + assert not [b for b in result.get("content", []) if b.get("type") == "tool_use" and b.get("name") == "advisor"] + + # --------------------------------------------------------------------------- # 4. Regression: top-level named params must be forwarded into executor sub-call # ---------------------------------------------------------------------------