AI Reserve Developer Documentation

Developer Documentation

Every frontier model. One API key.

The AI Reserve AI gateway serves 50+ models from a dozen providers — chat, reasoning, vision, image generation, and video — behind one endpoint, one key, and one bill. Point your OpenAI or Anthropic SDK (or Claude Code, Codex, and other harnesses) at one base URL and everything just works — and teams on Amazon Bedrock get the same seamless path via the native Converse wire format or our five official SDKs.

50+Models across OpenAI, Anthropic, Google, xAI & more
1 keyChat, tools, vision, image & video generation
3Wire formats: OpenAI, Anthropic, Bedrock Converse
5Official SDKs — Python, TypeScript, Go, Java, Rust

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:

PrioritySource
1Explicit constructor / builder value
2AUDACITY_API_KEY environment variable (base URL: AUDACITY_BASE_URL)
3Defaults — 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 nativelyclient.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:

  1. OpenAI API Key — paste your AI Reserve key (aireserve_api_…) and switch the toggle on.
  2. Override OpenAI Base URL — set to https://api.aireserve.com/v1 (note the /v1 suffix) and switch the toggle on.
  3. Leave the Anthropic key toggle off — Cursor has no Anthropic base-URL override, so Anthropic-slot traffic cannot reach the gateway.
  4. 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.
  5. 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.

FamilyModel IDsNotes
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

CapabilityWhere documented
StreamingStreaming — all chat models
Tool use / function callingTool use
Vision (image input)Images — per-model table
Video inputVideo input — per-model table
Prompt cachingPrompt caching — per-model minimums
Token countingPOST /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

FieldRequiredDescription
modelyesImage model ID — see the table below
promptyesText description of the desired image(s); 1–32,000 characters
nnoNumber of images to generate — integer 1–10 (model-dependent)
sizenoDimensions as a "WxH" string (e.g. 1024x1024); supported values differ per model
qualitynoProvider-specific quality tier (e.g. standard, hd, low, high)
response_formatnourl (default) — a signed download link — or b64_json — inline base64 bytes
usernoEnd-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_prompt appears when the provider rewrites your prompt before generating.

Models & pricing

ModelPricing
gemini-2.5-flash-imageToken-based (≈ $0.039 / image)
gpt-image-1Token-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 retirement. Google is shutting down the entire Imagen family on August 17, 2026, and the whole line is now delisted from the gateway: 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.
Reliability. Upstream image backends occasionally stall with a 503 for a few minutes. There is deliberately no automatic fallback to a different image model — silently swapping models would change output style and quality — so callers should retry instead. The SDKs already retry 503s with jittered backoff up to their configured retry budget.

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].b64Json
out, 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].B64Json
GenerateImageResponse 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).

ModelVideo input
gemini-2.5-flashyes
gemini-2.5-proyes
gemini-3-flash-previewyes
Every other modelno — 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:

  1. POST /v1/files with content_type and size_bytes — returns a file_id, a signed upload_url (valid ~15 minutes), the uri to reference in chat requests, and expires_at.
  2. Upload the bytes to upload_url over a resumable session (Google Cloud Storage protocol: one POST opens the session, then PUT the bytes).
  3. Reference the video with {"video": {"format": …, "source": {"uri": …}}} in a Converse call.

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.

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.

ValueGuidance
low~4× fewer video tokens. The right default for summarization, transcription-style tasks, and long footage
mediumBalanced sampling for general question-answering about a clip
highFiner sampling — reading small on-screen text, UI walkthroughs, fine visual detail
ultra_highMaximum 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 formatMIME type (content_type for uploads)
mp4video/mp4
movvideo/mov
mkvvideo/x-matroska
webmvideo/webm
flvvideo/x-flv
mpegvideo/mpeg
mpgvideo/mpg
wmvvideo/wmv
three_gpvideo/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/files rejects a larger size_bytes, or an unsupported content_type, with 400.
  • Upload URL expiry — the signed upload_url is 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.
boto3 / AWS SDK path. The Bedrock-compatible endpoint does not accept 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.

  1. POST /v1/videos/generations — submit a job. Answers 202 with a job object containing an id.
  2. GET /v1/videos/generations/{id} — poll the job until its status is terminal (succeeded or failed).

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

FieldRequiredDescription
modelyesVideo model ID — see the table below
promptyesText description of the desired video; 1–32,000 characters
duration_secondsnoClip 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
sizenoDimensions 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
usernoEnd-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:

