🐛 Fix Ollama cloud detection — ollama.com uses native /api/chat + Bearer auth

This commit is contained in:
2026-06-11 22:18:09 +05:30
parent b75fe44b98
commit fef7e71387

View File

@@ -37,13 +37,41 @@ impl OllamaProvider {
}
/// Returns true if this is an OpenAI-compatible endpoint (not native Ollama).
/// Heuristic: if api_key is set or URL is not localhost/127.0.0.1, treat as OpenAI-compatible.
/// OpenAI-compatible endpoints use /chat/completions with Bearer auth.
/// Ollama-native endpoints use /api/chat (local or cloud with Bearer).
fn is_openai_compatible(&self) -> bool {
if self.api_key.is_some() {
let lower = self.base_url.to_lowercase();
// ollama.com always uses native /api/chat (even with api_key)
if lower.contains("ollama.com") {
return false;
}
// Localhost is always native
if lower.contains("localhost") || lower.contains("127.0.0.1") || lower.contains("0.0.0.0") {
return false;
}
// URLs with /v1 or /openai/ in the path are OpenAI-compatible
if lower.contains("/v1") || lower.contains("/openai/") {
return true;
}
// Remote + api_key: probably OpenAI-compatible
// Remote + no api_key: probably native
self.api_key.is_some()
}
/// Normalize the base URL: strip /v1 for ollama.com, upgrade http→https
fn normalized_base_url(&self) -> String {
let lower = self.base_url.to_lowercase();
!lower.contains("localhost") && !lower.contains("127.0.0.1") && !lower.contains("0.0.0.0")
if lower.contains("ollama.com") {
// Strip /v1 suffix if present and upgrade to https
let cleaned = self.base_url
.replace("http://", "https://")
.trim_end_matches('/')
.trim_end_matches("/v1")
.to_string();
return cleaned;
}
self.base_url.trim_end_matches('/').to_string()
}
}
@@ -77,7 +105,7 @@ impl Provider for OllamaProvider {
"stream": false,
});
let url = format!("{}/chat/completions", self.base_url);
let url = format!("{}/chat/completions", self.normalized_base_url());
let mut request = self.client.post(&url).json(&body);
// Add bearer auth if api_key is set
@@ -135,20 +163,24 @@ impl Provider for OllamaProvider {
}
});
let url = format!("{}/api/chat", self.base_url);
let response = self
let url = format!("{}/api/chat", self.normalized_base_url());
let mut request = self
.client
.post(&url)
.json(&body)
.send()
.await
.json(&body);
// Add bearer auth if api_key is set (ollama.com cloud needs this)
if let Some(ref key) = self.api_key {
request = request.header("Authorization", format!("Bearer {}", key));
}
let response = request.send().await
.map_err(|e| gyandoot_core::ProviderError::Request(e))?;
if !response.status().is_success() {
return Err(gyandoot_core::ProviderError::Api {
status: response.status().as_u16(),
message: format!("Ollama API error: {}", response.status()),
});
let status = response.status().as_u16();
let error_text = response.text().await.unwrap_or_default();
return Err(gyandoot_core::ProviderError::from_response(status, &error_text));
}
let data: serde_json::Value = response.json().await.map_err(|e| {
@@ -198,22 +230,32 @@ mod tests {
}
#[test]
fn test_is_openai_compatible_with_api_key() {
fn test_localhost_with_key_is_native() {
let provider = OllamaProvider::new(
"http://localhost:11434".to_string(),
"llama3".to_string(),
Some("test-key".to_string()),
);
assert!(provider.is_openai_compatible());
assert!(!provider.is_openai_compatible());
}
#[test]
fn test_is_openai_compatible_with_remote_url() {
fn test_is_ollama_dot_com_is_native() {
let provider = OllamaProvider::new(
"http://ollama.com/v1".to_string(),
"qwen3.5".to_string(),
None,
);
assert!(!provider.is_openai_compatible());
}
#[test]
fn test_is_openai_compatible_with_openai_url() {
let provider = OllamaProvider::new(
"https://api.deepseek.com/v1".to_string(),
"deepseek-chat".to_string(),
Some("key".to_string()),
);
assert!(provider.is_openai_compatible());
}