aireserve-sdk 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aireserve_sdk-0.7.0/LICENSE +18 -0
- aireserve_sdk-0.7.0/PKG-INFO +515 -0
- aireserve_sdk-0.7.0/README.md +474 -0
- aireserve_sdk-0.7.0/pyproject.toml +39 -0
- aireserve_sdk-0.7.0/setup.cfg +4 -0
- aireserve_sdk-0.7.0/src/aireserve/__init__.py +63 -0
- aireserve_sdk-0.7.0/src/aireserve/_client.py +646 -0
- aireserve_sdk-0.7.0/src/aireserve/_compat.py +256 -0
- aireserve_sdk-0.7.0/src/aireserve/_gpu.py +389 -0
- aireserve_sdk-0.7.0/src/aireserve/_mapping.py +439 -0
- aireserve_sdk-0.7.0/src/aireserve/_sse.py +280 -0
- aireserve_sdk-0.7.0/src/aireserve/_version.py +2 -0
- aireserve_sdk-0.7.0/src/aireserve/exceptions.py +350 -0
- aireserve_sdk-0.7.0/src/aireserve/py.typed +0 -0
- aireserve_sdk-0.7.0/src/aireserve_sdk.egg-info/PKG-INFO +515 -0
- aireserve_sdk-0.7.0/src/aireserve_sdk.egg-info/SOURCES.txt +21 -0
- aireserve_sdk-0.7.0/src/aireserve_sdk.egg-info/dependency_links.txt +1 -0
- aireserve_sdk-0.7.0/src/aireserve_sdk.egg-info/top_level.txt +2 -0
- aireserve_sdk-0.7.0/src/audacity/__init__.py +23 -0
- aireserve_sdk-0.7.0/src/audacity/py.typed +0 -0
- aireserve_sdk-0.7.0/tests/test_compat.py +411 -0
- aireserve_sdk-0.7.0/tests/test_gpu.py +259 -0
- aireserve_sdk-0.7.0/tests/test_sdk.py +2629 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Audacity Investments SDK License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Audacity Investments. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software is proprietary to Audacity Investments. Permission is granted
|
|
6
|
+
to download, install, and use this software solely to access services
|
|
7
|
+
provided by Audacity Investments, subject to your agreement with Audacity
|
|
8
|
+
Investments.
|
|
9
|
+
|
|
10
|
+
You may not modify, distribute, sublicense, or create derivative works of
|
|
11
|
+
this software, in whole or in part, except as expressly permitted in
|
|
12
|
+
writing by Audacity Investments.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
17
|
+
AUDACITY INVESTMENTS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY
|
|
18
|
+
ARISING FROM THE USE OF THE SOFTWARE.
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aireserve-sdk
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: AI Reserve SDK — Bedrock-parity client for the AI Reserve LLM gateway
|
|
5
|
+
Author-email: AI Reserve <eng@aireserve.com>
|
|
6
|
+
License: Audacity Investments SDK License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Audacity Investments. All rights reserved.
|
|
9
|
+
|
|
10
|
+
This software is proprietary to Audacity Investments. Permission is granted
|
|
11
|
+
to download, install, and use this software solely to access services
|
|
12
|
+
provided by Audacity Investments, subject to your agreement with Audacity
|
|
13
|
+
Investments.
|
|
14
|
+
|
|
15
|
+
You may not modify, distribute, sublicense, or create derivative works of
|
|
16
|
+
this software, in whole or in part, except as expressly permitted in
|
|
17
|
+
writing by Audacity Investments.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
22
|
+
AUDACITY INVESTMENTS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY
|
|
23
|
+
ARISING FROM THE USE OF THE SOFTWARE.
|
|
24
|
+
|
|
25
|
+
Project-URL: Homepage, https://portal.aireserve.com
|
|
26
|
+
Project-URL: Repository, https://github.com/Audacity-Investments/aireserve-sdk
|
|
27
|
+
Keywords: aireserve,llm,ai,bedrock
|
|
28
|
+
Classifier: Programming Language :: Python :: 3
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Typing :: Typed
|
|
37
|
+
Requires-Python: >=3.9
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# aireserve-sdk
|
|
43
|
+
|
|
44
|
+
Python client for the AI Reserve LLM gateway. Exposes the same
|
|
45
|
+
**Amazon Bedrock Converse** surface so teams migrating off Bedrock can swap
|
|
46
|
+
the client constructor and keep the rest of their call-sites unchanged.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install aireserve-sdk
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
> **Renamed from `audacity-sdk` / `@audacity/sdk` in 0.7.0.** The old package
|
|
57
|
+
> names stop receiving updates. `from audacity import Audacity` and
|
|
58
|
+
> `new Audacity(...)` keep working for one release with a deprecation warning;
|
|
59
|
+
> migrate to `aireserve` / `Aireserve`. The legacy `AUDACITY_API_KEY` and
|
|
60
|
+
> `AUDACITY_BASE_URL` environment variables remain accepted as fallbacks.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
Zero runtime dependencies — stdlib only. Requires Python 3.9+.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
### Non-streaming (Converse)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from aireserve import Aireserve
|
|
73
|
+
|
|
74
|
+
client = Aireserve(api_key="aireserve_api_…") # or set AIRESERVE_API_KEY
|
|
75
|
+
|
|
76
|
+
response = client.converse(
|
|
77
|
+
modelId="gpt-5.4-mini",
|
|
78
|
+
messages=[{"role": "user", "content": [{"text": "What is 2+2?"}]}],
|
|
79
|
+
inferenceConfig={"maxTokens": 256, "temperature": 0.0},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
print(response["output"]["message"]["content"][0]["text"])
|
|
83
|
+
print(response["stopReason"]) # "end_turn"
|
|
84
|
+
print(response["usage"]) # {"inputTokens": …, "outputTokens": …, "totalTokens": …}
|
|
85
|
+
print(response["metrics"]) # {"latencyMs": …}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Streaming (ConverseStream)
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from aireserve import Aireserve
|
|
92
|
+
|
|
93
|
+
client = Aireserve()
|
|
94
|
+
|
|
95
|
+
stream_response = client.converse_stream(
|
|
96
|
+
modelId="gpt-5.4-mini",
|
|
97
|
+
messages=[{"role": "user", "content": [{"text": "Write me a haiku."}]}],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
for event in stream_response["stream"]: # boto3 parity: response["stream"]
|
|
101
|
+
if "contentBlockDelta" in event:
|
|
102
|
+
delta = event["contentBlockDelta"]["delta"]
|
|
103
|
+
print(delta.get("text", ""), end="", flush=True)
|
|
104
|
+
|
|
105
|
+
print() # newline at end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Migrating from boto3 bedrock-runtime
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# BEFORE — boto3
|
|
114
|
+
import boto3
|
|
115
|
+
client = boto3.client("bedrock-runtime", region_name="us-east-1")
|
|
116
|
+
|
|
117
|
+
response = client.converse(
|
|
118
|
+
modelId="anthropic.claude-3-sonnet-20240229-v1:0",
|
|
119
|
+
messages=[{"role": "user", "content": [{"text": "Hi"}]}],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# AFTER — AI Reserve SDK
|
|
123
|
+
from aireserve import Aireserve
|
|
124
|
+
client = Aireserve(api_key="aireserve_api_…") # only line that changes
|
|
125
|
+
|
|
126
|
+
response = client.converse(
|
|
127
|
+
modelId="gpt-5.4-mini", # use AI Reserve model ID
|
|
128
|
+
messages=[{"role": "user", "content": [{"text": "Hi"}]}],
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Streaming diff:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
# BEFORE — boto3
|
|
136
|
+
stream_resp = client.converse_stream(modelId=…, messages=…)
|
|
137
|
+
for event in stream_resp["stream"]:
|
|
138
|
+
…
|
|
139
|
+
|
|
140
|
+
# AFTER — AI Reserve SDK (identical call-site)
|
|
141
|
+
stream_resp = client.converse_stream(modelId=…, messages=…)
|
|
142
|
+
for event in stream_resp["stream"]:
|
|
143
|
+
…
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Tool use
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
response = client.converse(
|
|
152
|
+
modelId="gpt-5.4-mini",
|
|
153
|
+
messages=[{"role": "user", "content": [{"text": "What's the weather in NYC?"}]}],
|
|
154
|
+
toolConfig={
|
|
155
|
+
"tools": [{
|
|
156
|
+
"toolSpec": {
|
|
157
|
+
"name": "get_weather",
|
|
158
|
+
"description": "Get current weather for a city",
|
|
159
|
+
"inputSchema": {
|
|
160
|
+
"json": {
|
|
161
|
+
"type": "object",
|
|
162
|
+
"properties": {"city": {"type": "string"}},
|
|
163
|
+
"required": ["city"],
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
}],
|
|
168
|
+
"toolChoice": {"auto": {}},
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Assistant responds with a tool call
|
|
173
|
+
tool_use = response["output"]["message"]["content"][0]["toolUse"]
|
|
174
|
+
print(tool_use["name"]) # "get_weather"
|
|
175
|
+
print(tool_use["input"]) # {"city": "NYC"}
|
|
176
|
+
|
|
177
|
+
# Send tool result back
|
|
178
|
+
response2 = client.converse(
|
|
179
|
+
modelId="gpt-5.4-mini",
|
|
180
|
+
messages=[
|
|
181
|
+
{"role": "user", "content": [{"text": "What's the weather in NYC?"}]},
|
|
182
|
+
{"role": "assistant", "content": response["output"]["message"]["content"]},
|
|
183
|
+
{"role": "user", "content": [
|
|
184
|
+
{"toolResult": {
|
|
185
|
+
"toolUseId": tool_use["toolUseId"],
|
|
186
|
+
"content": [{"text": "Sunny, 72°F"}],
|
|
187
|
+
}}
|
|
188
|
+
]},
|
|
189
|
+
],
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Images (vision models)
|
|
196
|
+
|
|
197
|
+
Bedrock-style `image` content blocks are supported in user messages. Pass raw
|
|
198
|
+
bytes (encoded for you) or a URL (AI Reserve extension):
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
with open("chart.png", "rb") as f:
|
|
202
|
+
image_bytes = f.read()
|
|
203
|
+
|
|
204
|
+
response = client.converse(
|
|
205
|
+
modelId="gpt-5.5",
|
|
206
|
+
messages=[{
|
|
207
|
+
"role": "user",
|
|
208
|
+
"content": [
|
|
209
|
+
{"text": "What does this chart show?"},
|
|
210
|
+
{"image": {"format": "png", "source": {"bytes": image_bytes}}},
|
|
211
|
+
],
|
|
212
|
+
}],
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Or reference a hosted image directly (not available in Bedrock):
|
|
216
|
+
# {"image": {"format": "jpeg", "source": {"url": "https://example.com/photo.jpg"}}}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`format` is one of `png`, `jpeg`, `gif`, `webp`. Use a vision-capable model.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Video (Gemini models)
|
|
224
|
+
|
|
225
|
+
Bedrock-style `video` content blocks are supported in user messages. Video input
|
|
226
|
+
is only available on the Gemini family (`gemini-2.5-flash`, `gemini-2.5-pro`,
|
|
227
|
+
`gemini-3-flash-preview`); every other model rejects video with an HTTP 400.
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
with open("demo.mp4", "rb") as f:
|
|
231
|
+
video_bytes = f.read()
|
|
232
|
+
|
|
233
|
+
response = client.converse(
|
|
234
|
+
modelId="gemini-2.5-flash",
|
|
235
|
+
messages=[{
|
|
236
|
+
"role": "user",
|
|
237
|
+
"content": [
|
|
238
|
+
{"text": "Summarize what happens in this video."},
|
|
239
|
+
{"video": {"format": "mp4", "source": {"bytes": video_bytes}}},
|
|
240
|
+
],
|
|
241
|
+
}],
|
|
242
|
+
)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`format` is one of `mp4`, `mov`, `mkv`, `webm`, `flv`, `mpeg`, `mpg`, `wmv`,
|
|
246
|
+
`three_gp`. Inline (`bytes`) video is base64-encoded into the request — keep
|
|
247
|
+
those files under ~20 MB.
|
|
248
|
+
|
|
249
|
+
For larger files (up to 1 GB), upload once and reference by URI:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
upload = client.files.upload("large-demo.mp4", content_type="video/mp4")
|
|
253
|
+
|
|
254
|
+
response = client.converse(
|
|
255
|
+
modelId="gemini-2.5-flash",
|
|
256
|
+
messages=[{
|
|
257
|
+
"role": "user",
|
|
258
|
+
"content": [
|
|
259
|
+
{"text": "Summarize this video."},
|
|
260
|
+
{"video": {"format": "mp4", "source": {"uri": upload["uri"]}}},
|
|
261
|
+
],
|
|
262
|
+
}],
|
|
263
|
+
)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`client.files.upload(data, content_type=…)` accepts raw bytes or a file path
|
|
267
|
+
and returns `{"file_id", "upload_url", "uri", "expires_at"}`. Uploads are
|
|
268
|
+
**resumable**: the helper streams the file in 8 MB chunks over a GCS resumable
|
|
269
|
+
session and automatically resumes from the last confirmed byte after a network
|
|
270
|
+
drop (bounded retries). Uploaded files are transient inference inputs: they
|
|
271
|
+
expire after ~24 hours and are scoped to your API key's organization; the
|
|
272
|
+
signed upload URL itself is valid ~15 minutes.
|
|
273
|
+
|
|
274
|
+
To control video token cost, set `mediaResolution` on the request —
|
|
275
|
+
`"low"` processes video at ~4x fewer tokens (Gemini models; ignored elsewhere):
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
response = client.converse(
|
|
279
|
+
modelId="gemini-2.5-flash",
|
|
280
|
+
mediaResolution="low",
|
|
281
|
+
messages=[…],
|
|
282
|
+
)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Image generation
|
|
288
|
+
|
|
289
|
+
Generate images from a text prompt with `client.images.generate`. With
|
|
290
|
+
`response_format="b64_json"` the image bytes come back inline:
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
import base64
|
|
294
|
+
|
|
295
|
+
result = client.images.generate(
|
|
296
|
+
model="gpt-image-1",
|
|
297
|
+
prompt="A watercolor painting of a fox in a snowy forest",
|
|
298
|
+
size="1024x1024",
|
|
299
|
+
response_format="b64_json",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
with open("fox.png", "wb") as f:
|
|
303
|
+
f.write(base64.b64decode(result["data"][0]["b64_json"]))
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
With `response_format="url"` (the default) the gateway stores the image and
|
|
307
|
+
returns a signed download link that expires after ~24 hours:
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
import urllib.request
|
|
311
|
+
|
|
312
|
+
result = client.images.generate(
|
|
313
|
+
model="gemini-2.5-flash-image",
|
|
314
|
+
prompt="A watercolor painting of a fox in a snowy forest",
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
urllib.request.urlretrieve(result["data"][0]["url"], "fox.png")
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Optional parameters: `n` (1–10 images), `size` (`"WxH"`, model-dependent),
|
|
321
|
+
`quality` (e.g. `"standard"`, `"hd"`), and `user`. The response dict carries
|
|
322
|
+
`created`, `data` (each entry has `url` or `b64_json`, plus `revised_prompt`
|
|
323
|
+
when the provider rewrites your prompt) and optional `usage` token counts.
|
|
324
|
+
Errors map to the same exception classes as `converse` (401 →
|
|
325
|
+
`AccessDeniedException`, 429 → `ThrottlingException`, spend cap →
|
|
326
|
+
`ServiceQuotaExceededException`).
|
|
327
|
+
|
|
328
|
+
### Image models
|
|
329
|
+
|
|
330
|
+
| Model | Pricing |
|
|
331
|
+
|---|---|
|
|
332
|
+
| `gemini-2.5-flash-image` | token-based (≈ $0.039 / image) |
|
|
333
|
+
| `gemini-3.1-flash-image` | token-based + $0.067 / image at 1K resolution |
|
|
334
|
+
| `gpt-image-1` | token-based ($5.00 / 1M text input, $40.00 / 1M image output tokens) |
|
|
335
|
+
|
|
336
|
+
Per-image models bill a flat rate per generated image; token-based models
|
|
337
|
+
report token counts in the response `usage` field. Each request's cost is
|
|
338
|
+
recorded against your key like any other API call.
|
|
339
|
+
|
|
340
|
+
> **Reliability note.** Upstream image backends occasionally stall with a 503
|
|
341
|
+
> for a few minutes. There is deliberately **no automatic fallback** to a
|
|
342
|
+
> different image model (silently swapping models would change output style
|
|
343
|
+
> and quality) — the SDK already retries 503s with backoff up to
|
|
344
|
+
> `max_retries`, and callers should retry beyond that rather than switch
|
|
345
|
+
> models.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Prompt caching
|
|
350
|
+
|
|
351
|
+
Place a Bedrock-style `cachePoint` block after the stable prefix you want the
|
|
352
|
+
provider to cache (system prompt, large documents). Everything up to the cache
|
|
353
|
+
point is cached provider-side on Claude models; OpenAI/Gemini models cache
|
|
354
|
+
automatically and ignore the marker. At most 4 cache points per request.
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
response = client.converse(
|
|
358
|
+
modelId="claude-sonnet-4-5",
|
|
359
|
+
system=[
|
|
360
|
+
{"text": long_system_prompt},
|
|
361
|
+
{"cachePoint": {"type": "default"}},
|
|
362
|
+
],
|
|
363
|
+
messages=[{
|
|
364
|
+
"role": "user",
|
|
365
|
+
"content": [
|
|
366
|
+
{"text": big_reference_document},
|
|
367
|
+
{"cachePoint": {"type": "default"}},
|
|
368
|
+
{"text": "Summarise the key risks."},
|
|
369
|
+
],
|
|
370
|
+
}],
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Cache activity is reported in usage (Bedrock names):
|
|
374
|
+
print(response["usage"]["cacheReadInputTokens"]) # tokens served from cache
|
|
375
|
+
print(response["usage"]["cacheWriteInputTokens"]) # tokens written to cache
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
A `cachePoint` with nothing before it in the same message is silently ignored.
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## OpenAI & Anthropic wire formats (pass-through)
|
|
383
|
+
|
|
384
|
+
Prefer the OpenAI or Anthropic request shapes over Bedrock Converse? The same
|
|
385
|
+
client exposes both gateway-native formats directly — same key, same retry
|
|
386
|
+
policy, same exceptions, **no shape translation** (requests sent verbatim,
|
|
387
|
+
responses returned raw). Both formats work with every gateway model; the
|
|
388
|
+
gateway bridges the wire format for you.
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
# OpenAI format → POST /v1/chat/completions
|
|
392
|
+
response = client.chat.completions.create(
|
|
393
|
+
model="gpt-5.4-mini",
|
|
394
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
395
|
+
max_tokens=256,
|
|
396
|
+
)
|
|
397
|
+
print(response["choices"][0]["message"]["content"])
|
|
398
|
+
|
|
399
|
+
# Streaming: raw OpenAI chunks
|
|
400
|
+
for chunk in client.chat.completions.create(
|
|
401
|
+
model="claude-sonnet-4-6", # any gateway model
|
|
402
|
+
messages=[{"role": "user", "content": "Write a haiku."}],
|
|
403
|
+
stream=True,
|
|
404
|
+
):
|
|
405
|
+
delta = chunk["choices"][0]["delta"] if chunk.get("choices") else {}
|
|
406
|
+
print(delta.get("content", ""), end="", flush=True)
|
|
407
|
+
|
|
408
|
+
# Anthropic format → POST /v1/messages (Claude Code's wire format)
|
|
409
|
+
response = client.messages.create(
|
|
410
|
+
model="claude-sonnet-4-6",
|
|
411
|
+
max_tokens=256,
|
|
412
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
413
|
+
)
|
|
414
|
+
print(response["content"][0]["text"])
|
|
415
|
+
|
|
416
|
+
# Streaming: raw Anthropic events (message_start … message_stop)
|
|
417
|
+
for event in client.messages.create(
|
|
418
|
+
model="claude-sonnet-4-6",
|
|
419
|
+
max_tokens=256,
|
|
420
|
+
messages=[{"role": "user", "content": "Write a haiku."}],
|
|
421
|
+
stream=True,
|
|
422
|
+
):
|
|
423
|
+
if event["type"] == "content_block_delta":
|
|
424
|
+
print(event["delta"].get("text", ""), end="", flush=True)
|
|
425
|
+
|
|
426
|
+
# Free token counting → POST /v1/messages/count_tokens
|
|
427
|
+
count = client.messages.count_tokens(
|
|
428
|
+
model="claude-sonnet-4-6",
|
|
429
|
+
messages=[{"role": "user", "content": "How many tokens is this?"}],
|
|
430
|
+
)
|
|
431
|
+
print(count["input_tokens"])
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Fields are passed through untouched, so anything the gateway supports works —
|
|
435
|
+
no SDK release needed for new request fields. (Using the official `openai` or
|
|
436
|
+
`anthropic` packages instead also works: point their `base_url` at the
|
|
437
|
+
gateway — see the developer docs.)
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Error handling
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
from aireserve import Aireserve
|
|
445
|
+
from aireserve.exceptions import (
|
|
446
|
+
MissingApiKeyError,
|
|
447
|
+
AccessDeniedException,
|
|
448
|
+
ThrottlingException,
|
|
449
|
+
ServiceQuotaExceededException,
|
|
450
|
+
ModelStreamErrorException,
|
|
451
|
+
SdkError,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
client = Aireserve()
|
|
455
|
+
|
|
456
|
+
try:
|
|
457
|
+
response = client.converse(modelId="gpt-5.4-mini", messages=[…])
|
|
458
|
+
except client.exceptions.ThrottlingException as e:
|
|
459
|
+
print(f"Rate limited: {e.message}, retry after {e.retry_after_seconds}s")
|
|
460
|
+
except client.exceptions.AccessDeniedException as e:
|
|
461
|
+
print(f"Auth error [{e.status_code}]: {e.message}")
|
|
462
|
+
except client.exceptions.ServiceQuotaExceededException as e:
|
|
463
|
+
print(f"Budget exhausted: {e.message}")
|
|
464
|
+
except SdkError as e:
|
|
465
|
+
print(f"Network/decode error: {e.message}")
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Streaming errors surface when you iterate the stream:
|
|
469
|
+
|
|
470
|
+
```python
|
|
471
|
+
try:
|
|
472
|
+
for event in stream_response["stream"]:
|
|
473
|
+
…
|
|
474
|
+
except client.exceptions.ModelStreamErrorException as e:
|
|
475
|
+
print(f"Stream broken: {e.message}")
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## Configuration & environment variables
|
|
481
|
+
|
|
482
|
+
| Constructor parameter | Environment variable | Default |
|
|
483
|
+
|---|---|---|
|
|
484
|
+
| `api_key` | `AIRESERVE_API_KEY` | — (required) |
|
|
485
|
+
| `base_url` | `AIRESERVE_BASE_URL` | `https://api.aireserve.com` |
|
|
486
|
+
| `timeout` | — | `120.0` seconds |
|
|
487
|
+
| `max_retries` | — | `2` (3 total attempts) |
|
|
488
|
+
|
|
489
|
+
The legacy `AUDACITY_API_KEY` / `AUDACITY_BASE_URL` names still work as
|
|
490
|
+
fallbacks; when both are set, the `AIRESERVE_*` names win.
|
|
491
|
+
|
|
492
|
+
```python
|
|
493
|
+
client = Aireserve(
|
|
494
|
+
api_key="aireserve_api_…",
|
|
495
|
+
base_url="https://api.aireserve.com",
|
|
496
|
+
timeout=60.0,
|
|
497
|
+
max_retries=3,
|
|
498
|
+
)
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Retry behaviour
|
|
504
|
+
|
|
505
|
+
The SDK automatically retries transient failures with jittered exponential
|
|
506
|
+
backoff (capped at 20 s):
|
|
507
|
+
|
|
508
|
+
- Retried: `ThrottlingException` (429), `ModelTimeoutException` (408),
|
|
509
|
+
`ServiceUnavailableException` (502/503/504), `InternalServerException` (500),
|
|
510
|
+
network errors.
|
|
511
|
+
- Never retried: `AccessDeniedException`, `ValidationException`,
|
|
512
|
+
`ResourceNotFoundException`, `ServiceQuotaExceededException`
|
|
513
|
+
(including `BUDGET_EXCEEDED` at 429/402), or any 4xx except 408/429.
|
|
514
|
+
- Streaming: retries apply only before the first SSE byte; after that,
|
|
515
|
+
connection drops raise `ModelStreamErrorException`.
|