Skip to content

Commit 104a48f

Browse files
authored
Merge pull request #21 from aiunian/openrouter-support
Adding OpenRouter support
2 parents 11a28c9 + 5bf976a commit 104a48f

File tree

7 files changed

+432
-19
lines changed

7 files changed

+432
-19
lines changed

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212

1313
# Models
1414
#GEMINI_API_KEY=<gemini-api-key>
15+
# OpenRouter example with optional app URL and title headers
16+
#OPENROUTER_API_KEY=<openrouter-api-key>
17+
#OPENROUTER_APP_URL=https://github.com/orual/pattern/
18+
#OPENROUTER_APP_TITLE=Pattern
1519

1620
# Database
1721
SURREAL_SYNC_DATA=true

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pattern_cli/src/agent_ops.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -232,16 +232,17 @@ pub async fn load_model_embedding_providers(
232232
let oauth_client =
233233
OAuthClientBuilder::new(Arc::new(DB.clone()), config.user.id.clone()).build()?;
234234
// Wrap in GenAiClient with all endpoints available
235-
let genai_client = GenAiClient::with_endpoints(
236-
oauth_client,
237-
vec![
238-
genai::adapter::AdapterKind::Anthropic,
239-
genai::adapter::AdapterKind::Gemini,
240-
genai::adapter::AdapterKind::OpenAI,
241-
genai::adapter::AdapterKind::Groq,
242-
genai::adapter::AdapterKind::Cohere,
243-
],
244-
);
235+
let mut endpoints = vec![
236+
genai::adapter::AdapterKind::Anthropic,
237+
genai::adapter::AdapterKind::Gemini,
238+
genai::adapter::AdapterKind::OpenAI,
239+
genai::adapter::AdapterKind::Groq,
240+
genai::adapter::AdapterKind::Cohere,
241+
];
242+
if std::env::var("OPENROUTER_API_KEY").is_ok() {
243+
endpoints.push(genai::adapter::AdapterKind::OpenRouter);
244+
}
245+
let genai_client = GenAiClient::with_endpoints(oauth_client, endpoints);
245246
Arc::new(RwLock::new(genai_client))
246247
}
247248
#[cfg(not(feature = "oauth"))]

crates/pattern_core/src/model.rs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ impl ResponseOptions {
165165
pub enum ModelVendor {
166166
Anthropic,
167167
OpenAI,
168-
Gemini, // Google's Gemini models
168+
OpenRouter, // OpenRouter - routes to multiple providers via OpenAI-compatible API
169+
Gemini, // Google's Gemini models
169170
Cohere,
170171
Groq,
171172
Ollama,
@@ -176,7 +177,12 @@ impl ModelVendor {
176177
/// Check if this vendor uses OpenAI-compatible API
177178
pub fn is_openai_compatible(&self) -> bool {
178179
match self {
179-
Self::OpenAI | Self::Cohere | Self::Groq | Self::Ollama | Self::Other => true,
180+
Self::OpenAI
181+
| Self::OpenRouter
182+
| Self::Cohere
183+
| Self::Groq
184+
| Self::Ollama
185+
| Self::Other => true,
180186
Self::Anthropic | Self::Gemini => false,
181187
}
182188
}
@@ -186,6 +192,7 @@ impl ModelVendor {
186192
match provider.to_lowercase().as_str() {
187193
"anthropic" => Self::Anthropic,
188194
"openai" => Self::OpenAI,
195+
"openrouter" => Self::OpenRouter,
189196
"gemini" | "google" => Self::Gemini,
190197
"cohere" => Self::Cohere,
191198
"groq" => Self::Groq,
@@ -282,6 +289,9 @@ impl GenAiClient {
282289
if std::env::var("COHERE_API_KEY").is_ok() {
283290
available_endpoints.push(AdapterKind::Cohere);
284291
}
292+
if std::env::var("OPENROUTER_API_KEY").is_ok() {
293+
available_endpoints.push(AdapterKind::OpenRouter);
294+
}
285295

286296
Ok(Self {
287297
client,
@@ -317,23 +327,32 @@ impl ModelProvider for GenAiClient {
317327
};
318328

319329
for model in models {
330+
// For OpenRouter, we need to prefix model IDs with "openrouter::" so genai
331+
// can resolve them to the correct adapter. OpenRouter models use "/" as separator
332+
// (e.g., "anthropic/claude-opus-4.5") but genai uses "::" for namespacing.
333+
let model_id = if *endpoint == AdapterKind::OpenRouter {
334+
format!("openrouter::{}", model)
335+
} else {
336+
model.clone()
337+
};
338+
320339
// Try to resolve the service target - this validates authentication
321-
match self.client.resolve_service_target(&model).await {
340+
match self.client.resolve_service_target(&model_id).await {
322341
Ok(_) => {
323342
// Model is accessible, continue
324343
}
325344
Err(e) => {
326345
// Authentication failed for this model, skip it
327-
tracing::debug!("Skipping model {} due to auth error: {}", model, e);
346+
tracing::debug!("Skipping model {} due to auth error: {}", model_id, e);
328347
continue;
329348
}
330349
}
331350

332351
// Create basic ModelInfo from provider
333352
let model_info = ModelInfo {
334353
provider: endpoint.to_string(),
335-
id: model.clone(),
336-
name: model,
354+
id: model_id.clone(),
355+
name: model, // Keep original name for display
337356
capabilities: vec![],
338357
max_output_tokens: None,
339358
cost_per_1k_completion_tokens: None,

0 commit comments

Comments
 (0)