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
14 changes: 12 additions & 2 deletions src/config/entities/providers-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@
"name": { "type": "string" },
"type": {
"type": "string",
"enum": ["anthropic", "azure", "bedrock", "deepseek", "gemini", "openai"]
"enum": [
"anthropic",
"azure",
"bedrock",
"deepseek",
"gemini",
"openai",
"openrouter"
]
},
"config": { "type": "object" }
},
Expand Down Expand Up @@ -37,7 +45,9 @@
{
"if": {
"properties": {
"type": { "enum": ["anthropic", "deepseek", "gemini", "openai"] }
"type": {
"enum": ["anthropic", "deepseek", "gemini", "openai", "openrouter"]
}
},
"required": ["type"]
},
Expand Down
8 changes: 8 additions & 0 deletions src/config/entities/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub enum ProviderConfig {
Gemini(configs::GeminiProviderConfig),
#[serde(rename = "openai")]
OpenAI(configs::OpenAIProviderConfig),
#[serde(rename = "openrouter")]
OpenRouter(configs::OpenRouterProviderConfig),
}

impl ProviderConfig {
Expand All @@ -45,6 +47,7 @@ impl ProviderConfig {
Self::DeepSeek(_) => identifiers::DEEPSEEK,
Self::Gemini(_) => identifiers::GEMINI,
Self::OpenAI(_) => identifiers::OPENAI,
Self::OpenRouter(_) => identifiers::OPENROUTER,
}
}
}
Expand Down Expand Up @@ -131,6 +134,11 @@ mod tests {
"secret_access_key": "secret"
}
}), true, None)]
#[case::openrouter_ok(json!({
"name": "openrouter-primary",
"type": "openrouter",
"config": { "api_key": "test_key" }
}), true, None)]
#[case::missing_type(json!({
"name": "openai-primary",
"config": { "api_key": "test_key" }
Expand Down
10 changes: 8 additions & 2 deletions src/gateway/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,34 @@ pub mod deepseek;
pub mod gemini;
pub mod macros;
pub mod openai;
pub mod openrouter;

pub use anthropic::AnthropicDef;
pub use azure::AzureDef;
pub use bedrock::BedrockDef;
pub use deepseek::DeepSeek;
pub use gemini::GoogleDef;
pub use openai::OpenAIDef;
pub use openrouter::OpenRouter;

pub mod identifiers {
use super::{anthropic, azure, bedrock, deepseek, gemini, openai};
use super::{anthropic, azure, bedrock, deepseek, gemini, openai, openrouter};

pub const ANTHROPIC: &str = anthropic::IDENTIFIER;
pub const AZURE: &str = azure::IDENTIFIER;
pub const BEDROCK: &str = bedrock::IDENTIFIER;
pub const DEEPSEEK: &str = deepseek::IDENTIFIER;
pub const GEMINI: &str = gemini::IDENTIFIER;
pub const OPENAI: &str = openai::IDENTIFIER;
pub const OPENROUTER: &str = openrouter::IDENTIFIER;
}

pub mod configs {
pub use super::{
anthropic::AnthropicProviderConfig, azure::AzureProviderConfig,
bedrock::BedrockProviderConfig, deepseek::DeepSeekProviderConfig,
gemini::GeminiProviderConfig, openai::OpenAIProviderConfig,
openrouter::OpenRouterProviderConfig,
};
}

Expand All @@ -41,7 +45,8 @@ pub fn default_provider_registry() -> Result<ProviderRegistry> {
.register(BedrockDef)?
.register(DeepSeek)?
.register(GoogleDef)?
.register(OpenAIDef)?;
.register(OpenAIDef)?
.register(OpenRouter)?;
Ok(builder.build())
}

Expand All @@ -59,6 +64,7 @@ mod tests {
assert_eq!(registry.get("bedrock").unwrap().name(), "bedrock");
assert_eq!(registry.get("gemini").unwrap().name(), "gemini");
assert_eq!(registry.get("deepseek").unwrap().name(), "deepseek");
assert_eq!(registry.get("openrouter").unwrap().name(), "openrouter");
Comment thread
bzp2010 marked this conversation as resolved.
assert!(registry.get("missing").is_none());
}
}
42 changes: 42 additions & 0 deletions src/gateway/providers/openrouter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use serde::{Deserialize, Serialize};

use crate::gateway::providers::macros::provider;

/// Provider identifier string used to look up OpenRouter in the gateway registry.
pub const IDENTIFIER: &str = "openrouter";

/// Configuration for an OpenRouter provider deployment.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct OpenRouterProviderConfig {
pub api_key: String,

#[serde(skip_serializing_if = "Option::is_none")]
pub api_base: Option<String>,
}

provider!(OpenRouter {
display_name: "openrouter",
base_url: "https://openrouter.ai/api/v1",
chat_path: "/chat/completions",
auth: bearer,
});

