fix(azure_ai): forward the thinking-binding-controls beta header to Foundry (#48033)

This commit is contained in:
shrey-berri 2026-10-23 16:34:55 -07:00 • committed by GitHub
parent acfa7a29ad
commit 23ccff69a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 72 additions and 6 deletions

View file

@ -235,7 +235,10 @@ impl AnthropicBeta {
BetaProvider::Databricks,
],
Self::Compact20260112 => &[BetaProvider::AzureAi, BetaProvider::BedrockConverse],
Self::ComputerUse20250124 | Self::ComputerUse20251124 | Self::Context1m20250807 => &[],
Self::ComputerUse20250124
| Self::ComputerUse20251124
| Self::Context1m20250807
| Self::ThinkingBindingControls20260801 => &[],
Self::DangerousToolUse20260903 => &[BetaProvider::BedrockConverse],
Self::Effort20251124 => &[BetaProvider::VertexAi],
Self::FastMode20260201 | Self::Oauth20250420 => &[
@ -272,7 +275,6 @@ impl AnthropicBeta {
BetaProvider::Databricks,
],
Self::StructuredOutputs20251113 => &[BetaProvider::Bedrock, BetaProvider::VertexAi],
Self::ThinkingBindingControls20260801 => &[BetaProvider::AzureAi],
Self::ThinkingDisplayUpdates20260818 => &[BetaProvider::Databricks],
Self::ToolSearchTool20251019 => &[
BetaProvider::Anthropic,

View file

@ -67,7 +67,7 @@
"structured-outputs-2025-11-13": "structured-outputs-2025-11-13",
"text_editor_20241022": null,
"text_editor_20250124": null,
"thinking-binding-controls-2026-08-01": null,
"thinking-binding-controls-2026-08-01": "thinking-binding-controls-2026-08-01",
"thinking-display-updates-2026-08-18": "thinking-display-updates-2026-08-18",
"token-efficient-tools-2025-02-19": null,
"web-fetch-2025-09-10": "web-fetch-2025-09-10",

View file

@ -4278,6 +4278,9 @@ _ANTHROPIC_API_HEADER_PROVIDERS: Final = ",".join(
LlmProviders.VERTEX_AI.value,
)
)
_NATIVE_MESSAGES_ANTHROPIC_API_HEADER_PROVIDERS: Final = ",".join(
(LlmProviders.AZURE_AI.value, LlmProviders.DATABRICKS.value)
)
_ANTHROPIC_OAUTH_CREDENTIAL_PROVIDERS: Final = ",".join(sorted(ANTHROPIC_OAUTH_FORWARD_PROVIDERS))
@ -4306,7 +4309,7 @@ def add_provider_specific_headers_to_request(
custom_llm_provider=_ANTHROPIC_API_HEADER_PROVIDERS, extra_headers=anthropic_api_headers
),
ProviderSpecificHeader(
custom_llm_provider=LlmProviders.DATABRICKS.value,
custom_llm_provider=_NATIVE_MESSAGES_ANTHROPIC_API_HEADER_PROVIDERS,
extra_headers=anthropic_api_headers,
native_anthropic_messages_only=True,
),

View file

@ -8111,6 +8111,67 @@ def test_client_anthropic_api_headers_stay_off_openai_compatible_providers():
assert forwarded == {}
def test_client_anthropic_api_headers_stay_off_azure_ai_outside_native_messages():
forwarded = _headers_forwarded_to({"anthropic-beta": "thinking-binding-controls-2026-08-01"}, "azure_ai")
assert forwarded == {}
@pytest.mark.asyncio
@pytest.mark.usefixtures("local_beta_headers_config")
async def test_client_thinking_binding_beta_reaches_azure_ai_foundry_claude():
import httpx
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
data: Final[dict] = {"model": "claude-opus-4-8"} # mutable-ok: the function under test writes into it
add_provider_specific_headers_to_request(
data=data,
headers={
"content-type": "application/json",
"anthropic-beta": "thinking-binding-controls-2026-08-01",
"anthropic-version": "2026-08-01",
},
)
received: Final[list[httpx.Request]] = [] # mutable-ok: the fake Foundry endpoint records what the proxy sent
def foundry_answers(request: httpx.Request) -> httpx.Response:
received.append(request)
return httpx.Response(
status_code=200,
json={
"id": "msg_01",
"type": "message",
"role": "assistant",
"model": "claude-opus-4-8",
"content": [{"type": "text", "text": "hello"}],
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {"input_tokens": 12, "output_tokens": 6},
},
request=request,
)
upstream: Final = AsyncHTTPHandler(transport=httpx.MockTransport(foundry_answers))
try:
await litellm.anthropic.messages.acreate(
max_tokens=16,
messages=[{"role": "user", "content": "hi"}],
thinking={"type": "adaptive", "block_binding": {"prefix_mismatch_behavior": "drop_block"}},
model="azure_ai/claude-opus-4-8",
api_key="foundry-key",
api_base="https://my-resource.services.ai.azure.com/anthropic",
client=upstream,
provider_specific_header=data["provider_specific_header"],
)
finally:
await upstream.close()
assert received[0].headers["anthropic-beta"] == "thinking-binding-controls-2026-08-01"
assert received[0].headers["anthropic-version"] == "2026-08-01"
assert json.loads(received[0].content)["thinking"]["block_binding"] == {"prefix_mismatch_behavior": "drop_block"}
TYPESAFE_DANGER_ZONE_HEADERS = {
"TypeSafe-Danger-Zone-Images": "true",
"typesafe-danger-zone-long-context": "true",

View file

@ -429,13 +429,13 @@ class TestAnthropicBetaHeadersFiltering:
assert filtered == ["fine-grained-tool-streaming-2025-05-14"]
@pytest.mark.parametrize(
"provider", ["anthropic", "bedrock", "bedrock_converse", "vertex_ai", "databricks"]
"provider", ["anthropic", "azure_ai", "bedrock", "bedrock_converse", "vertex_ai", "databricks"]
)
def test_thinking_binding_controls_forwarded(self, provider):
"""`thinking.block_binding` (preserved thinking, Claude Fable 5.1) is only
accepted alongside thinking-binding-controls-2026-08-01. The body field is
forwarded untouched, so stripping the header (previously unknown, hence
dropped) makes Bedrock and Vertex reject the request with
dropped) makes Bedrock, Vertex and Azure AI Foundry reject the request with
"thinking.adaptive.block_binding: Extra inputs are not permitted"."""
filtered = filter_and_transform_beta_headers(
beta_headers=["thinking-binding-controls-2026-08-01"],