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.
@@ -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`.