Skip to content

Commit 5be9536

Browse files
feat(client): add support for short-lived tokens (#1608)
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
1 parent f1fd4fa commit 5be9536

17 files changed

Lines changed: 1170 additions & 33 deletions

.stats.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 152
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-dd99495ad509338e6de862802839360dfe394d5cd6d6ba6d13fec8fca92328b8.yml
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-a6eca1bd01e0c434af356fe5275c206057216a4e626d1051d294c27016cd6d05.yml
33
openapi_spec_hash: 68abda9122013a9ae3f084cfdbe8e8c1
4-
config_hash: 5635033cdc8c930255f8b529a78de722
4+
config_hash: 4975e16a94e8f9901428022044131888

README.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,109 @@ to add `OPENAI_API_KEY="My API Key"` to your `.env` file
7171
so that your API key is not stored in source control.
7272
[Get an API key here](https://platform.openai.com/settings/organization/api-keys).
7373

74+
### Workload Identity Authentication
75+
76+
For secure, automated environments like cloud-managed Kubernetes, Azure, and Google Cloud Platform, you can use workload identity authentication with short-lived tokens from cloud identity providers instead of long-lived API keys.
77+
78+
#### Kubernetes (service account tokens)
79+
80+
```python
81+
from openai import OpenAI
82+
from openai.auth import k8s_service_account_token_provider
83+
84+
client = OpenAI(
85+
workload_identity={
86+
"client_id": "your-client-id",
87+
"identity_provider_id": "idp-123",
88+
"service_account_id": "sa-456",
89+
"provider": k8s_service_account_token_provider(
90+
"/var/run/secrets/kubernetes.io/serviceaccount/token"
91+
),
92+
},
93+
organization="org-xyz",
94+
project="proj-abc",
95+
)
96+
97+
response = client.chat.completions.create(
98+
model="gpt-4",
99+
messages=[{"role": "user", "content": "Hello!"}],
100+
)
101+
```
102+
103+
#### Azure (managed identity)
104+
105+
```python
106+
from openai import OpenAI
107+
from openai.auth import azure_managed_identity_token_provider
108+
109+
client = OpenAI(
110+
workload_identity={
111+
"client_id": "your-client-id",
112+
"identity_provider_id": "idp-123",
113+
"service_account_id": "sa-456",
114+
"provider": azure_managed_identity_token_provider(
115+
resource="https://management.azure.com/",
116+
),
117+
},
118+
)
119+
```
120+
121+
#### Google Cloud Platform (compute engine metadata)
122+
123+
```python
124+
from openai import OpenAI
125+
from openai.auth import gcp_id_token_provider
126+
127+
client = OpenAI(
128+
workload_identity={
129+
"client_id": "your-client-id",
130+
"identity_provider_id": "idp-123",
131+
"service_account_id": "sa-456",
132+
"provider": gcp_id_token_provider(audience="https://api.openai.com/v1"),
133+
},
134+
)
135+
```
136+
137+
#### Custom subject token provider
138+
139+
```python
140+
from openai import OpenAI
141+
142+
143+
def get_custom_token() -> str:
144+
return "your-jwt-token"
145+
146+
147+
client = OpenAI(
148+
workload_identity={
149+
"client_id": "your-client-id",
150+
"identity_provider_id": "idp-123",
151+
"service_account_id": "sa-456",
152+
"provider": {
153+
"token_type": "jwt",
154+
"get_token": get_custom_token,
155+
},
156+
}
157+
)
158+
```
159+
160+
You can also customize the token refresh buffer (default is 1200 seconds (20 minutes) before expiration):
161+
162+
```python
163+
from openai import OpenAI
164+
from openai.auth import k8s_service_account_token_provider
165+
166+
client = OpenAI(
167+
workload_identity={
168+
"client_id": "your-client-id",
169+
"identity_provider_id": "idp-123",
170+
"service_account_id": "sa-456",
171+
"provider": k8s_service_account_token_provider("/var/token"),
172+
"refresh_buffer_seconds": 120.0,
173+
}
174+
)
175+
```
176+
74177
### Vision
75178

76179
With an image URL:

api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ from openai.types import (
1111
FunctionDefinition,
1212
FunctionParameters,
1313
Metadata,
14+
OAuthErrorCode,
1415
Reasoning,
1516
ReasoningEffort,
1617
ResponseFormatJSONObject,

src/openai/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS
1717
from ._exceptions import (
1818
APIError,
19+
OAuthError,
1920
OpenAIError,
2021
ConflictError,
2122
NotFoundError,
@@ -57,6 +58,7 @@
5758
"APIResponseValidationError",
5859
"BadRequestError",
5960
"AuthenticationError",
61+
"OAuthError",
6062
"PermissionDeniedError",
6163
"NotFoundError",
6264
"ConflictError",

src/openai/_base_client.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
cast,
3131
overload,
3232
)
33-
from typing_extensions import Literal, override, get_origin
33+
from typing_extensions import Unpack, Literal, override, get_origin
3434

3535
import anyio
3636
import httpx
@@ -81,6 +81,7 @@
8181
)
8282
from ._streaming import Stream, SSEDecoder, AsyncStream, SSEBytesDecoder
8383
from ._exceptions import (
84+
OpenAIError,
8485
APIStatusError,
8586
APITimeoutError,
8687
APIConnectionError,
@@ -936,6 +937,15 @@ def _prepare_request(
936937
"""
937938
return None
938939

940+
def _send_request(
941+
self,
942+
request: httpx.Request,
943+
*,
944+
stream: bool,
945+
**kwargs: Unpack[HttpxSendArgs],
946+
) -> httpx.Response:
947+
return self._client.send(request, stream=stream, **kwargs)
948+
939949
@overload
940950
def request(
941951
self,
@@ -1006,7 +1016,7 @@ def request(
10061016

10071017
response = None
10081018
try:
1009-
response = self._client.send(
1019+
response = self._send_request(
10101020
request,
10111021
stream=stream or self._should_stream_response_body(request=request),
10121022
**kwargs,
@@ -1025,6 +1035,9 @@ def request(
10251035

10261036
log.debug("Raising timeout error")
10271037
raise APITimeoutError(request=request) from err
1038+
except OpenAIError as err:
1039+
# Propagate OpenAIErrors as-is, without retrying or wrapping in APIConnectionError
1040+
raise err
10281041
except Exception as err:
10291042
log.debug("Encountered Exception", exc_info=True)
10301043

@@ -1530,6 +1543,15 @@ async def _prepare_request(
15301543
"""
15311544
return None
15321545

1546+
async def _send_request(
1547+
self,
1548+
request: httpx.Request,
1549+
*,
1550+
stream: bool,
1551+
**kwargs: Unpack[HttpxSendArgs],
1552+
) -> httpx.Response:
1553+
return await self._client.send(request, stream=stream, **kwargs)
1554+
15331555
@overload
15341556
async def request(
15351557
self,
@@ -1605,7 +1627,7 @@ async def request(
16051627

16061628
response = None
16071629
try:
1608-
response = await self._client.send(
1630+
response = await self._send_request(
16091631
request,
16101632
stream=stream or self._should_stream_response_body(request=request),
16111633
**kwargs,
@@ -1624,6 +1646,9 @@ async def request(
16241646

16251647
log.debug("Raising timeout error")
16261648
raise APITimeoutError(request=request) from err
1649+
except OpenAIError as err:
1650+
# Propagate OpenAIErrors as-is, without retrying or wrapping in APIConnectionError
1651+
raise err
16271652
except Exception as err:
16281653
log.debug("Encountered Exception", exc_info=True)
16291654

0 commit comments

Comments
 (0)