From fef7e71387eadf3e3209cdd12acf28259de5902f Mon Sep 17 00:00:00 2001 From: Tara Menon Date: Thu, 11 Jun 2026 22:18:09 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Ollama=20cloud=20detection?= =?UTF-8?q?=20=E2=80=94=20ollama.com=20uses=20native=20/api/chat=20+=20Bea?= =?UTF-8?q?rer=20auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/gyandoot-providers/src/ollama.rs | 74 +++++++++++++++++++------ 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/crates/gyandoot-providers/src/ollama.rs b/crates/gyandoot-providers/src/ollama.rs index a09e9d7..d9d2a9b 100644 --- a/crates/gyandoot-providers/src/ollama.rs +++ b/crates/gyandoot-providers/src/ollama.rs @@ -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()); }