StatusMeaning
queuedAccepted, not yet dispatched to the provider
runningGenerating. Jobs advance when you poll — keep polling every few seconds
succeededTerminal — result is populated
failedTerminal — 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.url is an MP4 hosted on the provider's CDN — download promptly and persist on your side; the URL is not permanent.
  • Extra metadata in result varies by model (for example wan-2.6 echoes width/height/duration; seedance-2.0 omits them).
  • On failure, error is a human-readable string and result stays null.

Models & pricing

ModelResolutionsDurationsApproximate pricing
wan-2.6720p, 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.0480p, 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 submitduration_seconds is not one of the values the model supports (for example wan-2.6 only 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_found on poll — the job id is unknown or belongs to a different workspace's key.
Content policy. Upstream video providers apply safety filtering to prompts and outputs. A prompt that violates the provider's content policy fails the generation — the job lands in 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.

ModelsCaching behaviorWhat you do
OpenAI (gpt-*)Automatic for prompts of 1,024+ tokensNothing
Gemini (gemini-*)Automatic (implicit caching)Nothing
Claude (claude-* and -bedrock variants)Opt-in per requestAdd 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.

ModelMinimum prefixNotes
Claude Sonnet (direct: claude-sonnet-4-6, claude-sonnet-4-5)1,024 tokensAnthropic documented minimum
Claude Sonnet on Bedrock (claude-sonnet-4-6-bedrock, claude-sonnet-4-5-bedrock)1,024 tokensSame as direct
Claude Opus (direct: claude-fable-5, claude-opus-4-8, claude-opus-4-7, claude-opus-4-6)1,024 tokensAnthropic documented minimum
Claude Haiku (direct: claude-haiku-4-5-20251001)2,048 tokensAnthropic documented minimum
Claude Opus / Haiku on Bedrock (claude-opus-4-6-bedrock, claude-haiku-4-5-bedrock, and other -bedrock Opus variants)4,096 tokensAWS Bedrock enforces a higher floor; sub-4,096 prefixes are accepted but never cached (no error, full input billing)
OpenAI (gpt-*)1,024 tokensAutomatic — no marker needed
Grok (grok-*)No published minimumAutomatic — no marker needed; cache reads surface in usage when xAI serves them
Gemini (gemini-2.5-*, gemini-3-*)1,024 (Flash) / 4,096 (Pro) tokensImplicit 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 cache
const 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 cache
resp, 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 cache
ConverseResponse 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 cache
use 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 cache

Verifying 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.

Packages & installation


LanguagePackageRequiresRuntime dependencies
Pythonaudacity-sdk (PyPI)Python 3.9+None — stdlib only
TypeScript / JS@audacity/sdk (npm)Node ≥ 18 or BunNone — global fetch, dual ESM/CJS, full .d.ts
Gogithub.com/Audacity-Investments/audacity-sdk-goGo 1.22+None — stdlib only
Javacom.audacityinvestments:audacity-sdk (Maven Central)Java 11+Gson only — uses java.net.http
Rustaudacity-sdk (crates.io)Rust (tokio)reqwest, serde, tokio
pip install audacity-sdk
npm install @audacity/sdk
# or
bun add @audacity/sdk
go 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 populated
package 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).

SurfaceEndpointWire format
client.chat.completions.create(…)POST /v1/chat/completionsOpenAI Chat Completions
client.messages.create(…)POST /v1/messagesAnthropic Messages (Claude Code's format)
client.messages.count_tokens(…) / countTokens(…)POST /v1/messages/count_tokensAnthropic 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 conceptAI Reserve equivalent
API key from platform.openai.comAPI key from your AI Reserve workspace (aireserve_api_…) — see Authentication
OPENAI_API_KEY environment variableAUDACITY_API_KEY, or pass the key to the constructor
https://api.openai.com/v1https://api.aireserve.com/v1 (keep the /v1 suffix)
OpenAI models onlyEvery gateway model — Claude, Gemini, Grok, Llama, … answer the same chat.completions call
OpenAI error objectsSame 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 conceptAI Reserve equivalent
API key from console.anthropic.comAPI key from your AI Reserve workspace (aireserve_api_…) — see Authentication
ANTHROPIC_API_KEY environment variableAUDACITY_API_KEY, or pass the key to the constructor
https://api.anthropic.comhttps://api.aireserve.com (host root — no /v1)
Claude models onlyEvery gateway model answers /v1/messages — GPT, Gemini, Grok, … via the same messages.create call
count_tokensIdentical — 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_startmessage_stop), and prompt cachingcache_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 conceptAI Reserve equivalent
AWS region + credential chain / IAMOne bearer key: AUDACITY_API_KEY
Model ARN (anthropic.claude-3-5-sonnet-…)AI Reserve model ID (claude-opus-4-8, gpt-5.5, …)
Converse / ConverseStreamIdentical — same names, same shapes
toolConfig, toolUse, toolResultIdentical
ThrottlingException, ValidationException, …Identical exception names, same retryability semantics
Bedrock standard retry modeSame 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?;

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 IDBedrock-routed variant
claude-fable-5claude-fable-5-bedrock
claude-opus-4-8claude-opus-4-8-bedrock
claude-opus-4-7claude-opus-4-7-bedrock
claude-opus-4-6claude-opus-4-6-bedrock
claude-sonnet-4-6claude-sonnet-4-6-bedrock
claude-sonnet-4-5claude-sonnet-4-5-bedrock
claude-haiku-4-5-20251001claude-haiku-4-5-bedrock
llama-4-maverickllama-4-maverick-bedrock
llama-4-scoutllama-4-scout-bedrock
mistral-largemistral-large-bedrock
deepseek-reasonerdeepseek-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:

