Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

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

# Database
SURREAL_SYNC_DATA=true
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 11 additions & 10 deletions crates/pattern_cli/src/agent_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,16 +232,17 @@ pub async fn load_model_embedding_providers(
let oauth_client =
OAuthClientBuilder::new(Arc::new(DB.clone()), config.user.id.clone()).build()?;
// Wrap in GenAiClient with all endpoints available
let genai_client = GenAiClient::with_endpoints(
oauth_client,
vec![
genai::adapter::AdapterKind::Anthropic,
genai::adapter::AdapterKind::Gemini,
genai::adapter::AdapterKind::OpenAI,
genai::adapter::AdapterKind::Groq,
genai::adapter::AdapterKind::Cohere,
],
);
let mut endpoints = vec![
genai::adapter::AdapterKind::Anthropic,
genai::adapter::AdapterKind::Gemini,
genai::adapter::AdapterKind::OpenAI,
genai::adapter::AdapterKind::Groq,
genai::adapter::AdapterKind::Cohere,
];
if std::env::var("OPENROUTER_API_KEY").is_ok() {
endpoints.push(genai::adapter::AdapterKind::OpenRouter);
}
let genai_client = GenAiClient::with_endpoints(oauth_client, endpoints);
Arc::new(RwLock::new(genai_client))
}
#[cfg(not(feature = "oauth"))]
Expand Down
31 changes: 25 additions & 6 deletions crates/pattern_core/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ impl ResponseOptions {
pub enum ModelVendor {
Anthropic,
OpenAI,
Gemini, // Google's Gemini models
OpenRouter, // OpenRouter - routes to multiple providers via OpenAI-compatible API
Gemini, // Google's Gemini models
Cohere,
Groq,
Ollama,
Expand All @@ -176,7 +177,12 @@ impl ModelVendor {
/// Check if this vendor uses OpenAI-compatible API
pub fn is_openai_compatible(&self) -> bool {
match self {
Self::OpenAI | Self::Cohere | Self::Groq | Self::Ollama | Self::Other => true,
Self::OpenAI
| Self::OpenRouter
| Self::Cohere
| Self::Groq
| Self::Ollama
| Self::Other => true,
Self::Anthropic | Self::Gemini => false,
}
}
Expand All @@ -186,6 +192,7 @@ impl ModelVendor {
match provider.to_lowercase().as_str() {
"anthropic" => Self::Anthropic,
"openai" => Self::OpenAI,
"openrouter" => Self::OpenRouter,
"gemini" | "google" => Self::Gemini,
"cohere" => Self::Cohere,
"groq" => Self::Groq,
Expand Down Expand Up @@ -282,6 +289,9 @@ impl GenAiClient {
if std::env::var("COHERE_API_KEY").is_ok() {
available_endpoints.push(AdapterKind::Cohere);
}
if std::env::var("OPENROUTER_API_KEY").is_ok() {
available_endpoints.push(AdapterKind::OpenRouter);
}

Ok(Self {
client,
Expand Down Expand Up @@ -317,23 +327,32 @@ impl ModelProvider for GenAiClient {
};

for model in models {
// For OpenRouter, we need to prefix model IDs with "openrouter::" so genai
// can resolve them to the correct adapter. OpenRouter models use "/" as separator
// (e.g., "anthropic/claude-opus-4.5") but genai uses "::" for namespacing.
let model_id = if *endpoint == AdapterKind::OpenRouter {
format!("openrouter::{}", model)
} else {
model.clone()
};

// Try to resolve the service target - this validates authentication
match self.client.resolve_service_target(&model).await {
match self.client.resolve_service_target(&model_id).await {
Ok(_) => {
// Model is accessible, continue
}
Err(e) => {
// Authentication failed for this model, skip it
tracing::debug!("Skipping model {} due to auth error: {}", model, e);
tracing::debug!("Skipping model {} due to auth error: {}", model_id, e);
continue;
}
}

// Create basic ModelInfo from provider
let model_info = ModelInfo {
provider: endpoint.to_string(),
id: model.clone(),
name: model,
id: model_id.clone(),
name: model, // Keep original name for display
capabilities: vec![],
max_output_tokens: None,
cost_per_1k_completion_tokens: None,
Expand Down
Loading
Loading