#[cfg(test)]
mod tests {
use super::OpenRouter;
use crate::gateway::traits::ProviderMeta;

#[test]
fn provider_macro_expands_correctly() {
let provider = OpenRouter;

pretty_assertions::assert_eq!(provider.name(), "openrouter");
pretty_assertions::assert_eq!(provider.default_base_url(), "https://openrouter.ai/api/v1");
pretty_assertions::assert_eq!(provider.chat_endpoint_path("ignored"), "/chat/completions");

pretty_assertions::assert_eq!(
provider.build_url(provider.default_base_url(), "ignored"),
"https://openrouter.ai/api/v1/chat/completions"
);
}
}
24 changes: 23 additions & 1 deletion src/proxy/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ fn provider_auth_and_base_url(config: &ProviderConfig) -> Result<(ProviderAuth,
ProviderAuth::ApiKey(config.api_key.clone()),
parse_base_url(config.api_base.as_deref())?,
),
ProviderConfig::OpenRouter(config) => (
ProviderAuth::ApiKey(config.api_key.clone()),
parse_base_url(config.api_base.as_deref())?,
),
};

Ok((auth, base_url_override))
Expand Down Expand Up @@ -155,7 +159,9 @@ mod tests {
use super::provider_auth_and_base_url;
use crate::{
config::entities::providers::ProviderConfig,
gateway::providers::configs::{AzureProviderConfig, BedrockProviderConfig},
gateway::providers::configs::{
AzureProviderConfig, BedrockProviderConfig, OpenRouterProviderConfig,
},
};

#[test]
Expand Down Expand Up @@ -197,6 +203,22 @@ mod tests {
);
}

#[test]
fn provider_auth_and_base_url_returns_openrouter_api_key_and_optional_base_url() {
let config = ProviderConfig::OpenRouter(OpenRouterProviderConfig {
api_key: "openrouter-key".into(),
api_base: Some("https://openrouter.ai/api/v1".into()),
});

let (auth, base_url_override) = provider_auth_and_base_url(&config).unwrap();

assert_eq!(auth.api_key_for("openrouter").unwrap(), "openrouter-key");
assert_eq!(
base_url_override.as_ref().map(Url::as_str),
Some("https://openrouter.ai/api/v1")
);
}
Comment thread
bzp2010 marked this conversation as resolved.

#[test]
fn provider_auth_and_base_url_returns_bedrock_static_credentials() {
let config = ProviderConfig::Bedrock(BedrockProviderConfig {
Expand Down
10 changes: 2 additions & 8 deletions ui/src/components/providers/provider-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { PROVIDER_TYPE_VARIANTS } from '@/lib/api/types';
import type { Provider, ProviderType } from '@/lib/api/types';

export interface ProviderFormProps {
Expand All @@ -36,14 +37,7 @@ export interface ProviderFormProps {
extraActions?: React.ReactNode;
}

const PROVIDER_TYPES: ProviderType[] = [
'openai',
'azure',
'anthropic',
'gemini',
'deepseek',
'bedrock',
];
const PROVIDER_TYPES = Array.from(PROVIDER_TYPE_VARIANTS);

function trimOptional(value: string): string | undefined {
const trimmed = value.trim();
Expand Down
2 changes: 2 additions & 0 deletions ui/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
"concurrency": "Concurrency",
"providers": {
"openai": "OpenAI",
"openrouter": "OpenRouter",
"azure": "Azure OpenAI",
"anthropic": "Anthropic",
"gemini": "Gemini",
Expand Down Expand Up @@ -216,6 +217,7 @@
"endpointHint": "Leave blank to use the standard runtime endpoint for the selected region.",
"types": {
"openai": "OpenAI",
"openrouter": "OpenRouter",
"azure": "Azure OpenAI",
"anthropic": "Anthropic",
"gemini": "Gemini",
Expand Down
2 changes: 2 additions & 0 deletions ui/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
"concurrency": "并发",
"providers": {
"openai": "OpenAI",
"openrouter": "OpenRouter",
"azure": "Azure OpenAI",
"anthropic": "Anthropic",
"gemini": "Gemini",
Expand Down Expand Up @@ -216,6 +217,7 @@
"endpointHint": "留空则根据所选 Region 使用标准运行时地址。",
"types": {
"openai": "OpenAI",
"openrouter": "OpenRouter",
"azure": "Azure OpenAI",
"anthropic": "Anthropic",
"gemini": "Gemini",
Expand Down
23 changes: 16 additions & 7 deletions ui/src/lib/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,17 @@ export interface Model {
rate_limit?: RateLimit;
}

export type ProviderType =
| 'anthropic'
| 'azure'
| 'bedrock'
| 'deepseek'
| 'gemini'
| 'openai';
export const PROVIDER_TYPE_VARIANTS = [
'openai',
'openrouter',
'azure',
'anthropic',
'gemini',
'deepseek',
'bedrock',
] as const;

export type ProviderType = (typeof PROVIDER_TYPE_VARIANTS)[number];

export interface ApiBaseProviderConfig {
api_key: string;
Expand Down Expand Up @@ -90,6 +94,11 @@ export type Provider =
type: 'openai';
config: ApiBaseProviderConfig;
}
| {
name: string;
type: 'openrouter';
config: ApiBaseProviderConfig;
}
| {
name: string;
type: 'bedrock';
Expand Down
Loading