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,
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user