# audacity-sdk (Rust)
Rust SDK for the [Audacity Investments](https://portal.audacityinvestments.com) 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`:
```toml
[dependencies]
audacity-sdk = "0.1.0"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
```
---
## Quickstart
### Converse (non-streaming)
```rust
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(())
}
```
### ConverseStream (streaming)
```rust
use audacity_sdk::{Client, ContentBlock, ConversationRole, ConverseStreamOutput, Message};
#[tokio::main]
async fn main() -> Result<(), audacity_sdk::Error> {
let client = Client::from_env()?;
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}");
}
}
}
println!();
Ok(())
}
```
---
## 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):
```rust
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())
```
`ImageFormat` is one of `Png`, `Jpeg`, `Gif`, `Webp`. Use a vision-capable model.
---
## 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.
```rust
use audacity_sdk::{CachePointBlock, ContentBlock, SystemContentBlock};
let response = client.converse()
.model_id("claude-sonnet-4-5")
.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
```
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:
```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
```rust
use audacity_sdk::{Client, 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}"),
}
```
### Error variants
| `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
| Explicit | `Config::builder().api_key("…")` | `.base_url("…")` |
| Environment | `AUDACITY_API_KEY` | `AUDACITY_BASE_URL` |
| Default | — | `https://portal.audacityinvestments.com` |
Other options:
```rust
use std::time::Duration;
let config = audacity_sdk::Config::builder()
.api_key("audacity_api_…")
.base_url("https://portal.audacityinvestments.com")
.timeout(Duration::from_secs(120))
.max_retries(2) // 3 total attempts
.build()?;
let client = audacity_sdk::Client::new(&config)?;
```
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](LICENSE).