OpenRouter API Client Library is a Rust client for interfacing with the OpenRouter API. The library is designed to be modular, type‑safe, and intuitive. It uses a type‑state builder pattern for configuring and validating the client at compile time, ensuring that all required configuration (such as setting the base URL and API key) happens before attempting a request.
Note: This project is still in development. Many features are planned but not yet fully implemented.
- Modular Organization: Organized into clear modules for models, API endpoints, common types, and utilities.
- Type‑State Builder: Guarantees compile‑time validation of client configuration (e.g. base URL, API key, custom headers) for a robust development experience.
- HTTP Integration: Uses reqwest with rustls‑tls for secure asynchronous HTTP requests.
- Robust Error Handling: Centralized error management using the
thiserrorcrate ensures consistent error types across the library. - Streaming Support: Supports streaming chat completions via Server‑Sent Events (SSE). The library gracefully skips over comment lines and non‑JSON payloads, letting you update UIs in real‑time.
- Structured Outputs: Optionally request structured responses with JSON Schema validation so that responses strictly follow your defined schema.
- Tool Calling Capability: Define function‑type tools that the model can invoke. Supports concurrent tool calls in a single response with proper validation against expected formats.
- Provider Preferences & Routing: Configure model fallbacks, routing preferences, and provider filtering via a strongly‑typed interface.
- Web Search Endpoint: Easily perform web search queries with type‑safe request and response models.
- Text Completion Endpoint: Send a prompt (with a required
modelandpromptfield) and receive generated text completions along with additional generation details. Extra parameters (e.g. temperature, top_p, etc.) can be provided as needed.
Add the following to your project's Cargo.toml:
cargo add openrouter_apiEnsure that you have Rust installed (tested with Rust v1.83.0) and that you're using Cargo for building and testing.
use openrouter_api::{OpenRouterClient, Ready, Result};
use openrouter_api::types::chat::{ChatCompletionRequest, Message};
#[tokio::main]
async fn main() -> Result<()> {
// Ensure your API key is set in the environment.
let api_key = std::env::var("OPENROUTER_API_KEY")
.expect("OPENROUTER_API_KEY must be set");
// Build the client (Unconfigured -> NoAuth -> Ready).
let client = OpenRouterClient::new()
.with_base_url("https://openrouter.ai/api/v1/")?
.with_api_key(api_key)?;
// Create a minimal chat completion request.
let request = ChatCompletionRequest {
model: "openai/gpt-4o".to_string(),
messages: vec![Message {
role: "user".to_string(),
content: "Hello, world!".to_string(),
name: None,
tool_calls: None,
}],
stream: None,
response_format: None,
tools: None,
provider: None,
models: None,
transforms: None,
};
// Invoke the chat completion endpoint.
let response = client.chat_completion(request).await?;
// Output the model's response.
if let Some(choice) = response.choices.first() {
println!("Chat Response: {}", choice.message.content);
}
Ok(())
}use openrouter_api::{OpenRouterClient, Ready, Result};
use openrouter_api::types::completion::{CompletionRequest, CompletionResponse};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<()> {
// Ensure your API key is set in the environment.
let api_key = std::env::var("OPENROUTER_API_KEY")
.expect("OPENROUTER_API_KEY must be set");
// Build the client (Unconfigured -> NoAuth -> Ready).
let client = OpenRouterClient::new()
.with_base_url("https://openrouter.ai/api/v1/")?
.with_api_key(api_key)?;
// Create a minimal text completion request.
let request = CompletionRequest {
model: "model".to_string(),
prompt: "Once upon a time".to_string(),
// Additional generation parameters can be set here.
extra_params: json!({
"temperature": 0.8,
"max_tokens": 50
}),
};
// Invoke the text completion endpoint.
let response: CompletionResponse = client.completions().text_completion(request).await?;
// Print out the generated text from the first choice.
if let Some(choice) = response.choices.first() {
println!("Text Completion: {}", choice.text);
}
Ok(())
}use openrouter_api::{OpenRouterClient, Ready, Result};
use openrouter_api::types::web_search::{WebSearchRequest, WebSearchResponse};
#[tokio::main]
async fn main() -> Result<()> {
// Ensure your API key is set in the environment.
let api_key = std::env::var("OPENROUTER_API_KEY")
.expect("OPENROUTER_API_KEY must be set");
// Build the client (Unconfigured -> NoAuth -> Ready).
let client = OpenRouterClient::new()
.with_base_url("https://openrouter.ai/api/v1/")?
.with_api_key(api_key)?;
// Create a minimal web search request.
let request = WebSearchRequest {
query: "rust programming".into(),
num_results: Some(5),
};
// Invoke the web search endpoint.
let response: WebSearchResponse = client.web_search()?.search(request).await?;
// Print out the search results.
println!("Search query: {}", response.query);
for result in response.results {
println!("Title: {}\nURL: {}\n", result.title, result.url);
}
Ok(())
}use openrouter_api::{OpenRouterClient, Ready, Result};
use openrouter_api::types::chat::{ChatCompletionRequest, Message, ChatCompletionChunk};
use futures::StreamExt;
#[tokio::main]
async fn main() -> Result<()> {
// Ensure your API key is set in the environment.
let api_key = std::env::var("OPENROUTER_API_KEY")
.expect("OPENROUTER_API_KEY must be set");
// Build the client.
let client = OpenRouterClient::new()
.with_base_url("https://openrouter.ai/api/v1/")?
.with_api_key(api_key)?;
// Create a chat completion request with streaming enabled.
let request = ChatCompletionRequest {
model: "openai/gpt-4o".to_string(),
messages: vec![Message {
role: "user".to_string(),
content: "Tell me a story.".to_string(),
name: None,
tool_calls: None,
}],
stream: Some(true),
response_format: None,
tools: None,
provider: None,
models: None,
transforms: None,
};
// Invoke the streaming chat completion endpoint.
let mut stream = client.chat()?.chat_completion_stream(request);
while let Some(chunk) = stream.next().await {
match chunk {
Ok(c) => {
// Incrementally output the content.
print!("{}", c.message.content);
}
Err(e) => eprintln!("Error during streaming: {}", e),
}
}
println!();
Ok(())
}The project is under active development. The roadmap outlines upcoming features and milestones:
- Client Framework:
- Type‑state builder pattern for configuration with compile‑time validations.
- Custom headers and robust error propagation.
- Chat Completion Endpoint:
- Synchronous chat completions with JSON decoding and streaming support.
- Core Data Models:
- Definitions for chat messages, requests, responses, and usage.
- Streaming Support:
- Streaming API for chat completions via Server‑Sent Events (SSE).
- Web Search Endpoint:
- New endpoint for web search queries with strongly‑typed request/response models.
- Text Completion Endpoint:
- New endpoint for text completions, accepting a prompt and returning generated text along with extra details.
- Tool Calling & Structured Outputs:
- Support for invoking callable functions and validating structured responses via JSON Schema.
- Provider Preferences & Routing:
- Configuration options for model fallbacks, routing, and provider filtering.
- Models Listing and Credits:
- Implement endpoints to list models and fetch credit details.
- Test Coverage:
- Expand unit and integration tests, including streaming-specific tests.
- Documentation Improvements:
- Enhance inline documentation, API docs, and usage examples in the
/examplesdirectory.
- Enhance inline documentation, API docs, and usage examples in the
- Continuous Integration (CI):
- Set up CI pipelines for automated builds and tests.
Contributions are welcome! Please open an issue or submit a pull request with your ideas or fixes. Follow the code style guidelines and ensure that all tests pass.
Distributed under either the MIT license or the Apache License, Version 2.0. See LICENSE for details.
Version: 0.1.2 • License: MIT / Apache‑2.0
The openrouter_api crate is a comprehensive client for interacting with the OpenRouter API. It provides strongly‑typed endpoints for chat completions, text completions, web search, and more. The crate is built using asynchronous Rust (with reqwest and tokio) and leverages advanced patterns such as type‑state and builder patterns for safe and flexible API usage.
- Core Concepts
- Installation
- Architecture & Module Overview
- Client Setup & Type‑State Pattern
- API Endpoints
- Error Handling
- Best Practices
- Examples
- Additional Resources
-
Type‑State Client Configuration: The client is built using a type‑state pattern to ensure that required parameters (e.g. API key and base URL) are set before making any API calls. This pattern leverages Rust’s type system (with
PhantomData) to prevent misconfiguration at compile time. -
Flexible Request Building: The
RequestBuilderlets you build rich API requests including optional provider preferences, structured output (using JSON Schema), and tool calling configurations. -
Asynchronous Streaming: The chat API supports streaming responses via asynchronous streams. This is implemented using crates like async-stream and tokio-util.
-
Error Handling & Schema Validation: A comprehensive custom error type wraps HTTP errors, API errors, configuration mistakes, and JSON Schema validation issues.
-
Tool Calling: Easily integrate with external functions using a structured representation of function calls. This enables the API to suggest or even invoke tools based on the conversation context.
Add the following to your Cargo.toml:
[dependencies]
openrouter_api = "0.1.2"
reqwest = { version = "0.11", features = ["json", "stream"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
async-stream = "0.3"
tokio-util = "0.7"
thiserror = "1.0"The crate is organized into several modules:
-
client: Contains the client configuration and type‑state builder implementation. It ensures proper initialization before any API calls are made. -
api: Provides submodules for each API endpoint (chat, completion, web search, etc.). Each submodule includes detailed implementations for constructing requests and handling responses. -
models: Defines domain models for structured outputs, provider preferences, and tool calling. -
types: Contains type definitions for chat messages, completions, and web search responses. -
error: Centralized error types (wrapping reqwest errors, API errors, configuration issues, and schema validation problems). -
tests: Integration tests showcasing how to simulate and validate API responses.
The client configuration follows a three‑step type‑state builder pattern:
- Unconfigured:
The client is created in an unconfigured state using
OpenRouterClient::new(). - NoAuth:
The base URL is set (via
.with_base_url()), transitioning the client into a state where authentication is not yet provided. - Ready:
The API key is added (via
.with_api_key()), and the client becomes fully configured for making API calls.
use openrouter_api::client::{OpenRouterClient, Unconfigured};
use std::time::Duration;
// Create an unconfigured client.
let client = OpenRouterClient::<Unconfigured>::new();
// Transition to a configured client (NoAuth state) by setting the base URL.
let client = client
.with_base_url("https://openrouter.ai/api/v1/")?
.with_timeout(Duration::from_secs(30))
.with_http_referer("https://your-app.com/")
.with_site_title("Your App Name");
// Supply the API key to transition into the Ready state.
let client = client.with_api_key(std::env::var("OPENROUTER_API_KEY")?)?;Note: The type‑state pattern prevents you from accidentally making API calls without proper configuration. Attempting to call an endpoint on a client that isn’t in the
Readystate will result in a compile‑time error.
The crate supports multiple endpoints, each with its own module and specialized request/response types.
The chat API supports both single‑shot and streaming completions.
use openrouter_api::types::chat::Message;
let messages = vec![Message {
role: "user".to_string(),
content: "Explain quantum computing".to_string(),
name: None,
tool_calls: None,
}];
// Issue a single chat completion call.
let chat_api = client.chat()?;
let response = chat_api.chat_completion(
openrouter_api::types::chat::ChatCompletionRequest {
model: "mistralai/mistral-small-latest".to_string(),
messages: messages.clone(),
stream: None,
response_format: None,
tools: None,
provider: None,
models: None,
transforms: None,
}
).await?;
println!("Assistant: {}", response.choices[0].message.content);Streaming is useful for real‑time applications. The stream returns chunks as they become available.
use futures::StreamExt;
let chat_api = client.chat()?;
let mut stream = chat_api.chat_completion_stream(
openrouter_api::types::chat::ChatCompletionRequest {
model: "mistralai/mistral-small-latest".to_string(),
messages: messages.clone(),
stream: Some(true),
response_format: None,
tools: None,
provider: None,
models: None,
transforms: None,
}
);
while let Some(chunk) = stream.next().await {
match chunk {
Ok(c) => {
// Each chunk may include partial content updates.
if let Some(choice) = c.choices.first() {
// Print the delta (partial content) if available.
print!("{}", choice.message.content);
}
}
Err(e) => {
eprintln!("Stream error: {:?}", e);
break;
}
}
}For pure text completions, the CompletionApi is used.
use openrouter_api::types::completion::CompletionRequest;
let request = CompletionRequest {
model: "openai/gpt-4".to_string(),
prompt: "Once upon a time".to_string(),
extra_params: serde_json::json!({ "temperature": 0.7 }),
};
let completion_api = openrouter_api::api::completion::CompletionApi::new(
client.http_client.clone().unwrap(), // Clone the reqwest client
&client.config,
);
let response = completion_api.text_completion(request).await?;
println!("Completed Text: {}", response.choices[0].text);The web search API allows you to perform simple search queries and receive structured results.
use openrouter_api::types::web_search::WebSearchRequest;
let search_request = WebSearchRequest {
query: "rust programming".to_string(),
num_results: Some(5),
};
let web_search_api = client.web_search()?;
let search_response = web_search_api.search(search_request).await?;
println!("Query: {}", search_response.query);
for result in search_response.results {
println!("Title: {} • URL: {}", result.title, result.url);
}Structured output allows you to validate the model’s response against a JSON Schema. This is configured via the RequestBuilder.
use openrouter_api::models::structured::JsonSchemaConfig;
use openrouter_api::api::request::RequestBuilder;
let schema_config = JsonSchemaConfig {
name: "Person".to_string(),
strict: true,
schema: serde_json::json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["name", "age"]
}),
};
let request_payload = RequestBuilder::new("mistralai/mistral-small-latest", messages.clone(), serde_json::json!({}))
.with_structured_output(schema_config, true, true) // validate & fallback options
.build();Tool calling lets you define external functions that the model can suggest invoking. This is useful for integrating live functions (e.g., fetching weather).
use openrouter_api::models::tool::{Tool, FunctionDescription};
let get_weather = FunctionDescription {
name: "get_weather".to_string(),
description: Some("Get weather information for a location".to_string()),
parameters: serde_json::json!({
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and state (e.g., 'San Francisco, CA')"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["location"]
}),
};
let request_payload = RequestBuilder::new("meta-llama/llama-3-70b-instruct", messages.clone(), serde_json::json!({}))
.build();
// Call the chat endpoint with tool information.
let response = client.chat()?.chat_completion(
openrouter_api::types::chat::ChatCompletionRequest {
model: "meta-llama/llama-3-70b-instruct".to_string(),
messages,
stream: None,
response_format: None,
tools: Some(vec![Tool::Function { function: get_weather }]),
provider: None,
models: None,
transforms: None,
}
).await?;
// Process the tool call response.
if let Some(choice) = response.choices.first() {
if let Some(tool_calls) = &choice.message.tool_calls {
for call in tool_calls {
if call.kind == "function" {
println!("Tool call detected for function: {}", call.function_call.name);
println!("Arguments: {}", call.function_call.arguments);
// Here, you would implement your logic to execute the tool and return the result.
}
}
}
}The crate defines a central Error enum that wraps different error kinds:
-
HTTP Errors: Errors coming from the reqwest library.
-
API Errors: Errors returned from the OpenRouter API (with status codes and error messages).
-
Configuration Errors: Issues with client setup (e.g., invalid API key or headers).
-
Schema Validation Errors: When a response fails JSON Schema validation for structured output.
match client.chat()?.chat_completion(
openrouter_api::types::chat::ChatCompletionRequest {
model: "openai/gpt-4".to_string(),
messages: messages.clone(),
stream: None,
response_format: None,
tools: None,
provider: None,
models: None,
transforms: None,
}
).await {
Ok(response) => {
println!("Chat completion succeeded: {:?}", response);
},
Err(e) => match e {
openrouter_api::error::Error::ApiError { code, message, .. } => {
eprintln!("API Error ({}): {}", code, message);
},
openrouter_api::error::Error::HttpError(ref err) if err.is_timeout() => {
eprintln!("Request timed out!");
},
_ => eprintln!("Unexpected error: {:?}", e),
},
}-
Use the Type‑State Pattern: Always configure your client fully (base URL, API key, headers) before issuing requests. The type‑state pattern will help catch misconfiguration at compile time.
-
Set Appropriate Timeouts & Headers: Leverage
with_timeout,with_http_referer, andwith_site_titleto ensure your requests are traceable and robust. -
Validate Structured Responses: When using structured output, configure JSON Schema validation to catch unexpected responses early.
-
Stream Responsibly: When using streaming endpoints, ensure you handle errors gracefully and close streams when finished.
-
Secure Your API Keys: Store sensitive API keys in environment variables or secure storage rather than hardcoding them.
Below is a complete example combining client initialization, a chat request, and error handling:
use openrouter_api::client::{OpenRouterClient, Unconfigured};
use openrouter_api::types::chat::{Message, ChatCompletionRequest};
use std::time::Duration;
use tokio;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize client (Unconfigured -> NoAuth -> Ready)
let client = OpenRouterClient::<Unconfigured>::new()
.with_base_url("https://openrouter.ai/api/v1/")?
.with_timeout(Duration::from_secs(30))
.with_http_referer("https://your-app.com/")
.with_site_title("Example App")
.with_api_key(std::env::var("OPENROUTER_API_KEY")?)?;
// Prepare chat messages.
let messages = vec![
Message {
role: "user".to_string(),
content: "What is a phantom type in Rust?".to_string(),
name: None,
tool_calls: None,
}
];
// Build the chat completion request.
let request = ChatCompletionRequest {
model: "openai/gpt-4".to_string(),
messages,
stream: None,
response_format: None,
tools: None,
provider: None,
models: None,
transforms: None,
};
// Execute the chat completion.
let chat_api = client.chat()?;
match chat_api.chat_completion(request).await {
Ok(response) => {
println!("Assistant says: {}", response.choices[0].message.content);
},
Err(err) => {
eprintln!("Error: {:?}", err);
}
}
Ok(())
}use openrouter_api::api::request::RequestBuilder;
use serde_json::json;
let provider_preferences = json!({
"order": ["openrouter"],
"allowFallbacks": true
});
let request_payload = RequestBuilder::new("openai/gpt-4", messages.clone(), json!({}))
.with_provider_preferences(provider_preferences, true, true)
.build();
println!("Payload: {}", serde_json::to_string_pretty(&request_payload)?);