Skip to content
Sign In Get Started

Rust SDK (floopy-sdk)

floopy-sdk is the official Floopy SDK for Rust. It wraps the official async-openai crate and points it at the Floopy gateway, so chat/embeddings/models stay a 1:1 drop-in while Floopy-only features (audit, experiments, constraints, decision export, feedback, routing dry-run, sessions) get first-class typed async methods.

Prefer the zero-SDK approach? The Rust page shows how to use the raw async-openai crate with Floopy headers. Use floopy-sdk when you also want the typed Floopy resources and a single client for both.

The crate is published as floopy-sdk and imported as floopy. It is fully async (Tokio). Requires Rust >= 1.82.

Terminal window
cargo add floopy-sdk async-openai tokio --features tokio/macros,tokio/rt-multi-thread

async-openai is re-exported as floopy::async_openai, so you can also depend on floopy-sdk alone and reach the request/response types through it.

use floopy::Floopy;
use floopy::async_openai::types::chat::{
ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Floopy::new(std::env::var("FLOOPY_API_KEY")?)?;
let request = CreateChatCompletionRequestArgs::default()
.model("gpt-4o")
.messages(vec![ChatCompletionRequestUserMessageArgs::default()
.content("Hello from Floopy!")
.build()?
.into()])
.build()?;
let response = client.openai().chat().create(request).await?;
println!("{:?}", response.choices[0].message.content);
Ok(())
}

Set FLOOPY_API_KEY in your environment. You can create one in the dashboard.

use async_openai::{Client, config::OpenAIConfig};
let client = Client::with_config(
OpenAIConfig::new().with_api_key(std::env::var("OPENAI_API_KEY")?),
);
use floopy::Floopy;
let fl = Floopy::new(std::env::var("FLOOPY_API_KEY")?)?;
let client = fl.openai();
let response = client.chat().create(request).await?;

client.openai() returns a lazily-built async_openai::Client pre-pointed at the gateway, so types and runtime behaviour are identical. Upstream updates reach you on cargo update without forks or parity drift.

FloopyOptions map to Floopy-* headers and are forwarded to every request (both OpenAI-compat calls and Floopy-only ones). Per-call overrides go through a trailing RequestOptions argument on every resource method.

use floopy::{Floopy, FloopyOptions, CacheOptions};
let client = Floopy::builder(std::env::var("FLOOPY_API_KEY")?)
.options(FloopyOptions {
cache: Some(CacheOptions { enabled: Some(true), bucket_max_size: Some(3) }),
prompt_id: Some("cd4249d5-44d5-46c8-8961-9eb3861e1f7e".into()),
prompt_version: Some("1".into()),
llm_security_enabled: Some(true),
})
.build()?;
OptionHeaderPurpose
cache.enabledFloopy-Cache-EnabledToggle exact + semantic cache
cache.bucket_max_sizeFloopy-Cache-Bucket-Max-SizeMax entries per semantic bucket
prompt_idFloopy-Prompt-IdStored prompt to resolve
prompt_versionFloopy-Prompt-VersionPinned version for prompt_id
llm_security_enabledfloopy-llm-security-enabledLLM firewall pre-check

See the Headers Reference for the full list.

Each resource maps to a public /v1/* gateway endpoint and is typed end-to-end. Pagination and export are futures::Streams.

use floopy::types::*;
use futures::StreamExt;
// Feedback
client.feedback().submit(
FeedbackSubmitParams { score: 9, useful: true, session_id: None }, None,
).await?;
// Decisions (paginated Streams)
let d = client.decisions().get("req_123", None).await?;
let stream = client.decisions().iter(DecisionListParams::default(), None);
futures::pin_mut!(stream);
while let Some(decision) = stream.next().await { let _ = decision?; }
// Experiments (auto X-Floopy-Confirm: experiments header)
let exp = client.experiments().create(ExperimentCreateParams {
name: "cost-vs-quality".into(),
variant_a_routing_rule_id: "rule_a".into(),
variant_b_routing_rule_id: "rule_b".into(),
description: None, split_percentage: None,
}, None).await?;
client.experiments().rollback(&exp.id, None).await?;
// Constraints (full-replace PUT)
client.constraints().put(
&OrgConstraints { cost_limit_monthly_usd: Some(100.0), ..Default::default() }, None,
).await?;
// Export (streamed JSONL + trailer)
let mut export = client.export()
.decisions_with_trailer(ExportDecisionsParams::new("a", "b"), None);
while let Some(row) = export.next().await { let _ = row?; }
println!("{:?}", export.trailer());
// Evaluations
let run = client.evaluations().create(EvaluationCreateParams {
dataset_id: "ds_1".into(), model: "gpt-4o".into(), prompt_id: None, config: None,
}, None).await?;
// Routing dry-run (Pro plan)
let explain = client.routing()
.explain(RoutingExplainParams::new("gpt-4o", vec![]), None).await?;
// Sessions — restore a stored conversation
let session = client.sessions().get("sess_1", None).await?;

Every Floopy-only call returns Result<T, floopy::Error>:

use floopy::{Error, Floopy};
use floopy::types::DecisionListParams;
match client.decisions().list(&DecisionListParams::default(), None).await {
Ok(page) => { /* ... */ }
Err(Error::RateLimit(d)) => {
let secs = d.retry_after_seconds.unwrap_or(1);
tokio::time::sleep(std::time::Duration::from_secs(secs)).await;
}
Err(Error::Plan(d)) => eprintln!("upgrade plan: feature {:?} not in plan", d.feature),
Err(e) => eprintln!("{e}"),
}

Error::status(), Error::request_id(), Error::feature() and Error::retry_after_seconds() are available on every variant. Errors from chat/embeddings are emitted by async-openai (async_openai::error::OpenAIError), not this crate.

  • The API key is only ever sent in the Authorization header and is never rendered by Debug; the SDK never logs request or response bodies.
  • TLS certificate verification is on by default (rustls + webpki-roots).
  • Releases are published to crates.io via Trusted Publishing (OIDC) — no long-lived registry token in the publishing path.

See the Security guide for gateway-side controls.