Skip to content

Commit 86267d2

Browse files
authored
feat: add Bedrock Runtime endpoint support (SDK-290) (#3623)
## Summary Add first-class Amazon Bedrock Runtime support to the Python SDK while preserving the existing Mantle endpoint and legacy clients. - Add `bedrock(endpoint="runtime" | "mantle")`; Mantle remains the default. - Select Runtime hostnames and SigV4 signing atomically: `bedrock-runtime.<region>.<partition>/openai/v1` signs with `bedrock`; Mantle continues to sign with `bedrock-mantle`. - Infer canonical endpoint families from explicit/environment base URLs and recognize FIPS, dual-stack, trailing-dot, China, European sovereign, ISO, ISOB, ISOE, and ISOF endpoints. - Reject invalid/injected regions, canonical HTTP, family/region mismatches, cross-origin requests, and ambiguous custom-host SigV4 configuration. - Preserve bearer keys, environment bearer precedence, refreshable sync/async token providers, named AWS profiles, default chains, static/session credentials, retry re-signing, and legacy custom-host compatibility. - Document Runtime installation, API versions/routes, Chat Completions, streaming, async use, bearer/SigV4 authentication, profiles, inference profiles, and deployment limitations in `bedrock.md` and `examples/bedrock_runtime.py`. - Extend the explicitly opt-in live harness across `us.openai.gpt-5.6-{sol,terra,luna}`, bearer/provider/profile/static/default-chain authentication, streaming, and Runtime Responses. - Extend packaged-wheel CI smoke coverage to verify both endpoint families, both auth modes, correct signing services, and botocore-free bearer authentication. ### Example ```python from openai import OpenAI from openai.providers import bedrock client = OpenAI( provider=bedrock(endpoint="runtime", region="us-west-2", api_key=None) ) completion = client.chat.completions.create( model="us.openai.gpt-5.6-sol", messages=[{"role": "user", "content": "Say hello!"}], stream=True, ) for chunk in completion: print(chunk.choices[0].delta.content or "", end="") ``` ## Verification - `pytest -q -n 0 tests/lib/test_bedrock_runtime.py tests/lib/test_bedrock_provider.py tests/lib/test_bedrock_auth_conformance.py tests/lib/test_bedrock_credential_chain.py tests/lib/test_bedrock.py` — **197 passed**, including **56 new Runtime cases**. - `pytest -q -n auto tests/lib tests/test_httpx2.py` — **330 passed**. - `ruff check .` and Ruff format checks for every changed file — passed. - Strict Pyright across all changed Python files — **0 errors**. - mypy across changed production, example, and wheel-validator files — passed. - `uv build --python .venv/bin/python` — wheel and source distribution built successfully. - `python scripts/utils/validate-bedrock-wheel.py` — packaged Mantle/Runtime bearer/SigV4 smoke checks passed. - `python scripts/utils/validate-python-version-wheel.py` — wheel/source Python metadata validated. Live AWS requests were not executed because AWS credentials are unavailable in this workspace; the guarded harness is ready for an authorized Bedrock account. ## References - [SDK-290: Add Bedrock Runtime support across OpenAI SDKs](https://linear.app/openai/issue/SDK-290/add-bedrock-runtime-support-across-openai-sdks) - [Reference Node implementation: openai/openai-node#2348](openai/openai-node#2348)
1 parent ff14a33 commit 86267d2

10 files changed

Lines changed: 1309 additions & 103 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1073,7 +1073,7 @@ client = OpenAI(
10731073

10741074
You can also pass `access_key_id` and `secret_access_key`, with an optional `session_token`, or a refreshable `credential_provider` that returns botocore-compatible credentials. Explicit bearer and AWS credential options are mutually exclusive.
10751075

1076-
Pass `base_url` to `bedrock(...)` or set `AWS_BEDROCK_BASE_URL` to override the derived `https://bedrock-mantle.<region>.api.aws/openai/v1` endpoint.
1076+
Pass `base_url` to `bedrock(...)` or set `AWS_BEDROCK_BASE_URL` to override the derived `https://bedrock-mantle.<region>.api.aws/openai/v1` endpoint. Custom URLs retain Mantle signing by default; pass `endpoint="runtime"` to use Runtime signing.
10771077

10781078
SigV4 requests require replayable, fully serialized request bodies. Standard JSON requests already meet this requirement, and response streaming is unaffected. Low-level one-shot request streams must be buffered before sending, or sent with bearer authentication and retries disabled.
10791079

bedrock.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# Amazon Bedrock
2+
3+
The Bedrock provider connects the standard synchronous and asynchronous OpenAI clients to Amazon Bedrock's
4+
OpenAI-compatible endpoints. The provider supports bearer tokens without additional dependencies. AWS Signature
5+
Version 4 (SigV4) requires the optional Bedrock dependencies:
6+
7+
```sh
8+
pip install 'openai[bedrock]'
9+
```
10+
11+
Runtime endpoint selection requires a Python SDK release that includes SDK-290. The SDK supports Python 3.10 and newer.
12+
13+
## Endpoint selection
14+
15+
| `endpoint` | Default API root | SigV4 signing service |
16+
| --- | --- | --- |
17+
| `"mantle"` (default) | `https://bedrock-mantle.<region>.api.aws/openai/v1` | `bedrock-mantle` |
18+
| `"runtime"` | `https://bedrock-runtime.<region>.amazonaws.com/openai/v1` | `bedrock` |
19+
20+
Runtime hostnames use the DNS suffix for the selected AWS partition. For example, the European sovereign region
21+
`eusc-de-east-1` uses `amazonaws.eu`. Canonical Runtime FIPS and dual-stack hostnames are also recognized.
22+
23+
The region comes from `region`, `AWS_REGION`, `AWS_DEFAULT_REGION`, or, for AWS authentication, the selected AWS profile.
24+
Pass `base_url` or set `AWS_BEDROCK_BASE_URL` to override the derived API root. When `endpoint` is omitted, canonical
25+
Mantle and Runtime URLs select the corresponding endpoint and signing service automatically; otherwise Mantle remains
26+
the default. Custom or proxy URLs likewise use Mantle signing by default; pass `endpoint="runtime"` when a custom host
27+
requires Runtime signing.
28+
29+
## Runtime Chat Completions
30+
31+
Use an inference-profile ID such as `us.openai.gpt-5.6-sol`, `us.openai.gpt-5.6-terra`, or
32+
`us.openai.gpt-5.6-luna`. These deployments do not accept the corresponding bare model ID. Global inference profiles,
33+
such as `global.openai.gpt-5.6-sol`, require an AWS account and permissions that allow the corresponding profile.
34+
35+
```python
36+
from openai import OpenAI
37+
from openai.providers import bedrock
38+
39+
client = OpenAI(
40+
provider=bedrock(
41+
endpoint="runtime",
42+
region="us-west-2",
43+
api_key=None,
44+
)
45+
)
46+
47+
completion = client.chat.completions.create(
48+
model="us.openai.gpt-5.6-sol",
49+
messages=[{"role": "user", "content": "Say hello!"}],
50+
)
51+
print(completion.choices[0].message.content)
52+
```
53+
54+
`api_key=None` prevents `AWS_BEARER_TOKEN_BEDROCK` from shadowing AWS credentials and forces SigV4 authentication. Omit
55+
`region` to use the normal environment or AWS profile region chain.
56+
57+
For streaming, set `stream=True`:
58+
59+
```python
60+
stream = client.chat.completions.create(
61+
model="us.openai.gpt-5.6-sol",
62+
messages=[{"role": "user", "content": "Say hello!"}],
63+
stream=True,
64+
)
65+
for chunk in stream:
66+
print(chunk.choices[0].delta.content or "", end="", flush=True)
67+
```
68+
69+
The asynchronous client uses the same provider configuration:
70+
71+
```python
72+
from openai import AsyncOpenAI
73+
74+
client = AsyncOpenAI(provider=bedrock(endpoint="runtime", region="us-west-2", api_key=None))
75+
completion = await client.chat.completions.create(
76+
model="us.openai.gpt-5.6-sol",
77+
messages=[{"role": "user", "content": "Say hello!"}],
78+
)
79+
```
80+
81+
See [`examples/bedrock_runtime.py`](examples/bedrock_runtime.py) for a runnable example supporting SigV4, bearer
82+
authentication, model selection, AWS profiles, and opt-in streaming.
83+
84+
## Authentication
85+
86+
Authentication is chosen in this order:
87+
88+
1. Explicit bearer credentials, static AWS credentials, a named profile, or an AWS credential provider.
89+
2. The bearer token in `AWS_BEARER_TOKEN_BEDROCK`, unless `api_key=None` disables this fallback.
90+
3. The default AWS credential chain.
91+
92+
Explicit bearer and AWS credential modes cannot be combined. A stale environment bearer token takes precedence over the
93+
implicit AWS credential chain; unset it or pass `api_key=None` when SigV4 is required.
94+
95+
### Bearer credentials
96+
97+
```python
98+
client = OpenAI(
99+
provider=bedrock(
100+
endpoint="runtime",
101+
region="us-west-2",
102+
api_key="your-bedrock-api-key",
103+
)
104+
)
105+
```
106+
107+
A callable token provider is invoked before every request attempt, including retries. `AsyncOpenAI` also accepts an
108+
asynchronous callable:
109+
110+
```python
111+
client = AsyncOpenAI(
112+
provider=bedrock(
113+
endpoint="runtime",
114+
region="us-west-2",
115+
token_provider=refresh_bedrock_token,
116+
)
117+
)
118+
```
119+
120+
### AWS credentials and profiles
121+
122+
Use the default AWS credential chain or select a named shared-config profile:
123+
124+
```python
125+
client = OpenAI(
126+
provider=bedrock(
127+
endpoint="runtime",
128+
profile="my-aws-profile",
129+
api_key=None,
130+
)
131+
)
132+
```
133+
134+
Temporary static credentials can include a session token:
135+
136+
```python
137+
client = OpenAI(
138+
provider=bedrock(
139+
endpoint="runtime",
140+
region="us-west-2",
141+
access_key_id="your-access-key",
142+
secret_access_key="your-secret-key",
143+
session_token="your-session-token",
144+
)
145+
)
146+
```
147+
148+
Pass `credential_provider` for refreshing AWS credentials. The provider is called for every signed request attempt.
149+
Signed requests require replayable request bodies and do not automatically follow redirects.
150+
151+
## API routes and support limitations
152+
153+
The SDK defaults to `/openai/v1`, matching
154+
[AWS's OpenAI model documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-openai.html).
155+
[AWS's Chat Completions documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-chat-completions-mantle.html)
156+
also describes a `/v1` Runtime route. Override `base_url` when a deployment requires that route:
157+
158+
```python
159+
client = OpenAI(
160+
provider=bedrock(
161+
endpoint="runtime",
162+
region="us-west-2",
163+
base_url="https://bedrock-runtime.us-west-2.amazonaws.com/v1",
164+
api_key=None,
165+
)
166+
)
167+
```
168+
169+
The provider exposes normal Chat Completions and Responses resources, but AWS determines which routes, models,
170+
inference profiles, authentication methods, and streaming features each deployment accepts. Runtime Responses and
171+
streaming should be live-validated for the selected model, profile, route, authentication mode, and AWS account.
172+
173+
Canonical AWS endpoints must use HTTPS and match the configured endpoint family and region. Bedrock credentials are
174+
never attached to a request whose origin differs from the configured API root. Explicitly configured custom or local
175+
HTTP proxies remain available when required; use them only inside a trusted environment.
176+
177+
## Opt-in live verification
178+
179+
The existing live harness requires explicit opt-in and valid AWS credentials:
180+
181+
```sh
182+
BEDROCK_LIVE_TEST=1 BEDROCK_LIVE_ENDPOINT=runtime AWS_REGION=us-west-2 \
183+
rye run pytest -q -s tests/lib/bedrock_live.py
184+
```
185+
186+
Runtime verification defaults to all three US GPT-5.6 inference profiles. Select authentication modes with
187+
`BEDROCK_LIVE_AUTHS=bearer,environment-bearer,token-provider,default-chain,profile,static`; select specific models with
188+
`BEDROCK_LIVE_MODELS`. Set `BEDROCK_LIVE_STREAM=1` to include streaming and `BEDROCK_LIVE_RESPONSES=1` to include
189+
Runtime Responses. Provide `AWS_PROFILE` or `BEDROCK_LIVE_PROFILE` for named-profile verification.

examples/bedrock_runtime.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Run Amazon Bedrock Runtime Chat Completions with bearer or AWS authentication.
2+
3+
AWS_REGION=us-west-2 python examples/bedrock_runtime.py
4+
AWS_REGION=us-west-2 BEDROCK_MODEL=us.openai.gpt-5.6-terra BEDROCK_STREAM=1 python examples/bedrock_runtime.py
5+
AWS_REGION=us-west-2 BEDROCK_AUTH=bearer python examples/bedrock_runtime.py
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import os
11+
12+
from openai import OpenAI
13+
from openai.providers import bedrock
14+
from openai.types.chat import ChatCompletionMessageParam
15+
16+
authentication = os.environ.get("BEDROCK_AUTH", "sigv4")
17+
region = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION")
18+
profile = os.environ.get("AWS_PROFILE") or None
19+
20+
if authentication == "bearer":
21+
token = os.environ.get("AWS_BEARER_TOKEN_BEDROCK")
22+
if not token:
23+
raise RuntimeError("Bearer authentication requires AWS_BEARER_TOKEN_BEDROCK.")
24+
provider = bedrock(endpoint="runtime", region=region, api_key=token)
25+
elif authentication == "sigv4":
26+
provider = bedrock(endpoint="runtime", region=region, profile=profile, api_key=None)
27+
else:
28+
raise RuntimeError("BEDROCK_AUTH must be either 'sigv4' or 'bearer'.")
29+
30+
client = OpenAI(provider=provider)
31+
model = os.environ.get("BEDROCK_MODEL", "us.openai.gpt-5.6-sol")
32+
messages: list[ChatCompletionMessageParam] = [{"role": "user", "content": "Say hello from Amazon Bedrock Runtime!"}]
33+
34+
if os.environ.get("BEDROCK_STREAM") == "1":
35+
stream = client.chat.completions.create(model=model, messages=messages, stream=True)
36+
for chunk in stream:
37+
print(chunk.choices[0].delta.content or "", end="", flush=True)
38+
print()
39+
else:
40+
completion = client.chat.completions.create(model=model, messages=messages)
41+
print(completion.choices[0].message.content)

scripts/utils/validate-bedrock-wheel.py

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,32 +42,41 @@ def handler(request):
4242
return httpx2.Response(200, request=request, json={})
4343
4444
45-
http_client = httpx2.Client(transport=httpx2.MockTransport(handler), trust_env=False)
46-
with OpenAI(
47-
provider=bedrock(region="us-east-1", api_key="bearer-token"),
48-
http_client=http_client,
49-
) as client:
50-
client.get("/models", cast_to=httpx2.Response)
51-
52-
assert requests[0].headers["Authorization"] == "Bearer bearer-token"
53-
assert not any(name == "botocore" or name.startswith("botocore.") for name in sys.modules)
45+
for endpoint, hostname in (
46+
("mantle", "bedrock-mantle.us-east-1.api.aws"),
47+
("runtime", "bedrock-runtime.us-east-1.amazonaws.com"),
48+
):
49+
requests.clear()
50+
http_client = httpx2.Client(transport=httpx2.MockTransport(handler), trust_env=False)
51+
with OpenAI(
52+
provider=bedrock(endpoint=endpoint, region="us-east-1", api_key="bearer-token"),
53+
http_client=http_client,
54+
) as client:
55+
client.get("/models", cast_to=httpx2.Response)
56+
57+
assert requests[0].url.host == hostname
58+
assert requests[0].headers["Authorization"] == "Bearer bearer-token"
59+
assert not any(name == "botocore" or name.startswith("botocore.") for name in sys.modules)
5460
5561
sys.meta_path.remove(blocker)
56-
requests.clear()
57-
http_client = httpx2.Client(transport=httpx2.MockTransport(handler), trust_env=False)
58-
with OpenAI(
59-
provider=bedrock(
60-
region="us-east-1",
61-
access_key_id="fixture-access-key",
62-
secret_access_key="fixture-secret-key",
63-
session_token="fixture-session-token",
64-
),
65-
http_client=http_client,
66-
) as client:
67-
client.get("/models", cast_to=httpx2.Response)
68-
69-
assert "Credential=fixture-access-key/" in requests[0].headers["Authorization"]
70-
assert requests[0].headers["X-Amz-Security-Token"] == "fixture-session-token"
62+
for endpoint, signing_service in (("mantle", "bedrock-mantle"), ("runtime", "bedrock")):
63+
requests.clear()
64+
http_client = httpx2.Client(transport=httpx2.MockTransport(handler), trust_env=False)
65+
with OpenAI(
66+
provider=bedrock(
67+
endpoint=endpoint,
68+
region="us-east-1",
69+
access_key_id="fixture-access-key",
70+
secret_access_key="fixture-secret-key",
71+
session_token="fixture-session-token",
72+
),
73+
http_client=http_client,
74+
) as client:
75+
client.get("/models", cast_to=httpx2.Response)
76+
77+
assert "Credential=fixture-access-key/" in requests[0].headers["Authorization"]
78+
assert f"/{signing_service}/aws4_request" in requests[0].headers["Authorization"]
79+
assert requests[0].headers["X-Amz-Security-Token"] == "fixture-session-token"
7180
"""
7281

7382

src/openai/lib/_bedrock_auth.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@ def _load_botocore() -> tuple[Any, Any, Any, Any]:
3737
"Install them with `pip install openai[bedrock]` and try again."
3838
) from exc
3939

40-
return SigV4Auth, AWSRequest, Credentials, Session
40+
return cast("tuple[Any, Any, Any, Any]", (SigV4Auth, AWSRequest, Credentials, Session))
4141

4242

4343
@dataclass(frozen=True)
4444
class BedrockAwsAuthConfig:
4545
region: str
4646
source: Literal["static", "profile", "provider", "default"]
47+
service: Literal["bedrock-mantle", "bedrock"] = "bedrock-mantle"
4748
region_source: Literal["explicit", "environment", "profile"] = "explicit"
4849
profile: str | None = None
4950
access_key_id: str | None = field(default=None, repr=False)
@@ -87,6 +88,7 @@ def resolve(
8788
secret_access_key: str | None,
8889
session_token: str | None,
8990
credentials_provider: AwsCredentialsProvider | None,
91+
service: Literal["bedrock-mantle", "bedrock"] = "bedrock-mantle",
9092
) -> BedrockAwsAuth:
9193
_, _, _, session_cls = _load_botocore()
9294

@@ -114,6 +116,7 @@ def resolve(
114116
config = BedrockAwsAuthConfig(
115117
region=resolved_region,
116118
source=source,
119+
service=service,
117120
region_source=region_source,
118121
profile=profile,
119122
access_key_id=access_key_id,
@@ -151,7 +154,7 @@ def sign(self, *, method: str, url: str, headers: Mapping[str, str], body: bytes
151154
data=body,
152155
headers=signed_headers,
153156
)
154-
self._sigv4_auth_cls(credentials, "bedrock-mantle", self.config.region).add_auth(aws_request)
157+
self._sigv4_auth_cls(credentials, self.config.service, self.config.region).add_auth(aws_request)
155158
except OpenAIError:
156159
raise
157160
except Exception as exc:

0 commit comments

Comments
 (0)