feat: multi-provider auth for ChatClient (OpenRouter, OpenAI, Ollama)

Auto-detect auth mode from base URL:
- openrouter.ai, api.openai.com → Bearer token
- api.riotpiao.com → apikey header
- localhost → no auth
Explicit override via with_auth_mode()
This commit is contained in:
2026-08-30 17:58:27 -07:00
parent ae1a2ef9a2
commit 343a4f224f
+62 -6
View File
@@ -21,7 +21,46 @@ pub struct Usage {
pub total_tokens: u32, pub total_tokens: u32,
} }
/// Chat client for the gateway. /// Auth mode for different OpenAI-compatible providers.
#[derive(Debug, Clone)]
pub enum AuthMode {
/// `apikey: <key>` header (riotpiao gateway, Ollama)
ApiKeyHeader,
/// `Authorization: Bearer <key>` (OpenRouter, OpenAI, Anthropic)
Bearer,
/// No auth header
None,
}
impl AuthMode {
/// Detect from base URL or explicit env var.
pub fn detect(base_url: &str, api_key: &str) -> Self {
if api_key.is_empty() {
return Self::None;
}
// OpenRouter, OpenAI, Anthropic, Together, etc. use Bearer
if base_url.contains("openrouter.ai")
|| base_url.contains("api.openai.com")
|| base_url.contains("api.anthropic.com")
|| base_url.contains("api.together.xyz")
|| base_url.contains("api.groq.com")
{
Self::Bearer
} else {
// Default: apikey header (riotpiao gateway)
Self::ApiKeyHeader
}
}
}
/// Chat client for OpenAI-compatible providers.
///
/// Supports:
/// - riotpiao gateway (apikey header)
/// - OpenRouter (Bearer token, `openrouter.ai/api/v1`)
/// - OpenAI (Bearer token)
/// - Ollama (no auth or apikey)
/// - Any OpenAI-compatible endpoint
pub struct ChatClient { pub struct ChatClient {
base_url: String, base_url: String,
api_key: String, api_key: String,
@@ -29,6 +68,7 @@ pub struct ChatClient {
http: Client, http: Client,
timeout: Duration, timeout: Duration,
max_retries: u32, max_retries: u32,
auth_mode: AuthMode,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@@ -82,16 +122,26 @@ impl ChatClient {
/// * `api_key` - Authentication key /// * `api_key` - Authentication key
/// * `model` - Model identifier (e.g., `qwen2.5:3b-instruct`) /// * `model` - Model identifier (e.g., `qwen2.5:3b-instruct`)
pub fn new(base_url: impl Into<String>, api_key: impl Into<String>, model: impl Into<String>) -> Result<Self> { pub fn new(base_url: impl Into<String>, api_key: impl Into<String>, model: impl Into<String>) -> Result<Self> {
let base_url = base_url.into();
let api_key = api_key.into();
let auth_mode = AuthMode::detect(&base_url, &api_key);
Ok(Self { Ok(Self {
base_url: base_url.into(), base_url,
api_key: api_key.into(), api_key,
model: model.into(), model: model.into(),
http: Client::new(), http: Client::new(),
timeout: Duration::from_secs(300), timeout: Duration::from_secs(300),
max_retries: 3, max_retries: 3,
auth_mode,
}) })
} }
/// Create with explicit auth mode.
pub fn with_auth_mode(mut self, mode: AuthMode) -> Self {
self.auth_mode = mode;
self
}
/// Set custom timeout. /// Set custom timeout.
pub fn with_timeout(mut self, timeout: Duration) -> Self { pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout; self.timeout = timeout;
@@ -145,9 +195,15 @@ impl ChatClient {
for attempt in 0..self.max_retries { for attempt in 0..self.max_retries {
let mut req = self.http.post(&url); let mut req = self.http.post(&url);
// Only add apikey header if it's not empty (for backward compatibility) // Apply auth based on provider
if !self.api_key.is_empty() && !self.api_key.starts_with("http") { match &self.auth_mode {
req = req.header("apikey", &self.api_key); AuthMode::ApiKeyHeader => {
req = req.header("apikey", &self.api_key);
}
AuthMode::Bearer => {
req = req.header("Authorization", format!("Bearer {}", self.api_key));
}
AuthMode::None => {}
} }
let response = req let response = req