This commit is contained in:
Humphrey 2026-09-08 07:08:15 -04:00 committed by GitHub
commit 767020f635
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 89 additions and 0 deletions

View file

@ -10,6 +10,16 @@ from litellm.secret_managers.main import get_secret_str
CLAUDE_PLATFORM_SERVICE_NAME: Final = "aws-external-anthropic"
CLAUDE_PLATFORM_BEDROCK_ROUTE: Final = "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):
@ -42,6 +52,20 @@ class BedrockClaudePlatformMixin(BaseAWSLLM):
return str(workspace_id)
return get_secret_str("ANTHROPIC_AWS_WORKSPACE_ID") or get_secret_str("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: Final = (
optional_params.get("aws_region_name")

View file

@ -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: Final = self.get_anthropic_headers(
api_key=api_key,

View file

@ -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"}