mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
fix(bedrock/claude_platform): strip body params the AWS endpoint rejects
The bedrock/claude_platform/ route forwards params into the Messages API
request body that the Claude Platform on AWS (aws-external-anthropic)
endpoint rejects as unknown fields ("Extra inputs are not permitted"). Two
categories:
1. Auth/routing config. workspace_id and aws_* litellm_params are auth/routing
config: workspace_id is sent as the anthropic-workspace-id header and aws_*
params feed SigV4 signing. A standard proxy config
- model_name: claude-sonnet-4-6
litellm_params:
model: bedrock/claude_platform/claude-sonnet-4-6
aws_region_name: us-east-1
workspace_id: wrkspc_xxx
fails with 400 'workspace_id: Extra inputs are not permitted' (and after
removing that, 'aws_region_name: ...'). BedrockClaudePlatformConfig inherits
AnthropicConfig, which forwards optional_params into the body verbatim.
2. Unsupported Messages API fields. context_management is a valid first-party
Anthropic beta field that the AWS endpoint does not support yet, so it is
rejected the same way. It must be stripped on this route specifically.
Add filter_claude_platform_request_body() in common_utils and route both the
chat-completions transform_request and the native /v1/messages
transform_anthropic_messages_request through it. Auth/routing params are
dropped silently; unsupported feature params (context_management) are dropped
with a verbose_logger.warning since they carry user intent. Filters a copy so
sign_request still sees aws_region_name and callers' dicts are not mutated.
Affects v1.85.0 through v1.89.3 and current main.
This commit is contained in:
parent
8bca05d311
commit
a2c0cf9a46
4 changed files with 268 additions and 2 deletions
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Literal, Optional, Tuple
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
|
||||
|
|
@ -9,6 +10,58 @@ CLAUDE_PLATFORM_SERVICE_NAME: Literal["aws-external-anthropic"] = (
|
|||
)
|
||||
CLAUDE_PLATFORM_BEDROCK_ROUTE = "claude_platform/"
|
||||
|
||||
# Auth/routing params consumed by validate_environment / sign_request that
|
||||
# must not be forwarded in the Messages API request body (together with any
|
||||
# key prefixed "aws_") — the API rejects unknown fields with
|
||||
# "Extra inputs are not permitted".
|
||||
CLAUDE_PLATFORM_NON_REQUEST_PARAMS = {
|
||||
"workspace_id",
|
||||
"anthropic_workspace_id",
|
||||
"anthropic-workspace-id",
|
||||
}
|
||||
|
||||
# Messages API fields that are valid on Anthropic's first-party API but are
|
||||
# not yet supported by the Claude Platform on AWS (aws-external-anthropic)
|
||||
# endpoint, which rejects them with "Extra inputs are not permitted". Unlike
|
||||
# the auth params above these carry user intent, so dropping them is logged at
|
||||
# WARNING — the request succeeds but the requested feature is not applied.
|
||||
CLAUDE_PLATFORM_UNSUPPORTED_REQUEST_PARAMS = {
|
||||
"context_management",
|
||||
}
|
||||
|
||||
|
||||
def filter_claude_platform_request_body(params: dict) -> dict:
|
||||
"""Return a copy of ``params`` with fields the Claude Platform on AWS
|
||||
endpoint rejects removed.
|
||||
|
||||
Strips auth/routing config (workspace-id aliases and any ``aws_``-prefixed
|
||||
key) silently, since those are consumed by validate_environment /
|
||||
sign_request and never belong in the body. Strips Messages API fields the
|
||||
AWS endpoint does not support yet (e.g. ``context_management``) with a
|
||||
WARNING, since those reflect user intent that will not be applied on this
|
||||
route.
|
||||
|
||||
Filters a copy so callers' ``sign_request`` still sees ``aws_region_name``.
|
||||
"""
|
||||
dropped_unsupported = [
|
||||
k for k in params if k in CLAUDE_PLATFORM_UNSUPPORTED_REQUEST_PARAMS
|
||||
]
|
||||
if dropped_unsupported:
|
||||
verbose_logger.warning(
|
||||
"bedrock/claude_platform: dropping unsupported Messages API "
|
||||
"param(s) %s from the request body — the Claude Platform on AWS "
|
||||
"(aws-external-anthropic) endpoint does not support them and "
|
||||
"rejects unknown fields. The request will proceed without them.",
|
||||
dropped_unsupported,
|
||||
)
|
||||
return {
|
||||
k: v
|
||||
for k, v in params.items()
|
||||
if k not in CLAUDE_PLATFORM_NON_REQUEST_PARAMS
|
||||
and k not in CLAUDE_PLATFORM_UNSUPPORTED_REQUEST_PARAMS
|
||||
and not k.startswith("aws_")
|
||||
}
|
||||
|
||||
|
||||
def strip_claude_platform_route(model: str) -> str:
|
||||
if model.startswith(CLAUDE_PLATFORM_BEDROCK_ROUTE):
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ from litellm.llms.anthropic.experimental_pass_through.messages.transformation im
|
|||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
from .common_utils import BedrockClaudePlatformMixin, strip_claude_platform_route
|
||||
from .common_utils import (
|
||||
BedrockClaudePlatformMixin,
|
||||
filter_claude_platform_request_body,
|
||||
strip_claude_platform_route,
|
||||
)
|
||||
|
||||
|
||||
class BedrockClaudePlatformMessagesConfig(
|
||||
|
|
@ -62,6 +66,15 @@ class BedrockClaudePlatformMessagesConfig(
|
|||
litellm_params: GenericLiteLLMParams,
|
||||
headers: dict,
|
||||
) -> Dict:
|
||||
# Strip auth/routing config (workspace_id, aws_*) and Messages API
|
||||
# fields the AWS endpoint does not support (e.g. context_management)
|
||||
# from the body — the API rejects unknown fields with "Extra inputs
|
||||
# are not permitted".
|
||||
anthropic_messages_optional_request_params = (
|
||||
filter_claude_platform_request_body(
|
||||
anthropic_messages_optional_request_params
|
||||
)
|
||||
)
|
||||
return super().transform_anthropic_messages_request(
|
||||
model=strip_claude_platform_route(model),
|
||||
messages=messages,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
|||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
||||
from .common_utils import BedrockClaudePlatformMixin
|
||||
from .common_utils import (
|
||||
BedrockClaudePlatformMixin,
|
||||
filter_claude_platform_request_body,
|
||||
)
|
||||
|
||||
|
||||
class BedrockClaudePlatformConfig(BedrockClaudePlatformMixin, AnthropicConfig):
|
||||
|
|
@ -82,6 +85,28 @@ class BedrockClaudePlatformConfig(BedrockClaudePlatformMixin, AnthropicConfig):
|
|||
anthropic_headers["anthropic-workspace-id"] = workspace_id
|
||||
return {**headers, **anthropic_headers}
|
||||
|
||||
def transform_request(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[AllMessageValues],
|
||||
optional_params: dict,
|
||||
litellm_params: dict,
|
||||
headers: dict,
|
||||
) -> dict:
|
||||
# Strip auth/routing config (workspace_id, aws_*) and Messages API
|
||||
# fields the AWS endpoint does not support (e.g. context_management)
|
||||
# from the body — the API rejects unknown fields with "Extra inputs
|
||||
# are not permitted". Filters a copy so sign_request still sees
|
||||
# aws_region_name.
|
||||
optional_params = filter_claude_platform_request_body(optional_params)
|
||||
return super().transform_request(
|
||||
model=model,
|
||||
messages=messages,
|
||||
optional_params=optional_params,
|
||||
litellm_params=litellm_params,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
def get_model_response_iterator(
|
||||
self,
|
||||
streaming_response: Any,
|
||||
|
|
|
|||
|
|
@ -313,6 +313,181 @@ async def test_anthropic_messages_routes_bedrock_claude_platform_to_messages_api
|
|||
assert requests[0]["body"]["model"] == "claude-sonnet-4-6"
|
||||
|
||||
|
||||
def test_claude_platform_strips_auth_params_from_request_body():
|
||||
"""
|
||||
Regression: workspace_id is consumed by validate_environment (sent as the
|
||||
anthropic-workspace-id header) and aws_* params by sign_request, but
|
||||
transform_request used to forward them into the Messages API body, which
|
||||
rejects unknown fields: "workspace_id: Extra inputs are not permitted".
|
||||
"""
|
||||
from litellm.llms.bedrock.claude_platform.transformation import (
|
||||
BedrockClaudePlatformConfig,
|
||||
)
|
||||
|
||||
config = BedrockClaudePlatformConfig()
|
||||
optional_params = {
|
||||
"workspace_id": "wrkspc_test",
|
||||
"aws_region_name": "us-west-2",
|
||||
"max_tokens": 10,
|
||||
}
|
||||
|
||||
request_body = config.transform_request(
|
||||
model="claude-sonnet-4-6",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
optional_params=optional_params,
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert "workspace_id" not in request_body
|
||||
assert "aws_region_name" not in request_body
|
||||
assert request_body["max_tokens"] == 10
|
||||
# sign_request still needs the aws_* params — the original dict must not
|
||||
# be mutated by the body transformation.
|
||||
assert optional_params["aws_region_name"] == "us-west-2"
|
||||
assert optional_params["workspace_id"] == "wrkspc_test"
|
||||
|
||||
|
||||
def test_claude_platform_messages_strips_auth_params_from_request_body():
|
||||
"""
|
||||
Same regression as above for the native /v1/messages path.
|
||||
"""
|
||||
import litellm
|
||||
from litellm.types.utils import LlmProviders
|
||||
|
||||
config = litellm.ProviderConfigManager.get_provider_anthropic_messages_config(
|
||||
model="claude_platform/claude-sonnet-4-6",
|
||||
provider=LlmProviders.BEDROCK,
|
||||
)
|
||||
assert config is not None
|
||||
|
||||
request_body = config.transform_anthropic_messages_request(
|
||||
model="claude_platform/claude-sonnet-4-6",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
anthropic_messages_optional_request_params={
|
||||
"workspace_id": "wrkspc_test",
|
||||
"aws_region_name": "us-west-2",
|
||||
"max_tokens": 10,
|
||||
},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert "workspace_id" not in request_body
|
||||
assert "aws_region_name" not in request_body
|
||||
assert request_body["max_tokens"] == 10
|
||||
|
||||
|
||||
def test_claude_platform_strips_unsupported_context_management_param(caplog):
|
||||
"""
|
||||
Regression: context_management is a valid first-party Anthropic Messages
|
||||
API field, but the Claude Platform on AWS (aws-external-anthropic)
|
||||
endpoint does not support it and rejects it with
|
||||
"context_management: Extra inputs are not permitted". It must be dropped
|
||||
from the body, and — unlike the silent auth-param strip — the drop is
|
||||
logged at WARNING because it reflects user intent that won't be applied.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from litellm.llms.bedrock.claude_platform.transformation import (
|
||||
BedrockClaudePlatformConfig,
|
||||
)
|
||||
|
||||
config = BedrockClaudePlatformConfig()
|
||||
optional_params = {
|
||||
"workspace_id": "wrkspc_test",
|
||||
"context_management": {"edits": [{"type": "clear_tool_uses_20250919"}]},
|
||||
"max_tokens": 10,
|
||||
}
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
|
||||
request_body = config.transform_request(
|
||||
model="claude-sonnet-4-6",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
optional_params=optional_params,
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert "context_management" not in request_body
|
||||
assert request_body["max_tokens"] == 10
|
||||
# original dict is not mutated
|
||||
assert "context_management" in optional_params
|
||||
# the drop is surfaced to the user
|
||||
assert any(
|
||||
"context_management" in record.message and record.levelno == logging.WARNING
|
||||
for record in caplog.records
|
||||
)
|
||||
|
||||
|
||||
def test_claude_platform_messages_strips_unsupported_context_management_param():
|
||||
"""
|
||||
Same context_management strip on the native /v1/messages path.
|
||||
"""
|
||||
import litellm
|
||||
from litellm.types.utils import LlmProviders
|
||||
|
||||
config = litellm.ProviderConfigManager.get_provider_anthropic_messages_config(
|
||||
model="claude_platform/claude-sonnet-4-6",
|
||||
provider=LlmProviders.BEDROCK,
|
||||
)
|
||||
assert config is not None
|
||||
|
||||
request_body = config.transform_anthropic_messages_request(
|
||||
model="claude_platform/claude-sonnet-4-6",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
anthropic_messages_optional_request_params={
|
||||
"context_management": {"edits": [{"type": "clear_tool_uses_20250919"}]},
|
||||
"max_tokens": 10,
|
||||
},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert "context_management" not in request_body
|
||||
assert request_body["max_tokens"] == 10
|
||||
|
||||
|
||||
def test_chat_completion_claude_platform_sigv4_body_has_no_auth_params():
|
||||
"""
|
||||
End-to-end (mocked transport): a config-driven SigV4 call with
|
||||
workspace_id + aws_region_name must not leak either param into the wire
|
||||
body. Uses SigV4 (no api_key) since that is how proxy configs pass
|
||||
aws_region_name.
|
||||
"""
|
||||
import litellm
|
||||
|
||||
requests = []
|
||||
|
||||
def mock_post(self, url, data=None, headers=None, **kwargs):
|
||||
requests.append(_capture_request(url=url, headers=headers or {}, data=data))
|
||||
return _anthropic_response(url)
|
||||
|
||||
mock_credentials = Credentials("test-key", "test-secret", "test-token")
|
||||
|
||||
with (
|
||||
patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.post", mock_post),
|
||||
patch(
|
||||
"litellm.llms.bedrock.base_aws_llm.BaseAWSLLM.get_credentials",
|
||||
return_value=mock_credentials,
|
||||
),
|
||||
):
|
||||
response = litellm.completion(
|
||||
model="bedrock/claude_platform/claude-sonnet-4-6",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
max_tokens=10,
|
||||
aws_region_name="us-west-2",
|
||||
workspace_id="wrkspc_test",
|
||||
)
|
||||
|
||||
assert response.choices[0].message.content == "ok"
|
||||
assert len(requests) == 1
|
||||
body = requests[0]["body"]
|
||||
assert "workspace_id" not in body
|
||||
assert "aws_region_name" not in body
|
||||
assert requests[0]["headers"]["anthropic-workspace-id"] == "wrkspc_test"
|
||||
|
||||
|
||||
def test_sigv4_no_duplicate_content_type_when_caller_sets_lowercase():
|
||||
"""
|
||||
Regression: get_anthropic_headers() supplies "content-type" (lowercase).
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue