feat: multi-provider auth for ChatClient (OpenRouter, OpenAI, Ollama)
Build and Push / Test (push) Failing after 3m40s
Build and Push / Build and push image (push) Skipped

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 aa49770fa4
commit 6685648622
+61 -5
View File
@@ -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,10 +195,16 @@ 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") {
// 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
.header("Content-Type", "application/json")