🐛 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). /// 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 { 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; 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(); 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, "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); let mut request = self.client.post(&url).json(&body);
// Add bearer auth if api_key is set // 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 url = format!("{}/api/chat", self.normalized_base_url());
let response = self let mut request = self
.client .client
.post(&url) .post(&url)
.json(&body) .json(&body);
.send()
.await // 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))?; .map_err(|e| gyandoot_core::ProviderError::Request(e))?;
if !response.status().is_success() { if !response.status().is_success() {
return Err(gyandoot_core::ProviderError::Api { let status = response.status().as_u16();
status: response.status().as_u16(), let error_text = response.text().await.unwrap_or_default();
message: format!("Ollama API error: {}", response.status()), return Err(gyandoot_core::ProviderError::from_response(status, &error_text));
});
} }
let data: serde_json::Value = response.json().await.map_err(|e| { let data: serde_json::Value = response.json().await.map_err(|e| {
@@ -198,22 +230,32 @@ mod tests {
} }
#[test] #[test]
fn test_is_openai_compatible_with_api_key() { fn test_localhost_with_key_is_native() {
let provider = OllamaProvider::new( let provider = OllamaProvider::new(
"http://localhost:11434".to_string(), "http://localhost:11434".to_string(),
"llama3".to_string(), "llama3".to_string(),
Some("test-key".to_string()), Some("test-key".to_string()),
); );
assert!(provider.is_openai_compatible()); assert!(!provider.is_openai_compatible());
} }
#[test] #[test]
fn test_is_openai_compatible_with_remote_url() { fn test_is_ollama_dot_com_is_native() {
let provider = OllamaProvider::new( let provider = OllamaProvider::new(
"http://ollama.com/v1".to_string(), "http://ollama.com/v1".to_string(),
"qwen3.5".to_string(), "qwen3.5".to_string(),
None, 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()); assert!(provider.is_openai_compatible());
} }