Rust
Obstruo exposes an OpenAI-compatible API. The async-openai crate supports custom base URLs and works with Obstruo out of the box.
Prerequisites
- An ObstruoKey and endpoint URL - see Quick Start
- A Logical Model configured in your project
- Rust 1.75 or later
async-openai
Add the dependency to Cargo.toml:
Chat completions
use async_openai::{
config::OpenAIConfig,
types::{ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs},
Client,
};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = OpenAIConfig::new()
.with_api_key(std::env::var("OBSTRUO_API_KEY")?)
.with_api_base(std::env::var("OBSTRUO_BASE_URL")?);
let client = Client::with_config(config);
let request = CreateChatCompletionRequestArgs::default()
.model("obstruo-chat")
.messages([ChatCompletionRequestUserMessageArgs::default()
.content("Summarise this contract: ...")
.build()?
.into()])
.build()?;
let response = client.chat().create(request).await?;
println!("{}", response.choices[0].message.content.as_deref().unwrap_or(""));
Ok(())
}
Streaming
use futures::StreamExt;
let request = CreateChatCompletionRequestArgs::default()
.model("obstruo-chat")
.messages([ChatCompletionRequestUserMessageArgs::default()
.content("Tell me a story.")
.build()?
.into()])
.build()?;
let mut stream = client.chat().create_stream(request).await?;
while let Some(result) = stream.next().await {
match result {
Ok(response) => {
for choice in &response.choices {
if let Some(content) = &choice.delta.content {
print!("{}", content);
}
}
}
Err(e) => eprintln!("Error: {e}"),
}
}
Embeddings
use async_openai::types::{CreateEmbeddingRequestArgs, EmbeddingInput};
let request = CreateEmbeddingRequestArgs::default()
.model("obstruo-embedding")
.input(EmbeddingInput::String("The quick brown fox".to_string()))
.build()?;
let response = client.embeddings().create(request).await?;
let vector = &response.data[0].embedding;
Environment variables
async-openai reads OPENAI_API_KEY and OPENAI_API_BASE automatically:
export OPENAI_API_KEY="your-obstruo-key"
export OPENAI_API_BASE="https://api.obstruo.ai/your-org/v1"
Or set them explicitly:
let config = OpenAIConfig::new()
.with_api_key(std::env::var("OBSTRUO_API_KEY")?)
.with_api_base(std::env::var("OBSTRUO_BASE_URL")?);
let client = Client::with_config(config);
Reading gateway metadata
async-openai deserialises only known OpenAI fields. To access the obstruo key, use reqwest directly:
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Routing {
resolved_model: String,
provider_type: String,
duration_ms: u64,
}
#[derive(Debug, Deserialize)]
struct Redaction {
total_entities: u32,
by_category: std::collections::HashMap<String, u32>,
duration_ms: u64,
}
#[derive(Debug, Deserialize)]
struct ObstruoMeta {
routing: Routing,
redaction: Redaction,
}
let body = serde_json::json!({
"model": "obstruo-chat",
"messages": [{"role": "user", "content": "Hello!"}]
});
let resp = reqwest::Client::new()
.post(format!("{}/chat/completions", std::env::var("OBSTRUO_BASE_URL")?))
.header(AUTHORIZATION, format!("Bearer {}", std::env::var("OBSTRUO_API_KEY")?))
.header(CONTENT_TYPE, "application/json")
.json(&body)
.send()
.await?
.json::<serde_json::Value>()
.await?;
if let Some(meta) = resp.get("obstruo") {
let routing = &meta["routing"];
let redaction = &meta["redaction"];
println!("{}", routing["resolved_model"]); // "gpt-4o-mini"
println!("{}", routing["provider_type"]); // "OpenAI"
println!("{}", redaction["total_entities"]); // 0 = no PII found
}
Next steps
- Configure PII redaction: Redaction
- Configure safety rules: Guardrails