ComponentAdded 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 idNotes
us.anthropic.claude-opus-4-6-v1claude-opus-4-6-bedrockServed via AWS Bedrock
us.anthropic.claude-sonnet-4-6claude-sonnet-4-6-bedrockServed via AWS Bedrock
us.anthropic.claude-sonnet-4-5-20250929-v1:0claude-sonnet-4-5-bedrockServed via AWS Bedrock
us.anthropic.claude-haiku-4-5-20251001-v1:0claude-haiku-4-5-bedrockServed via AWS Bedrock
us.meta.llama4-maverick-17b-instruct-v1:0llama-4-maverick-bedrockServed via AWS Bedrock
us.meta.llama4-scout-17b-instruct-v1:0llama-4-scout-bedrockServed via AWS Bedrock
mistral.mistral-large-3-675b-instructmistral-large-bedrockServed via AWS Bedrock (on-demand id — no region prefix)
us.deepseek.r1-v1:0deepseek-reasoner-bedrockServed via AWS Bedrock
us.anthropic.claude-fable-5claude-fable-5-bedrockTemporarily served via Anthropic direct (Bedrock access pending)
us.anthropic.claude-opus-4-8claude-opus-4-8-bedrockTemporarily served via Anthropic direct (Bedrock access pending)
us.anthropic.claude-opus-4-7claude-opus-4-7-bedrockTemporarily 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

OperationStatusUse instead
converse()Supported
Tool use (toolConfig, toolUse, toolResult)Supported
Images in messagesSupported
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 blocksNot yet — returns ValidationExceptionaudacity-sdk (video), /v1/chat/completions
Guardrails, Knowledge Bases, AgentsOut 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() and converse_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.

ExceptionRetried automaticallyTypical cause
ValidationExceptionnoMalformed request (HTTP 400)
AccessDeniedExceptionnoBad or missing key; model not allowed (401/403)
ResourceNotFoundExceptionnoUnknown model (404)
ServiceQuotaExceededExceptionnoBudget / usage cap exhausted (402, or 429 with BUDGET_EXCEEDED)
ThrottlingExceptionyesRate limited (429) — honors Retry-After
ModelTimeoutExceptionyesModel timeout (408)
ModelErrorExceptionnoModel-level failure
ModelStreamErrorExceptionnoStream interrupted after the first byte
ServiceUnavailableExceptionyesUpstream unavailable (502/503/504)
InternalServerExceptionyesGateway error (500)
MissingApiKeyErrorNo key resolved; fails before any network call
SdkErrornetwork onlyConnection / 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, &quota):
        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-After header.
  • 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


OptionEnvironment variableDefault
API keyAUDACITY_API_KEY— (required)
Base URLAUDACITY_BASE_URLhttps://api.aireserve.com
Timeout (per attempt)120 s
Max retries2 (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.maxTokensmodel, max_tokens
system blocksA single leading system message
toolConfig.tools[].toolSpectools[].function with JSON Schema parameters
toolResult blocksrole: "tool" messages keyed by tool_call_id
image blocks (bytes or URL)image_url content parts (bytes become base64 data URLs)
Stream events (messageStartmetadata)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

CommandAction
/model [id]Pick a model (no argument shows a numbered picker)
/autoToggle auto-approve for mutating tools
/keyRe-enter and save your API key
/compactSummarize older turns to shrink context, keeping recent turns verbatim
/clearReset the conversation
/exitQuit

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.

Because the CLI resends the full conversation each turn, long working sessions naturally benefit from provider-side prompt caching — repeated context is billed at cached-input rates on supported models, and cached token counts are metered per request in your usage reporting.

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.