Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,41 @@ In addition to the options provided in the base `OpenAI` client, the following o

An example of using the client with Microsoft Entra ID (formerly known as Azure Active Directory) can be found [here](https://github.com/openai/openai-python/blob/main/examples/azure_ad.py).

## AWS Bedrock Mantle

To use this library with [AWS Bedrock Mantle](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html), use the `AwsOpenAI`
class instead of the `OpenAI` class.

> [!IMPORTANT]
> This requires `botocore` to be installed for SigV4 request signing. Install it with: `pip install 'openai[aws]'`

```py
from openai import AwsOpenAI

# uses the default botocore credential chain (env vars, ~/.aws/credentials, IAM role, etc.)
client = AwsOpenAI(
region="us-west-2",
)

response = client.responses.create(
model="openai.gpt-oss-120b",
input=[
{
"role": "user",
"content": "How do I output all files in a directory using Python?",
},
],
)
print(response.output_text)
```

In addition to the options provided in the base `OpenAI` client, the following options are provided:

- `region` (or the `AWS_REGION` / `AWS_DEFAULT_REGION` environment variable)
- `credential_provider` - a callable that returns credentials with `access_key`, `secret_key`, and optional `token` attributes

An example of using the client with a custom credential provider and STS assume-role refresh can be found [here](https://github.com/openai/openai-python/blob/main/examples/aws_credential_provider.py).

## Versioning

This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
Expand Down
80 changes: 80 additions & 0 deletions examples/aws_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Example: Using AwsOpenAI (sync) and AsyncAwsOpenAI (async) with SigV4 signing.

Requires:
- botocore installed (pip install botocore)
- AWS credentials configured (env vars, ~/.aws/credentials, IAM role, etc.)
- AWS_REGION or AWS_DEFAULT_REGION set (or pass region= explicitly)

Run:
export AWS_REGION=us-west-2
PYTHONPATH=src python3 examples/aws_client.py
"""

import asyncio

from openai.lib.aws import AwsOpenAI, AsyncAwsOpenAI

# --- Synchronous usage ---

client = AwsOpenAI(region="us-west-2")

response = client.chat.completions.create(
model="openai.gpt-oss-120b",
messages=[{"role": "user", "content": "Hello, how are you?"}],
)

print("Sync:", response.choices[0].message.content)


# --- Asynchronous usage ---


async def main() -> None:
async_client = AsyncAwsOpenAI(region="us-west-2")

response = await async_client.chat.completions.create(
model="openai.gpt-oss-120b",
messages=[{"role": "user", "content": "Hello from async!"}],
)

print("Async:", response.choices[0].message.content)


asyncio.run(main())


# --- Streaming usage (sync) ---

print("\nStreaming: ", end="")
stream = client.chat.completions.create(
model="openai.gpt-oss-120b",
messages=[{"role": "user", "content": "Count from 1 to 5."}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
print()


# --- Streaming usage (async) ---


async def stream_async() -> None:
async_client = AsyncAwsOpenAI(region="us-west-2")

print("Async streaming: ", end="")
stream = await async_client.chat.completions.create(
model="openai.gpt-oss-120b",
messages=[{"role": "user", "content": "Count from 1 to 5."}],
stream=True,
)
async for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
print()


asyncio.run(stream_async())
144 changes: 144 additions & 0 deletions examples/aws_credential_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""Example: Using AwsOpenAI with a custom credential provider and auto-refresh.

This shows how to:
1. Use a custom credential provider that returns fresh credentials on each call
2. Use botocore's RefreshableCredentials for automatic STS assume-role refresh
3. Use an async credential provider with AsyncAwsOpenAI

Requires:
- botocore installed (pip install botocore)
- boto3 installed (pip install boto3) — for the STS assume-role example
- AWS credentials configured for the initial session
- AWS_REGION or AWS_DEFAULT_REGION set (or pass region= explicitly)

Run:
export AWS_REGION=us-west-2
PYTHONPATH=src python3 examples/aws_credential_provider.py
"""

from __future__ import annotations

import asyncio
from typing import Any, Callable
from dataclasses import dataclass

from openai.lib.aws import AwsOpenAI, AsyncAwsOpenAI

# ---------------------------------------------------------------------------
# 1. Simple custom credential provider
# ---------------------------------------------------------------------------


@dataclass
class MyCredentials:
"""Minimal object satisfying the Credentials protocol."""

access_key: str
secret_key: str
token: str | None = None


def my_credential_provider() -> MyCredentials:
"""Return credentials from your own secret store, vault, etc.

This callable is invoked before every request, so returning fresh
credentials here is all you need for auto-refresh.
"""
# Replace with your actual credential fetching logic
return MyCredentials(
access_key="AKIA...",
secret_key="wJalr...",
token="FwoGZX...", # optional session token
)


client = AwsOpenAI(
region="us-west-2",
credential_provider=my_credential_provider,
)

response = client.chat.completions.create(
model="openai.gpt-oss-120b",
messages=[{"role": "user", "content": "Hello from custom credentials!"}],
)
print("Custom provider:", response.choices[0].message.content)


# ---------------------------------------------------------------------------
# 2. Auto-refreshing STS assume-role credentials via botocore
# ---------------------------------------------------------------------------


def make_sts_credential_provider(role_arn: str, session_name: str = "bedrock-mantle") -> Callable[[], Any]:
"""Create a credential provider that assumes an IAM role and auto-refreshes.

botocore's RefreshableCredentials handles expiry checks and refresh
transparently — accessing .access_key / .secret_key / .token on the
returned object triggers a refresh if the credentials are expired.
"""
import botocore.session # type: ignore[import-untyped, import-not-found]
import botocore.credentials # type: ignore[import-untyped, import-not-found]

session: Any = botocore.session.get_session() # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
sts: Any = session.create_client("sts") # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]

def fetch_credentials() -> dict[str, Any]:
resp: Any = sts.assume_role(RoleArn=role_arn, RoleSessionName=session_name)["Credentials"] # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
return {
"access_key": resp["AccessKeyId"],
"secret_key": resp["SecretAccessKey"],
"token": resp["SessionToken"],
"expiry_time": resp["Expiration"].isoformat(), # pyright: ignore[reportUnknownMemberType]
}

refreshable: Any = botocore.credentials.RefreshableCredentials.create_from_metadata( # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
metadata=fetch_credentials(),
refresh_using=fetch_credentials,
method="sts-assume-role",
)

# Return a provider that gives back the refreshable object.
# Accessing its attributes auto-refreshes when expired.
def provider() -> Any:
return refreshable # pyright: ignore[reportUnknownVariableType]

return provider


# Uncomment to use:
# sts_client = AwsOpenAI(
# region="us-west-2",
# credential_provider=make_sts_credential_provider("arn:aws:iam::123456789012:role/MyRole"),
# )


# ---------------------------------------------------------------------------
# 3. Async credential provider
# ---------------------------------------------------------------------------


async def async_credential_provider() -> MyCredentials:
"""An async provider — useful when credentials come from an async API."""
# Simulate async credential fetch (e.g., from an async HTTP vault client)
await asyncio.sleep(0)
return MyCredentials(
access_key="AKIA...",
secret_key="wJalr...",
token="FwoGZX...",
)


async def main() -> None:
async_client = AsyncAwsOpenAI(
region="us-west-2",
credential_provider=async_credential_provider,
)

response = await async_client.chat.completions.create(
model="openai.gpt-oss-120b",
messages=[{"role": "user", "content": "Hello from async credentials!"}],
)
print("Async provider:", response.choices[0].message.content)


asyncio.run(main())
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"]
realtime = ["websockets >= 13, < 16"]
datalib = ["numpy >= 1", "pandas >= 1.2.3", "pandas-stubs >= 1.1.0.11"]
voice_helpers = ["sounddevice>=0.5.1", "numpy>=2.0.2"]
aws = ["botocore >= 1.29.0"]

[tool.rye]
managed = true
Expand All @@ -68,6 +69,7 @@ dev-dependencies = [
"rich>=13.7.1",
"inline-snapshot>=0.28.0",
"azure-identity >=1.14.1",
"botocore >=1.29.0",
"types-tqdm > 4",
"types-pyaudio > 0",
"trio >=0.22.2",
Expand Down
4 changes: 4 additions & 0 deletions src/openai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@
from ._utils._resources_proxy import resources as resources

from .lib import azure as _azure, pydantic_function_tool as pydantic_function_tool
from .lib.aws import (
AwsOpenAI as AwsOpenAI,
AsyncAwsOpenAI as AsyncAwsOpenAI,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/openai/__init__.py is Stainless-generated — the AwsOpenAI / AsyncAwsOpenAI exports added there would be dropped on the next generated release unless they're added to the generator config (similar to how the AzureOpenAI exports persist today). The pyproject.toml change for the [aws] extra may have the same concern.

@apcha-oai Could you advise on how you'd like the __init__.py exports and pyproject.toml dependency handled? Should these go through the Stainless generator config, or is there a preferred approach for adding new provider integrations?

from .version import VERSION as VERSION
from .lib.azure import AzureOpenAI as AzureOpenAI, AsyncAzureOpenAI as AsyncAzureOpenAI
from .lib._old_api import *
Expand Down
Loading