Rust
Rust has no first-party OpenAI SDK. This page uses async-openai, the
community-maintained crate most Rust projects use. Its OpenAIConfig carries
both settings you need to change — with_api_base and with_api_key — and nothing
else about your code changes. The provider key stays in the vault.
Install
cargo add async-openai
Configure
use async_openai::{
config::OpenAIConfig,
types::{
ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs,
},
Client,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let org = std::env::var("VULNETIX_ORG_UUID")?;
let config = OpenAIConfig::new()
.with_api_base(format!("https://guardrails.vulnetix.com/openai/{org}/v1"))
.with_api_key(std::env::var("VULNETIX_API_KEY")?); // Vulnetix key, not an OpenAI key
let client = Client::with_config(config);
let request = CreateChatCompletionRequestArgs::default()
.model("gpt-4o-mini")
.messages([ChatCompletionRequestUserMessageArgs::default()
.content("Hello")
.build()?
.into()])
.build()?;
let response = client.chat().create(request).await?;
println!("{}", response.choices[0].message.content.clone().unwrap_or_default());
Ok(())
}
Swap openai in the path for any other provider slug and pass that provider’s own
model string — the gateway forwards model verbatim.
Streaming
Unchanged. The gateway relays the provider’s SSE stream.
use futures::StreamExt;
let mut stream = client.chat().create_stream(request).await?;
while let Some(result) = stream.next().await {
let chunk = result?;
for choice in chunk.choices {
if let Some(content) = choice.delta.content {
print!("{content}");
}
}
}
Handling a guardrail block
A request your policy refuses never reaches the provider. It returns an
OpenAI-shaped 403, which the crate surfaces as OpenAIError::ApiError, carrying
the parsed error object.
use async_openai::error::OpenAIError;
match client.chat().create(request).await {
Ok(response) => { /* … */ }
Err(OpenAIError::ApiError(err)) => {
match err.code.as_deref() {
Some("request_blocked") => {
// A content guardrail matched. The rule name is in the message.
eprintln!("AI firewall blocked this request: {}", err.message);
}
Some("provider_denied") | Some("model_denied")
| Some("model_not_allowed") | Some("provider_key_missing") => {
eprintln!("AI firewall policy refused this call: {}", err.message);
}
_ => eprintln!("provider error: {}", err.message),
}
}
Err(e) => return Err(e.into()),
}
err.code is what tells your organisation refused this apart from the provider
refused this:
code | Meaning |
|---|---|
request_blocked | A content guardrail matched. |
provider_denied | Policy does not allow this provider. |
model_denied / model_not_allowed | Policy does not allow this model. |
provider_key_missing | No provider key in the vault for this provider. |
ApiError models the standard OpenAI error fields (message, type, param,
code) and drops Vulnetix’s blocked_by and violations. The rule name also
appears in message; if you need the structured fields, call the endpoint with
reqwest and deserialise the body yourself — the envelope is in
curl & raw HTTP.
Ok, not Err. A pii_redact rule rewrites the
matched spans in your prompt to the literal [REDACTED] and forwards the request.
The completion comes back normally, about redacted text.Tool / function calling
Tools are forwarded untouched. Guardrails inspect the request you send, which includes the tool-result messages you push onto the message list before the next call.
Gotchas
with_api_keytakes your Vulnetix key. A provider key here is the most common401.OpenAIConfig::new()falls back toOPENAI_API_KEYfrom the environment. Set the key explicitly so a stale provider key on the machine cannot win.with_api_basemust include the/v1suffix.- This is a community crate. If you use a different one, the two settings are the
same — point its base URL at
…/{orgUuid}/v1and give it the Vulnetix key.