mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-14 02:22:54 +00:00
fix(anthropic,bedrock,vertex): forward output_config.effort + 400 on garbage reasoning_effort
Follow-up bugs surfaced by the QA sweep on PR #27039 (https://github.com/BerriAI/litellm/pull/27039#issuecomment-4363363610). 1. Stop stripping output_config.effort on Bedrock + Vertex adaptive routes. - Vertex AI Claude 4.6/4.7 accepts output_config.effort on rawPredict (verified end-to-end against us-east5 / global). The strip helper now no-ops for effort. - Bedrock Converse routes output_config into additionalModelRequestFields for anthropic base models so the requested adaptive tier (low/medium/ high/xhigh/max) actually reaches the wire instead of all collapsing to identical thinking. - Bedrock Invoke chat transformation (AmazonAnthropicClaudeConfig) stops popping output_config from the post-AnthropicConfig request body. - Bedrock Invoke /v1/messages allowlist (BedrockInvokeAnthropicMessagesRequest) now lists output_config so the runtime allowlist filter forwards it. 2. Validate effort across Bedrock Converse so 'disabled' / 'invalid' / '' / unsupported tiers (xhigh/max on Sonnet 4.6 or budget-mode 4.5 models) surface as a clean 400 BadRequestError instead of 500. 3. ValueError -> BadRequestError throughout (AnthropicConfig.map_openai_params, _apply_output_config, AmazonConverseConfig._handle_reasoning_effort_parameter). Empty-string effort is now rejected (was silently passing the 'if effort and ...' short-circuit). 4. Floor reasoning_effort='minimal' at the Anthropic provider minimum (1024 budget_tokens) via new ANTHROPIC_MIN_THINKING_BUDGET_TOKENS so it's a usable tier on direct Anthropic / Azure AI Anthropic / Vertex AI Anthropic / Bedrock Invoke (all of which 400 below 1024). 5. model_prices: dedupe duplicate supports_max_reasoning_effort key on claude-opus-4-7 / claude-opus-4-7-20260416. Adds regression tests across all five affected paths; existing tests asserting the silent-strip behavior were updated to reflect the new pass-through and clean 400 surfaces. Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
This commit is contained in:
co-authored by
Mateo Wang
parent
c94a8d6514
commit
d03401f34d
@@ -399,6 +399,16 @@ BEDROCK_MAX_POLICY_SIZE = int(os.getenv("BEDROCK_MAX_POLICY_SIZE", 75))
|
||||
BEDROCK_MIN_THINKING_BUDGET_TOKENS = int(
|
||||
os.getenv("BEDROCK_MIN_THINKING_BUDGET_TOKENS", 1024)
|
||||
)
|
||||
# Anthropic's Messages API rejects ``thinking.budget_tokens < 1024`` with a
|
||||
# 400. ``reasoning_effort='minimal'`` historically mapped to 128 (the global
|
||||
# default) which always 400'd against direct Anthropic, Azure AI Anthropic,
|
||||
# Vertex AI Anthropic, and Bedrock Invoke. Use the provider minimum so
|
||||
# ``minimal`` is a usable tier on all Anthropic-backed routes; Bedrock
|
||||
# Converse already clamped to 1024 server-side, so this just unifies the
|
||||
# behavior.
|
||||
ANTHROPIC_MIN_THINKING_BUDGET_TOKENS = int(
|
||||
os.getenv("ANTHROPIC_MIN_THINKING_BUDGET_TOKENS", 1024)
|
||||
)
|
||||
REPLICATE_POLLING_DELAY_SECONDS = float(
|
||||
os.getenv("REPLICATE_POLLING_DELAY_SECONDS", 0.5)
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.constants import (
|
||||
ANTHROPIC_MIN_THINKING_BUDGET_TOKENS,
|
||||
ANTHROPIC_WEB_SEARCH_TOOL_MAX_USES,
|
||||
DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS,
|
||||
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
|
||||
@@ -819,9 +820,16 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
||||
budget_tokens=DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
|
||||
)
|
||||
elif reasoning_effort == "minimal":
|
||||
# Anthropic Messages API rejects ``budget_tokens < 1024`` with a
|
||||
# 400. Floor at the provider minimum so ``minimal`` is a usable
|
||||
# tier on Anthropic / Azure AI Anthropic / Vertex AI Anthropic /
|
||||
# Bedrock Invoke. Bedrock Converse already clamps server-side.
|
||||
return AnthropicThinkingParam(
|
||||
type="enabled",
|
||||
budget_tokens=DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET,
|
||||
budget_tokens=max(
|
||||
DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET,
|
||||
ANTHROPIC_MIN_THINKING_BUDGET_TOKENS,
|
||||
),
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unmapped reasoning effort: {reasoning_effort}")
|
||||
@@ -1088,9 +1096,21 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
||||
elif param == "thinking":
|
||||
optional_params["thinking"] = value
|
||||
elif param == "reasoning_effort" and isinstance(value, str):
|
||||
mapped_thinking = AnthropicConfig._map_reasoning_effort(
|
||||
reasoning_effort=value, model=model
|
||||
)
|
||||
# Wrap the ``ValueError`` ``_map_reasoning_effort`` raises on
|
||||
# unmapped efforts (``disabled`` / ``invalid`` / ``""`` /
|
||||
# ``xhigh``/``max`` on budget-mode Claude 4.5) into a clean
|
||||
# 400 ``BadRequestError`` instead of letting it surface as
|
||||
# 500.
|
||||
try:
|
||||
mapped_thinking = AnthropicConfig._map_reasoning_effort(
|
||||
reasoning_effort=value, model=model
|
||||
)
|
||||
except ValueError as e:
|
||||
raise litellm.exceptions.BadRequestError(
|
||||
message=str(e),
|
||||
model=model,
|
||||
llm_provider=self.custom_llm_provider or "anthropic",
|
||||
)
|
||||
if mapped_thinking is None:
|
||||
optional_params.pop("thinking", None)
|
||||
optional_params.pop("output_config", None)
|
||||
@@ -1526,18 +1546,31 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
||||
def _apply_output_config(
|
||||
self, data: dict, model: str, optional_params: dict
|
||||
) -> None:
|
||||
"""Validate and apply output_config to the request data."""
|
||||
"""Validate and apply output_config to the request data.
|
||||
|
||||
Validation errors raise ``BadRequestError`` (clean 400) so callers
|
||||
passing ``effort="disabled"`` / ``effort=""`` / unsupported tiers
|
||||
for the model see a client-side error rather than a 500.
|
||||
"""
|
||||
if "output_config" not in optional_params:
|
||||
return
|
||||
output_config = optional_params.get("output_config")
|
||||
if not output_config or not isinstance(output_config, dict):
|
||||
return
|
||||
effort = output_config.get("effort")
|
||||
# ``effort=""`` (empty string) and unmapped strings should be treated
|
||||
# as invalid, not silently passed through. We use ``effort is not None``
|
||||
# here so empty string fails the membership check below. (The legacy
|
||||
# ``if effort and ...`` short-circuit silently accepted ``""``.)
|
||||
valid_efforts = ["high", "medium", "low", "xhigh", "max"]
|
||||
if effort and effort not in valid_efforts:
|
||||
raise ValueError(
|
||||
f"Invalid effort value: {effort}. Must be one of: "
|
||||
f"'high', 'medium', 'low', 'xhigh', 'max'"
|
||||
if effort is not None and effort not in valid_efforts:
|
||||
raise litellm.exceptions.BadRequestError(
|
||||
message=(
|
||||
f"Invalid effort value: {effort!r}. Must be one of: "
|
||||
f"'high', 'medium', 'low', 'xhigh', 'max'"
|
||||
),
|
||||
model=model,
|
||||
llm_provider=self.custom_llm_provider or "anthropic",
|
||||
)
|
||||
# ``max`` is for Opus 4.6+ output effort (not Sonnet 4.6, not Opus 4.5).
|
||||
# Accept known Opus 4.6/4.7 id patterns and/or ``supports_max_reasoning_effort``
|
||||
@@ -1547,14 +1580,24 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
||||
or self._is_opus_4_7_model(model)
|
||||
or self._supports_effort_level(model, "max")
|
||||
):
|
||||
raise ValueError(
|
||||
f"effort='max' is not supported by this model. Got model: {model}"
|
||||
raise litellm.exceptions.BadRequestError(
|
||||
message=(
|
||||
f"effort='max' is not supported by this model. "
|
||||
f"Got model: {model}"
|
||||
),
|
||||
model=model,
|
||||
llm_provider=self.custom_llm_provider or "anthropic",
|
||||
)
|
||||
# ``xhigh`` is data-driven via ``supports_xhigh_reasoning_effort`` so
|
||||
# enabling it for a new model is a pure model-map change.
|
||||
if effort == "xhigh" and not self._supports_effort_level(model, "xhigh"):
|
||||
raise ValueError(
|
||||
f"effort='xhigh' is not supported by this model. Got model: {model}"
|
||||
raise litellm.exceptions.BadRequestError(
|
||||
message=(
|
||||
f"effort='xhigh' is not supported by this model. "
|
||||
f"Got model: {model}"
|
||||
),
|
||||
model=model,
|
||||
llm_provider=self.custom_llm_provider or "anthropic",
|
||||
)
|
||||
data["output_config"] = output_config
|
||||
|
||||
|
||||
@@ -415,6 +415,15 @@ class AmazonConverseConfig(BaseConfig):
|
||||
- Nova 2 models: Transform to reasoningConfig structure
|
||||
- Other models (Anthropic, etc.): Convert to thinking parameter
|
||||
|
||||
For Claude 4.6 / 4.7 (adaptive thinking) the tier is carried via
|
||||
``output_config.effort`` rather than ``thinking.budget_tokens``. We
|
||||
validate the effort with the same rules ``AnthropicConfig._apply_output_config``
|
||||
uses (low/medium/high/xhigh/max + per-model gating) and stage the
|
||||
validated dict on ``optional_params["output_config"]`` so it rides
|
||||
along to ``additionalModelRequestFields`` on the Anthropic-on-Bedrock
|
||||
wire path. Without this the silent strip in ``_prepare_request_params``
|
||||
collapsed every adaptive tier to identical behavior.
|
||||
|
||||
Args:
|
||||
model: The model identifier
|
||||
reasoning_effort: The reasoning effort value
|
||||
@@ -448,14 +457,115 @@ class AmazonConverseConfig(BaseConfig):
|
||||
)
|
||||
optional_params.update(reasoning_config)
|
||||
else:
|
||||
# Anthropic and other models: convert to thinking parameter
|
||||
mapped_thinking = AnthropicConfig._map_reasoning_effort(
|
||||
reasoning_effort=reasoning_effort, model=model
|
||||
)
|
||||
# Anthropic and other models: convert to thinking parameter.
|
||||
# Wrap the ``ValueError`` ``_map_reasoning_effort`` raises on
|
||||
# unmapped efforts (``disabled`` / ``invalid`` / ``""`` /
|
||||
# ``xhigh``/``max`` on budget-mode Claude 4.5) into a clean 400
|
||||
# ``BadRequestError`` instead of letting it surface as 500.
|
||||
try:
|
||||
mapped_thinking = AnthropicConfig._map_reasoning_effort(
|
||||
reasoning_effort=reasoning_effort, model=model
|
||||
)
|
||||
except ValueError as e:
|
||||
raise litellm.exceptions.BadRequestError(
|
||||
message=str(e),
|
||||
model=model,
|
||||
llm_provider="bedrock_converse",
|
||||
)
|
||||
if mapped_thinking is None:
|
||||
optional_params.pop("thinking", None)
|
||||
optional_params.pop("output_config", None)
|
||||
else:
|
||||
optional_params["thinking"] = mapped_thinking
|
||||
# Adaptive-thinking models (Claude 4.6 / 4.7) take the tier
|
||||
# via output_config.effort. Mirror the mapping used by
|
||||
# AnthropicConfig.map_openai_params and apply the same
|
||||
# validation rules so unmapped/garbage efforts surface as a
|
||||
# 400 instead of being silently flattened on the wire.
|
||||
if AnthropicConfig._is_claude_4_6_model(
|
||||
model
|
||||
) or AnthropicConfig._is_claude_4_7_model(model):
|
||||
effort_map = {
|
||||
"low": "low",
|
||||
"minimal": "low",
|
||||
"medium": "medium",
|
||||
"high": "high",
|
||||
"xhigh": "xhigh",
|
||||
"max": "max",
|
||||
}
|
||||
mapped_effort = effort_map.get(reasoning_effort, reasoning_effort)
|
||||
self._validate_anthropic_adaptive_effort(
|
||||
model=model, effort=mapped_effort
|
||||
)
|
||||
optional_params["output_config"] = {"effort": mapped_effort}
|
||||
|
||||
@staticmethod
|
||||
def _supports_effort_level_on_bedrock(model: str, level: str) -> bool:
|
||||
"""Look up ``supports_{level}_reasoning_effort`` for a Bedrock-routed
|
||||
model id directly in ``litellm.model_cost`` so the bedrock provider
|
||||
prefix is irrelevant to the lookup. ``AnthropicConfig._supports_effort_level``
|
||||
hard-codes ``custom_llm_provider="anthropic"`` and returns False for
|
||||
the same effort level on a Bedrock model id.
|
||||
"""
|
||||
try:
|
||||
base_model = BedrockModelInfo.get_base_model(model)
|
||||
for key in (model, base_model, f"bedrock/{base_model}"):
|
||||
if key and key in litellm.model_cost:
|
||||
if (
|
||||
litellm.model_cost[key].get(
|
||||
f"supports_{level}_reasoning_effort"
|
||||
)
|
||||
is True
|
||||
):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _validate_anthropic_adaptive_effort(model: str, effort: str) -> None:
|
||||
"""Validate ``output_config.effort`` for adaptive-thinking Claude 4.6/4.7
|
||||
on Bedrock. Raises ``BadRequestError`` (clean 400) instead of letting
|
||||
a downstream ``ValueError`` surface as 500.
|
||||
"""
|
||||
valid_efforts = {"high", "medium", "low", "xhigh", "max"}
|
||||
if effort not in valid_efforts:
|
||||
raise litellm.exceptions.BadRequestError(
|
||||
message=(
|
||||
f"Invalid reasoning_effort/output_config.effort value: "
|
||||
f"{effort!r}. Must be one of: 'low', 'medium', 'high', "
|
||||
f"'xhigh', 'max', or 'none'."
|
||||
),
|
||||
model=model,
|
||||
llm_provider="bedrock_converse",
|
||||
)
|
||||
if effort == "max" and not (
|
||||
AnthropicConfig._is_opus_4_6_model(model)
|
||||
or AnthropicConfig._is_opus_4_7_model(model)
|
||||
or AmazonConverseConfig._supports_effort_level_on_bedrock(model, "max")
|
||||
):
|
||||
raise litellm.exceptions.BadRequestError(
|
||||
message=(
|
||||
f"effort='max' is not supported by this model. "
|
||||
f"Got model: {model}"
|
||||
),
|
||||
model=model,
|
||||
llm_provider="bedrock_converse",
|
||||
)
|
||||
if (
|
||||
effort == "xhigh"
|
||||
and not AmazonConverseConfig._supports_effort_level_on_bedrock(
|
||||
model, "xhigh"
|
||||
)
|
||||
):
|
||||
raise litellm.exceptions.BadRequestError(
|
||||
message=(
|
||||
f"effort='xhigh' is not supported by this model. "
|
||||
f"Got model: {model}"
|
||||
),
|
||||
model=model,
|
||||
llm_provider="bedrock_converse",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _clamp_thinking_budget_tokens(optional_params: dict) -> None:
|
||||
@@ -1196,9 +1306,15 @@ class AmazonConverseConfig(BaseConfig):
|
||||
+ supported_config_params
|
||||
)
|
||||
inference_params.pop("json_mode", None) # used for handling json_schema
|
||||
# Anthropic-only key. Bedrock expects `outputConfig` (camelCase) and
|
||||
# will reject `output_config` if it leaks through pass-through routes.
|
||||
inference_params.pop("output_config", None)
|
||||
|
||||
# Anthropic-only ``output_config`` (snake_case) is the adaptive-
|
||||
# thinking effort payload (e.g. ``{"effort": "max"}``) for Claude
|
||||
# 4.6/4.7. On Bedrock Converse it must ride along inside
|
||||
# ``additionalModelRequestFields`` so the model actually sees the
|
||||
# tier; stripping it (the prior behavior) silently flattened every
|
||||
# adaptive tier to identical thinking. Only the Bedrock-native
|
||||
# ``outputConfig`` (camelCase) goes at the top level.
|
||||
anthropic_output_config = inference_params.pop("output_config", None)
|
||||
|
||||
# Extract requestMetadata before processing other parameters
|
||||
request_metadata = inference_params.pop("requestMetadata", None)
|
||||
@@ -1208,9 +1324,6 @@ class AmazonConverseConfig(BaseConfig):
|
||||
output_config: Optional[OutputConfigBlock] = inference_params.pop(
|
||||
"outputConfig", None
|
||||
)
|
||||
inference_params.pop(
|
||||
"output_config", None
|
||||
) # Bedrock Converse doesn't support it
|
||||
|
||||
# keep supported params in 'inference_params', and set all model-specific params in 'additional_request_params'
|
||||
additional_request_params = {
|
||||
@@ -1253,6 +1366,20 @@ class AmazonConverseConfig(BaseConfig):
|
||||
additional_request_params
|
||||
)
|
||||
|
||||
# Re-attach the Anthropic ``output_config`` (e.g. adaptive thinking
|
||||
# ``{"effort": "max"}``) onto additional_request_params for Anthropic
|
||||
# Bedrock models so the wire request carries the requested tier. Other
|
||||
# model families (Nova, GPT-OSS, ...) don't accept it; drop it for them.
|
||||
if anthropic_output_config is not None and isinstance(
|
||||
anthropic_output_config, dict
|
||||
):
|
||||
base_model = BedrockModelInfo.get_base_model(model)
|
||||
if base_model.startswith("anthropic"):
|
||||
effort = anthropic_output_config.get("effort")
|
||||
if effort is not None:
|
||||
self._validate_anthropic_adaptive_effort(model=model, effort=effort)
|
||||
additional_request_params["output_config"] = anthropic_output_config
|
||||
|
||||
return (
|
||||
inference_params,
|
||||
additional_request_params,
|
||||
|
||||
+5
-1
@@ -169,7 +169,11 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
||||
anthropic_request.pop("model", None)
|
||||
anthropic_request.pop("stream", None)
|
||||
anthropic_request.pop("output_format", None)
|
||||
anthropic_request.pop("output_config", None)
|
||||
# ``output_config`` (e.g. ``{"effort": "max"}``) is the adaptive-thinking
|
||||
# tier payload for Claude 4.6 / 4.7. Bedrock Invoke accepts it for
|
||||
# those models — stripping it (the prior behavior) silently flattened
|
||||
# every adaptive tier on this route. Forward it; if the model rejects
|
||||
# it the surfaced error is correct, vs. swallowing the user's knob.
|
||||
if "anthropic_version" not in anthropic_request:
|
||||
anthropic_request["anthropic_version"] = self.anthropic_version
|
||||
|
||||
|
||||
+5
-4
@@ -159,10 +159,11 @@ class VertexAIPartnerModelsAnthropicMessagesConfig(AnthropicMessagesConfig, Vert
|
||||
"model", None
|
||||
) # do not pass model in request body to vertex ai
|
||||
|
||||
# Vertex AI Claude accepts ``output_config.format`` (structured outputs)
|
||||
# and ``output_format``, but rejects ``output_config.effort`` with 400
|
||||
# "Extra inputs are not permitted". Sanitize in place so the supported
|
||||
# bits flow through.
|
||||
# Vertex AI Claude accepts ``output_config.format`` (structured outputs),
|
||||
# ``output_format``, and ``output_config.effort`` (adaptive-thinking
|
||||
# tier on Claude 4.6 / 4.7, verified end-to-end). The shared sanitize
|
||||
# helper now no-ops for ``effort`` and remains the single hook for any
|
||||
# future Vertex-only key drift.
|
||||
sanitize_vertex_anthropic_output_params(anthropic_messages_request)
|
||||
|
||||
return anthropic_messages_request
|
||||
|
||||
@@ -11,11 +11,12 @@ keeps the parent module's import surface narrow.
|
||||
"""
|
||||
|
||||
# Keys inside ``output_config`` that Vertex AI Claude does not accept.
|
||||
# Today only ``effort`` triggers "Extra inputs are not permitted"; add new
|
||||
# entries here as Vertex parity drifts. Keep this list narrow — anything
|
||||
# Vertex DOES accept (e.g. ``format`` for structured outputs) must be
|
||||
# preserved so callers can rely on Anthropic-native features.
|
||||
VERTEX_UNSUPPORTED_OUTPUT_CONFIG_KEYS: frozenset = frozenset({"effort"})
|
||||
# Vertex now accepts ``output_config.effort`` for the adaptive-thinking
|
||||
# Claude 4.6 / 4.7 models on direct ``:rawPredict`` (verified end-to-end
|
||||
# against ``us-east5`` for ``opus-4-6`` and ``global`` for ``opus-4-7``).
|
||||
# Keep this set narrow and only add a key here once a 400 "Extra inputs are
|
||||
# not permitted" is reproducible against the live Vertex endpoint.
|
||||
VERTEX_UNSUPPORTED_OUTPUT_CONFIG_KEYS: frozenset = frozenset()
|
||||
|
||||
|
||||
def sanitize_vertex_anthropic_output_params(data: dict) -> None:
|
||||
|
||||
@@ -106,11 +106,10 @@ class VertexAIAnthropicConfig(AnthropicConfig):
|
||||
|
||||
data.pop("model", None) # vertex anthropic doesn't accept 'model' parameter
|
||||
|
||||
# Vertex AI Claude accepts ``output_config.format`` (structured outputs /
|
||||
# JSON Schema) but NOT ``output_config.effort`` — sending ``effort`` to
|
||||
# Vertex returns 400 "Extra inputs are not permitted". Sanitize in place:
|
||||
# forward the structured-output bits, drop the unsupported keys.
|
||||
# Same treatment for the legacy top-level ``output_format`` field.
|
||||
# Sanitize ``output_config`` / ``output_format`` for Vertex parity.
|
||||
# Vertex now accepts ``output_config.effort`` for adaptive-thinking Claude
|
||||
# 4.6 / 4.7 models, so the helper is a no-op for ``effort``; it remains
|
||||
# the single hook for future Vertex-only sanitization.
|
||||
sanitize_vertex_anthropic_output_params(data)
|
||||
|
||||
tools = optional_params.get("tools")
|
||||
|
||||
@@ -1041,3 +1041,12 @@ class BedrockInvokeAnthropicMessagesRequest(TypedDict, total=False):
|
||||
# `metadata` is part of the common Anthropic Messages API shape.
|
||||
thinking: dict
|
||||
metadata: dict
|
||||
|
||||
# ``output_config`` is the adaptive-thinking effort payload for
|
||||
# Claude 4.6 / 4.7 (e.g. ``{"effort": "max"}``). Bedrock Invoke
|
||||
# accepts it for these models when ``thinking={"type": "adaptive"}``.
|
||||
# Without this field in the allowlist, the runtime filter in
|
||||
# ``AmazonAnthropicClaudeMessagesConfig.transform_anthropic_messages_request``
|
||||
# silently drops it and every adaptive tier collapses to identical
|
||||
# behavior on /v1/messages.
|
||||
output_config: dict
|
||||
|
||||
@@ -9491,7 +9491,6 @@
|
||||
"us": 1.1,
|
||||
"fast": 6.0
|
||||
},
|
||||
"supports_max_reasoning_effort": true,
|
||||
"supports_minimal_reasoning_effort": true
|
||||
},
|
||||
"claude-opus-4-7-20260416": {
|
||||
@@ -9526,7 +9525,6 @@
|
||||
"us": 1.1,
|
||||
"fast": 6.0
|
||||
},
|
||||
"supports_max_reasoning_effort": true,
|
||||
"supports_minimal_reasoning_effort": true
|
||||
},
|
||||
"claude-sonnet-4-20250514": {
|
||||
|
||||
@@ -8,6 +8,7 @@ sys.path.insert(
|
||||
) # Adds the parent directory to the system path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import litellm
|
||||
from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
|
||||
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
|
||||
@@ -1631,8 +1632,10 @@ def test_effort_validation():
|
||||
)
|
||||
assert result["output_config"]["effort"] == effort
|
||||
|
||||
# Invalid value should raise error
|
||||
with pytest.raises(ValueError, match="Invalid effort value"):
|
||||
# Invalid value should raise BadRequestError (clean 400, not a 500).
|
||||
with pytest.raises(
|
||||
litellm.exceptions.BadRequestError, match="Invalid effort value"
|
||||
):
|
||||
optional_params = {"output_config": {"effort": "invalid"}}
|
||||
config.transform_request(
|
||||
model="claude-opus-4-5-20251101",
|
||||
@@ -1682,12 +1685,18 @@ def test_effort_validation_with_opus_46():
|
||||
|
||||
|
||||
def test_max_effort_rejected_for_opus_45():
|
||||
"""Test that effort='max' is rejected when using Claude Opus 4.5."""
|
||||
"""Test that effort='max' is rejected when using Claude Opus 4.5.
|
||||
|
||||
Surfaces as a clean 400 BadRequestError, not a 500 ValueError.
|
||||
"""
|
||||
config = AnthropicConfig()
|
||||
|
||||
messages = [{"role": "user", "content": "Test"}]
|
||||
|
||||
with pytest.raises(ValueError, match="effort='max' is not supported by this model"):
|
||||
with pytest.raises(
|
||||
litellm.exceptions.BadRequestError,
|
||||
match="effort='max' is not supported by this model",
|
||||
):
|
||||
optional_params = {"output_config": {"effort": "max"}}
|
||||
config.transform_request(
|
||||
model="claude-opus-4-5-20251101",
|
||||
@@ -2153,12 +2162,14 @@ def test_reasoning_effort_maps_to_budget_thinking_for_non_opus_4_6():
|
||||
"""
|
||||
config = AnthropicConfig()
|
||||
|
||||
# Test with Claude Sonnet 4.5 (non-Opus 4.6 model)
|
||||
# Test with Claude Sonnet 4.5 (non-Opus 4.6 model).
|
||||
# ``minimal`` floors at the Anthropic provider minimum (1024) because
|
||||
# Anthropic / Azure / Vertex / Bedrock Invoke 400 below that.
|
||||
test_cases = [
|
||||
("low", 1024), # DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET
|
||||
("medium", 2048), # DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET
|
||||
("high", 4096), # DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET
|
||||
("minimal", 128), # DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET
|
||||
("minimal", 1024), # ANTHROPIC_MIN_THINKING_BUDGET_TOKENS (provider floor)
|
||||
]
|
||||
|
||||
for effort, expected_budget in test_cases:
|
||||
@@ -2245,11 +2256,17 @@ def test_reasoning_effort_does_not_set_output_config_for_older_models():
|
||||
|
||||
|
||||
def test_max_effort_rejected_for_sonnet_46():
|
||||
"""Test that effort='max' is rejected for Sonnet 4.6 (Opus-only effort level)."""
|
||||
"""Test that effort='max' is rejected for Sonnet 4.6 (Opus-only effort level).
|
||||
|
||||
Surfaces as a clean 400 BadRequestError, not a 500 ValueError.
|
||||
"""
|
||||
config = AnthropicConfig()
|
||||
messages = [{"role": "user", "content": "Test"}]
|
||||
|
||||
with pytest.raises(ValueError, match="effort='max' is not supported by this model"):
|
||||
with pytest.raises(
|
||||
litellm.exceptions.BadRequestError,
|
||||
match="effort='max' is not supported by this model",
|
||||
):
|
||||
config.transform_request(
|
||||
model="claude-sonnet-4-6-20260219",
|
||||
messages=messages,
|
||||
@@ -2335,6 +2352,81 @@ def test_reasoning_effort_none_omits_thinking_and_output_config(model):
|
||||
assert "output_config" not in result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"effort",
|
||||
["disabled", "invalid", ""],
|
||||
)
|
||||
def test_reasoning_effort_garbage_raises_bad_request(effort):
|
||||
"""Unmapped / garbage / empty-string reasoning_effort surfaces as a clean
|
||||
400 ``BadRequestError`` instead of letting ``ValueError`` propagate as 500.
|
||||
"""
|
||||
config = AnthropicConfig()
|
||||
|
||||
with pytest.raises(litellm.exceptions.BadRequestError):
|
||||
config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": effort},
|
||||
optional_params={},
|
||||
model="claude-sonnet-4-5-20250929",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"effort",
|
||||
["xhigh", "max"],
|
||||
)
|
||||
def test_reasoning_effort_unsupported_tier_on_budget_model_raises_bad_request(
|
||||
effort,
|
||||
):
|
||||
"""``xhigh`` / ``max`` aren't defined for budget-mode (4.5) Claude models;
|
||||
surface as a clean 400 instead of 500.
|
||||
"""
|
||||
config = AnthropicConfig()
|
||||
|
||||
with pytest.raises(litellm.exceptions.BadRequestError):
|
||||
config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": effort},
|
||||
optional_params={},
|
||||
model="claude-sonnet-4-5-20250929",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
def test_output_config_effort_empty_string_raises_bad_request():
|
||||
"""``output_config={"effort": ""}`` must be rejected with a 400 — the
|
||||
legacy ``if effort and ...`` short-circuit silently let it pass
|
||||
through (verified end-to-end on the QA sweep for PR #27039).
|
||||
"""
|
||||
config = AnthropicConfig()
|
||||
|
||||
with pytest.raises(litellm.exceptions.BadRequestError, match="Invalid effort"):
|
||||
config.transform_request(
|
||||
model="claude-opus-4-7",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
optional_params={"output_config": {"effort": ""}, "max_tokens": 32},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
|
||||
def test_reasoning_effort_minimal_floors_at_anthropic_provider_minimum():
|
||||
"""Anthropic Messages API rejects ``budget_tokens < 1024``. ``minimal``
|
||||
must floor at the provider minimum so it's a usable tier on direct
|
||||
Anthropic / Azure AI Anthropic / Vertex AI Anthropic / Bedrock Invoke.
|
||||
"""
|
||||
config = AnthropicConfig()
|
||||
|
||||
result = config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": "minimal"},
|
||||
optional_params={},
|
||||
model="claude-sonnet-4-5-20250929",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result["thinking"]["type"] == "enabled"
|
||||
assert result["thinking"]["budget_tokens"] >= 1024
|
||||
|
||||
|
||||
def test_effort_beta_header_still_injected_for_older_models():
|
||||
"""
|
||||
Test that is_effort_used still returns True for pre-4.6 models
|
||||
|
||||
+12
-11
@@ -406,36 +406,37 @@ def test_opus_4_5_model_detection():
|
||||
# f"computer-use beta should be kept, got: {anthropic_beta}"
|
||||
|
||||
|
||||
def test_output_config_removed_from_bedrock_chat_invoke_request():
|
||||
def test_output_config_forwarded_for_bedrock_chat_invoke_request():
|
||||
"""
|
||||
Test that output_config parameter is stripped from Bedrock Chat Invoke requests.
|
||||
Bedrock Invoke (chat/completions route) must forward
|
||||
``output_config`` for Anthropic adaptive-thinking models. The earlier
|
||||
behavior stripped it unconditionally, which silently flattened every
|
||||
adaptive tier (``low``/``medium``/``high``/``xhigh``/``max``) to identical
|
||||
behavior on the wire.
|
||||
|
||||
Bedrock Invoke API doesn't support the output_config parameter (Anthropic-only).
|
||||
Ensures the chat/invoke path mirrors the messages/invoke path fix.
|
||||
|
||||
Fixes: https://github.com/BerriAI/litellm/issues/22797
|
||||
The wire QA at https://github.com/BerriAI/litellm/pull/27039 showed
|
||||
``thinking.type: adaptive`` was forwarded but ``output_config.effort``
|
||||
was always missing, even though direct curls to Anthropic's Bedrock
|
||||
Invoke endpoint accept it.
|
||||
"""
|
||||
config = AmazonAnthropicClaudeConfig()
|
||||
|
||||
messages = [{"role": "user", "content": "test"}]
|
||||
|
||||
# Inject output_config into optional_params (simulates Anthropic SDK forwarding it)
|
||||
optional_params = {
|
||||
"max_tokens": 100,
|
||||
"output_config": {"effort": "high"},
|
||||
}
|
||||
|
||||
result = config.transform_request(
|
||||
model="anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
model="anthropic.claude-opus-4-7",
|
||||
messages=messages,
|
||||
optional_params=optional_params,
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert (
|
||||
"output_config" not in result
|
||||
), f"output_config should be stripped for Bedrock Chat Invoke, got keys: {list(result.keys())}"
|
||||
assert result.get("output_config") == {"effort": "high"}
|
||||
# Verify normal params survive
|
||||
assert result["max_tokens"] == 100
|
||||
|
||||
|
||||
@@ -310,6 +310,111 @@ def test_reasoning_effort_none_omits_thinking_for_anthropic_converse(model):
|
||||
assert "thinking" not in optional_params
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model,effort,expected_effort",
|
||||
[
|
||||
("bedrock/converse/us.anthropic.claude-opus-4-7", "low", "low"),
|
||||
("bedrock/converse/us.anthropic.claude-opus-4-7", "medium", "medium"),
|
||||
("bedrock/converse/us.anthropic.claude-opus-4-7", "high", "high"),
|
||||
("bedrock/converse/us.anthropic.claude-opus-4-7", "xhigh", "xhigh"),
|
||||
("bedrock/converse/us.anthropic.claude-opus-4-7", "max", "max"),
|
||||
("bedrock/converse/us.anthropic.claude-opus-4-6-v1", "max", "max"),
|
||||
("bedrock/converse/us.anthropic.claude-sonnet-4-6", "high", "high"),
|
||||
("bedrock/converse/us.anthropic.claude-sonnet-4-6", "minimal", "low"),
|
||||
],
|
||||
)
|
||||
def test_reasoning_effort_sets_output_config_for_adaptive_models_converse(
|
||||
model, effort, expected_effort
|
||||
):
|
||||
"""Adaptive-thinking Claude 4.6 / 4.7 on Bedrock Converse must carry the
|
||||
requested tier via ``output_config.effort``. The prior strip silently
|
||||
flattened every adaptive tier on the wire (verified in the PR #27039
|
||||
QA sweep)."""
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
optional_params = config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": effort},
|
||||
optional_params={},
|
||||
model=model,
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert optional_params["thinking"]["type"] == "adaptive"
|
||||
assert optional_params["output_config"] == {"effort": expected_effort}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model",
|
||||
[
|
||||
"bedrock/converse/us.anthropic.claude-opus-4-7",
|
||||
"bedrock/converse/us.anthropic.claude-opus-4-6-v1",
|
||||
"bedrock/converse/us.anthropic.claude-sonnet-4-6",
|
||||
],
|
||||
)
|
||||
def test_output_config_effort_forwarded_into_additional_request_fields(model):
|
||||
"""``output_config`` must ride along inside ``additionalModelRequestFields``
|
||||
so the Anthropic-on-Bedrock wire request actually carries the effort
|
||||
tier. The prior ``inference_params.pop("output_config")`` dropped it
|
||||
on the floor for every adaptive tier."""
|
||||
config = AmazonConverseConfig()
|
||||
messages = [{"role": "user", "content": "hi"}]
|
||||
|
||||
result = config._transform_request(
|
||||
model=model,
|
||||
messages=messages,
|
||||
optional_params={
|
||||
"maxTokens": 256,
|
||||
"thinking": {"type": "adaptive"},
|
||||
"output_config": {"effort": "high"},
|
||||
},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
additional = result.get("additionalModelRequestFields", {})
|
||||
assert additional.get("output_config") == {"effort": "high"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"effort",
|
||||
["disabled", "invalid", ""],
|
||||
)
|
||||
def test_reasoning_effort_garbage_raises_bad_request_converse(effort):
|
||||
"""Garbage / empty-string reasoning_effort on Bedrock Converse Anthropic
|
||||
must surface as a clean 400 ``BadRequestError`` instead of 500. The
|
||||
earlier ``ValueError`` from ``_map_reasoning_effort`` propagated up as
|
||||
a generic 500 in the proxy and ate the request."""
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
with pytest.raises(litellm.exceptions.BadRequestError):
|
||||
config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": effort},
|
||||
optional_params={},
|
||||
model="bedrock/converse/us.anthropic.claude-opus-4-7",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
def test_output_config_effort_unsupported_max_on_sonnet_46_raises_bad_request():
|
||||
"""``effort='max'`` is Opus-only. On Sonnet 4.6 the explicit-output_config
|
||||
path must surface a 400 (matching the chat-completion validation), not
|
||||
silently forward an unsupported tier to Bedrock."""
|
||||
config = AmazonConverseConfig()
|
||||
messages = [{"role": "user", "content": "hi"}]
|
||||
|
||||
with pytest.raises(litellm.exceptions.BadRequestError):
|
||||
config._transform_request(
|
||||
model="bedrock/converse/us.anthropic.claude-sonnet-4-6",
|
||||
messages=messages,
|
||||
optional_params={
|
||||
"maxTokens": 256,
|
||||
"output_config": {"effort": "max"},
|
||||
},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
|
||||
def test_get_supported_openai_params():
|
||||
config = AmazonConverseConfig()
|
||||
supported_params = config.get_supported_openai_params(
|
||||
|
||||
+36
-18
@@ -592,12 +592,16 @@ def test_remove_scope_from_cache_control():
|
||||
assert request["messages"][0]["content"][0]["cache_control"]["type"] == "ephemeral"
|
||||
|
||||
|
||||
def test_bedrock_messages_strips_output_config():
|
||||
def test_bedrock_messages_forwards_output_config():
|
||||
"""
|
||||
Ensure output_config is stripped from the request before sending to
|
||||
Bedrock Invoke, which doesn't support this Anthropic-specific parameter.
|
||||
``output_config`` is the adaptive-thinking effort payload for Claude
|
||||
4.6 / 4.7 (e.g. ``{"effort": "max"}``). Bedrock Invoke accepts it for
|
||||
those models — the prior behavior of stripping it silently flattened
|
||||
every adaptive tier on /v1/messages so ``low`` / ``medium`` / ``high`` /
|
||||
``xhigh`` / ``max`` all collapsed to identical thinking with no tier
|
||||
differentiation.
|
||||
|
||||
Regression test for: https://github.com/BerriAI/litellm/issues/22797
|
||||
Regression coverage for the QA bug listed on PR #27039.
|
||||
"""
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
@@ -611,25 +615,24 @@ def test_bedrock_messages_strips_output_config():
|
||||
}
|
||||
|
||||
result = cfg.transform_anthropic_messages_request(
|
||||
model="anthropic.claude-3-haiku-20240307-v1:0",
|
||||
model="anthropic.claude-opus-4-7",
|
||||
messages=messages,
|
||||
anthropic_messages_optional_request_params=optional_params,
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert (
|
||||
"output_config" not in result
|
||||
), "output_config should be stripped — Bedrock Invoke rejects it"
|
||||
assert result.get("output_config") == {"effort": "high"}
|
||||
# Other params should be preserved
|
||||
assert result.get("max_tokens") == 4096
|
||||
|
||||
|
||||
def test_bedrock_messages_strips_output_config_with_output_format():
|
||||
def test_bedrock_messages_forwards_output_config_with_output_format():
|
||||
"""
|
||||
When both output_config and output_format are present, both should be
|
||||
stripped (output_format is converted to inline schema, output_config
|
||||
is simply dropped).
|
||||
When both output_config and output_format are present, output_format is
|
||||
converted to inline schema (Bedrock Invoke doesn't accept output_format
|
||||
natively), and output_config is forwarded for Claude 4.6/4.7 adaptive
|
||||
thinking.
|
||||
"""
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
@@ -648,14 +651,14 @@ def test_bedrock_messages_strips_output_config_with_output_format():
|
||||
}
|
||||
|
||||
result = cfg.transform_anthropic_messages_request(
|
||||
model="anthropic.claude-3-haiku-20240307-v1:0",
|
||||
model="anthropic.claude-opus-4-7",
|
||||
messages=messages,
|
||||
anthropic_messages_optional_request_params=optional_params,
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert "output_config" not in result
|
||||
assert result.get("output_config") == {"effort": "low"}
|
||||
assert "output_format" not in result
|
||||
|
||||
|
||||
@@ -728,13 +731,18 @@ def test_bedrock_messages_allowlist_filters_anthropic_only_fields():
|
||||
"mcp_servers",
|
||||
"container",
|
||||
"inference_geo",
|
||||
"output_config",
|
||||
"context_management",
|
||||
"model",
|
||||
"stream",
|
||||
):
|
||||
assert bad not in result, f"{bad} should be stripped by the allowlist"
|
||||
|
||||
# ``output_config`` rides along — Bedrock Invoke accepts it for Claude
|
||||
# 4.6/4.7 adaptive thinking and stripping it silently flattens every
|
||||
# adaptive tier. (Bedrock will reject it for non-adaptive models, which
|
||||
# is the correct behavior — surface the model error rather than swallow
|
||||
# the knob.)
|
||||
assert result.get("output_config") == {"effort": "low"}
|
||||
# Supported fields pass through.
|
||||
assert result["max_tokens"] == 4096
|
||||
assert result["temperature"] == 0.5
|
||||
@@ -882,7 +890,10 @@ async def test_promote_message_start_cache_when_message_stop_omits_cache_fields(
|
||||
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
|
||||
"usage": {"input_tokens": 10, "output_tokens": 181},
|
||||
}
|
||||
yield {"type": "message_stop", "usage": {"input_tokens": 10, "output_tokens": 181}}
|
||||
yield {
|
||||
"type": "message_stop",
|
||||
"usage": {"input_tokens": 10, "output_tokens": 181},
|
||||
}
|
||||
|
||||
merged: list[dict] = []
|
||||
async for chunk in cfg._promote_message_stop_usage(_stream()):
|
||||
@@ -936,7 +947,11 @@ async def test_unified_bedrock_messages_cache_on_start_only_never_negative_cost(
|
||||
},
|
||||
},
|
||||
}
|
||||
yield {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}
|
||||
yield {
|
||||
"type": "content_block_start",
|
||||
"index": 0,
|
||||
"content_block": {"type": "text", "text": ""},
|
||||
}
|
||||
yield {
|
||||
"type": "content_block_delta",
|
||||
"index": 0,
|
||||
@@ -948,7 +963,10 @@ async def test_unified_bedrock_messages_cache_on_start_only_never_negative_cost(
|
||||
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
|
||||
"usage": {"output_tokens": 181, "input_tokens": 10},
|
||||
}
|
||||
yield {"type": "message_stop", "usage": {"input_tokens": 10, "output_tokens": 181}}
|
||||
yield {
|
||||
"type": "message_stop",
|
||||
"usage": {"input_tokens": 10, "output_tokens": 181},
|
||||
}
|
||||
|
||||
logging_obj = LiteLLMLoggingObj(
|
||||
model="bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
|
||||
+34
-34
@@ -498,11 +498,14 @@ def test_vertex_ai_partner_models_anthropic_remove_prompt_caching_scope_beta_hea
|
||||
), "Header should be removed if no supported values remain"
|
||||
|
||||
|
||||
def test_vertex_ai_anthropic_output_config_effort_only_dropped():
|
||||
def test_vertex_ai_anthropic_output_config_effort_only_forwarded():
|
||||
"""
|
||||
``output_config`` containing only ``effort`` (an Anthropic-only key Vertex
|
||||
rejects with "Extra inputs are not permitted") is dropped entirely so the
|
||||
request body has no empty dict.
|
||||
Vertex AI Claude 4.6 / 4.7 accept ``output_config.effort`` on direct
|
||||
``:rawPredict`` (verified end-to-end against ``us-east5`` for
|
||||
``claude-opus-4-6`` and ``global`` for ``claude-opus-4-7``). The earlier
|
||||
strip silently flattened every adaptive tier on Vertex, so ``low`` /
|
||||
``medium`` / ``high`` / ``xhigh`` / ``max`` all produced identical
|
||||
thinking with no tier differentiation.
|
||||
"""
|
||||
config = VertexAIAnthropicConfig()
|
||||
|
||||
@@ -515,16 +518,14 @@ def test_vertex_ai_anthropic_output_config_effort_only_dropped():
|
||||
}
|
||||
|
||||
result = config.transform_request(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
model="claude-opus-4-6",
|
||||
messages=messages,
|
||||
optional_params=optional_params,
|
||||
litellm_params={},
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
assert (
|
||||
"output_config" not in result
|
||||
), "output_config containing only effort must be dropped"
|
||||
assert result["output_config"] == {"effort": "high"}
|
||||
assert result["max_tokens"] == 1024
|
||||
assert "messages" in result
|
||||
|
||||
@@ -566,13 +567,15 @@ def test_vertex_ai_anthropic_output_config_format_passes_through():
|
||||
assert result["output_config"] == output_config
|
||||
|
||||
|
||||
def test_vertex_ai_anthropic_output_config_format_plus_effort_strips_only_effort():
|
||||
def test_vertex_ai_anthropic_output_config_format_plus_effort_preserved():
|
||||
"""
|
||||
Greptile P1 on PR #23396: when ``output_config`` contains BOTH ``format``
|
||||
and ``effort``, the prior conditional-passthrough logic forwarded the
|
||||
full dict including the unsupported ``effort`` key, reproducing the
|
||||
400 error the fix was meant to resolve. Only ``effort`` (and any future
|
||||
Vertex-unsupported keys) should be filtered; ``format`` must survive.
|
||||
Vertex AI Claude 4.6 / 4.7 accept ``output_config.effort`` on direct
|
||||
``:rawPredict`` (verified end-to-end against ``us-east5`` for
|
||||
``claude-opus-4-6`` and ``global`` for ``claude-opus-4-7``). Since the
|
||||
strip was unjustified, ``effort`` must now ride along with ``format``.
|
||||
|
||||
We use a Claude 4.6 model id here because ``_apply_output_config`` only
|
||||
accepts ``effort`` on adaptive-thinking 4.6/4.7 model ids.
|
||||
"""
|
||||
config = VertexAIAnthropicConfig()
|
||||
messages = [{"role": "user", "content": "Return a person object."}]
|
||||
@@ -591,7 +594,7 @@ def test_vertex_ai_anthropic_output_config_format_plus_effort_strips_only_effort
|
||||
optional_params = {"max_tokens": 1024, "output_config": output_config}
|
||||
|
||||
result = config.transform_request(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
model="claude-opus-4-6",
|
||||
messages=messages,
|
||||
optional_params=optional_params,
|
||||
litellm_params={},
|
||||
@@ -599,9 +602,7 @@ def test_vertex_ai_anthropic_output_config_format_plus_effort_strips_only_effort
|
||||
)
|
||||
|
||||
assert "output_config" in result
|
||||
assert (
|
||||
"effort" not in result["output_config"]
|
||||
), "effort must be stripped — Vertex returns 400 on unknown keys"
|
||||
assert result["output_config"]["effort"] == "high"
|
||||
assert result["output_config"]["format"] == output_config["format"]
|
||||
|
||||
|
||||
@@ -623,14 +624,14 @@ def test_vertex_ai_anthropic_output_config_non_dict_dropped():
|
||||
assert "output_config" not in result
|
||||
|
||||
|
||||
def test_vertex_ai_anthropic_output_format_preserved_output_config_effort_dropped():
|
||||
def test_vertex_ai_anthropic_output_format_and_output_config_effort_preserved():
|
||||
"""
|
||||
When the request carries both ``output_format`` (top-level structured
|
||||
outputs) AND an ``output_config`` whose only useful key for Vertex is
|
||||
``effort``: ``output_format`` must be forwarded (Vertex accepts it),
|
||||
while ``output_config`` is dropped because Vertex returns 400 on
|
||||
``effort``. This replaces the old "drop both" behavior, which was the
|
||||
silent strip the bug report flagged.
|
||||
Vertex AI Claude 4.6 / 4.7 accept ``output_config.effort`` on direct
|
||||
``:rawPredict`` (verified end-to-end). When both ``output_format`` and
|
||||
``output_config: {effort}`` are present, both must be forwarded — the
|
||||
earlier ``effort`` strip caused silent loss of the requested adaptive
|
||||
thinking tier on Vertex routes (``low``/``medium``/``high``/``xhigh``/``max``
|
||||
all collapsed to identical adaptive thinking with no tier differentiation).
|
||||
"""
|
||||
config = VertexAIAnthropicConfig()
|
||||
messages = [{"role": "user", "content": "Extract structured data"}]
|
||||
@@ -653,7 +654,7 @@ def test_vertex_ai_anthropic_output_format_preserved_output_config_effort_droppe
|
||||
}
|
||||
|
||||
test_data = {
|
||||
"model": "claude-3-5-sonnet-20241022",
|
||||
"model": "claude-opus-4-6",
|
||||
"messages": messages,
|
||||
"max_tokens": 2048,
|
||||
"output_format": output_format,
|
||||
@@ -671,7 +672,7 @@ def test_vertex_ai_anthropic_output_format_preserved_output_config_effort_droppe
|
||||
|
||||
try:
|
||||
result = config.transform_request(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
model="claude-opus-4-6",
|
||||
messages=messages,
|
||||
optional_params=optional_params,
|
||||
litellm_params={},
|
||||
@@ -680,9 +681,8 @@ def test_vertex_ai_anthropic_output_format_preserved_output_config_effort_droppe
|
||||
|
||||
# output_format flows through unchanged — Vertex AI Claude accepts it.
|
||||
assert result["output_format"] == output_format
|
||||
# output_config containing only ``effort`` is dropped to avoid the
|
||||
# 400 "Extra inputs are not permitted" the silent strip used to mask.
|
||||
assert "output_config" not in result
|
||||
# output_config.effort now flows through (Vertex accepts it on 4.6/4.7).
|
||||
assert result["output_config"] == {"effort": "high"}
|
||||
assert result["max_tokens"] == 2048
|
||||
assert "model" not in result, "model is still stripped (Vertex routes by URL)"
|
||||
finally:
|
||||
@@ -702,10 +702,10 @@ def test_sanitize_vertex_anthropic_output_params_unit():
|
||||
sanitize_vertex_anthropic_output_params(data)
|
||||
assert data == {"max_tokens": 8}
|
||||
|
||||
# Effort-only → dropped entirely.
|
||||
# Effort-only → preserved (Vertex 4.6/4.7 accept it on rawPredict).
|
||||
data = {"output_config": {"effort": "high"}}
|
||||
sanitize_vertex_anthropic_output_params(data)
|
||||
assert "output_config" not in data
|
||||
assert data["output_config"] == {"effort": "high"}
|
||||
|
||||
# Format-only → preserved unchanged.
|
||||
fmt = {"format": {"type": "json_schema", "schema": {"type": "object"}}}
|
||||
@@ -713,10 +713,10 @@ def test_sanitize_vertex_anthropic_output_params_unit():
|
||||
sanitize_vertex_anthropic_output_params(data)
|
||||
assert data["output_config"] == fmt
|
||||
|
||||
# Mixed → effort filtered, format kept.
|
||||
# Mixed → both effort and format kept (no current Vertex-unsupported keys).
|
||||
data = {"output_config": {"format": fmt["format"], "effort": "high"}}
|
||||
sanitize_vertex_anthropic_output_params(data)
|
||||
assert data["output_config"] == fmt
|
||||
assert data["output_config"] == {"format": fmt["format"], "effort": "high"}
|
||||
|
||||
# Non-dict → dropped defensively.
|
||||
data = {"output_config": "garbage"}
|
||||
|
||||
Reference in New Issue
Block a user