diff --git a/clients/aws-sdk-bedrock-runtime/.changes/next-release/aws-sdk-bedrock-runtime-feature-0215e886f8084bdd9303da46e8740056.json b/clients/aws-sdk-bedrock-runtime/.changes/next-release/aws-sdk-bedrock-runtime-feature-0215e886f8084bdd9303da46e8740056.json new file mode 100644 index 00000000..ecdd4d93 --- /dev/null +++ b/clients/aws-sdk-bedrock-runtime/.changes/next-release/aws-sdk-bedrock-runtime-feature-0215e886f8084bdd9303da46e8740056.json @@ -0,0 +1,4 @@ +{ + "type": "feature", + "description": "Document and validate standard request-compression thresholds and disable controls inherited from the shared runtime. Current Bedrock operations remain uncompressed because their service model does not advertise support." +} diff --git a/clients/aws-sdk-bedrock-runtime/README.md b/clients/aws-sdk-bedrock-runtime/README.md index 9fc85a63..b363b964 100644 --- a/clients/aws-sdk-bedrock-runtime/README.md +++ b/clients/aws-sdk-bedrock-runtime/README.md @@ -18,3 +18,41 @@ python -m pip install aws-sdk-bedrock-runtime the Bedrock Runtime client, available operations, and types. - [Developer Guide](https://docs.aws.amazon.com/sdk-for-python/v4/guide/) - configuration, authentication, and usage of the AWS SDK for Python v4. + +## Request compression + +> **Pending upstream support:** This integration requires a Smithy runtime release +> containing [request compression](https://github.com/smithy-lang/smithy-python/pull/855) +> and the normal client regeneration/dependency update. The configuration below +> is not available with the currently declared dependencies. + +Request compression reduces the bytes uploaded to an operation that advertises +gzip support. It does not reduce model input tokens. The client exposes two +configuration settings: + +- `disable_request_compression`: disable automatic compression. +- `request_min_compression_size_bytes`: the automatic compression threshold, + defaulting to 10,240 bytes, with an accepted range of 0 through 10,485,760. + +For operations with advertised gzip support, set the threshold to zero to +compress eligible buffered requests regardless of size: + +```python +from aws_sdk_bedrock_runtime.config import AsyncBedrockRuntimeConfig + +config = await AsyncBedrockRuntimeConfig.resolve( + region="us-east-1", + request_min_compression_size_bytes=0, +) +``` + +The current Bedrock Runtime service model does not advertise request compression. +Current Bedrock operations therefore remain uncompressed, even with a zero +threshold. Zstd is not currently supported. + +The configuration cannot enable server-side compression support. Bedrock and +Mantle endpoints must accept compressed requests before applications can use it +with those endpoints. + +This client covers modeled Bedrock Runtime operations. Mantle's provider-compatible +APIs use a separate client integration and need compression support there too. diff --git a/clients/aws-sdk-bedrock-runtime/tests/test_request_compression.py b/clients/aws-sdk-bedrock-runtime/tests/test_request_compression.py new file mode 100644 index 00000000..bae326a5 --- /dev/null +++ b/clients/aws-sdk-bedrock-runtime/tests/test_request_compression.py @@ -0,0 +1,156 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from pathlib import Path +from typing import Unpack + +import pytest +from smithy_core import URI +from smithy_core.aio.types import AsyncBytesReader +from smithy_core.types import TypedProperties +from smithy_http.testing import MockHTTPClient + +from aws_sdk_bedrock_runtime.config import ( + AsyncBedrockRuntimeConfig, + _AsyncBedrockRuntimeConfigOverrides, +) +from aws_sdk_bedrock_runtime.models import ( + CONVERSE, + CONVERSE_STREAM, + INVOKE_MODEL, + INVOKE_MODEL_WITH_RESPONSE_STREAM, + ContentBlockText, + ConverseInput, + ConverseStreamInput, + InvokeModelInput, + InvokeModelWithResponseStreamInput, + Message, +) + + +@pytest.fixture(autouse=True) +def isolate_aws_compression_environment(monkeypatch: pytest.MonkeyPatch) -> None: + for variable in ( + "AWS_DISABLE_REQUEST_COMPRESSION", + "AWS_REQUEST_MIN_COMPRESSION_SIZE_BYTES", + "AWS_PROFILE", + "AWS_DEFAULT_PROFILE", + ): + monkeypatch.delenv(variable, raising=False) + + +async def resolve_config( + tmp_path: Path, **overrides: Unpack[_AsyncBedrockRuntimeConfigOverrides] +) -> AsyncBedrockRuntimeConfig: + resolved_overrides: _AsyncBedrockRuntimeConfigOverrides = { + "region": "us-west-2", + "transport": MockHTTPClient(), + **overrides, + } + return await AsyncBedrockRuntimeConfig.resolve( + config_file_path=str(tmp_path / "config"), + credentials_file_path=str(tmp_path / "credentials"), + **resolved_overrides, + ) + + +async def test_compression_defaults_are_available_on_generated_config( + tmp_path: Path, +) -> None: + config = await resolve_config(tmp_path) + assert config.disable_request_compression is False + assert config.request_min_compression_size_bytes == 10240 + + +@pytest.mark.parametrize("minimum_size", [0, 10485760]) +async def test_compression_threshold_boundaries_survive_generated_config_resolution( + tmp_path: Path, minimum_size: int +) -> None: + config = await resolve_config( + tmp_path, request_min_compression_size_bytes=minimum_size + ) + assert config.request_min_compression_size_bytes == minimum_size + + +async def test_standard_compression_settings_resolve_from_environment( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("AWS_DISABLE_REQUEST_COMPRESSION", "true") + monkeypatch.setenv("AWS_REQUEST_MIN_COMPRESSION_SIZE_BYTES", "256") + config = await resolve_config(tmp_path) + assert config.disable_request_compression is True + assert config.request_min_compression_size_bytes == 256 + + +async def test_client_settings_override_environment( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("AWS_DISABLE_REQUEST_COMPRESSION", "true") + monkeypatch.setenv("AWS_REQUEST_MIN_COMPRESSION_SIZE_BYTES", "256") + config = await resolve_config( + tmp_path, + disable_request_compression=False, + request_min_compression_size_bytes=0, + ) + assert config.disable_request_compression is False + assert config.request_min_compression_size_bytes == 0 + + +@pytest.mark.parametrize( + "kind", ["converse", "converse-stream", "invoke", "invoke-with-response-stream"] +) +@pytest.mark.parametrize("minimum_size", [None, 0]) +async def test_unsupported_bedrock_request_stays_uncompressed( + tmp_path: Path, kind: str, minimum_size: int | None +) -> None: + if minimum_size is None: + config = await resolve_config(tmp_path) + else: + config = await resolve_config( + tmp_path, request_min_compression_size_bytes=minimum_size + ) + assert config.protocol is not None + messages = [Message(role="user", content=[ContentBlockText(value="hello")])] + if kind == "converse": + operation, input = ( + CONVERSE, + ConverseInput(model_id="test-model", messages=messages), + ) + elif kind == "converse-stream": + operation, input = ( + CONVERSE_STREAM, + ConverseStreamInput(model_id="test-model", messages=messages), + ) + elif kind == "invoke": + operation, input = ( + INVOKE_MODEL, + InvokeModelInput( + model_id="test-model", + body=b'{"prompt":"hello"}', + content_type="application/json", + ), + ) + else: + operation, input = ( + INVOKE_MODEL_WITH_RESPONSE_STREAM, + InvokeModelWithResponseStreamInput( + model_id="test-model", + body=b'{"prompt":"hello"}', + content_type="application/json", + ), + ) + request = config.protocol.serialize_request( + operation=operation, + input=input, + endpoint=URI(host="bedrock-runtime.us-west-2.amazonaws.com"), + context=TypedProperties({"config": config}), + ) + assert "Content-Encoding" not in request.fields + assert request.method == "POST" + assert request.destination.path == f"/model/test-model/{kind}" + assert request.fields["Content-Type"].as_string() == "application/json" + wire = await AsyncBytesReader(request.body).read() + if kind.startswith("converse"): + assert wire == b'{"messages":[{"role":"user","content":[{"text":"hello"}]}]}' + else: + assert wire == b'{"prompt":"hello"}'