mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
Merge pull request #41189 from BerriAI/litellm_per_turn_control_beta
fix(anthropic): add the per-turn-control beta when a message carries output_config
This commit is contained in:
commit
d3929287fe
9 changed files with 166 additions and 24 deletions
|
|
@ -22,6 +22,7 @@
|
|||
"mcp-servers-2025-12-04": null,
|
||||
"oauth-2025-04-20": "oauth-2025-04-20",
|
||||
"output-128k-2025-02-19": "output-128k-2025-02-19",
|
||||
"per-turn-control-2026-07-01": "per-turn-control-2026-07-01",
|
||||
"prompt-caching-scope-2026-01-05": "prompt-caching-scope-2026-01-05",
|
||||
"skills-2025-10-02": "skills-2025-10-02",
|
||||
"structured-outputs-2025-11-13": "structured-outputs-2025-11-13",
|
||||
|
|
@ -52,6 +53,7 @@
|
|||
"mcp-servers-2025-12-04": null,
|
||||
"output-128k-2025-02-19": null,
|
||||
"structured-output-2024-03-01": null,
|
||||
"per-turn-control-2026-07-01": null,
|
||||
"prompt-caching-scope-2026-01-05": "prompt-caching-scope-2026-01-05",
|
||||
"skills-2025-10-02": "skills-2025-10-02",
|
||||
"structured-outputs-2025-11-13": "structured-outputs-2025-11-13",
|
||||
|
|
@ -82,6 +84,7 @@
|
|||
"mcp-servers-2025-12-04": null,
|
||||
"output-128k-2025-02-19": null,
|
||||
"structured-output-2024-03-01": null,
|
||||
"per-turn-control-2026-07-01": null,
|
||||
"prompt-caching-scope-2026-01-05": null,
|
||||
"skills-2025-10-02": null,
|
||||
"structured-outputs-2025-11-13": "structured-outputs-2025-11-13",
|
||||
|
|
@ -113,6 +116,7 @@
|
|||
"mcp-servers-2025-12-04": null,
|
||||
"output-128k-2025-02-19": null,
|
||||
"structured-output-2024-03-01": null,
|
||||
"per-turn-control-2026-07-01": null,
|
||||
"prompt-caching-scope-2026-01-05": null,
|
||||
"skills-2025-10-02": null,
|
||||
"structured-outputs-2025-11-13": null,
|
||||
|
|
@ -144,6 +148,7 @@
|
|||
"mcp-servers-2025-12-04": null,
|
||||
"output-128k-2025-02-19": null,
|
||||
"structured-output-2024-03-01": null,
|
||||
"per-turn-control-2026-07-01": null,
|
||||
"prompt-caching-scope-2026-01-05": null,
|
||||
"skills-2025-10-02": null,
|
||||
"structured-outputs-2025-11-13": null,
|
||||
|
|
@ -176,6 +181,7 @@
|
|||
"mcp-servers-2025-12-04": null,
|
||||
"oauth-2025-04-20": "oauth-2025-04-20",
|
||||
"output-128k-2025-02-19": "output-128k-2025-02-19",
|
||||
"per-turn-control-2026-07-01": null,
|
||||
"prompt-caching-scope-2026-01-05": "prompt-caching-scope-2026-01-05",
|
||||
"skills-2025-10-02": "skills-2025-10-02",
|
||||
"structured-outputs-2025-11-13": "structured-outputs-2025-11-13",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ DROP_UNFITTING_REASONING_EFFORT_WARNING: Final = (
|
|||
)
|
||||
|
||||
|
||||
def _messages_carry_output_config(messages: Sequence[object]) -> bool:
|
||||
return any(isinstance(message, Mapping) and "output_config" in message for message in messages)
|
||||
|
||||
|
||||
class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
||||
@property
|
||||
def custom_llm_provider(self) -> str | None:
|
||||
|
|
@ -331,6 +335,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
headers = self._update_headers_with_anthropic_beta(
|
||||
headers=headers,
|
||||
optional_params=optional_params,
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
return headers, api_base
|
||||
|
|
@ -664,6 +669,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
headers: dict,
|
||||
optional_params: dict,
|
||||
custom_llm_provider: str = "anthropic",
|
||||
messages: Sequence[object] = (),
|
||||
) -> dict:
|
||||
"""
|
||||
Auto-inject anthropic-beta headers based on features used.
|
||||
|
|
@ -673,24 +679,30 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
- tool_search: adds provider-specific tool search header
|
||||
- output_format: adds 'structured-outputs-2025-11-13'
|
||||
- speed: adds 'fast-mode-2026-02-01'
|
||||
- a message carrying output_config: adds 'per-turn-control-2026-07-01'
|
||||
|
||||
Args:
|
||||
headers: Request headers dict
|
||||
optional_params: Optional parameters including tools, context_management, output_format, speed
|
||||
custom_llm_provider: Provider name for looking up correct tool search header
|
||||
messages: Request messages, scanned for per-message output_config
|
||||
"""
|
||||
beta_values: Final[set] = set()
|
||||
|
||||
# Get existing beta headers if any
|
||||
existing_beta: Final = headers.get("anthropic-beta")
|
||||
if existing_beta:
|
||||
beta_values.update(b.strip() for b in existing_beta.split(","))
|
||||
existing_beta: Final = tuple(
|
||||
piece.strip()
|
||||
for key, value in headers.items()
|
||||
if key.lower() == "anthropic-beta"
|
||||
for piece in value.split(",")
|
||||
if piece.strip()
|
||||
)
|
||||
beta_values.update(existing_beta)
|
||||
|
||||
# Check for context management
|
||||
context_management_param: Final = optional_params.get("context_management")
|
||||
if context_management_param is not None:
|
||||
# Check edits array for compact_20260112 type
|
||||
edits: Final = context_management_param.get("edits", [])
|
||||
edits: Final = context_management_param.get("edits", ())
|
||||
has_compact = False
|
||||
has_other = False
|
||||
|
||||
|
|
@ -722,24 +734,18 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
if optional_params.get("speed") == "fast":
|
||||
beta_values.add(ANTHROPIC_BETA_HEADER_VALUES.FAST_MODE_2026_02_01.value)
|
||||
|
||||
# Check for advisor tool
|
||||
tools = optional_params.get("tools")
|
||||
if tools:
|
||||
for tool in tools:
|
||||
if isinstance(tool, dict) and tool.get("type") == ANTHROPIC_ADVISOR_TOOL_TYPE:
|
||||
beta_values.add(ANTHROPIC_BETA_HEADER_VALUES.ADVISOR_TOOL_2026_03_01.value)
|
||||
break
|
||||
if _messages_carry_output_config(messages):
|
||||
beta_values.add(ANTHROPIC_BETA_HEADER_VALUES.PER_TURN_CONTROL_2026_07_01.value)
|
||||
|
||||
# Check for tool search tools
|
||||
tools = optional_params.get("tools")
|
||||
if tools:
|
||||
anthropic_model_info: Final = AnthropicModelInfo()
|
||||
if anthropic_model_info.is_tool_search_used(tools):
|
||||
# Use provider-specific tool search header
|
||||
tool_search_header: Final = get_tool_search_beta_header(custom_llm_provider)
|
||||
beta_values.add(tool_search_header)
|
||||
tools: Final = optional_params.get("tools")
|
||||
if any(isinstance(tool, dict) and tool.get("type") == ANTHROPIC_ADVISOR_TOOL_TYPE for tool in tools or ()):
|
||||
beta_values.add(ANTHROPIC_BETA_HEADER_VALUES.ADVISOR_TOOL_2026_03_01.value)
|
||||
|
||||
if beta_values:
|
||||
headers["anthropic-beta"] = ",".join(sorted(beta_values))
|
||||
if AnthropicModelInfo().is_tool_search_used(tools):
|
||||
beta_values.add(get_tool_search_beta_header(custom_llm_provider))
|
||||
|
||||
return headers
|
||||
if not beta_values:
|
||||
return headers
|
||||
merged: Final = {key: value for key, value in headers.items() if key.lower() != "anthropic-beta"}
|
||||
merged["anthropic-beta"] = ",".join(sorted(beta_values))
|
||||
return merged
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ class AzureAnthropicMessagesConfig(AnthropicMessagesConfig):
|
|||
headers = self._update_headers_with_anthropic_beta(
|
||||
headers=headers,
|
||||
optional_params=optional_params,
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
return headers, api_base
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ class BedrockClaudePlatformMessagesConfig(BedrockClaudePlatformMixin, AnthropicM
|
|||
headers = self._update_headers_with_anthropic_beta(
|
||||
headers=headers,
|
||||
optional_params=optional_params,
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
return headers, api_base
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ class DeepSeekAnthropicMessagesConfig(AnthropicMessagesConfig):
|
|||
headers=headers,
|
||||
optional_params=optional_params,
|
||||
custom_llm_provider=self.custom_llm_provider or "deepseek",
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
return headers, api_base
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class GithubCopilotAnthropicMessagesConfig(AnthropicMessagesConfig):
|
|||
headers["anthropic-version"] = "2023-06-01"
|
||||
|
||||
headers = self._update_headers_with_anthropic_beta(
|
||||
headers, optional_params, custom_llm_provider="github_copilot"
|
||||
headers, optional_params, custom_llm_provider="github_copilot", messages=messages
|
||||
)
|
||||
|
||||
return headers, dynamic_api_base
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ class OpenAILikeAnthropicMessagesConfig(AnthropicMessagesConfig):
|
|||
merged: Final = self._update_headers_with_anthropic_beta(
|
||||
headers=normalized,
|
||||
optional_params=optional_params,
|
||||
messages=messages,
|
||||
)
|
||||
return merged, api_base
|
||||
|
||||
|
|
|
|||
|
|
@ -746,6 +746,7 @@ class ANTHROPIC_BETA_HEADER_VALUES(str, Enum):
|
|||
ADVANCED_TOOL_USE_2025_11_20 = "advanced-tool-use-2025-11-20"
|
||||
FAST_MODE_2026_02_01 = "fast-mode-2026-02-01"
|
||||
ADVISOR_TOOL_2026_03_01 = "advisor-tool-2026-03-01"
|
||||
PER_TURN_CONTROL_2026_07_01 = "per-turn-control-2026-07-01"
|
||||
|
||||
|
||||
# Tool search beta header constant (for Anthropic direct API and Microsoft Foundry)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
import pytest
|
||||
|
||||
from litellm import anthropic_beta_headers_manager
|
||||
from litellm.anthropic_beta_headers_manager import update_headers_with_filtered_beta
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
|
||||
AnthropicMessagesConfig,
|
||||
)
|
||||
from litellm.llms.openai_like.json_loader import SimpleProviderConfig
|
||||
from litellm.llms.openai_like.messages.transformation import (
|
||||
JSONProviderAnthropicMessagesConfig,
|
||||
)
|
||||
|
||||
PER_TURN_CONTROL = "per-turn-control-2026-07-01"
|
||||
|
||||
CLAUDE_CODE_BETAS = (
|
||||
"claude-code-20250219,interleaved-thinking-2025-05-14,context-management-2025-06-27,"
|
||||
"per-turn-control-2026-07-01,effort-2025-11-24"
|
||||
)
|
||||
|
||||
|
||||
def _claude_code_turn(system_output_config):
|
||||
return [
|
||||
{"role": "user", "content": [{"type": "text", "text": "Hello"}]},
|
||||
{
|
||||
"role": "system",
|
||||
"content": [{"type": "text", "text": "# Environment"}],
|
||||
"output_config": system_output_config,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _betas(headers):
|
||||
return {beta for beta in headers.get("anthropic-beta", "").split(",") if beta}
|
||||
|
||||
|
||||
def _validate(messages, headers=None, optional_params=None):
|
||||
validated, _ = AnthropicMessagesConfig().validate_anthropic_messages_environment(
|
||||
headers=dict(headers or {}),
|
||||
model="claude-fable-5-1",
|
||||
messages=messages,
|
||||
optional_params=dict(optional_params or {"max_tokens": 64000, "output_config": {"effort": "high"}}),
|
||||
litellm_params={},
|
||||
api_key="sk-ant-test",
|
||||
)
|
||||
return validated
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def bundled_beta_allowlist(monkeypatch):
|
||||
monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "True")
|
||||
monkeypatch.setattr(anthropic_beta_headers_manager, "_BETA_HEADERS_CONFIG", None)
|
||||
yield
|
||||
monkeypatch.setattr(anthropic_beta_headers_manager, "_BETA_HEADERS_CONFIG", None)
|
||||
|
||||
|
||||
def test_per_message_output_config_adds_per_turn_control_beta():
|
||||
headers = _validate(_claude_code_turn({"effort": "high"}))
|
||||
|
||||
assert PER_TURN_CONTROL in _betas(headers)
|
||||
|
||||
|
||||
def test_top_level_output_config_alone_does_not_add_per_turn_control_beta():
|
||||
headers = _validate([{"role": "user", "content": "Hello"}])
|
||||
|
||||
assert PER_TURN_CONTROL not in _betas(headers)
|
||||
|
||||
|
||||
def test_string_messages_are_skipped_when_scanning_for_output_config():
|
||||
headers = _validate(["not a message dict", {"role": "user", "content": "Hello"}])
|
||||
|
||||
assert PER_TURN_CONTROL not in _betas(headers)
|
||||
|
||||
|
||||
def test_forwarded_client_betas_survive_alongside_the_added_one():
|
||||
headers = _validate(_claude_code_turn({"effort": "low"}), headers={"anthropic-beta": CLAUDE_CODE_BETAS})
|
||||
|
||||
assert _betas(headers) >= set(CLAUDE_CODE_BETAS.split(","))
|
||||
assert PER_TURN_CONTROL in _betas(headers)
|
||||
|
||||
|
||||
def test_case_variant_client_beta_header_is_merged():
|
||||
headers = _validate(
|
||||
_claude_code_turn({"effort": "low"}), headers={"Anthropic-Beta": "interleaved-thinking-2025-05-14"}
|
||||
)
|
||||
|
||||
assert [key for key in headers if key.lower() == "anthropic-beta"] == ["anthropic-beta"]
|
||||
assert _betas(headers) == {"interleaved-thinking-2025-05-14", PER_TURN_CONTROL}
|
||||
|
||||
|
||||
def test_added_per_turn_control_beta_survives_the_anthropic_allowlist():
|
||||
headers = _validate(_claude_code_turn({"effort": "high"}))
|
||||
|
||||
filtered = update_headers_with_filtered_beta(headers=headers, provider="anthropic")
|
||||
|
||||
assert PER_TURN_CONTROL in _betas(filtered)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ["bedrock", "bedrock_converse", "vertex_ai", "azure_ai", "databricks"])
|
||||
def test_per_turn_control_beta_is_dropped_for_providers_without_it(provider):
|
||||
filtered = update_headers_with_filtered_beta(headers={"anthropic-beta": PER_TURN_CONTROL}, provider=provider)
|
||||
|
||||
assert "anthropic-beta" not in filtered
|
||||
|
||||
|
||||
def test_json_provider_passthrough_adds_per_turn_control_beta():
|
||||
config = JSONProviderAnthropicMessagesConfig(
|
||||
SimpleProviderConfig(
|
||||
"anthropic_like",
|
||||
{
|
||||
"base_url": "https://example.invalid",
|
||||
"api_key_env": "ANTHROPIC_LIKE_API_KEY",
|
||||
"supported_endpoints": ["/v1/messages"],
|
||||
},
|
||||
)
|
||||
)
|
||||
headers, _ = config.validate_anthropic_messages_environment(
|
||||
headers={},
|
||||
model="claude-fable-5-1",
|
||||
messages=_claude_code_turn({"effort": "medium"}),
|
||||
optional_params={"max_tokens": 1024},
|
||||
litellm_params={},
|
||||
api_key="test",
|
||||
)
|
||||
|
||||
assert PER_TURN_CONTROL in _betas(headers)
|
||||
Loading…
Add table
Reference in a new issue