From 93a704253b6c016ab21698ed738f0d20256d67e0 Mon Sep 17 00:00:00 2001 From: HumphreySun98 Date: Thu, 4 Jun 2026 16:11:59 -0500 Subject: [PATCH] fix(bedrock/claude_platform): strip workspace_id aliases from request body `workspace_id` (and the aliases `aws_workspace_id`, `anthropic-workspace-id`, `anthropic_workspace_id`) is consumed only as the `anthropic-workspace-id` HTTP header. They were left in `optional_params` after `validate_environment` read them, so the inherited `AnthropicConfig.transform_request` serialized each alias as a top-level field of the JSON request body. Anthropic's `/v1/messages` rejects unknown top-level fields, so the gateway returned an error. Centralize the key list as `_WORKSPACE_ID_PARAM_KEYS`, add a `_pop_workspace_id_params` helper on `BedrockClaudePlatformMixin`, and invoke it from `validate_environment` once the header is set so every alias is dropped from both `optional_params` and `litellm_params`. Closes #29272 --- .../bedrock/claude_platform/common_utils.py | 24 ++++++++ .../bedrock/claude_platform/transformation.py | 7 +++ .../bedrock/test_claude_platform_provider.py | 58 +++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/litellm/llms/bedrock/claude_platform/common_utils.py b/litellm/llms/bedrock/claude_platform/common_utils.py index 3abb8710de7..aab9ffda670 100644 --- a/litellm/llms/bedrock/claude_platform/common_utils.py +++ b/litellm/llms/bedrock/claude_platform/common_utils.py @@ -9,6 +9,16 @@ CLAUDE_PLATFORM_SERVICE_NAME: Literal["aws-external-anthropic"] = ( ) CLAUDE_PLATFORM_BEDROCK_ROUTE = "claude_platform/" +# Aliases litellm accepts for the workspace identifier; all of them are +# consumed only as the `anthropic-workspace-id` HTTP header and must be +# stripped from request-body params before transformation (#29272). +_WORKSPACE_ID_PARAM_KEYS: Tuple[str, ...] = ( + "workspace_id", + "aws_workspace_id", + "anthropic-workspace-id", + "anthropic_workspace_id", +) + def strip_claude_platform_route(model: str) -> str: if model.startswith(CLAUDE_PLATFORM_BEDROCK_ROUTE): @@ -37,6 +47,20 @@ class BedrockClaudePlatformMixin(BaseAWSLLM): "ANTHROPIC_WORKSPACE_ID" ) + @staticmethod + def _pop_workspace_id_params(optional_params: dict, litellm_params: dict) -> None: + """Strip every workspace_id alias from both param dicts. + + ``workspace_id`` (and its aliases) is consumed only as the + ``anthropic-workspace-id`` HTTP header. If we leave the keys in + ``optional_params``, the inherited ``AnthropicConfig.transform_request`` + serializes them into the JSON body, and Anthropic's ``/v1/messages`` + rejects unknown top-level fields (#29272). + """ + for key in _WORKSPACE_ID_PARAM_KEYS: + optional_params.pop(key, None) + litellm_params.pop(key, None) + def _get_required_aws_region_name(self, optional_params: dict) -> str: aws_region_name = ( optional_params.get("aws_region_name") diff --git a/litellm/llms/bedrock/claude_platform/transformation.py b/litellm/llms/bedrock/claude_platform/transformation.py index c20dc63444f..752ccbda4c0 100644 --- a/litellm/llms/bedrock/claude_platform/transformation.py +++ b/litellm/llms/bedrock/claude_platform/transformation.py @@ -41,6 +41,13 @@ class BedrockClaudePlatformConfig(BedrockClaudePlatformMixin, AnthropicConfig): model=model, ) + # Drop workspace_id alias keys from optional_params/litellm_params now + # that we've turned the value into the `anthropic-workspace-id` header. + # Without this they survive into `AnthropicConfig.transform_request` and + # ship as a top-level field in the JSON body, which Anthropic's + # /v1/messages rejects (#29272). + self._pop_workspace_id_params(optional_params, litellm_params) + api_key = api_key or get_secret_str("ANTHROPIC_AWS_API_KEY") anthropic_headers = self.get_anthropic_headers( api_key=api_key, diff --git a/tests/test_litellm/llms/bedrock/test_claude_platform_provider.py b/tests/test_litellm/llms/bedrock/test_claude_platform_provider.py index dbded8e0a2e..0abc2af9441 100644 --- a/tests/test_litellm/llms/bedrock/test_claude_platform_provider.py +++ b/tests/test_litellm/llms/bedrock/test_claude_platform_provider.py @@ -362,3 +362,61 @@ def test_sigv4_no_duplicate_content_type_when_caller_sets_lowercase(): "Duplicate keys produce 'application/json, application/json' in the " "SigV4 canonical string and cause a 401." ) + + +def test_claude_platform_strips_workspace_id_aliases_from_request_body(): + """Regression for #29272. + + `workspace_id` (and its aliases `aws_workspace_id`, `anthropic-workspace-id`, + `anthropic_workspace_id`) is consumed only as the + `anthropic-workspace-id` header. They were left in `optional_params`, so + the inherited `AnthropicConfig.transform_request` serialized them as + top-level fields of the JSON body and Anthropic's `/v1/messages` + rejected the request with `unknown field`. + + Verify every alias is popped from both `optional_params` and + `litellm_params` once `validate_environment` runs. + """ + from litellm.llms.bedrock.claude_platform.transformation import ( + BedrockClaudePlatformConfig, + ) + + config = BedrockClaudePlatformConfig() + + optional_params = { + "workspace_id": "wrkspc_a", + "aws_workspace_id": "wrkspc_b", + "anthropic-workspace-id": "wrkspc_c", + "anthropic_workspace_id": "wrkspc_d", + "max_tokens": 1024, # must survive + } + litellm_params = { + "workspace_id": "wrkspc_e", + "anthropic_workspace_id": "wrkspc_f", + "metadata": {"trace_id": "abc"}, # must survive + } + + headers = config.validate_environment( + api_key="fake-platform-key", + headers={}, + model="claude-sonnet-4-6", + messages=[{"role": "user", "content": "hello"}], + optional_params=optional_params, + litellm_params=litellm_params, + ) + + assert headers["anthropic-workspace-id"] == "wrkspc_a" + + # No alias survives into params that downstream transformers serialize. + for key in ( + "workspace_id", + "aws_workspace_id", + "anthropic-workspace-id", + "anthropic_workspace_id", + ): + assert key not in optional_params, f"{key} leaked into optional_params" + assert key not in litellm_params, f"{key} leaked into litellm_params" + + # Unrelated params untouched. + assert optional_params["max_tokens"] == 1024 + assert litellm_params["metadata"] == {"trace_id": "abc"}