litellm/tests/integration/providers/_sagemaker_chat_container.py
devin-ai-integration[bot] 0126c8db73
fix(sagemaker): keep AWS credentials out of the sagemaker_chat request body (#45724)
* fix(sagemaker): keep AWS credentials out of the sagemaker_chat request body

sagemaker_chat and sagemaker_nova built the container body from every optional
param, so aws_access_key_id, aws_secret_access_key, aws_session_token and the
other aws_* auth params, plus model_id and sagemaker_base_url, were sent in the
JSON body to the endpoint container. The request is still SigV4-signed with the
same credentials, which stay in optional_params for signing

* fix(sagemaker): limit the sagemaker_chat body filter to aws_ params

model_id and sagemaker_base_url are not credentials, so they stay in the
body as before and the change sticks to what the fix is about

* test(sagemaker): assert the whole sagemaker_chat request body

* fix(sagemaker): filter the sagemaker_chat body with the shared AWS auth param list

* test(sagemaker): cover the sagemaker_chat body on every endpoint, credential source, and outage

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-10 15:30:54 -07:00

132 lines
4.6 KiB
Python

import json
import struct
import uuid
import zlib
from collections.abc import Mapping
from types import MappingProxyType
from typing import Final
from integration._support.wire import Reply, Request, Wire
from pydantic import JsonValue, TypeAdapter
ENDPOINT: Final = "integration-vllm-endpoint"
SERVED_MODEL: Final = "integration-org/served-chat-model"
ACCESS_KEY: Final = "AKIAINTEGRATION000003"
SECRET_KEY: Final = "synthetic-secret-key-for-testing"
REGION: Final = "us-east-1"
PROMPT: Final = "synthetic inference component request"
CONTENT: Final = ("sagemaker ", "wire control")
NO_CACHE: Final[Mapping[str, JsonValue]] = MappingProxyType({"cache": {"no-cache": True}})
JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
_MARKER_PREFIX: Final = "marker-"
def answered_choices() -> list[JsonValue]:
return [
{
"finish_reason": "stop",
"index": 0,
"message": {"role": "assistant", "content": "".join(CONTENT)},
"provider_specific_fields": {},
}
]
def _completion(identity: str) -> bytes:
return json.dumps(
{
"id": identity,
"object": "chat.completion",
"created": 1,
"model": SERVED_MODEL,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "".join(CONTENT)},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 11, "completion_tokens": 4, "total_tokens": 15},
}
).encode()
def _chunk(identity: str, delta: Mapping[str, JsonValue], finish_reason: str | None, **extra: JsonValue) -> bytes:
payload: Final = {
"id": identity,
"object": "chat.completion.chunk",
"created": 1,
"model": SERVED_MODEL,
"choices": [{"index": 0, "delta": dict(delta), "finish_reason": finish_reason}],
**extra,
}
return f"data: {json.dumps(payload, separators=(',', ':'))}\n\n".encode()
def _event_header(name: str, value: str) -> bytes:
name_bytes: Final = name.encode()
value_bytes: Final = value.encode()
return struct.pack("!B", len(name_bytes)) + name_bytes + b"\x07" + struct.pack("!H", len(value_bytes)) + value_bytes
def _payload_part(body: bytes) -> bytes:
headers: Final = (
_event_header(":event-type", "PayloadPart")
+ _event_header(":content-type", "application/octet-stream")
+ _event_header(":message-type", "event")
)
prelude: Final = struct.pack("!II", 12 + len(headers) + len(body) + 4, len(headers))
message: Final = prelude + struct.pack("!I", zlib.crc32(prelude) & 0xFFFFFFFF) + headers + body
return message + struct.pack("!I", zlib.crc32(message) & 0xFFFFFFFF)
def container_reply(identity: str, stream: bool) -> Reply:
if not stream:
return Reply(body=_completion(identity))
chunks: Final = (
_chunk(identity, {"role": "assistant", "content": CONTENT[0]}, None),
_chunk(identity, {"content": CONTENT[1]}, None),
_chunk(identity, {}, "stop", usage={"prompt_tokens": 11, "completion_tokens": 4, "total_tokens": 15}),
)
return Reply(
chunks=tuple(_payload_part(chunk) for chunk in chunks), content_type="application/vnd.amazon.eventstream"
)
def marked_prompt() -> str:
return f"{PROMPT} {_MARKER_PREFIX}{uuid.uuid4().hex}"
def marker(text: str) -> str:
_, separator, rest = text.partition(_MARKER_PREFIX)
assert separator and text.count(_MARKER_PREFIX) == 1, text
return rest[:32]
def container_body(url: str, prompt: str, **extra: JsonValue) -> dict[str, JsonValue]:
return {
"model": ENDPOINT,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 16,
"sagemaker_base_url": url,
**extra,
}
def only_request(wire: Wire) -> Request:
(request,) = wire.drain()
assert (request.method, request.target) == ("POST", "/"), request
return request
def by_marker(received: tuple[Request, ...]) -> Mapping[str, Request]:
forwarded: Final = {marker(request.body.decode()): request for request in received}
assert len(forwarded) == len(received), [request.body for request in received]
return forwarded
def assert_signed(request: Request, access_key: str, region: str, session_token: str | None) -> None:
authorization: Final = request.headers["authorization"]
assert authorization.startswith(f"AWS4-HMAC-SHA256 Credential={access_key}/"), authorization
assert f"/{region}/sagemaker/aws4_request" in authorization, authorization
assert request.headers.get("x-amz-security-token") == session_token, dict(request.headers)