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
This commit is contained in:
mateo-berri 2026-11-02 13:05:16 -08:00
parent 55385e483f
commit 083990f6d7
3 changed files with 49 additions and 4 deletions

View file

@ -15,7 +15,7 @@ from typing import Final, cast
from pydantic import TypeAdapter from pydantic import TypeAdapter
import litellm 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.constants import PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO
from litellm.integrations.anthropic_cache_control_hook import AnthropicCacheControlHook from litellm.integrations.anthropic_cache_control_hook import AnthropicCacheControlHook
from litellm.litellm_core_utils.core_helpers import as_str_mapping 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: if short_circuit_response is not None:
return short_circuit_response 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). # Run registered MessagesInterceptors (e.g. advisor orchestration loop).
# Named params on `anthropic_messages` are bound to locals, not `**kwargs`, # Named params on `anthropic_messages` are bound to locals, not `**kwargs`,
# so forward them explicitly — otherwise interceptor sub-calls drop them. # so forward them explicitly — otherwise interceptor sub-calls drop them.
for interceptor in get_messages_interceptors(): 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( return await interceptor.handle(
model=model, model=model,
messages=messages, messages=messages,

View file

@ -51,9 +51,9 @@ _interceptors = [
### `AdvisorOrchestrationHandler` ### `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:** **What it does:**
- Translates the advisor tool to a regular function tool the provider understands. - Translates the advisor tool to a regular function tool the provider understands.

View file

@ -190,6 +190,46 @@ async def test_anthropic_provider_bypasses_interceptor():
assert any("Native anthropic" in b.get("text", "") for b in text_blocks) 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 # 4. Regression: top-level named params must be forwarded into executor sub-call
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------