Introduction
The AI Reserve AI gateway serves 50+ frontier models from OpenAI,
Anthropic, Google, xAI, DeepSeek, Meta, Mistral, and more behind a single host —
https://api.aireserve.com — with one API key, unified billing,
per-user and per-team spend controls, and full usage analytics. Everything a modern AI
stack needs runs through it: streaming chat, tool use, vision,
image generation, video input,
video generation, and
prompt caching.
You do not have to change how you code. The gateway natively speaks three wire formats, so almost any existing client points at it with a base-URL change:
OpenAI-compatible
/v1/chat/completions, /v1/responses, /v1/models, images and files — the OpenAI SDK, Vercel AI SDK, Codex, and any OpenAI-compatible framework work with a base-URL swap. See Use your own SDK or the Migrating from OpenAI guide.
Anthropic-compatible
/v1/messages and count_tokens — the Anthropic SDK, Claude Code, and the Claude Agent SDK connect via ANTHROPIC_BASE_URL. Same key, same models, plus every non-Claude model too. See Migrating from Anthropic.
Bedrock Converse — wire format & SDKs
Unmodified boto3 / AWS SDK clients point endpoint_url at the gateway (guide), or use our five official SDKs — Python, TypeScript, Go, Java, Rust — that expose the exact Converse surface. Migration is a constructor swap, not a rewrite.
Production behavior built in
Jittered exponential backoff on 429/5xx, Retry-After honored, budget errors never retried, streams piped without buffering, request IDs surfaced on every server error, and 321 offline conformance tests across the SDK matrix.
Authentication
Every request authenticates with a single bearer key issued from your
AI Reserve workspace (Profile → My API Keys;
you'll be asked to sign in first) — keys look like aireserve_api_… (older
audacity_api_… keys keep working). There is no region, no credential chain,
no IAM role, and no signing. The SDKs resolve the key the way the AWS SDKs resolve
credentials, in priority order:
| Priority | Source |
|---|---|
| 1 | Explicit constructor / builder value |
| 2 | AUDACITY_API_KEY environment variable (base URL: AUDACITY_BASE_URL) |
| 3 | Defaults — base URL https://api.aireserve.com, timeout 120 s, 2 retries |
If no key can be resolved, the SDK fails fast with a clear client-side error
(MissingApiKeyError or the language equivalent) — before any network call is made.
Keep keys in a secret manager or environment variable; never commit them.
OpenAI & Anthropic SDKs — connect what you already use
The fastest way in: keep your existing code. The gateway speaks the provider-native wire
formats directly, so stock OpenAI and Anthropic SDKs, agent harnesses like Claude
Code and Codex, and framework providers work unmodified — change the base URL, use
your AI Reserve API key, keep everything else. Cross-provider calls are bridged for you:
Claude models answer OpenAI-SDK calls and GPT models answer Anthropic-SDK calls, all
metered against the same key. (Teams on AWS Bedrock have an equally seamless path — see
Migrating from Bedrock and the official
AI Reserve SDKs.) Runnable versions of every snippet below live in
examples/gateway/ in the repository.
The one gotcha — base URL suffix. OpenAI-flavored clients expect the
/v1 suffix on the base URL (they append /chat/completions).
Anthropic-flavored clients expect the host root without /v1
(they append /v1/messages themselves). Each snippet below shows the right form.
OpenAI SDK
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.aireserve.com/v1", # note the /v1 suffix
api_key=os.environ["AUDACITY_API_KEY"],
)
# Works with GPT models — and with Claude/Gemini/... model IDs too.
res = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "hello"}],
)
print(res.choices[0].message.content)
# Streaming works unchanged too:
for chunk in client.chat.completions.create(
model="claude-sonnet-4-6", # cross-provider: Claude via the OpenAI SDK
messages=[{"role": "user", "content": "hello"}],
stream=True,
):
print(chunk.choices[0].delta.content or "", end="")import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.aireserve.com/v1", // note the /v1 suffix
apiKey: process.env.AUDACITY_API_KEY,
});
// Works with GPT models — and with Claude/Gemini/... model IDs too.
const res = await client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "hello" }],
});Anthropic SDK
import os
from anthropic import Anthropic
client = Anthropic(
base_url="https://api.aireserve.com", # host root — NO /v1
api_key=os.environ["AUDACITY_API_KEY"],
)
# Works with Claude models — and with GPT model IDs too (bridged).
res = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=256,
messages=[{"role": "user", "content": "hello"}],
)
print(res.content[0].text)import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
baseURL: "https://api.aireserve.com", // host root — NO /v1
apiKey: process.env.AUDACITY_API_KEY,
});
// Works with Claude models — and with GPT model IDs too (bridged).
const res = await client.messages.create({
model: "claude-haiku-4-5-20251001",
max_tokens: 256,
messages: [{ role: "user", content: "hello" }],
});
Prefer to stay on one dependency? The official AI Reserve SDKs
(Python & TypeScript) expose these same two wire formats natively —
client.chat.completions.create(...) and
client.messages.create(...) on the AI Reserve client.
Vercel AI SDK (and other OpenAI-compatible providers)
import { createOpenAI } from "@ai-sdk/openai";
import { streamText } from "ai";
const audacity = createOpenAI({
baseURL: "https://api.aireserve.com/v1", // /v1 suffix
apiKey: process.env.AUDACITY_API_KEY,
});
const result = streamText({
model: audacity.chat("gpt-4o-mini"),
prompt: "hello",
});
Claude Code (CLI)
Claude Code is configured entirely through environment variables — no code change, no
plugin. Set the base URL to the host root (no /v1), pass your AI Reserve key
as the auth token, and make sure ANTHROPIC_API_KEY is empty so the CLI
doesn't prefer it:
# Point Claude Code at the AI Reserve gateway
export ANTHROPIC_BASE_URL="https://api.aireserve.com" # host root — NO /v1
export ANTHROPIC_AUTH_TOKEN="$AUDACITY_API_KEY" # your aireserve_api_… key
export ANTHROPIC_API_KEY="" # must be empty
# Optional: pin the models Claude Code uses (any gateway model ID works)
export ANTHROPIC_MODEL="claude-sonnet-4-6"
export ANTHROPIC_SMALL_FAST_MODEL="claude-haiku-4-5-20251001"
claude # every request is now metered and billed through your AI Reserve key
Add the exports to your shell profile (or the env block of
~/.claude/settings.json) to make the routing permanent. Requests hit
/v1/messages — including count_tokens — so the full agent
loop, tool use, and prompt caching work unchanged.
Claude Agent SDK
The embeddable agent runtime takes the same three variables via
options.env:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "hello",
options: {
model: "claude-haiku-4-5-20251001",
env: {
...process.env, // options.env REPLACES the environment — keep PATH etc.
ANTHROPIC_BASE_URL: "https://api.aireserve.com", // host root
ANTHROPIC_AUTH_TOKEN: process.env.AUDACITY_API_KEY!,
ANTHROPIC_API_KEY: "",
},
},
})) { /* ... */ }
Codex SDK / Codex CLI
The Codex runtime speaks the OpenAI Responses format, served at
/v1/responses:
import { Codex } from "@openai/codex-sdk";
const codex = new Codex({
baseUrl: "https://api.aireserve.com/v1", // /v1 suffix
apiKey: process.env.AUDACITY_API_KEY,
});
const thread = codex.startThread({ model: "gpt-4o-mini" });
const result = await thread.run("hello");
If your codex install insists on ChatGPT-plan auth for the built-in provider, define a
custom provider in ~/.codex/config.toml instead:
[model_providers.audacity]
name = "AI Reserve gateway"
base_url = "https://api.aireserve.com/v1"
env_key = "AUDACITY_API_KEY"
wire_api = "responses"
requires_openai_auth = false
model_provider = "audacity"
model = "gpt-4o-mini"
Cursor (IDE)
Cursor's custom-model support lets the entire IDE — chat, agent mode, inline edits — run through the AI Reserve gateway, so every request is metered and billed on your key. The configuration lives in Cursor Settings → Models → API Keys:
- OpenAI API Key — paste your AI Reserve key (
aireserve_api_…) and switch the toggle on. - Override OpenAI Base URL — set to
https://api.aireserve.com/v1(note the/v1suffix) and switch the toggle on. - Leave the Anthropic key toggle off — Cursor has no Anthropic base-URL override, so Anthropic-slot traffic cannot reach the gateway.
- Under the model list, choose Add Custom Model and add the gateway model IDs you want, e.g.
fable-5,opus-4-8,sonnet-4-6,gpt-5.4-mini. - Pick your custom model in the chat model selector and send a message — the request appears in your AI Reserve usage dashboard within seconds.
Use the prefix-free Claude aliases. Cursor routes any model named
claude-* to its built-in Anthropic provider slot on the client side — the
request never reaches your base URL. The gateway therefore publishes alias IDs without the
prefix: fable-5, opus-4-8, sonnet-4-6. Same upstream
models, same pricing; only the name differs.
Start a fresh chat per task. Cursor does not trim conversation context for
custom API models, so a long-running chat grows monotonically until it exceeds the model's
context window. Oversized requests fail fast with 413 and a clear message
rather than hanging. New task, new chat — you also get better answers that way.
Model discovery
GET /v1/models returns the OpenAI-shaped model list, so SDK and framework
discovery calls (client.models.list(), LangChain provider probes) work out of
the box. Anthropic-native clients can also call
POST /v1/messages/count_tokens for free token counting.
Models
One API key unlocks every model below through a single, flat ID namespace — the same
model / modelId string works across the Converse SDKs, the
OpenAI-compatible endpoints, and the Bedrock wire-format routes. The live catalog is
always available programmatically:
curl https://api.aireserve.com/v1/models \
-H "Authorization: Bearer $AUDACITY_API_KEY"
GET /v1/models returns the OpenAI-shaped list, so model pickers and
framework integrations that enumerate models work unchanged. Treat it as the source of
truth — the tables below are a human-readable snapshot.
Chat & reasoning models
Reasoning models and small max_tokens. Models that think
before answering (gemini-3-flash-preview, gpt-5.x,
grok-4-1-fast-reasoning, …) spend part of the token budget on internal
reasoning. With a tight cap (e.g. max_tokens: 50) the budget can be consumed
before any visible text is produced — the response then comes back with
finish_reason / stop_reason = max_tokens (or length) and empty
content. That's the model, not an error: raise max_tokens (a few hundred is a
safe floor for reasoning models) or pick a non-reasoning model for short outputs.
| Family | Model IDs | Notes |
|---|---|---|
| OpenAI GPT | gpt-5.5, gpt-5.4, gpt-5.4-mini,
gpt-4o, gpt-4o-mini, gpt-4-turbo |
Vision on 4o/5.x; automatic prompt caching |
| Anthropic Claude | claude-fable-5, claude-opus-4-8, claude-opus-4-7,
claude-opus-4-6, claude-sonnet-4-6, claude-sonnet-4-5,
claude-haiku-4-5-20251001 |
Vision; explicit prompt caching (minimums);
-bedrock variants available (below) |
| Google Gemini | gemini-3-flash-preview, gemini-2.5-pro, gemini-2.5-flash |
Vision + native video input (table); implicit caching |
| xAI Grok | grok-4-3, grok-4-1-fast-reasoning,
grok-4-1-fast-non-reasoning, grok-3, grok-3-mini |
Automatic caching |
| DeepSeek | deepseek-reasoner, deepseek-chat,
deepseek-v4-pro, deepseek-v4-flash |
deepseek-reasoner-bedrock variant available |
| Meta Llama | llama-4-maverick, llama-4-scout |
-bedrock variants available |
| Mistral | mistral-large, mistral-small |
mistral-large-bedrock variant available |
| Moonshot / Kimi | kimi-k2.7-code, kimi-k2.6, moonshot-v1-8k,
moonshot-v1-32k, moonshot-v1-128k |
kimi-k2.7-code is coding-tuned |
| Alibaba Qwen | qwen-3.7-max, qwen-3.7-plus |
|
| Perplexity Sonar | sonar-pro, sonar, sonar-reasoning-pro,
sonar-deep-research |
Web-grounded answers with citations |
| NVIDIA Nemotron | nemotron-3-ultra, nemotron-3-super, nemotron-3-nano |
Open-weight, managed hosting |
| Other open-weight | minimax-m3, glm-5.2, gpt-oss-120b, gpt-oss-20b |
Managed hosting |
Convenience aliases: claude-sonnet and claude-opus track our
recommended current Sonnet and Opus releases, so pinned integrations can opt into
upgrades by using the alias instead of a dated ID. Retired model IDs return
404 ResourceNotFoundException — enumerate /v1/models rather
than hardcoding lists.
Bedrock-routed variants & AWS model IDs
Eleven models are also offered via AWS Bedrock serving under
-bedrock IDs — same shapes, same key, one-string A/B against the direct
path. See Bedrock-routed models for the full mapping.
On the Bedrock wire-format routes (/model/{id}/converse[-stream]) the
gateway additionally accepts native AWS model IDs (us.anthropic.…,
inference-profile ARNs) so unmodified boto3 code works without renaming models — see
AWS SDK (boto3).
Image generation
Via POST /v1/images/generations (guide & pricing):
gemini-2.5-flash-image, gpt-image-1.
The retired dall-e-3, imagen-3, imagen-4,
imagen-4-fast, and imagen-4-ultra IDs return errors — migrate to
gemini-2.5-flash-image or gpt-image-1.
Video generation
Via the async POST /v1/videos/generations job API
(guide & pricing): wan-2.6 and
seedance-2.0.
Capability reference
| Capability | Where documented |
|---|---|
| Streaming | Streaming — all chat models |
| Tool use / function calling | Tool use |
| Vision (image input) | Images — per-model table |
| Video input | Video input — per-model table |
| Prompt caching | Prompt caching — per-model minimums |
| Token counting | POST /v1/messages/count_tokens (Anthropic-shape) |
Tool use
Function calling uses Bedrock's exact toolConfig / toolUse /
toolResult shapes. Define tools with a JSON Schema, the model responds with a
toolUse block (stopReason == "tool_use"), you execute it and send
back a toolResult block. Streaming tool calls arrive as
contentBlockStart (tool identity) followed by contentBlockDelta
events carrying incremental JSON argument fragments — exactly as Bedrock streams them.
response = client.converse(
modelId="gpt-5.4-mini",
messages=[{"role": "user", "content": [{"text": "What's the weather in NYC?"}]}],
toolConfig={
"tools": [{
"toolSpec": {
"name": "get_weather",
"description": "Get current weather for a city",
"inputSchema": {
"json": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
}
},
}
}],
"toolChoice": {"auto": {}},
},
)
# Assistant responds with a tool call
tool_use = response["output"]["message"]["content"][0]["toolUse"]
print(tool_use["name"]) # "get_weather"
print(tool_use["input"]) # {"city": "NYC"}
# Send the tool result back
response2 = client.converse(
modelId="gpt-5.4-mini",
messages=[
{"role": "user", "content": [{"text": "What's the weather in NYC?"}]},
{"role": "assistant", "content": response["output"]["message"]["content"]},
{"role": "user", "content": [
{"toolResult": {
"toolUseId": tool_use["toolUseId"],
"content": [{"text": "Sunny, 72°F"}],
}}
]},
],
)const response = await client.send(
new ConverseCommand({
modelId: "gpt-5.4-mini",
messages: [{ role: "user", content: [{ text: "What's the weather in NYC?" }] }],
toolConfig: {
tools: [{
toolSpec: {
name: "get_weather",
description: "Get current weather for a city",
inputSchema: {
json: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
},
}],
toolChoice: { auto: {} },
},
})
);
// response.stopReason === "tool_use"
const block = response.output?.message?.content?.[0];
if (block && "toolUse" in block) {
console.log(block.toolUse.name); // "get_weather"
console.log(block.toolUse.input); // { city: "NYC" }
}resp, err := client.Converse(ctx, &audacityruntime.ConverseInput{
ModelId: audacity.String("gpt-5.4-mini"),
Messages: []types.Message{{
Role: types.ConversationRoleUser,
Content: []types.ContentBlock{&types.ContentBlockMemberText{Value: "Weather in London?"}},
}},
ToolConfig: &types.ToolConfiguration{
Tools: []types.Tool{{
ToolSpec: &types.ToolSpecification{
Name: "get_weather",
Description: audacity.String("Returns current weather"),
InputSchema: &types.ToolInputSchema{
Json: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"city": map[string]interface{}{"type": "string"},
},
"required": []string{"city"},
},
},
},
}},
ToolChoice: &types.ToolChoiceMemberAuto{},
},
})
// resp.StopReason == "tool_use"
// resp.Output.(*types.ConverseOutputMemberMessage).Value.Content[0].(*types.ContentBlockMemberToolUse)Map<String, Object> schema = new LinkedHashMap<>();
schema.put("type", "object");
Map<String, Object> props = new LinkedHashMap<>();
Map<String, Object> cityProp = new LinkedHashMap<>();
cityProp.put("type", "string");
props.put("city", cityProp);
schema.put("properties", props);
schema.put("required", List.of("city"));
ConverseResponse resp = client.converse(request -> request
.modelId("gpt-5.4-mini")
.messages(Message.builder()
.role(ConversationRole.USER)
.content(ContentBlock.fromText("What's the weather in NYC?"))
.build())
.toolConfig(tc -> tc
.tools(Tool.builder()
.toolSpec(ts -> ts
.name("get_weather")
.description("Get current weather for a city")
.inputSchema(ToolInputSchema.builder().json(schema).build()))
.build())
.toolChoice(ToolChoice.auto())));
ContentBlock block = resp.output().message().content().get(0);
if (block.toolUse() != null) {
System.out.println("Tool: " + block.toolUse().name());
System.out.println("Args: " + block.toolUse().input());
}// Rust follows the identical canonical shape: a ToolConfiguration with
// toolSpec { name, description, inputSchema.json } entries and a toolChoice
// of Auto / Any / Tool{name}. The assistant reply carries
// ContentBlock::ToolUse { tool_use_id, name, input } blocks and
// stop_reason == "tool_use"; return results with ContentBlock::ToolResult.
//
// See the crate docs for the builder signatures — they mirror
// aws-sdk-bedrockruntime member-for-member.Images (vision)
Bedrock-style image content blocks are supported in user messages on
vision-capable models. Hand the SDK raw bytes (it base64-encodes them for
you, Bedrock parity) or — an AI Reserve extension Bedrock doesn't offer — a
hosted URL, which is passed through verbatim so your payload stays tiny.
Supported formats: png, jpeg, gif, webp.
with open("chart.png", "rb") as f:
image_bytes = f.read()
response = client.converse(
modelId="gpt-5.5",
messages=[{
"role": "user",
"content": [
{"text": "What does this chart show?"},
{"image": {"format": "png", "source": {"bytes": image_bytes}}},
],
}],
)
# Or reference a hosted image directly (not available in Bedrock):
# {"image": {"format": "jpeg", "source": {"url": "https://example.com/photo.jpg"}}}import { readFile } from "node:fs/promises";
const imageBytes = new Uint8Array(await readFile("chart.png"));
const response = await client.send(
new ConverseCommand({
modelId: "gpt-5.5",
messages: [
{
role: "user",
content: [
{ text: "What does this chart show?" },
{ image: { format: "png", source: { bytes: imageBytes } } },
],
},
],
})
);
// Or reference a hosted image directly (not available in Bedrock):
// { image: { format: "jpeg", source: { url: "https://example.com/photo.jpg" } } }imageBytes, err := os.ReadFile("chart.png")
if err != nil {
log.Fatal(err)
}
resp, err := client.Converse(ctx, &audacityruntime.ConverseInput{
ModelId: audacity.String("gpt-5.5"),
Messages: []types.Message{{
Role: types.ConversationRoleUser,
Content: []types.ContentBlock{
&types.ContentBlockMemberText{Value: "What does this chart show?"},
&types.ContentBlockMemberImage{Value: types.ImageBlock{
Format: types.ImageFormatPng,
Source: &types.ImageSourceMemberBytes{Value: imageBytes},
}},
},
}},
})
// Or reference a hosted image directly (not available in Bedrock):
// Source: &types.ImageSourceMemberUrl{Value: "https://example.com/photo.jpg"}byte[] imageBytes = java.nio.file.Files.readAllBytes(java.nio.file.Path.of("chart.png"));
ConverseResponse resp = client.converse(request -> request
.modelId("gpt-5.5")
.messages(Message.builder()
.role(ConversationRole.USER)
.content(
ContentBlock.fromText("What does this chart show?"),
ContentBlock.fromImage(ImageBlock.builder()
.format(ImageFormat.PNG)
.source(ImageSource.fromBytes(imageBytes))
.build()))
.build()));
// Or reference a hosted image directly (not available in Bedrock):
// ImageSource.fromUrl("https://example.com/photo.jpg")use audacity_sdk::{ContentBlock, ImageBlock, ImageFormat, ImageSource};
let image_bytes = std::fs::read("chart.png")?;
let response = client.converse()
.model_id("gpt-5.5")
.messages(
Message::builder()
.role(ConversationRole::User)
.content(ContentBlock::Text("What does this chart show?".into()))
.content(ContentBlock::Image(ImageBlock {
format: ImageFormat::Png,
source: ImageSource::Bytes(image_bytes),
}))
.build()?
)
.send()
.await?;
// Or reference a hosted image directly (not available in Bedrock):
// source: ImageSource::Url("https://example.com/photo.jpg".into())Payload guidance
- Base64 inflates bytes by ~33%; with the gateway's 30 MB request cap, keep inline images at roughly 20 MB raw or less.
- For large or frequently reused images, prefer
source.url— the request stays small and the provider fetches the image directly. - Text-only conversations are wire-identical to before image support existed; nothing changes unless a turn actually contains an image.
- Image blocks are valid in user messages only (Bedrock parity); blocks in assistant turns are ignored.
Image generation
The gateway also generates images: an OpenAI-compatible endpoint sits
alongside chat completions, authenticated with the same API key (as
Authorization: Bearer or x-api-key) and governed by the same
per-client rate limits as every other /v1 route. Requests are synchronous —
multi-image or high-quality generations can take tens of seconds, so set client timeouts
accordingly.
curl https://api.aireserve.com/v1/images/generations \
-H "Authorization: Bearer $AUDACITY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gemini-2.5-flash-image",
"prompt": "A watercolor painting of a fox in a snowy forest",
"n": 1,
"size": "1024x1024",
"response_format": "url"
}'
Request parameters
| Field | Required | Description |
|---|---|---|
model | yes | Image model ID — see the table below |
prompt | yes | Text description of the desired image(s); 1–32,000 characters |
n | no | Number of images to generate — integer 1–10 (model-dependent) |
size | no | Dimensions as a "WxH" string (e.g. 1024x1024); supported values differ per model |
quality | no | Provider-specific quality tier (e.g. standard, hd, low, high) |
response_format | no | url (default) — a signed download link — or b64_json — inline base64 bytes |
user | no | End-user identifier forwarded to the provider for abuse attribution |
Response
The response follows the OpenAI images shape — created (unix seconds), a
data array with one entry per generated image, and an optional
usage object on token-priced models:
{
"created": 1752000000,
"data": [
{
"url": "https://storage.googleapis.com/…?X-Goog-Signature=…",
"revised_prompt": "A watercolor painting of a fox…"
}
],
"usage": { "input_tokens": 12, "output_tokens": 1024, "total_tokens": 1036 }
}
- With
response_format: "url"(the default), each image is stored by the gateway and returned as a signed download URL valid for ~24 hours — download promptly and persist on your side. - With
response_format: "b64_json", each entry carries the base64-encoded PNG bytes inline instead. revised_promptappears when the provider rewrites your prompt before generating.
Models & pricing
| Model | Pricing |
|---|---|
gemini-2.5-flash-image | Token-based (≈ $0.039 / image) |
gpt-image-1 | Token-based ($5.00 / 1M text input, $40.00 / 1M image output tokens) |
Token-based models report token counts in the response usage object. Every
request's cost is metered against your key exactly like chat traffic and counts toward
the same spend caps.
imagen-4, imagen-4-fast, and imagen-4-ultra IDs
all return errors. The migration target is gemini-2.5-flash-image
(GA, token-based, ≈ $0.039 / image); gpt-image-1 also remains available.
Using the SDKs
All five SDKs ship an image-generation helper as of SDK v0.4.0, with the
same auth, error mapping (401 → AccessDeniedException, 402 →
ServiceQuotaExceededException, 429 → ThrottlingException), and
retry policy as Converse:
result = client.images.generate(
model="gemini-2.5-flash-image",
prompt="A watercolor painting of a fox in a snowy forest",
)
print(result["data"][0]["url"]) # signed download URL, valid ~24 h
# Inline bytes instead: response_format="b64_json" → result["data"][0]["b64_json"]const result = await client.images.generate({
model: "gemini-2.5-flash-image",
prompt: "A watercolor painting of a fox in a snowy forest",
});
console.log(result.data[0].url); // signed download URL, valid ~24 h
// Inline bytes instead: responseFormat: "b64_json" → result.data[0].b64Jsonout, err := client.GenerateImage(ctx, &audacityruntime.GenerateImageInput{
Model: audacity.String("gemini-2.5-flash-image"),
Prompt: audacity.String("A watercolor painting of a fox in a snowy forest"),
})
if err != nil {
log.Fatal(err)
}
fmt.Println(out.Data[0].Url) // signed download URL, valid ~24 h
// Inline bytes instead: ResponseFormat: audacity.String("b64_json") → out.Data[0].B64JsonGenerateImageResponse result = client.generateImage(b -> b
.model("gemini-2.5-flash-image")
.prompt("A watercolor painting of a fox in a snowy forest"));
System.out.println(result.data().get(0).url()); // signed download URL, valid ~24 h
// Inline bytes instead: .responseFormat("b64_json") → result.data().get(0).b64Json()let result = client.generate_image()
.model("gemini-2.5-flash-image")
.prompt("A watercolor painting of a fox in a snowy forest")
.send()
.await?;
println!("{}", result.data[0].url.as_deref().unwrap()); // signed URL, valid ~24 h
// Inline bytes instead: .response_format("b64_json") → result.data[0].b64_json
Errors reuse the chat-completions codes: 400 invalid_request_error for a
malformed body, 401 invalid_api_key, 402 usage_cap_exceeded when
a spend cap is reached (never retried), and 429 rate_limit_exceeded with a
Retry-After header. See Errors & retries for the
full exception mapping in each SDK.
Video input (understanding)
Send a video to a model and ask questions about it — summarize a
recording, extract action items from a meeting, describe what happens in a clip.
Bedrock-style video content blocks are supported in user messages, mirroring
the bytes | s3Location pattern of Bedrock Converse's
video source. This is the input-side counterpart of
Video generation, which creates video from a
text prompt — don't confuse the two.
Model support
Video input is available on the Gemini family only. Sending a video block to any other model returns an HTTP 400 before the request reaches the provider (see Limits & errors).
| Model | Video input |
|---|---|
gemini-2.5-flash | yes |
gemini-2.5-pro | yes |
gemini-3-flash-preview | yes |
| Every other model | no — HTTP 400 |
Inline video (≤ 20 MB)
For small files, hand the SDK raw bytes — it base64-encodes them into the request for you. Base64 inflates bytes by ~33% against the gateway's 30 MB request cap, so inline video is capped at 20 MB raw per request (larger inline payloads are rejected with HTTP 413 — use the upload flow below instead). All five SDKs support video input as of SDK v0.3.0.
with open("demo.mp4", "rb") as f:
video_bytes = f.read()
response = client.converse(
modelId="gemini-2.5-flash",
messages=[{
"role": "user",
"content": [
{"text": "Summarize what happens in this video."},
{"video": {"format": "mp4", "source": {"bytes": video_bytes}}},
],
}],
)
print(response["output"]["message"]["content"][0]["text"])import { readFile } from "node:fs/promises";
const videoBytes = new Uint8Array(await readFile("demo.mp4"));
const response = await client.send(
new ConverseCommand({
modelId: "gemini-2.5-flash",
messages: [
{
role: "user",
content: [
{ text: "Summarize what happens in this video." },
{ video: { format: "mp4", source: { bytes: videoBytes } } },
],
},
],
})
);videoBytes, err := os.ReadFile("demo.mp4")
if err != nil {
log.Fatal(err)
}
resp, err := client.Converse(ctx, &audacityruntime.ConverseInput{
ModelId: audacity.String("gemini-2.5-flash"),
Messages: []types.Message{{
Role: types.ConversationRoleUser,
Content: []types.ContentBlock{
&types.ContentBlockMemberText{Value: "What happens in this video?"},
&types.ContentBlockMemberVideo{Value: types.VideoBlock{
Format: types.VideoFormatMp4,
Source: &types.VideoSourceMemberBytes{Value: videoBytes},
}},
},
}},
})byte[] videoBytes = java.nio.file.Files.readAllBytes(java.nio.file.Path.of("demo.mp4"));
ConverseResponse resp = client.converse(request -> request
.modelId("gemini-2.5-flash")
.messages(Message.builder()
.role(ConversationRole.USER)
.content(
ContentBlock.fromText("What happens in this video?"),
ContentBlock.fromVideo(VideoBlock.builder()
.format(VideoFormat.MP4)
.source(VideoSource.fromBytes(videoBytes))
.build()))
.build()));use audacity_sdk::{ContentBlock, VideoBlock, VideoFormat, VideoSource};
let video_bytes = std::fs::read("demo.mp4")?;
let response = client.converse()
.model_id("gemini-2.5-flash")
.messages(
Message::builder()
.role(ConversationRole::User)
.content(ContentBlock::Text("What happens in this clip?".into()))
.content(ContentBlock::Video(VideoBlock {
format: VideoFormat::Mp4,
source: VideoSource::Bytes(video_bytes),
}))
.build()?
)
.send()
.await?;Large videos: upload, then reference by URI
For files over 20 MB (up to 1 GB), upload once and reference
the returned audacity://files/… URI in as many requests as you like. The
flow has three steps:
POST /v1/fileswithcontent_typeandsize_bytes— returns afile_id, a signedupload_url(valid ~15 minutes), theurito reference in chat requests, andexpires_at.- Upload the bytes to
upload_urlover a resumable session (Google Cloud Storage protocol: one POST opens the session, then PUT the bytes). - Reference the video with
{"video": {"format": …, "source": {"uri": …}}}in aConversecall.
The file-create step from curl:
# 1) Create an upload slot
curl -s https://api.aireserve.com/v1/files \
-H "Authorization: Bearer $AUDACITY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content_type": "video/mp4", "size_bytes": 52428800}'
{
"file_id": "3f8a1e2c-9d4b-4c6a-b7e1-2a5c8d9f0e13",
"upload_url": "https://storage.googleapis.com/…&X-Goog-Signature=…",
"uri": "audacity://files/3f8a1e2c-9d4b-4c6a-b7e1-2a5c8d9f0e13",
"expires_at": "2026-07-09T05:20:00.000Z"
}
# 2) Open the resumable session (the session URI comes back in the Location header)
SESSION_URI=$(curl -si -X POST "$UPLOAD_URL" \
-H "x-goog-resumable: start" \
-H "Content-Type: video/mp4" \
| awk 'tolower($1)=="location:" {print $2}' | tr -d "\r")
# 3) Upload the bytes (a single PUT works; the SDK helpers chunk + auto-resume)
curl -X PUT "$SESSION_URI" --data-binary @keynote.mp4
GET /v1/files/{file_id} reports upload status — "pending"
before the bytes land, then "uploaded" with size_bytes and
content_type.
The SDK helpers (client.files.upload in Python/TypeScript,
UploadFile/uploadFile in Go/Java, upload_file() in
Rust) run this whole flow for you and stream the file in 8 MB chunks, automatically
resuming from the last confirmed byte after a network drop. The full loop in Python:
from audacity import Audacity
client = Audacity() # AUDACITY_API_KEY from the environment
# Upload once — accepts raw bytes or a file path; resumable under the hood
upload = client.files.upload("keynote.mp4", content_type="video/mp4")
# {"file_id": …, "upload_url": …, "uri": "audacity://files/…", "expires_at": …}
# Reference the uploaded video by URI — reusable across requests for ~24 h
response = client.converse(
modelId="gemini-2.5-pro",
mediaResolution="low", # optional cost knob — see below
messages=[{
"role": "user",
"content": [
{"text": "Summarize this keynote and list the action items."},
{"video": {"format": "mp4", "source": {"uri": upload["uri"]}}},
],
}],
)
print(response["output"]["message"]["content"][0]["text"])
Uploaded files are transient inference inputs: they expire after ~24 hours and are scoped to your workspace's API keys — a URI leaked to another workspace resolves against that workspace's namespace and simply does not exist. Re-referencing the same URI across conversation turns is free; the gateway caches the provider-side staging, so a large video is not re-transferred on every request.
file
content part — inline as a data URL in file_data, or by reference in
file_id — with the MIME type in format:
{"type": "file", "file": {"file_data": "data:video/mp4;base64,…", "format": "video/mp4"}}
{"type": "file", "file": {"file_id": "audacity://files/3f8a1e2c-…", "format": "video/mp4"}}
A per-part "detail" field and a request-level
"media_resolution" field control media resolution (next section).
Media resolution (video token cost)
Video is tokenized frame by frame on Gemini, so long clips get expensive fast. The
request-level mediaResolution option (wire name:
media_resolution) controls how densely video is sampled —
"low" processes video at roughly 4× fewer tokens. Non-Gemini
models ignore the field; when unset, the model's default applies.
| Value | Guidance |
|---|---|
low | ~4× fewer video tokens. The right default for summarization, transcription-style tasks, and long footage |
medium | Balanced sampling for general question-answering about a clip |
high | Finer sampling — reading small on-screen text, UI walkthroughs, fine visual detail |
ultra_high | Maximum fidelity at maximum token cost — dense charts or frame-critical analysis |
Raw-protocol callers can also set detail per content part; an explicit
per-part value wins over the request-level field. On Gemini 2.5 models
the highest resolution across all parts applies to the whole request; true per-part
granularity starts with Gemini 3.
Supported formats
SDK format | MIME type (content_type for uploads) |
|---|---|
mp4 | video/mp4 |
mov | video/mov |
mkv | video/x-matroska |
webm | video/webm |
flv | video/x-flv |
mpeg | video/mpeg |
mpg | video/mpg |
wmv | video/wmv |
three_gp | video/3gpp |
Limits & errors
- Inline cap — 20 MB of raw video per request (all video parts combined). Exceeding it returns 413 with a message pointing to the upload flow.
- Upload cap — 1 GB per file.
POST /v1/filesrejects a largersize_bytes, or an unsupportedcontent_type, with 400. - Upload URL expiry — the signed
upload_urlis valid ~15 minutes; create a fresh slot if it lapses. - File expiry — uploaded files auto-delete after ~24 hours. Referencing an expired or unknown URI fails with 400:
video file "audacity://files/…" not found or expired; upload via POST /v1/files and retry. - Non-video model — sending video to anything outside the Gemini family returns 400 before the provider is contacted. The SDKs raise
ValidationException; the raw response looks like:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "model \"gpt-5.4-mini\" does not support video input; video is supported on: gemini-2.5-flash, gemini-2.5-pro, gemini-3-flash-preview",
"request_id": "…"
}
}
- Video blocks are valid in user messages only, matching image input.
video content blocks — it returns a
ValidationException listing its supported block types. Use one of the
AI Reserve SDKs or the raw OpenAI-protocol wire format above for video.
Video generation
This section is about creating video from a text prompt. To send an existing video to a model for analysis, see Video input above.
The gateway generates video too. Unlike image generation, video is
asynchronous: a generation can take from tens of seconds to several
minutes, so you submit a job and poll for its result instead of holding one HTTP
request open. Authentication is the same API key as every other route (as
Authorization: Bearer or x-api-key), and the same per-client
rate limits and spend caps apply.
POST /v1/videos/generations— submit a job. Answers 202 with a job object containing anid.GET /v1/videos/generations/{id}— poll the job until itsstatusis terminal (succeededorfailed).
Quickstart
# 1) Submit — returns 202 with the job id
JOB=$(curl -s https://api.aireserve.com/v1/videos/generations \
-H "Authorization: Bearer $AUDACITY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "wan-2.6",
"prompt": "A drone shot over a misty pine forest at sunrise",
"duration_seconds": 5,
"size": "1280x720"
}')
JOB_ID=$(echo "$JOB" | jq -r .id)
# 2) Poll every few seconds until the job is terminal
while :; do
JOB=$(curl -s "https://api.aireserve.com/v1/videos/generations/$JOB_ID" \
-H "Authorization: Bearer $AUDACITY_API_KEY")
STATUS=$(echo "$JOB" | jq -r .status)
echo "status: $STATUS"
case "$STATUS" in succeeded|failed) break ;; esac
sleep 3
done
# 3) Download the finished clip (mp4)
curl -o clip.mp4 "$(echo "$JOB" | jq -r .result.video.url)"
Request parameters
| Field | Required | Description |
|---|---|---|
model | yes | Video model ID — see the table below |
prompt | yes | Text description of the desired video; 1–32,000 characters |
duration_seconds | no | Clip length in seconds — each model accepts a specific set of values (see the models table below: wan-2.6 accepts 5, 10, or 15; seedance-2.0 accepts integers 4–15). An unsupported value is rejected synchronously with a 400 listing the allowed values. Omit it to use the model's default |
size | no | Dimensions as a "WxH" string. The gateway maps it to the nearest resolution tier and aspect ratio the model supports — e.g. 1280x720 becomes 720p at 16:9 |
user | no | End-user identifier forwarded for abuse attribution |
The job object
Both endpoints return the same shape — object is always
"video.generation.job" and status moves strictly forward:
| Status | Meaning |
|---|---|
queued | Accepted, not yet dispatched to the provider |
running | Generating. Jobs advance when you poll — keep polling every few seconds |
succeeded | Terminal — result is populated |
failed | Terminal — error carries the reason; the job stays readable by id |
A finished job looks like this:
{
"id": "3f8a1e2c-9d4b-4c6a-b7e1-2a5c8d9f0e13",
"object": "video.generation.job",
"kind": "video",
"model": "wan-2.6",
"status": "succeeded",
"created_at": 1752000000,
"completed_at": 1752000041,
"result": {
"video": {
"url": "https://…/output.mp4",
"width": 1280,
"height": 720,
"duration": 5
}
},
"error": null
}
- On success,
result.video.urlis an MP4 hosted on the provider's CDN — download promptly and persist on your side; the URL is not permanent. - Extra metadata in
resultvaries by model (for examplewan-2.6echoes width/height/duration;seedance-2.0omits them). - On failure,
erroris a human-readable string andresultstaysnull.
Models & pricing
| Model | Resolutions | Durations | Approximate pricing |
|---|---|---|---|
wan-2.6 | 720p, 1080p (16:9, 9:16, 1:1, 4:3, 3:4) | 5, 10, or 15 s | ~$0.10/s at 720p, ~$0.15/s at 1080p |
seedance-2.0 | 480p, 720p, 1080p, 4k (16:9, 9:16, 1:1, 21:9) | 4–15 s | ~$0.14/s at 480p, ~$0.30/s at 720p, ~$0.68/s at 1080p, ~$1.56/s at 4k |
Video is billed per second of finished video at the tier actually
generated — a 5-second wan-2.6 clip at 720p is roughly $0.50. Only videos that
finish successfully are billed. Each job's cost is metered against your key exactly like
chat and image traffic, appears in your usage reporting, and counts toward the same spend
caps.
Polling from Python
A production-shaped loop with a hard deadline: poll every few seconds, treat 5xx poll
errors as transient (the job keeps running server-side), and give up after a sensible
timeout. A typical 5-second wan-2.6 clip completes in under a minute; longer
clips and higher tiers take proportionally longer.
import os, time, requests
BASE = "https://api.aireserve.com"
HEADERS = {"Authorization": f"Bearer {os.environ['AUDACITY_API_KEY']}"}
job = requests.post(
f"{BASE}/v1/videos/generations",
headers=HEADERS,
json={
"model": "wan-2.6",
"prompt": "A drone shot over a misty pine forest at sunrise",
"duration_seconds": 5,
"size": "1280x720",
},
timeout=30,
)
job.raise_for_status()
job = job.json()
deadline = time.monotonic() + 600 # give up after 10 minutes
while job["status"] in ("queued", "running"):
if time.monotonic() > deadline:
raise TimeoutError(f"video job {job['id']} did not finish in time")
time.sleep(3)
resp = requests.get(
f"{BASE}/v1/videos/generations/{job['id']}", headers=HEADERS, timeout=30
)
if resp.status_code >= 500:
continue # transient — retry the next poll
resp.raise_for_status()
job = resp.json()
if job["status"] == "succeeded":
url = job["result"]["video"]["url"]
with open("clip.mp4", "wb") as f: # download promptly — URLs expire
f.write(requests.get(url, timeout=120).content)
else:
print("generation failed:", job["error"])
Errors
Submission errors reuse the familiar codes: 400 invalid_request_error for a
malformed body, 401 invalid_api_key, 402 usage_cap_exceeded when
a spend cap is reached, and 429 rate_limit_exceeded with a
Retry-After header. Three video-specific cases:
- 400 at submit —
duration_secondsis not one of the values the model supports (for examplewan-2.6only accepts 5, 10, or 15). The message lists the allowed values; no job is created. - 502 at submit — the provider rejected the job synchronously (for example a prompt rejected by upstream validation). The response carries the provider's message and a
job_id; the failed job remains readable by id. - 404
job_not_foundon poll — the job id is unknown or belongs to a different workspace's key.
failed with the provider's message in
error (or, if rejected at submit, in the 502 response).
Prompt caching
When consecutive requests share a long prefix — a large system prompt, a pasted document, a growing conversation — providers can cache that prefix server-side. Cached input tokens bill at a steep discount (~90% off cache reads on Anthropic models; the first request pays a one-time ~25% premium on cache writes) and time-to-first-token improves because the provider skips re-processing the prefix.
| Models | Caching behavior | What you do |
|---|---|---|
OpenAI (gpt-*) | Automatic for prompts of 1,024+ tokens | Nothing |
Gemini (gemini-*) | Automatic (implicit caching) | Nothing |
Claude (claude-* and -bedrock variants) | Opt-in per request | Add a cache_control marker |
Marking a prefix on Claude models
The gateway accepts the Anthropic-style marker in OpenAI-compatible format: write the
message content as an array of parts and put
"cache_control": {"type": "ephemeral"} on the content block where the
cacheable prefix ends — everything up to and including that block is cached. The gateway
forwards the marker to Anthropic (or AWS Bedrock for -bedrock variants)
unchanged. A message may carry the marker on its last content block only,
and a request may contain at most four markers.
curl https://api.aireserve.com/v1/chat/completions \
-H "Authorization: Bearer $AUDACITY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-6",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a contracts analyst. <full playbook + the 40-page agreement text…>",
"cache_control": {"type": "ephemeral"}
}
]
},
{"role": "user", "content": "Summarize the termination clauses."}
],
"max_tokens": 1024
}'
- Minimum size. Each model has a minimum cacheable prefix (see the table below); shorter prefixes are processed normally with no error and no cache entry — the marker is silently ignored.
- Lifetime. Cache entries live for 5 minutes by default, refreshed on each hit — steady traffic keeps the prefix warm.
- Exact-prefix match. Reuse requires a byte-identical prefix up to the marker; put stable content (system prompt, documents) first and variable content after it.
Minimum cacheable prefix per model
Minimums differ by model and by route. In particular, AWS Bedrock enforces a
higher 4,096-token minimum for Opus and Haiku than Anthropic's direct
API does — a prefix between 1,024 and 4,095 tokens that caches fine on
claude-opus-4-6 will get zero cache activity on
claude-opus-4-6-bedrock. This is an AWS-side constraint (verified against
Bedrock directly), not gateway behavior.
| Model | Minimum prefix | Notes |
|---|---|---|
Claude Sonnet (direct: claude-sonnet-4-6, claude-sonnet-4-5) | 1,024 tokens | Anthropic documented minimum |
Claude Sonnet on Bedrock (claude-sonnet-4-6-bedrock, claude-sonnet-4-5-bedrock) | 1,024 tokens | Same as direct |
Claude Opus (direct: claude-fable-5, claude-opus-4-8, claude-opus-4-7, claude-opus-4-6) | 1,024 tokens | Anthropic documented minimum |
Claude Haiku (direct: claude-haiku-4-5-20251001) | 2,048 tokens | Anthropic documented minimum |
Claude Opus / Haiku on Bedrock (claude-opus-4-6-bedrock, claude-haiku-4-5-bedrock, and other -bedrock Opus variants) | 4,096 tokens | AWS Bedrock enforces a higher floor; sub-4,096 prefixes are accepted but never cached (no error, full input billing) |
OpenAI (gpt-*) | 1,024 tokens | Automatic — no marker needed |
Grok (grok-*) | No published minimum | Automatic — no marker needed; cache reads surface in usage when xAI serves them |
Gemini (gemini-2.5-*, gemini-3-*) | 1,024 (Flash) / 4,096 (Pro) tokens | Implicit caching is automatic and best-effort: cache hits depend on Google-side capacity and recency, so identical prompts may report cached_tokens only on some responses |
Placeholder Bedrock variants. claude-fable-5-bedrock,
claude-opus-4-8-bedrock, and claude-opus-4-7-bedrock temporarily
serve via Anthropic's direct API (see Bedrock-routed models),
so today their effective caching minimum is the direct-API 1,024 tokens.
Treat 4,096 as the stable planning number — it becomes exact when those IDs are re-pointed
to Bedrock, with no client change.
If your prefix is below the model's minimum, requests still succeed — you simply pay the full input rate. Sizing the stable prefix above the minimum (for Bedrock Opus/Haiku, above 4,096 tokens) is what unlocks the cache discount.
Using the typed SDKs (cachePoint)
As of SDK v0.2.0, all five SDKs support prompt caching natively via a
Bedrock-style cachePoint content block. Place a cache point after the stable
prefix you want cached — in the system list, in message content,
or both. The SDK translates each cache point into the cache_control wire
marker on the preceding content part; the block itself is never sent. The same limits
apply: at most four cache points per request, and a cache point with nothing before it in
the same message is silently ignored.
response = client.converse(
modelId="claude-sonnet-4-6",
system=[
{"text": long_system_prompt},
{"cachePoint": {"type": "default"}},
],
messages=[{
"role": "user",
"content": [
{"text": big_reference_document},
{"cachePoint": {"type": "default"}},
{"text": "Summarise the key risks."},
],
}],
)
# Cache activity is reported in usage (Bedrock names):
print(response["usage"]["cacheReadInputTokens"]) # tokens served from cache
print(response["usage"]["cacheWriteInputTokens"]) # tokens written to cacheconst response = await client.send(
new ConverseCommand({
modelId: "claude-sonnet-4-6",
system: [
{ text: longSystemPrompt },
{ cachePoint: { type: "default" } },
],
messages: [
{
role: "user",
content: [
{ text: bigReferenceDocument },
{ cachePoint: { type: "default" } },
{ text: "Summarise the key risks." },
],
},
],
})
);
// Cache activity is reported in usage (Bedrock names):
console.log(response.usage?.cacheReadInputTokens); // tokens served from cache
console.log(response.usage?.cacheWriteInputTokens); // tokens written to cacheresp, err := client.Converse(ctx, &audacityruntime.ConverseInput{
ModelId: audacity.String("claude-sonnet-4-6"),
System: []types.SystemContentBlock{
{Text: longSystemPrompt},
{CachePoint: &types.CachePointBlock{Type: types.CachePointTypeDefault}},
},
Messages: []types.Message{{
Role: types.ConversationRoleUser,
Content: []types.ContentBlock{
&types.ContentBlockMemberText{Value: bigReferenceDocument},
&types.ContentBlockMemberCachePoint{
Value: types.CachePointBlock{Type: types.CachePointTypeDefault},
},
&types.ContentBlockMemberText{Value: "Summarise the key risks."},
},
}},
})
// Cache activity is reported in usage (Bedrock names):
fmt.Println(resp.Usage.CacheReadInputTokens) // tokens served from cache
fmt.Println(resp.Usage.CacheWriteInputTokens) // tokens written to cacheConverseResponse resp = client.converse(request -> request
.modelId("claude-sonnet-4-6")
.system(
SystemContentBlock.fromText(longSystemPrompt),
SystemContentBlock.fromCachePoint(CachePointBlock.defaultType()))
.messages(Message.builder()
.role(ConversationRole.USER)
.content(
ContentBlock.fromText(bigReferenceDocument),
ContentBlock.fromCachePoint(CachePointBlock.defaultType()),
ContentBlock.fromText("Summarise the key risks."))
.build()));
// Cache activity is reported in usage (Bedrock names):
System.out.println(resp.usage().cacheReadInputTokens()); // tokens served from cache
System.out.println(resp.usage().cacheWriteInputTokens()); // tokens written to cacheuse audacity_sdk::{CachePointBlock, ContentBlock, SystemContentBlock};
let response = client.converse()
.model_id("claude-sonnet-4-6")
.system(SystemContentBlock::text(long_system_prompt))
.system(SystemContentBlock::cache_point())
.messages(
Message::builder()
.role(ConversationRole::User)
.content(ContentBlock::Text(big_reference_document))
.content(ContentBlock::CachePoint(CachePointBlock::new()))
.content(ContentBlock::Text("Summarise the key risks.".into()))
.build()?
)
.send()
.await?;
// Cache activity is reported in usage (Bedrock names):
println!("{}", response.usage().cache_read_input_tokens); // tokens served from cache
println!("{}", response.usage().cache_write_input_tokens); // tokens written to cacheVerifying and billing
Cache activity is reported in the response usage object. On the wire the
fields are cache_creation_input_tokens (tokens written to the cache) and
cache_read_input_tokens (tokens served from it); the SDKs surface them under
the Bedrock names cacheWriteInputTokens and cacheReadInputTokens.
Both are metered per request in your usage reporting and billed at the premium write /
discounted read rates — a non-zero cache-read count confirms the cache is doing its job.
cachePoint support requires
SDK v0.2.0 or later in every language. On OpenAI and Gemini models
caching is automatic — the marker is ignored there, so the same code runs unchanged
across providers.
Packages & installation
| Language | Package | Requires | Runtime dependencies |
|---|---|---|---|
| Python | audacity-sdk (PyPI) | Python 3.9+ | None — stdlib only |
| TypeScript / JS | @audacity/sdk (npm) | Node ≥ 18 or Bun | None — global fetch, dual ESM/CJS, full .d.ts |
| Go | github.com/Audacity-Investments/audacity-sdk-go | Go 1.22+ | None — stdlib only |
| Java | com.audacityinvestments:audacity-sdk (Maven Central) | Java 11+ | Gson only — uses java.net.http |
| Rust | audacity-sdk (crates.io) | Rust (tokio) | reqwest, serde, tokio |
pip install audacity-sdknpm install @audacity/sdk
# or
bun add @audacity/sdkgo get github.com/Audacity-Investments/audacity-sdk-go<dependency>
<groupId>com.audacityinvestments</groupId>
<artifactId>audacity-sdk</artifactId>
<version>0.4.0</version>
</dependency>[dependencies]
audacity-sdk = "0.4.0"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
The Python distribution is named audacity-sdk, but the import stays
audacity — exactly the boto3 pattern of installing one name and importing another.
SDK quickstart
A complete request/response round trip with the official AI Reserve SDKs. Set
AUDACITY_API_KEY in your environment, pick a model, and run — the shapes below
will look familiar to anyone who has used Bedrock's Converse. (Using the OpenAI
or Anthropic SDK instead? See connect what you already use.)
from audacity import Audacity
client = Audacity(api_key="aireserve_api_…") # or set AUDACITY_API_KEY
response = client.converse(
modelId="gpt-5.4-mini",
messages=[{"role": "user", "content": [{"text": "What is 2+2?"}]}],
inferenceConfig={"maxTokens": 256, "temperature": 0.0},
)
print(response["output"]["message"]["content"][0]["text"])
print(response["stopReason"]) # "end_turn"
print(response["usage"]) # {"inputTokens": …, "outputTokens": …, "totalTokens": …}
print(response["metrics"]) # {"latencyMs": …}import { AudacityRuntimeClient, ConverseCommand } from "@audacity/sdk";
const client = new AudacityRuntimeClient({ apiKey: process.env.AUDACITY_API_KEY });
const response = await client.send(
new ConverseCommand({
modelId: "gpt-5.4-mini",
messages: [{ role: "user", content: [{ text: "Hello!" }] }],
inferenceConfig: { maxTokens: 500, temperature: 0.2 },
})
);
console.log(response.output?.message?.content?.[0]?.text);
// response.stopReason, response.usage, response.metrics.latencyMs also populatedpackage main
import (
"context"
"fmt"
"log"
"github.com/Audacity-Investments/audacity-sdk-go"
"github.com/Audacity-Investments/audacity-sdk-go/audacityruntime"
"github.com/Audacity-Investments/audacity-sdk-go/audacityruntime/types"
)
func main() {
// Reads AUDACITY_API_KEY from the environment.
client := audacityruntime.New(audacityruntime.Options{})
resp, err := client.Converse(context.Background(), &audacityruntime.ConverseInput{
ModelId: audacity.String("gpt-5.4-mini"),
Messages: []types.Message{{
Role: types.ConversationRoleUser,
Content: []types.ContentBlock{
&types.ContentBlockMemberText{Value: "Hello, world!"},
},
}},
InferenceConfig: &types.InferenceConfiguration{
MaxTokens: audacity.Int32(500),
Temperature: audacity.Float32(0.2),
},
})
if err != nil {
log.Fatal(err)
}
out := resp.Output.(*types.ConverseOutputMemberMessage)
text := out.Value.Content[0].(*types.ContentBlockMemberText).Value
fmt.Println(text)
}import com.audacity.sdk.*;
var client = AudacityRuntimeClient.builder()
.apiKey(System.getenv("AUDACITY_API_KEY"))
.build();
ConverseResponse response = client.converse(request -> request
.modelId("gpt-5.4-mini")
.messages(Message.builder()
.role(ConversationRole.USER)
.content(ContentBlock.fromText("What is 2 + 2?"))
.build())
.inferenceConfig(cfg -> cfg.maxTokens(500).temperature(0.2f)));
System.out.println(response.output().message().content().get(0).text());
System.out.printf("Stop reason : %s%n", response.stopReason());
System.out.printf("Input tokens: %d%n", response.usage().inputTokens());
System.out.printf("Latency ms : %d%n", response.metrics().latencyMs());use audacity_sdk::{Client, ContentBlock, ConversationRole, Message};
#[tokio::main]
async fn main() -> Result<(), audacity_sdk::Error> {
// Reads AUDACITY_API_KEY from the environment.
let client = Client::from_env()?;
let response = client.converse()
.model_id("gpt-5.4-mini")
.messages(
Message::builder()
.role(ConversationRole::User)
.content(ContentBlock::Text("Hello!".into()))
.build()?
)
.inference_config(
audacity_sdk::InferenceConfiguration::builder()
.max_tokens(500)
.temperature(0.2)
.build()
)
.send()
.await?;
let text = response
.output().unwrap()
.as_message().unwrap()
.content().first().unwrap()
.as_text().unwrap();
println!("{text}");
Ok(())
}Streaming
ConverseStream returns the same Bedrock event union you already handle:
messageStart → contentBlockStart → contentBlockDelta → contentBlockStop →
messageStop → metadata. Under the hood the gateway streams server-sent events;
the SDK translates each chunk into typed Bedrock events, and the final
metadata event always carries token usage and latency.
Streams are never cut off by the request timeout — the timeout applies only until response headers arrive, so long generations run to completion.
stream_response = client.converse_stream(
modelId="gpt-5.4-mini",
messages=[{"role": "user", "content": [{"text": "Write me a haiku."}]}],
)
for event in stream_response["stream"]: # boto3 parity: response["stream"]
if "contentBlockDelta" in event:
delta = event["contentBlockDelta"]["delta"]
print(delta.get("text", ""), end="", flush=True)import { ConverseStreamCommand } from "@audacity/sdk";
const { stream } = await client.send(
new ConverseStreamCommand({
modelId: "gpt-5.4-mini",
messages: [{ role: "user", content: [{ text: "Tell me a story" }] }],
})
);
for await (const event of stream) {
if ("contentBlockDelta" in event) {
const delta = event.contentBlockDelta.delta;
if ("text" in delta) process.stdout.write(delta.text);
}
}streamResp, err := client.ConverseStream(ctx, &audacityruntime.ConverseStreamInput{
ModelId: audacity.String("gpt-5.4-mini"),
Messages: []types.Message{{
Role: types.ConversationRoleUser,
Content: []types.ContentBlock{&types.ContentBlockMemberText{Value: "Tell me a story"}},
}},
})
if err != nil {
log.Fatal(err)
}
stream := streamResp.GetStream()
defer stream.Close()
for event := range stream.Events() {
switch e := event.(type) {
case *types.ConverseStreamOutputMemberContentBlockDelta:
if td, ok := e.Value.Delta.(*types.ContentBlockDeltaMemberText); ok {
fmt.Print(td.Value)
}
case *types.ConverseStreamOutputMemberMetadata:
fmt.Printf("\n[tokens in=%d out=%d latency=%dms]\n",
e.Value.Usage.InputTokens, e.Value.Usage.OutputTokens, e.Value.Metrics.LatencyMs)
}
}
if err := stream.Err(); err != nil {
log.Fatal(err)
}// Push handler (AWS SDK v2 style)
client.converseStream(
request -> request
.modelId("gpt-5.4-mini")
.messages(Message.builder()
.role(ConversationRole.USER)
.content(ContentBlock.fromText("Tell me a short story."))
.build()),
ConverseStreamHandler.builder()
.onContentBlockDelta(e -> {
if (e.delta().text() != null) System.out.print(e.delta().text());
})
.onMessageStop(e -> System.out.println("\nStop: " + e.stopReason()))
.onMetadata(e -> System.out.printf("Tokens: %d%n", e.usage().totalTokens()))
.build());
// Or a pull-based iterator:
try (var iter = client.converseStreamIterator(request -> request
.modelId("gpt-5.4-mini")
.messages(Message.builder()
.role(ConversationRole.USER)
.content(ContentBlock.fromText("Hello!"))
.build()))) {
while (iter.hasNext()) {
ConverseStreamOutput event = iter.next();
// handle event
}
}use audacity_sdk::ConverseStreamOutput;
let mut output = client.converse_stream()
.model_id("gpt-5.4-mini")
.messages(
Message::builder()
.role(ConversationRole::User)
.content(ContentBlock::Text("Tell me a joke".into()))
.build()?
)
.send()
.await?;
while let Some(event) = output.stream.recv().await? {
if let ConverseStreamOutput::ContentBlockDelta(e) = event {
if let audacity_sdk::ContentBlockDeltaPayload::Text(t) = e.delta {
print!("{t}");
}
}
}OpenAI & Anthropic formats — native in the AI Reserve SDKs
All five AI Reserve SDKs — Python, TypeScript, Go, Java, and Rust — speak all three of the gateway's wire formats from one client: the Bedrock Converse surface you've seen above, plus verbatim pass-throughs of the OpenAI and Anthropic formats. Same API key, same retries and exception taxonomy, no extra dependency — and no shape translation: requests go out exactly as you write them, responses come back exactly as the gateway returns them. Both formats work with every gateway model; the gateway bridges the wire format (GPT models answer Anthropic-format calls and vice versa).
| Surface | Endpoint | Wire format |
|---|---|---|
client.chat.completions.create(…) | POST /v1/chat/completions | OpenAI Chat Completions |
client.messages.create(…) | POST /v1/messages | Anthropic Messages (Claude Code's format) |
client.messages.count_tokens(…) / countTokens(…) | POST /v1/messages/count_tokens | Anthropic Messages (free — no inference) |
OpenAI format
from audacity import Audacity
client = Audacity() # reads AUDACITY_API_KEY
response = client.chat.completions.create(
model="gpt-5.4-mini",
messages=[{"role": "user", "content": "Hello!"}],
max_tokens=256,
)
print(response["choices"][0]["message"]["content"])
# stream=True returns a generator of raw OpenAI chunks
for chunk in client.chat.completions.create(
model="claude-sonnet-4-6", # any gateway model
messages=[{"role": "user", "content": "Write a haiku."}],
stream=True,
):
delta = chunk["choices"][0]["delta"] if chunk.get("choices") else {}
print(delta.get("content", ""), end="", flush=True)import { Audacity } from "@audacity/sdk";
const client = new Audacity(); // reads AUDACITY_API_KEY
const response = await client.chat.completions.create({
model: "gpt-5.4-mini",
messages: [{ role: "user", content: "Hello!" }],
max_tokens: 256,
});
console.log(response.choices[0]?.message?.content);
// stream: true switches the return type to an AsyncIterable of raw chunks
const stream = await client.chat.completions.create({
model: "claude-sonnet-4-6", // any gateway model
messages: [{ role: "user", content: "Write a haiku." }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices?.[0]?.delta?.content ?? "");
}resp, err := client.Chat.Completions.Create(ctx, &audacityruntime.ChatCompletionCreateParams{
Model: "gpt-5.4-mini",
Messages: []map[string]interface{}{{"role": "user", "content": "Hello!"}},
MaxTokens: audacity.Int32(256),
Extra: map[string]interface{}{"seed": 7}, // any other OpenAI field
})
if err != nil { log.Fatal(err) }
fmt.Println(resp.Raw["choices"])
// CreateStream returns raw chunk events; ends at data: [DONE]
stream, err := client.Chat.Completions.CreateStream(ctx, &audacityruntime.ChatCompletionCreateParams{
Model: "claude-sonnet-4-6", // any gateway model
Messages: []map[string]interface{}{{"role": "user", "content": "Write a haiku."}},
})
if err != nil { log.Fatal(err) }
defer stream.Close()
for event := range stream.Events() {
fmt.Print(event)
}
if err := stream.Err(); err != nil { log.Fatal(err) }Map<String, Object> resp = client.chat().completions().create(p -> p
.model("gpt-5.4-mini")
.addMessage("user", "Hello!")
.maxTokens(256)
.put("reasoning_effort", "low")); // any field the gateway supports
// Streaming yields raw OpenAI chunks; ends at data: [DONE].
// The iterator holds a connection — use try-with-resources.
try (var stream = client.chat().completions().createStreaming(p -> p
.model("claude-sonnet-4-6") // any gateway model
.addMessage("user", "Write a haiku."))) {
stream.forEachRemaining(System.out::println);
}let response = client.chat_completions().create(json!({
"model": "gpt-5.4-mini",
"messages": [{"role": "user", "content": "Hello!"}],
"max_tokens": 256,
})).await?;
println!("{}", response["choices"][0]["message"]["content"]);
// create_stream yields raw chunk objects; ends at data: [DONE]
let mut stream = client.chat_completions().create_stream(json!({
"model": "claude-sonnet-4-6", // any gateway model
"messages": [{"role": "user", "content": "Write a haiku."}],
})).await?;
while let Some(chunk) = stream.recv().await? {
if let Some(delta) = chunk["choices"][0]["delta"]["content"].as_str() {
print!("{delta}");
}
}Anthropic format
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=256,
messages=[{"role": "user", "content": "Hello!"}],
)
print(response["content"][0]["text"])
# stream=True yields raw Anthropic events (message_start … message_stop)
for event in client.messages.create(
model="claude-sonnet-4-6",
max_tokens=256,
messages=[{"role": "user", "content": "Write a haiku."}],
stream=True,
):
if event["type"] == "content_block_delta":
print(event["delta"].get("text", ""), end="", flush=True)
# Free token counting
count = client.messages.count_tokens(
model="claude-sonnet-4-6",
messages=[{"role": "user", "content": "How many tokens is this?"}],
)
print(count["input_tokens"])const message = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 256,
messages: [{ role: "user", content: "Hello!" }],
});
console.log(message.content[0]?.["text"]);
// stream: true yields raw Anthropic events (message_start … message_stop)
const events = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 256,
messages: [{ role: "user", content: "Write a haiku." }],
stream: true,
});
for await (const event of events) {
if (event.type === "content_block_delta") {
process.stdout.write((event["delta"] as { text?: string }).text ?? "");
}
}
// Free token counting
const count = await client.messages.countTokens({
model: "claude-sonnet-4-6",
messages: [{ role: "user", content: "How many tokens is this?" }],
});
console.log(count.input_tokens);msg, err := client.Messages.Create(ctx, &audacityruntime.MessageCreateParams{
Model: "claude-sonnet-4-6",
MaxTokens: audacity.Int32(256),
Messages: []map[string]interface{}{{"role": "user", "content": "Hello!"}},
})
if err != nil { log.Fatal(err) }
fmt.Println(msg.Raw["content"])
// Free token counting
count, err := client.Messages.CountTokens(ctx, &audacityruntime.CountTokensParams{
Model: "claude-sonnet-4-6",
Messages: []map[string]interface{}{{"role": "user", "content": "How many tokens?"}},
})
if err != nil { log.Fatal(err) }
fmt.Println(count.Raw["input_tokens"])Map<String, Object> msg = client.messages().create(p -> p
.model("claude-sonnet-4-6")
.maxTokens(256)
.addMessage("user", "Hello!"));
// Streaming yields raw Anthropic events (message_start … message_stop)
try (var events = client.messages().createStreaming(p -> p
.model("claude-sonnet-4-6")
.maxTokens(256)
.addMessage("user", "Write a haiku."))) {
events.forEachRemaining(System.out::println);
}
// Free token counting
Map<String, Object> count = client.messages().countTokens(p -> p
.model("claude-sonnet-4-6")
.addMessage("user", "How many tokens?"));
System.out.println(count.get("input_tokens"));let message = client.messages().create(json!({
"model": "claude-sonnet-4-6",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Hello!"}],
})).await?;
println!("{}", message["content"][0]["text"]);
// Streaming yields raw Anthropic events (message_start … message_stop)
let mut events = client.messages().create_stream(json!({
"model": "claude-sonnet-4-6",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Write a haiku."}],
})).await?;
while let Some(event) = events.recv().await? {
if event["type"] == "content_block_delta" {
print!("{}", event["delta"]["text"].as_str().unwrap_or(""));
}
}
// Free token counting
let count = client.messages().count_tokens(json!({
"model": "claude-sonnet-4-6",
"messages": [{"role": "user", "content": "How many tokens?"}],
})).await?;
println!("{}", count["input_tokens"]);
Pass-through means future-proof. The SDK never renames or strips request
fields, so any parameter the gateway supports — including ones added after your SDK
version shipped — works immediately. Streaming follows each provider's own protocol:
OpenAI streams end at data: [DONE]; Anthropic streams end with a
message_stop event. Mid-stream errors raise
ModelStreamErrorException, and standard HTTP errors map through the same
taxonomy as Converse calls (429 retried with backoff, budget errors
never retried).
Available in all five SDKs from v0.5.0: audacity-sdk (Python),
@audacity/sdk (TypeScript), audacity-sdk-go (Go),
com.audacityinvestments:audacity-sdk (Java), and audacity-sdk
(Rust). Prefer no new dependency at all? Point the official
openai/anthropic packages at the gateway
(guide).
Migrating from OpenAI
Migration is intentionally boring. If you call OpenAI through the official
openai package, the change is two lines in client
construction — the base URL and the API key. Message shapes, tool definitions,
streaming loops, temperature and every other request field pass through
the gateway verbatim, so nothing else in your codebase changes.
| OpenAI concept | AI Reserve equivalent |
|---|---|
API key from platform.openai.com | API key from your AI Reserve workspace (aireserve_api_…) — see Authentication |
OPENAI_API_KEY environment variable | AUDACITY_API_KEY, or pass the key to the constructor |
https://api.openai.com/v1 | https://api.aireserve.com/v1 (keep the /v1 suffix) |
| OpenAI models only | Every gateway model — Claude, Gemini, Grok, Llama, … answer the same chat.completions call |
| OpenAI error objects | Same shapes on standard HTTP statuses (400/401/404/429/5xx) — see Errors & retries |
import os
from openai import OpenAI
client = OpenAI(
- api_key=os.environ["OPENAI_API_KEY"],
+ base_url="https://api.aireserve.com/v1", # note the /v1 suffix
+ api_key=os.environ["AUDACITY_API_KEY"],
)
# Everything below is unchanged — and now works with any gateway model:
res = client.chat.completions.create(
- model="gpt-4o-mini",
+ model="gpt-5.4-mini", # or claude-sonnet-4-6, gemini-2.5-flash, …
messages=[{"role": "user", "content": "hello"}],
)
# Streaming call sites are identical:
for chunk in client.chat.completions.create(model=…, messages=…, stream=True):
print(chunk.choices[0].delta.content or "", end="") import OpenAI from "openai";
const client = new OpenAI({
- apiKey: process.env.OPENAI_API_KEY,
+ baseURL: "https://api.aireserve.com/v1", // note the /v1 suffix
+ apiKey: process.env.AUDACITY_API_KEY,
});
// Everything below is unchanged — and now works with any gateway model:
const res = await client.chat.completions.create({
- model: "gpt-4o-mini",
+ model: "gpt-5.4-mini", // or claude-sonnet-4-6, gemini-2.5-flash, …
messages: [{ role: "user", content: "hello" }],
});Prefer one dependency across all of your model traffic? The AI Reserve SDKs expose the OpenAI format natively (v0.5.0 in all five languages), with the gateway's retry policy and exception taxonomy built in. The calling convention is identical — only the import and constructor change:
from audacity import Audacity
client = Audacity() # reads AUDACITY_API_KEY
response = client.chat.completions.create( # same calling convention
model="gpt-5.4-mini",
messages=[{"role": "user", "content": "Hello!"}],
)
What carries over verbatim. Messages, tools /
tool_choice, response_format, sampling parameters, and
streaming loops — requests pass through unmodified, and streams still end at
data: [DONE]. What changes: the key and its environment variable, the
base URL, and the model namespace — every gateway model is now callable from the same
code, and GET /v1/models enumerates the live catalog.
Migrating from Anthropic
Same story as OpenAI: keep the official
anthropic package and change two lines in client
construction. System prompts, tool use, streaming events, and
cache_control blocks all pass through the gateway verbatim.
Base URL — host root, no /v1. The Anthropic SDK appends
/v1/messages itself, so the base URL is
https://api.aireserve.com — unlike OpenAI-flavored clients,
which need the /v1 suffix.
| Anthropic concept | AI Reserve equivalent |
|---|---|
API key from console.anthropic.com | API key from your AI Reserve workspace (aireserve_api_…) — see Authentication |
ANTHROPIC_API_KEY environment variable | AUDACITY_API_KEY, or pass the key to the constructor |
https://api.anthropic.com | https://api.aireserve.com (host root — no /v1) |
| Claude models only | Every gateway model answers /v1/messages — GPT, Gemini, Grok, … via the same messages.create call |
count_tokens | Identical — POST /v1/messages/count_tokens, free, no inference |
import os
from anthropic import Anthropic
client = Anthropic(
- api_key=os.environ["ANTHROPIC_API_KEY"],
+ base_url="https://api.aireserve.com", # host root — NO /v1
+ api_key=os.environ["AUDACITY_API_KEY"],
)
# Everything below is unchanged:
res = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=256,
messages=[{"role": "user", "content": "hello"}],
)
# Streaming still yields message_start … message_stop events:
with client.messages.stream(model=…, max_tokens=…, messages=…) as stream:
for text in stream.text_stream:
print(text, end="") import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY,
+ baseURL: "https://api.aireserve.com", // host root — NO /v1
+ apiKey: process.env.AUDACITY_API_KEY,
});
// Everything below is unchanged:
const res = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 256,
messages: [{ role: "user", content: "hello" }],
});
The AI Reserve SDKs speak the Anthropic format natively too
(v0.5.0 in all five languages) — same messages.create /
count_tokens convention, one dependency for every wire format:
from audacity import Audacity
client = Audacity() # reads AUDACITY_API_KEY
response = client.messages.create( # same calling convention
model="claude-sonnet-4-6",
max_tokens=256,
messages=[{"role": "user", "content": "Hello!"}],
)
Claude Code and the Claude Agent SDK migrate without touching this
section at all — they are configured entirely through
ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN environment
variables. See the Claude Code recipe.
What carries over verbatim. System prompts, tool use
(tool_use / tool_result blocks), streaming events
(message_start … message_stop), and
prompt caching — cache_control markers are
forwarded to Anthropic unchanged, with the same minimums and 5-minute lifetime. What
changes: the key and its environment variable, the base URL, and the model namespace —
/v1/messages answers for every gateway model, not just Claude.
Migrating from Bedrock
Migration is intentionally boring. In every language, the change is confined to
imports and client construction — plus swapping Bedrock model ARNs for
AI Reserve model IDs (for example gpt-5.4-mini, gpt-5.5,
claude-opus-4-8). Message shapes, tool configs, streaming loops, and error
handling carry over verbatim.
| Bedrock concept | AI Reserve equivalent |
|---|---|
| AWS region + credential chain / IAM | One bearer key: AUDACITY_API_KEY |
Model ARN (anthropic.claude-3-5-sonnet-…) | AI Reserve model ID (claude-opus-4-8, gpt-5.5, …) |
Converse / ConverseStream | Identical — same names, same shapes |
toolConfig, toolUse, toolResult | Identical |
ThrottlingException, ValidationException, … | Identical exception names, same retryability semantics |
| Bedrock standard retry mode | Same policy: jittered backoff, Retry-After honored |
-import boto3
-client = boto3.client("bedrock-runtime", region_name="us-east-1")
+from audacity import Audacity
+client = Audacity(api_key="aireserve_api_…") # only line that changes
response = client.converse(
- modelId="anthropic.claude-3-sonnet-20240229-v1:0",
+ modelId="gpt-5.4-mini", # use an AI Reserve model ID
messages=[{"role": "user", "content": [{"text": "Hi"}]}],
)
# Streaming call sites are identical:
stream_resp = client.converse_stream(modelId=…, messages=…)
for event in stream_resp["stream"]:
…-import {
- BedrockRuntimeClient,
- ConverseCommand,
- ConverseStreamCommand,
-} from "@aws-sdk/client-bedrock-runtime";
+import {
+ AudacityRuntimeClient as BedrockRuntimeClient,
+ ConverseCommand,
+ ConverseStreamCommand,
+} from "@audacity/sdk";
-const client = new BedrockRuntimeClient({ region: "us-east-1" });
+const client = new BedrockRuntimeClient({ apiKey: process.env.AUDACITY_API_KEY });
const response = await client.send(
new ConverseCommand({
modelId: "gpt-5.4-mini",
messages: [{ role: "user", content: [{ text: "Hi" }] }],
})
); import (
- "github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
- "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"
+ "github.com/Audacity-Investments/audacity-sdk-go/audacityruntime"
+ "github.com/Audacity-Investments/audacity-sdk-go/audacityruntime/types"
)
-client := bedrockruntime.NewFromConfig(cfg)
+client := audacityruntime.New(audacityruntime.Options{}) // reads AUDACITY_API_KEY
resp, err := client.Converse(ctx, &audacityruntime.ConverseInput{
- ModelId: aws.String("anthropic.claude-3-5-sonnet-20241022-v2:0"),
+ ModelId: audacity.String("gpt-5.4-mini"),
Messages: []types.Message{{ … }},
})-BedrockRuntimeClient client = BedrockRuntimeClient.builder()
- .region(Region.US_EAST_1)
- .credentialsProvider(DefaultCredentialsProvider.create())
- .build();
+var client = AudacityRuntimeClient.builder()
+ .apiKey(System.getenv("AUDACITY_API_KEY"))
+ .build();
ConverseResponse response = client.converse(req -> req
- .modelId("anthropic.claude-3-5-sonnet-20241022-v2:0")
+ .modelId("gpt-5.4-mini")
.messages(Message.builder()
.role(ConversationRole.USER)
.content(ContentBlock.fromText("Hi"))
.build())
.inferenceConfig(cfg -> cfg.maxTokens(500).temperature(0.2f)));-use aws_sdk_bedrockruntime::Client;
-use aws_sdk_bedrockruntime::types::{ContentBlock, ConversationRole, Message};
+use audacity_sdk::{Client, ContentBlock, ConversationRole, Message};
-let config = aws_config::load_from_env().await;
-let client = Client::new(&config);
+let client = audacity_sdk::Client::from_env()?; // reads AUDACITY_API_KEY
let response = client.converse()
- .model_id("anthropic.claude-3-5-sonnet-20241022-v2:0")
+ .model_id("gpt-5.4-mini")
.messages(
Message::builder()
.role(ConversationRole::User)
.content(ContentBlock::Text("Hello".into()))
.build()?
)
.send()
.await?;converseStream in the AI Reserve SDK is synchronous
(a blocking push handler or a pull iterator) rather than the AWS async
CompletableFuture publisher — for most codebases this removes boilerplate rather
than adding it.
Bedrock-routed models
Some teams prefer a model's serving behavior via AWS Bedrock over the provider's
direct API. AI Reserve offers both paths behind the same endpoint: append
-bedrock to a model ID and the gateway routes that request through AWS
Bedrock instead of the provider directly. Everything else — SDK, key, shapes,
streaming, errors — is identical, so you can A/B the two paths by changing only the
model string.
# Direct provider API
client.converse(modelId="claude-opus-4-8", messages=…)
# Same model, routed through AWS Bedrock
client.converse(modelId="claude-opus-4-8-bedrock", messages=…)
| Direct model ID | Bedrock-routed variant |
|---|---|
claude-fable-5 | claude-fable-5-bedrock |
claude-opus-4-8 | claude-opus-4-8-bedrock |
claude-opus-4-7 | claude-opus-4-7-bedrock |
claude-opus-4-6 | claude-opus-4-6-bedrock |
claude-sonnet-4-6 | claude-sonnet-4-6-bedrock |
claude-sonnet-4-5 | claude-sonnet-4-5-bedrock |
claude-haiku-4-5-20251001 | claude-haiku-4-5-bedrock |
llama-4-maverick | llama-4-maverick-bedrock |
llama-4-scout | llama-4-scout-bedrock |
mistral-large | mistral-large-bedrock |
deepseek-reasoner | deepseek-reasoner-bedrock |
Models that Bedrock does not serve (OpenAI GPT, Gemini, Grok, Perplexity Sonar, Moonshot)
have no -bedrock variant — the direct IDs remain the only path for those.
All variants route through AWS Bedrock except claude-fable-5-bedrock,
claude-opus-4-8-bedrock, and claude-opus-4-7-bedrock, which
temporarily serve via Anthropic's direct API while Bedrock access for those models is
being enabled on our AWS account — the IDs are stable and will be re-pointed
gateway-side with no client change.
Latency
Compared to calling a provider (or Bedrock) directly from your own code, the AI Reserve gateway adds a small, fixed toll — authentication, rate limiting, quota checks, and request translation:
| Component | Added latency |
|---|---|
| Auth + rate limit (gateway edge) | ~10–30 ms |
| Quota / spend-cap checks (proxy) | ~10–30 ms |
| Request translation (router) | ~20–60 ms |
| Cross-cloud hop (region dependent) | ~5–40 ms |
Rule of thumb: ~60–200 ms added to time-to-first-token, and effectively zero between tokens — streaming responses are piped through without re-buffering, so inter-token latency is determined entirely by the model provider. Against typical generation times of seconds to tens of seconds, the overhead is low single-digit percent. The toll does not grow with context size or generation length.
Note that provider-side differences (for example, Bedrock vs. direct-API
time-to-first-token for the same Claude model, which varies by region and load) are
independent of the gateway and typically larger than the gateway's own overhead. The
-bedrock variants make measuring both paths for your workload a
one-string A/B test.
AWS SDK (boto3) / Bedrock migration
Already on Bedrock? Keep your code. The gateway serves the Bedrock
Converse API at the same paths the AWS SDKs call
(POST /model/{modelId}/converse and
POST /model/{modelId}/converse-stream), so an unmodified
boto3 bedrock-runtime client works by overriding the endpoint
and credentials — every converse() / converse_stream() call
site, message shape, toolConfig, and modelId string
stays byte-identical:
import boto3
-# Before — real AWS Bedrock
-client = boto3.client("bedrock-runtime", region_name="us-east-1")
+# After — AI Reserve gateway (the whole migration)
+client = boto3.client(
+ "bedrock-runtime",
+ endpoint_url="https://api.aireserve.com",
+ aws_access_key_id=os.environ["AUDACITY_API_KEY"], # your aireserve_api_… key
+ aws_secret_access_key="audacity", # ignored — any value
+ region_name="us-east-1", # ignored — any value
+)
response = client.converse(
modelId="us.anthropic.claude-sonnet-4-5-20250929-v1:0", # unchanged
messages=[{"role": "user", "content": [{"text": "Hi"}]}],
inferenceConfig={"maxTokens": 256},
)
Why the secret key and region don't matter: boto3 signs every request
with SigV4 and puts the access-key ID in the Authorization header's
Credential field. The gateway authenticates the
aireserve_api_… key it finds there — the key itself is the bearer secret,
carried over TLS, exactly the same trust model as the x-api-key header on
every other endpoint. The SigV4 signature (which is what the fake secret key
produces) is ignored, and no region routing exists — there is one gateway.
Full example
import os
import boto3
client = boto3.client(
service_name="bedrock-runtime",
endpoint_url="https://api.aireserve.com",
aws_access_key_id=os.environ["AUDACITY_API_KEY"],
aws_secret_access_key="audacity",
region_name="us-east-1",
)
response = client.converse(
modelId="claude-sonnet-4-6-bedrock",
messages=[{"role": "user", "content": [{"text": "Say hello in five words."}]}],
inferenceConfig={"maxTokens": 64, "temperature": 0.5},
)
print(response["output"]["message"]["content"][0]["text"])
print(response["usage"]) # {"inputTokens": …, "outputTokens": …, "totalTokens": …}
print(response["stopReason"]) # "end_turn"
Streaming works the same way — converse_stream() responses
are real AWS binary event streams (application/vnd.amazon.eventstream,
per-frame CRC32 checksums), yielding the standard Bedrock event union
(messageStart → contentBlockDelta → … → messageStop → metadata) with token
usage in the final metadata event:
stream_response = client.converse_stream(
modelId="claude-sonnet-4-6-bedrock",
messages=[{"role": "user", "content": [{"text": "Write me a haiku."}]}],
)
for event in stream_response["stream"]:
if "contentBlockDelta" in event:
print(event["contentBlockDelta"]["delta"].get("text", ""), end="", flush=True)
elif "metadata" in event:
print("\n", event["metadata"]["usage"]) # {"inputTokens": …, "outputTokens": …, "totalTokens": …}
The same two overrides work from the AWS JS SDK
(@aws-sdk/client-bedrock-runtime):
import { BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime";
const client = new BedrockRuntimeClient({
endpoint: "https://api.aireserve.com",
region: "us-east-1", // ignored — any value
credentials: {
accessKeyId: process.env.AUDACITY_API_KEY!, // your aireserve_api_… key
secretAccessKey: "audacity", // ignored — any value
},
});
const response = await client.send(new ConverseCommand({
modelId: "claude-sonnet-4-6-bedrock",
messages: [{ role: "user", content: [{ text: "Hi" }] }],
}));
Model IDs
Your existing AWS model ids work as-is. The gateway resolves the
Bedrock model ids below to their AI Reserve catalog equivalents, so migrating code
keeps its modelId strings untouched. Region inference-profile prefixes
(us. / eu. / apac.) are optional, and full Bedrock ARNs
(foundation-model or inference-profile) are accepted too.
| AWS model id (as-is) | AI Reserve id | Notes |
|---|---|---|
us.anthropic.claude-opus-4-6-v1 | claude-opus-4-6-bedrock | Served via AWS Bedrock |
us.anthropic.claude-sonnet-4-6 | claude-sonnet-4-6-bedrock | Served via AWS Bedrock |
us.anthropic.claude-sonnet-4-5-20250929-v1:0 | claude-sonnet-4-5-bedrock | Served via AWS Bedrock |
us.anthropic.claude-haiku-4-5-20251001-v1:0 | claude-haiku-4-5-bedrock | Served via AWS Bedrock |
us.meta.llama4-maverick-17b-instruct-v1:0 | llama-4-maverick-bedrock | Served via AWS Bedrock |
us.meta.llama4-scout-17b-instruct-v1:0 | llama-4-scout-bedrock | Served via AWS Bedrock |
mistral.mistral-large-3-675b-instruct | mistral-large-bedrock | Served via AWS Bedrock (on-demand id — no region prefix) |
us.deepseek.r1-v1:0 | deepseek-reasoner-bedrock | Served via AWS Bedrock |
us.anthropic.claude-fable-5 | claude-fable-5-bedrock | Temporarily served via Anthropic direct (Bedrock access pending) |
us.anthropic.claude-opus-4-8 | claude-opus-4-8-bedrock | Temporarily served via Anthropic direct (Bedrock access pending) |
us.anthropic.claude-opus-4-7 | claude-opus-4-7-bedrock | Temporarily served via Anthropic direct (Bedrock access pending) |
AI Reserve catalog names also work — the same IDs every other endpoint
takes (anything GET /v1/models returns), including non-Bedrock models like
gpt-5.4-mini or gemini-2.5-flash: the Converse surface is bridged
to every provider, not just Anthropic-on-Bedrock. AWS ids not in the table above
return ResourceNotFoundException.
Supported operations
| Operation | Status | Use instead |
|---|---|---|
converse() | Supported | — |
Tool use (toolConfig, toolUse, toolResult) | Supported | — |
| Images in messages | Supported | — |
Prompt caching (cachePoint) | Supported | — |
converse_stream() | Supported | — |
Tool-use streaming (contentBlockStart + toolUse input deltas) | Supported | — |
invoke_model() / …_with_response_stream() | Not served (404) | converse(), or the audacity-sdk |
| Documents / video content blocks | Not yet — returns ValidationException | audacity-sdk (video), /v1/chat/completions |
| Guardrails, Knowledge Bases, Agents | Out of scope | — |
Errors arrive in the AWS wire shape (__type +
x-amzn-ErrorType), so boto3 raises its normal typed exceptions:
bad key → AccessDeniedException, rate limit →
ThrottlingException (with Retry-After), spend cap →
ServiceQuotaExceededException, unknown model →
ResourceNotFoundException. For converse_stream(), errors
before the first event use the same HTTP shapes (raised from the
converse_stream() call itself); a failure after streaming has
started arrives as an in-stream internalServerException event frame,
which boto3 raises as an EventStreamError from the stream iterator —
the connection is never silently dropped mid-stream.
Which path should I use?
- boto3 / AWS SDK against the gateway — you have an existing Bedrock codebase (or a vendor tool that only speaks Bedrock) and want zero code changes beyond the client constructor —
converse()andconverse_stream()both supported. - audacity-sdk — you want the same Converse surface plus streaming, retries tuned for the gateway, typed exceptions, file upload, and image generation. The recommended default for new integrations.
- OpenAI-compatible API (
/v1/chat/completions) — your stack is already built on OpenAI SDKs, LangChain, or anything OpenAI-shaped. Full streaming, broadest ecosystem support.
Errors & retries
Every SDK raises the same Bedrock-named exceptions, so your existing
error-handling paths — including catch-and-backoff logic keyed on
ThrottlingException — port over unchanged. Every server-derived error carries
message, statusCode, errorCode, requestId
(quote it to support), retryAfterSeconds, and the raw response body.
| Exception | Retried automatically | Typical cause |
|---|---|---|
ValidationException | no | Malformed request (HTTP 400) |
AccessDeniedException | no | Bad or missing key; model not allowed (401/403) |
ResourceNotFoundException | no | Unknown model (404) |
ServiceQuotaExceededException | no | Budget / usage cap exhausted (402, or 429 with BUDGET_EXCEEDED) |
ThrottlingException | yes | Rate limited (429) — honors Retry-After |
ModelTimeoutException | yes | Model timeout (408) |
ModelErrorException | no | Model-level failure |
ModelStreamErrorException | no | Stream interrupted after the first byte |
ServiceUnavailableException | yes | Upstream unavailable (502/503/504) |
InternalServerException | yes | Gateway error (500) |
MissingApiKeyError | — | No key resolved; fails before any network call |
SdkError | network only | Connection / decode failure |
from audacity.exceptions import SdkError
try:
response = client.converse(modelId="gpt-5.4-mini", messages=[…])
except client.exceptions.ThrottlingException as e:
print(f"Rate limited: {e.message}, retry after {e.retry_after_seconds}s")
except client.exceptions.AccessDeniedException as e:
print(f"Auth error [{e.status_code}]: {e.message}")
except client.exceptions.ServiceQuotaExceededException as e:
print(f"Budget exhausted: {e.message}")
except SdkError as e:
print(f"Network/decode error: {e.message}")import {
AccessDeniedException,
ThrottlingException,
MissingApiKeyError,
AudacityError,
} from "@audacity/sdk";
try {
const res = await client.send(new ConverseCommand({ /* … */ }));
} catch (err) {
if (err instanceof MissingApiKeyError) {
console.error("No API key configured");
} else if (err instanceof AccessDeniedException) {
console.error("Auth failed:", err.message, "requestId:", err.requestId);
} else if (err instanceof ThrottlingException) {
console.error("Rate limited. Retry after:", err.retryAfterSeconds, "s");
} else if (err instanceof AudacityError) {
// All SDK errors are instances of AudacityError
console.error(err.name, err.statusCode, err.errorCode);
}
}_, err := client.Converse(ctx, input)
switch {
case err == nil:
// success
case errors.Is(err, &types.MissingAPIKeyError{}):
log.Fatal("set AUDACITY_API_KEY")
default:
var throttle *types.ThrottlingException
var quota *types.ServiceQuotaExceededException
var accessDenied *types.AccessDeniedException
switch {
case errors.As(err, &throttle):
fmt.Printf("rate limited (retry-after=%v)\n", throttle.RetryAfterSeconds)
case errors.As(err, "a):
fmt.Println("budget exhausted — will not retry")
case errors.As(err, &accessDenied):
fmt.Println("check your API key")
default:
log.Fatal(err)
}
}try {
ConverseResponse resp = client.converse(request -> request
.modelId("gpt-5.4-mini")
.messages(Message.builder()
.role(ConversationRole.USER)
.content(ContentBlock.fromText("Hi"))
.build()));
} catch (ThrottlingException e) {
System.err.printf("Rate limited. Retry-After: %s s%n", e.retryAfterSeconds());
} catch (AccessDeniedException e) {
System.err.println("Auth failed: " + e.getMessage());
} catch (ServiceQuotaExceededException e) {
System.err.println("Budget or quota exceeded — not retried");
} catch (AudacityException e) {
System.err.printf("SDK error [%d] %s%n", e.statusCode(), e.getMessage());
}use audacity_sdk::Error;
match client.converse().model_id("m").messages(msg).send().await {
Ok(resp) => { /* use resp */ }
Err(Error::Throttling(d)) => {
eprintln!("Rate limited (retry after {:?}s): {}", d.retry_after_seconds, d.message);
}
Err(Error::AccessDenied(d)) => {
eprintln!("Access denied [{}]: {}", d.error_code.unwrap_or_default(), d.message);
}
Err(Error::MissingApiKey) => {
eprintln!("Set AUDACITY_API_KEY");
}
Err(e) => eprintln!("Other error: {e}"),
}Retry policy (Bedrock standard-mode analog)
- Attempts =
maxRetries + 1; the default is 2 retries (3 total attempts). - Retried: network errors, HTTP 429, 500, 502, 503, 504, and 408 — with full-jitter exponential backoff capped at 20 s, honoring any
Retry-Afterheader. - Never retried: auth failures, validation errors, unknown models, and budget exhaustion (
BUDGET_EXCEEDED) — spending errors must never silently retry. - Streaming: retries apply only until response headers arrive. Once the first byte of the stream is consumed, a drop surfaces as
ModelStreamErrorException— a partial generation is never silently replayed.
Configuration
| Option | Environment variable | Default |
|---|---|---|
| API key | AUDACITY_API_KEY | — (required) |
| Base URL | AUDACITY_BASE_URL | https://api.aireserve.com |
| Timeout (per attempt) | — | 120 s |
| Max retries | — | 2 (up to 3 attempts) |
Timeout semantics are consistent everywhere: the timeout bounds connection + headers (and,
for non-streaming Converse, the full body read). For ConverseStream
it applies only until headers arrive — the stream body is never bounded, so
long generations are never cut off mid-stream.
client = Audacity(
api_key="aireserve_api_…",
base_url="https://api.aireserve.com",
timeout=60.0,
max_retries=3,
)const client = new AudacityRuntimeClient({
apiKey: "aireserve_api_...",
baseUrl: "https://api.aireserve.com",
timeoutMs: 30_000,
maxRetries: 3,
});
// Every operation also accepts an abortSignal (AWS SDK v3 parity),
// which cancels an in-flight stream:
const aborter = new AbortController();
const { stream } = await client.send(
new ConverseStreamCommand({ modelId, messages }),
{ abortSignal: aborter.signal },
);client := audacityruntime.New(audacityruntime.Options{
APIKey: "aireserve_api_…", // falls back to AUDACITY_API_KEY
BaseURL: "https://…", // falls back to AUDACITY_BASE_URL, then default
HTTPClient: &http.Client{}, // custom transport / TLS config
MaxRetries: 3, // audacityruntime.NoRetries disables retries
Timeout: 60 * time.Second, // audacityruntime.NoTimeout disables it
})
// If you provide a custom HTTPClient, leave its Timeout unset — an
// http.Client timeout bounds the whole body and would kill long streams.var client = AudacityRuntimeClient.builder()
.apiKey("aireserve_api_…")
.baseUrl("https://api.aireserve.com")
.timeout(Duration.ofSeconds(60))
.maxRetries(3)
.build();use std::time::Duration;
let config = audacity_sdk::Config::builder()
.api_key("aireserve_api_…")
.base_url("https://api.aireserve.com")
.timeout(Duration::from_secs(120))
.max_retries(2) // 3 total attempts
.build()?;
let client = audacity_sdk::Client::new(&config)?;Wire protocol
Under every SDK sits one HTTP endpoint. If you prefer to integrate directly — or want to audit exactly what your traffic looks like — this is the entire surface:
POST https://api.aireserve.com/v1/chat/completions
Authorization: Bearer aireserve_api_…
Content-Type: application/json
The body is OpenAI chat-completions compatible; streaming responses are standard server-sent
events terminated by data: [DONE], with a final usage chunk always included.
The SDKs translate between this wire format and the Bedrock Converse shapes:
| You write (Bedrock shape) | The SDK sends (OpenAI wire) |
|---|---|
modelId, inferenceConfig.maxTokens | model, max_tokens |
system blocks | A single leading system message |
toolConfig.tools[].toolSpec | tools[].function with JSON Schema parameters |
toolResult blocks | role: "tool" messages keyed by tool_call_id |
image blocks (bytes or URL) | image_url content parts (bytes become base64 data URLs) |
Stream events (messageStart … metadata) | Translated from OpenAI SSE chunks by a per-block state machine |
The full normative mapping — every field, every edge case, the SSE state machine, and the
error-code table — lives in the cross-language specification (SPEC.md) that all
five SDKs are tested against. The complete REST reference with request/response schemas is
available as a separate document: AI Reserve Chat Completions API.
AI Reserve Code CLI
audacity is a local, Claude-Code-style coding agent for your terminal whose
brain is your AI Reserve API key — it talks only to the AI Reserve gateway,
never to an external Claude or OpenAI endpoint. The model can read files, search, edit,
and run commands in your working directory, with explicit approval required for anything
that mutates state.
# Install
npm install -g audacity-cli
# Interactive REPL
audacity
# One-shot: run a single prompt to completion, then exit
audacity "fix the failing test"
On first run it prompts for your aireserve_api_… key and a model, then saves
them to ~/.audacity/config.json (mode 600) — enter the key once and it is
remembered across every shell and session on the machine.
Session commands
| Command | Action |
|---|---|
/model [id] | Pick a model (no argument shows a numbered picker) |
/auto | Toggle auto-approve for mutating tools |
/key | Re-enter and save your API key |
/compact | Summarize older turns to shrink context, keeping recent turns verbatim |
/clear | Reset the conversation |
/exit | Quit |
Permissions model
Read-only tools (read_file, list_files, grep) run
without prompting. Mutating tools (edit_file, write_file,
run_bash) show you exactly what will run and ask for approval first — bypass
with --yes/-y or the /auto toggle. In one-shot mode,
mutating tools always require -y.
Versioning & support
- Versioning. All SDKs follow semantic versioning from
0.1.x. The Bedrock-parity surface (Converse,ConverseStream, content blocks, exceptions) is treated as stable; additions are backwards-compatible. - Conformance. Every release of every SDK passes the shared cross-language conformance suite — request mapping, stream-event ordering, both server error shapes, retry behavior, SSE edge cases (split UTF-8 runes, oversized lines, comment frames) — fully offline and deterministic.
- Request IDs. Every server error carries a
requestId. Include it when contacting us and we can trace the exact request through the gateway. - License. The SDKs are source-available under a proprietary license. © AI Reserve Investments. All rights reserved.
Ready to start? Get an API key, install the SDK for your language, and your first Converse call is five lines away.