audacity-sdk (Rust)
Rust SDK for the Audacity Investments AI gateway. Exposes an Amazon Bedrock Converse-shaped API so teams migrating off Bedrock can swap the client construction + API key and keep the rest of their code.
Installation
Add to Cargo.toml:
[]
= "0.5.1"
= { = "1", = ["rt-multi-thread", "macros"] }
Quickstart
Converse (non-streaming)
use ;
async
ConverseStream (streaming)
use ;
async
OpenAI & Anthropic native formats (pass-through)
The gateway natively serves the OpenAI Chat Completions and Anthropic
Messages wire formats, and the SDK exposes both directly — same auth, retry,
and error handling, no shape translation. Params are any
serde_json::json! object (or your own Serialize struct) sent verbatim, so
any field the gateway supports works with no SDK release needed. Responses
come back as raw serde_json::Values in the provider's own shape. Both
formats work with every gateway model — the gateway bridges the format.
OpenAI format
use json;
let client = from_env?;
// Non-streaming: POST /v1/chat/completions, raw OpenAI-shaped response.
let response = client.chat_completions.create.await?;
println!;
// Streaming: raw chunk objects; the stream ends at the gateway's
// `data: [DONE]` sentinel (which is not yielded).
let mut stream = client.chat_completions.create_stream.await?;
while let Some = stream.recv.await?
Anthropic format
The wire format used by the anthropic SDKs and Claude Code — usable with any gateway model, not just Claude:
use json;
// Non-streaming: POST /v1/messages, raw Anthropic-shaped response.
let response = client.messages.create.await?;
println!;
// Streaming: raw Anthropic events (message_start … message_stop).
let mut stream = client.messages.create_stream.await?;
while let Some = stream.recv.await?
// Token counting (free — no inference): POST /v1/messages/count_tokens.
let count = client.messages.count_tokens.await?;
println!;
create_stream sets stream: true on the body for you (everything else
passes through untouched); calling create with stream: true in the params
is rejected client-side. Streaming failures surface as
Error::ModelStreamError: an OpenAI stream that ends without [DONE], an
Anthropic stream that ends before message_stop, or a mid-stream transport
drop. In-stream error payloads map through the same error taxonomy as
non-streaming calls (e.g. rate limits → Error::Throttling).
Images (vision models)
Bedrock-style image content blocks are supported in user messages. Pass raw bytes (base64-encoded for you) or a URL (Audacity extension):
use ;
let image_bytes = read?;
let response = client.converse
.model_id
.messages
.send
.await?;
// Or reference a hosted image directly (not available in Bedrock):
// source: ImageSource::Url("https://example.com/photo.jpg".into())
ImageFormat is one of Png, Jpeg, Gif, Webp. Use a vision-capable model.
Video input
Bedrock-style video content blocks are supported in user messages. Pass raw bytes; they are base64-encoded into an inline data URL for you:
use ;
let video_bytes = read?;
let response = client.converse
.model_id
.messages
.send
.await?;
VideoFormat is one of Mp4, Mov, Mkv, Webm, Flv, Mpeg, Mpg,
Wmv, ThreeGp.
Video is Gemini-only at the gateway: use gemini-2.5-flash,
gemini-2.5-pro, or gemini-3-flash-preview — other models reject video
input with an HTTP 400. Keep inline video ≤ ~20 MB (base64 encoding inflates
the request body against the gateway's cap); upload larger files first and
reference them by URI.
Media resolution (cheaper video tokens)
Set media_resolution to control how densely video (and image) input is
sampled on Gemini models — Low cuts video token cost roughly 4x. Values
are Low, Medium, High, UltraHigh; non-Gemini models ignore the field:
use MediaResolution;
let response = client.converse
.model_id
.media_resolution
.messages
.send
.await?;
Large videos: upload + URI
For videos up to 1 GB, upload once via the file-upload helper and
reference the returned audacity://files/… URI with VideoSource::Uri
(mirrors Bedrock Converse's bytes | s3Location pattern):
use ;
let video_bytes = read?;
// Under the hood: POST /v1/files issues a presigned upload URL (~15 min
// validity), then the bytes go up over a resumable upload session in 8 MiB
// chunks. If the connection drops mid-upload, the SDK automatically resumes
// from the last byte the server confirmed (bounded retries with jittered
// backoff) — no need to restart a large upload from zero.
let upload = client.upload_file
.data
.content_type
.send
.await?;
let response = client.converse
.model_id
.messages
.send
.await?;
Uploaded files are transient inference inputs: they auto-delete after ~24 h, so upload shortly before use and re-upload for later sessions. Files are namespaced per API key's client — a URI from one client is not visible to another.
Image generation
Generate images from a text prompt with generate_image. With
response_format("b64_json") the image bytes come back inline:
use Engine as _;
let result = client.generate_image
.model
.prompt
.size
.response_format
.send
.await?;
let b64 = result.data.b64_json.as_deref.unwrap;
let bytes = STANDARD.decode?;
write?;
With response_format "url" (the default) the gateway stores the image and
returns a signed download URL that expires after ~24 hours:
let result = client.generate_image
.model
.prompt
.send
.await?;
println!; // signed URL, valid ~24 h
Optional builder methods: n (1–10 images), size ("WxH", model-dependent),
quality (e.g. "standard", "hd"), and user. The output carries
created, data (each entry has url or b64_json, plus revised_prompt
when the provider rewrites your prompt) and optional usage token counts.
Errors map to the same Error variants as converse (401 →
Error::AccessDenied, 429 → Error::Throttling, spend cap →
Error::ServiceQuotaExceeded).
Image models
| Model | Pricing |
|---|---|
imagen-4 |
$0.04 / image |
imagen-4-fast |
$0.02 / image |
imagen-4-ultra |
$0.06 / image |
gemini-2.5-flash-image |
token-based (≈ $0.039 / image) |
gpt-image-1 |
token-based ($5.00 / 1M text input, $40.00 / 1M image output tokens) |
Per-image models bill a flat rate per generated image; token-based models
report token counts in the output's usage. Each request's cost is recorded
against your key like any other API call.
Reliability note. 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) — the SDK already retries 503s with backoff up to max_retries, and
callers should retry beyond that rather than switch models.
Prompt caching
Place a Bedrock-style cache-point block after the stable prefix you want the provider to cache (system prompt, large documents). Everything up to the cache point is cached provider-side on Claude models; OpenAI/Gemini models cache automatically and ignore the marker. At most 4 cache points per request.
use ;
let response = client.converse
.model_id
.system
.system
.messages
.send
.await?;
// Cache activity is reported in usage (Bedrock names):
println!; // tokens served from cache
println!; // tokens written to cache
A cache point with nothing before it in the same message is silently ignored.
Migrating from aws-sdk-bedrockruntime
The SDK is designed to be a drop-in. Here is a side-by-side diff:
-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()))
+ .content(ContentBlock::Text("Hello".into()))
.build()?
)
.send()
.await?;
Error handling
use ;
match client.converse.model_id.messages.send.await
Error variants
| Variant | Retryable | Trigger |
|---|---|---|
Validation |
no | 400 / invalid request |
AccessDenied |
no | 401/403 / bad key |
ServiceQuotaExceeded |
no | 402 / budget exceeded |
ResourceNotFound |
no | 404 / unknown model |
ModelTimeout |
yes | 408 / TIMEOUT_ERROR |
Throttling |
yes | 429 / rate limited |
ModelError |
no | model-side error |
ModelStreamError |
no | stream-specific error |
ServiceUnavailable |
yes | 502/503/504 |
InternalServer |
yes | 500 |
MissingApiKey |
— | no key in env/config |
Sdk |
yes (network) | network / decode failure |
Every server error carries ErrorDetails { message, status_code, error_code, request_id, retry_after_seconds, raw_body }.
Configuration
| Source | API key | Base URL |
|---|---|---|
| Explicit | Config::builder().api_key("…") |
.base_url("…") |
| Environment | AUDACITY_API_KEY |
AUDACITY_BASE_URL |
| Default | — | https://api.audacityinvestments.com |
Other options:
use Duration;
let config = builder
.api_key
.base_url
.timeout
.max_retries // 3 total attempts
.build?;
let client = new?;
The timeout bounds each converse attempt end to end (including the body
read). For converse_stream it only bounds the wait for response headers —
the SSE body read is unbounded, so long generations are never cut off
mid-stream.
License
Copyright Audacity Investments. All rights reserved.
See LICENSE.