Complete reference for OpenRouter AI integration
- API Overview
- Authentication
- Endpoints
- Request Format
- Response Format
- Error Handling
- Rate Limits
- Models Available
- Best Practices
- Code Examples
OpenRouter is a unified API gateway that provides access to multiple AI models (GPT-4, Claude, Gemini, etc.) through a single interface.
Base URL: https://openrouter.ai/api/v1
Protocol: HTTPS (TLS 1.2+)
Format: JSON
Authentication: Bearer token
| Benefit | Description |
|---|---|
| Unified API | One interface for multiple models |
| Free tier | Google Gemini 2.0 Flash is free |
| Reliability | Automatic failover between providers |
| Simplicity | Standard OpenAI-compatible format |
| Flexibility | Easy to switch models |
-
Get API Key:
- Visit: https://openrouter.ai/
- Sign up (free)
- Go to Settings β Keys
- Create new key
- Copy:
sk-or-v1-xxxxxxxxxxxxxxxx
-
Configure in Project:
# Create .env file
echo 'OPENROUTER_API_KEY=sk-or-v1-YOUR_KEY_HERE' > .env
echo 'OPENROUTER_MODEL=google/gemini-2.0-flash-exp:free' >> .env- Load in Code:
use std::env;
fn get_api_key() -> Result<String, String> {
dotenvy::dotenv().ok();
env::var("OPENROUTER_API_KEY")
.map_err(|_| "API key not set".to_string())
}// β NEVER hardcode API keys
const API_KEY: &str = "sk-or-v1-abc123...";
// β
Use environment variables
let api_key = env::var("OPENROUTER_API_KEY")?;
// β
β
Best: Use system keyring (Linux/macOS/Windows)
#[cfg(unix)]
fn get_api_key() -> Result<String> {
keyring::Entry::new("linara-terminal", "api-key")?.get_password()
}POST /chat/completions
Purpose: Generate text completions (our main use case)
URL: https://openrouter.ai/api/v1/chat/completions
Headers:
Authorization: Bearer sk-or-v1-YOUR_KEY_HERE
Content-Type: application/json
HTTP-Referer: https://github.com/zoxilsi/Linara-Terminal (optional)
X-Title: Linara Terminal (optional)
{
"model": "google/gemini-2.0-flash-exp:free",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Hello!"
}
]
}interface ChatCompletionRequest {
model: string; // Required: Model identifier
messages: Message[]; // Required: Conversation history
temperature?: number; // Optional: 0.0-2.0 (default: 1.0)
top_p?: number; // Optional: 0.0-1.0 (default: 1.0)
max_tokens?: number; // Optional: Max response length
stream?: boolean; // Optional: Stream response (default: false)
stop?: string | string[]; // Optional: Stop sequences
}
interface Message {
role: "system" | "user" | "assistant";
content: string;
}#[derive(Serialize)]
struct OpenRouterRequest {
model: String,
messages: Vec<OpenRouterMessage>,
}
#[derive(Serialize)]
struct OpenRouterMessage {
role: String, // "system" | "user" | "assistant"
content: String, // Message text
}
// Build request
let request = OpenRouterRequest {
model: "google/gemini-2.0-flash-exp:free".to_string(),
messages: vec![
OpenRouterMessage {
role: "system".to_string(),
content: "Convert natural language to Linux commands.".to_string(),
},
OpenRouterMessage {
role: "user".to_string(),
content: format!("Input: {}\nOutput:", user_input),
},
],
};{
"model": "google/gemini-2.0-flash-exp:free",
"messages": [
{
"role": "system",
"content": "Convert natural language to Linux commands. Respond with ONLY the command."
},
{
"role": "user",
"content": "list all files"
}
]
}Expected Response: ls -la
{
"model": "google/gemini-2.0-flash-exp:free",
"messages": [
{
"role": "system",
"content": "Convert natural language to Linux commands."
},
{
"role": "user",
"content": "create a folder called test"
}
]
}Expected Response: mkdir test
{
"id": "gen-1234567890abcdef",
"model": "google/gemini-2.0-flash-exp:free",
"object": "chat.completion",
"created": 1234567890,
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "ls -la"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 15,
"completion_tokens": 3,
"total_tokens": 18
}
}interface ChatCompletionResponse {
id: string; // Unique request ID
model: string; // Model used
object: "chat.completion"; // Type identifier
created: number; // Unix timestamp
choices: Choice[]; // Response options
usage: Usage; // Token usage
}
interface Choice {
index: number; // Choice index (0 for first)
message: Message; // AI response
finish_reason: string; // "stop" | "length" | "content_filter"
}
interface Usage {
prompt_tokens: number; // Input tokens
completion_tokens: number; // Output tokens
total_tokens: number; // Sum
}#[derive(Deserialize)]
struct OpenRouterResponse {
choices: Vec<OpenRouterChoice>,
}
#[derive(Deserialize)]
struct OpenRouterChoice {
message: OpenRouterMessage,
}
#[derive(Deserialize)]
struct OpenRouterMessage {
role: String,
content: String,
}
// Parse response
let response: OpenRouterResponse = http_response.json().await?;
let command = response.choices[0].message.content.trim();| Code | Meaning | Action |
|---|---|---|
200 |
Success | Parse response |
400 |
Bad Request | Check request format |
401 |
Unauthorized | Verify API key |
429 |
Too Many Requests | Retry with backoff |
500 |
Server Error | Retry |
503 |
Service Unavailable | Retry later |
{
"error": {
"message": "Rate limit exceeded",
"type": "rate_limit_error",
"code": 429
}
}match http_response.status() {
status if status.is_success() => {
// Parse and return command
let data: OpenRouterResponse = http_response.json().await?;
Ok(data.choices[0].message.content.clone())
}
status if status == 429 => {
// Rate limited - retry with backoff
Err("API_RATE_LIMIT: Too many requests".into())
}
status if status.is_server_error() => {
// Transient error - retry
Err("API_TRANSIENT: Server error".into())
}
status => {
// Client error - don't retry
let body = http_response.text().await?;
Err(format!("API error {}: {}", status, body).into())
}
}// Retry up to 3 times
for attempt in 0..3 {
match make_api_call().await {
Ok(response) if response.status() == 429 => {
// Rate limited - calculate backoff
let backoff_ms = match attempt {
0 => 300, // First retry: 300ms
1 => 800, // Second retry: 800ms
_ => 0
};
if backoff_ms > 0 {
sleep(Duration::from_millis(backoff_ms)).await;
}
continue; // Try again
}
Ok(response) => return Ok(response), // Success!
Err(e) => last_error = Some(e), // Network error
}
}
// All retries failed
Err(last_error.unwrap())| Limit Type | Value | Notes |
|---|---|---|
| Requests per minute | ~60 | Varies by load |
| Tokens per request | 1M context | Very generous |
| Cost | $0.00 | Completely free |
| Uptime | Best effort | May have occasional outages |
{
"error": {
"message": "Rate limit exceeded. Please try again later.",
"type": "rate_limit_error",
"code": 429
}
}- Detect: Check for HTTP 429 status
- Wait: Exponential backoff (300ms, 800ms)
- Retry: Automatic retry up to 3 times
- Inform: Show user-friendly message
if error.contains("429") || error.contains("rate-limited") {
println!("π¦ Rate limited by free model.");
println!(" β’ Wait 30 seconds and try again");
println!(" β’ Or add your own OpenRouter key");
println!(" β’ Or switch to a different model");
}Model ID: google/gemini-2.0-flash-exp:free
| Feature | Value |
|---|---|
| Provider | |
| Cost | $0.00 (free) |
| Context Window | 1,048,576 tokens (1M) |
| Speed | Very fast (~500ms) |
| Quality | Excellent for code |
| Release | December 2024 |
# .env
OPENROUTER_MODEL=anthropic/claude-3.5-sonnet| Feature | Value |
|---|---|
| Cost | $3.00 / 1M input tokens |
| Context | 200K tokens |
| Speed | Medium (~1s) |
| Quality | Excellent reasoning |
# .env
OPENROUTER_MODEL=openai/gpt-4-turbo| Feature | Value |
|---|---|
| Cost | $10.00 / 1M input tokens |
| Context | 128K tokens |
| Speed | Slow (~2s) |
| Quality | Excellent general |
// Get model from environment
fn get_openrouter_model() -> String {
env::var("OPENROUTER_MODEL")
.unwrap_or_else(|_| "google/gemini-2.0-flash-exp:free".to_string())
}
// Use in request
let request = OpenRouterRequest {
model: get_openrouter_model(),
messages: vec![...],
};Bad prompt:
"Convert this to a command: list files"
Good prompt:
"You are a Linux terminal command generator. Convert natural language to
valid Linux commands. Respond with ONLY the command, no explanations.
Input: list files
Output:"
// Always validate AI output
fn validate_command(cmd: &str) -> bool {
// Not empty
!cmd.is_empty() &&
// Reasonable length
cmd.len() < 200 &&
// Contains alphanumeric
cmd.chars().any(|c| c.is_alphanumeric()) &&
// First token is executable
looks_like_valid_command(cmd)
}// Cache successful responses
struct CacheEntry {
command: String,
timestamp: SystemTime,
}
// 5-minute TTL
fn get_cached(&self, input: &str) -> Option<String> {
if let Some(entry) = self.cache.get(input) {
if entry.timestamp.elapsed()? < Duration::from_secs(300) {
return Some(entry.command.clone());
}
}
None
}// User-friendly error messages
match result {
Err(e) if e.contains("429") => {
println!("π¦ Rate limited. Wait 30s or add own key.");
}
Err(e) if e.contains("timeout") => {
println!("β° AI timed out. Try again.");
}
Err(e) => {
println!("β Error: {}", e);
}
Ok(cmd) => { /* Success */ }
}// Reuse HTTP connections
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.pool_max_idle_per_host(20) // Keep 20 connections
.tcp_keepalive(Duration::from_secs(60)) // 60s keepalive
.build()?;use reqwest;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tokio::time::{timeout, sleep};
#[derive(Serialize)]
struct OpenRouterRequest {
model: String,
messages: Vec<OpenRouterMessage>,
}
#[derive(Serialize, Deserialize)]
struct OpenRouterMessage {
role: String,
content: String,
}
#[derive(Deserialize)]
struct OpenRouterResponse {
choices: Vec<OpenRouterChoice>,
}
#[derive(Deserialize)]
struct OpenRouterChoice {
message: OpenRouterMessage,
}
async fn generate_command(
client: &reqwest::Client,
natural_input: &str,
api_key: &str
) -> Result<String, Box<dyn std::error::Error>> {
// Build request
let request = OpenRouterRequest {
model: "google/gemini-2.0-flash-exp:free".to_string(),
messages: vec![
OpenRouterMessage {
role: "system".to_string(),
content: "Convert natural language to Linux commands. \
Respond with ONLY the command.".to_string(),
},
OpenRouterMessage {
role: "user".to_string(),
content: format!("Input: {}\nOutput:", natural_input),
},
],
};
// Make API call with timeout
let response = timeout(
Duration::from_secs(10),
client
.post("https://openrouter.ai/api/v1/chat/completions")
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.json(&request)
.send()
).await??;
// Check status
if !response.status().is_success() {
let status = response.status();
let body = response.text().await?;
return Err(format!("API error {}: {}", status, body).into());
}
// Parse response
let data: OpenRouterResponse = response.json().await?;
let command = data.choices[0].message.content.trim();
// Clean markdown if present
let command = command
.trim_start_matches("```bash")
.trim_start_matches("```")
.trim_end_matches("```")
.trim();
Ok(command.to_string())
}
// Usage
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let api_key = std::env::var("OPENROUTER_API_KEY")?;
let command = generate_command(&client, "list all files", &api_key).await?;
println!("Command: {}", command); // "ls -la"
Ok(())
}# Test connection
curl https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "google/gemini-2.0-flash-exp:free",
"messages": [
{"role": "user", "content": "Say hello"}
]
}'
# Expected response
{
"choices": [
{"message": {"content": "Hello!"}}
]
}| Issue | Cause | Solution |
|---|---|---|
401 Unauthorized |
Invalid API key | Check .env file |
429 Rate Limit |
Too many requests | Wait 30s or upgrade |
Connection timeout |
Network issue | Check internet connection |
Empty response |
Model overloaded | Retry or switch model |
Invalid JSON |
Malformed request | Validate request structure |
# Enable verbose logging
RUST_LOG=debug cargo run
# Check what's being sent
curl -v https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d @request.json- OpenRouter Docs: https://openrouter.ai/docs
- API Reference: https://openrouter.ai/docs/api-reference
- Model List: https://openrouter.ai/models
- Pricing: https://openrouter.ai/models (scroll for pricing)
- Status Page: https://status.openrouter.ai/
This API documentation covered:
β
Authentication: API key setup and security
β
Endpoints: Chat completions endpoint
β
Request/Response: JSON format and parsing
β
Error Handling: Status codes and retry logic
β
Rate Limits: Free tier limits and handling
β
Models: Available models and switching
β
Best Practices: Optimization techniques
β
Examples: Complete working code
Next steps:
- Get your API key from OpenRouter
- Add to
.envfile - Test with example code
- Integrate into your project