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:
@@ -21,7 +21,46 @@ pub struct Usage {
|
||||
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 {
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
@@ -29,6 +68,7 @@ pub struct ChatClient {
|
||||
http: Client,
|
||||
timeout: Duration,
|
||||
max_retries: u32,
|
||||
auth_mode: AuthMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -82,16 +122,26 @@ impl ChatClient {
|
||||
/// * `api_key` - Authentication key
|
||||
/// * `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> {
|
||||
let base_url = base_url.into();
|
||||
let api_key = api_key.into();
|
||||
let auth_mode = AuthMode::detect(&base_url, &api_key);
|
||||
Ok(Self {
|
||||
base_url: base_url.into(),
|
||||
api_key: api_key.into(),
|
||||
base_url,
|
||||
api_key,
|
||||
model: model.into(),
|
||||
http: Client::new(),
|
||||
timeout: Duration::from_secs(300),
|
||||
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.
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
@@ -145,9 +195,15 @@ impl ChatClient {
|
||||
|
||||
for attempt in 0..self.max_retries {
|
||||
let mut req = self.http.post(&url);
|
||||
// Only add apikey header if it's not empty (for backward compatibility)
|
||||
if !self.api_key.is_empty() && !self.api_key.starts_with("http") {
|
||||
req = req.header("apikey", &self.api_key);
|
||||
// Apply auth based on provider
|
||||
match &self.auth_mode {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user