SDK guides
Rust
Use api14 from Rust with the async-openai crate, with setup, chat completion, streaming and error handling examples you can copy.
Use async-openai with api14. Configure the API base with OpenAIConfig::with_api_base and pass model ids such as openai/gpt-5-mini.
Install
cargo add async-openai futures serde_json
cargo add tokio --features full
export API14_API_KEY="sk-api14-..."
Configure
use async_openai::{config::OpenAIConfig, Client};
let config = OpenAIConfig::new()
.with_api_base("https://api14.com/api/v1")
.with_api_key(std::env::var("API14_API_KEY")?);
let client = Client::with_config(config);
Read the key from the environment as above rather than hard-coding it. To switch between staging and production, read the base URL the same way, for example std::env::var("API14_BASE_URL").unwrap_or("https://api14.com/api/v1".into()).
Chat
use async_openai::{
config::OpenAIConfig,
types::{ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs},
Client,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = OpenAIConfig::new()
.with_api_base("https://api14.com/api/v1")
.with_api_key(std::env::var("API14_API_KEY")?);
let client = Client::with_config(config);
let request = CreateChatCompletionRequestArgs::default()
.model("openai/gpt-5-mini")
.messages([ChatCompletionRequestUserMessageArgs::default()
.content("Explain vector databases in two sentences.")
.build()?
.into()])
.build()?;
let response = client.chat().create(request).await?;
if let Some(content) = &response.choices[0].message.content {
println!("{content}");
}
Ok(())
}
Streaming
use futures::StreamExt;
let request = CreateChatCompletionRequestArgs::default()
.model("anthropic/claude-sonnet-4.5")
.messages([ChatCompletionRequestUserMessageArgs::default()
.content("Write a haiku about APIs.")
.build()?
.into()])
.build()?;
let mut stream = client.chat().create_stream(request).await?;
while let Some(result) = stream.next().await {
match result {
Ok(chunk) => {
for choice in chunk.choices {
if let Some(text) = choice.delta.content {
print!("{text}");
}
}
}
Err(err) => eprintln!("stream error: {err}"),
}
}
See Streaming.
Tool calling
use async_openai::types::{
ChatCompletionRequestAssistantMessageArgs, ChatCompletionRequestMessage,
ChatCompletionRequestToolMessageArgs, ChatCompletionToolArgs, ChatCompletionToolType,
FunctionObjectArgs,
};
use serde_json::{json, Value};
let tools = vec![ChatCompletionToolArgs::default()
.r#type(ChatCompletionToolType::Function)
.function(FunctionObjectArgs::default()
.name("get_weather")
.description("Current weather for a city")
.parameters(json!({
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}))
.build()?)
.build()?];
let mut messages: Vec<ChatCompletionRequestMessage> = vec![ChatCompletionRequestUserMessageArgs::default()
.content("What's the weather in Istanbul?")
.build()?
.into()];
loop {
let request = CreateChatCompletionRequestArgs::default()
.model("openai/gpt-5-mini")
.messages(messages.clone())
.tools(tools.clone())
.build()?;
let message = client.chat().create(request).await?.choices.remove(0).message;
let Some(calls) = message.tool_calls else {
println!("{}", message.content.unwrap_or_default());
break;
};
messages.push(ChatCompletionRequestAssistantMessageArgs::default()
.tool_calls(calls.clone())
.build()?
.into());
for call in calls {
let args: Value = serde_json::from_str(&call.function.arguments)?;
let result = get_weather(args["city"].as_str().unwrap_or_default()); // your function
messages.push(ChatCompletionRequestToolMessageArgs::default()
.tool_call_id(call.id)
.content(result)
.build()?
.into());
}
}
See Tool calling.
Structured output
use async_openai::types::{ResponseFormat, ResponseFormatJsonSchema};
let request = CreateChatCompletionRequestArgs::default()
.model("openai/gpt-5-mini")
.messages([ChatCompletionRequestUserMessageArgs::default()
.content("Extract: Ada Lovelace, born 1815.")
.build()?
.into()])
.response_format(ResponseFormat::JsonSchema {
json_schema: ResponseFormatJsonSchema {
name: "person".into(),
description: None,
strict: Some(true),
schema: Some(json!({
"type": "object",
"properties": { "name": { "type": "string" }, "born": { "type": "integer" } },
"required": ["name", "born"],
"additionalProperties": false
})),
},
})
.build()?;
let response = client.chat().create(request).await?;
let person: Value = serde_json::from_str(response.choices[0].message.content.as_deref().unwrap_or("{}"))?;
See Structured outputs.
Image input
use async_openai::types::{
ChatCompletionRequestMessageContentPartImageArgs, ChatCompletionRequestMessageContentPartTextArgs,
ImageUrlArgs,
};
let message = ChatCompletionRequestUserMessageArgs::default()
.content(vec![
ChatCompletionRequestMessageContentPartTextArgs::default()
.text("What is in this picture?")
.build()?
.into(),
ChatCompletionRequestMessageContentPartImageArgs::default()
.image_url(ImageUrlArgs::default().url("https://example.com/photo.jpg").build()?)
.build()?
.into(),
])
.build()?;
Send it with google/gemini-2.5-flash or another model with image input. See Vision & files.
Embeddings
use async_openai::types::CreateEmbeddingRequestArgs;
let request = CreateEmbeddingRequestArgs::default()
.model("openai/text-embedding-3-small")
.input(["The quick brown fox", "Lorem ipsum"])
.build()?;
let response = client.embeddings().create(request).await?;
println!("{}", response.data[0].embedding.len());
Endpoints the crate has no type for (video jobs, rerank) can be called with reqwest, see the HTTP examples in the code block below.
Errors
Gateway errors are parsed into OpenAIError::ApiError; branch on the machine code:
use async_openai::error::OpenAIError;
match client.chat().create(request).await {
Ok(response) => println!("{:?}", response.choices[0].message.content),
Err(OpenAIError::ApiError(err)) => match err.code.as_deref() {
Some("insufficient_balance") => eprintln!("Balance too low, add funds"),
Some("rate_limited") | Some("daily_limit_reached") => eprintln!("Slow down: {}", err.message),
_ => eprintln!("API error: {}", err.message),
},
Err(other) => eprintln!("{other}"),
}
See Errors.
Timeouts and retries
async-openai already backs off and retries 429 rate_limited responses. Its default backoff keeps trying for up to 15 minutes, which is too long for most apps and pointless for daily_limit_reached. Cap it, and give the HTTP client a timeout (cargo add backoff reqwest, matching the versions async-openai uses):
use std::time::Duration;
let http = reqwest::Client::builder()
.timeout(Duration::from_secs(120))
.build()?;
let client = Client::with_config(config)
.with_http_client(http)
.with_backoff(backoff::ExponentialBackoffBuilder::new()
.with_max_elapsed_time(Some(Duration::from_secs(90)))
.build());
See Rate limits.
All capabilities
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api14.com/api/v1",
apiKey: process.env.API14_API_KEY,
});
const result = await client.chat.completions.create({
"model": "openai/gpt-5-mini",
"messages": [
{
"role": "user",
"content": "Explain vector databases in two sentences."
}
]
});
console.log(result.choices[0].message.content);