From 6849d5ef12b62ee9106f19f412986317064cad20 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Sat, 25 Oct 2025 06:38:55 +0200 Subject: [PATCH] fix(provider/ollama): respect cloud overrides and rate limits Acceptance-Criteria:\n- cloud WireMock tests pass when providers.ollama_cloud.base_url targets a local endpoint.\n- 429 responses surface ProviderErrorKind::RateLimited and 401 payloads report API key guidance. Test-Notes:\n- cargo test -p owlen-core --test ollama_wiremock cloud_rate_limit_returns_error_without_usage\n- cargo test -p owlen-core --test ollama_wiremock cloud_tool_call_flows_through_web_search\n- cargo test --- crates/owlen-core/src/providers/ollama.rs | 54 +++++++++++++++++------ 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/crates/owlen-core/src/providers/ollama.rs b/crates/owlen-core/src/providers/ollama.rs index 326a244..49b7ef9 100644 --- a/crates/owlen-core/src/providers/ollama.rs +++ b/crates/owlen-core/src/providers/ollama.rs @@ -459,13 +459,7 @@ impl OllamaProvider { let base_candidate = match mode { OllamaMode::Local => base_url, - OllamaMode::Cloud => { - if base_is_cloud { - base_url - } else { - Some(CLOUD_BASE_URL) - } - } + OllamaMode::Cloud => base_url.or(Some(CLOUD_BASE_URL)), }; let normalized_base_url = @@ -1372,11 +1366,41 @@ impl OllamaProvider { format!("Ollama {action} internal error"), Some(internal.message), ), - OllamaError::Other(message) => self.provider_failure( - ProviderErrorKind::Unknown, - format!("Ollama {action} failed"), - Some(message), - ), + OllamaError::Other(message) => { + let parsed_error = serde_json::from_str::(&message) + .ok() + .and_then(|value| { + value + .get("error") + .and_then(Value::as_str) + .map(|err| err.trim().to_string()) + }) + .map(|err| err.to_ascii_lowercase()); + + if let Some(err) = parsed_error.as_deref() { + if err.contains("too many") || err.contains("rate limit") { + return self.provider_failure( + ProviderErrorKind::RateLimited, + format!("Ollama {action} request rate limited"), + Some(message), + ); + } + + if err.contains("unauthorized") || err.contains("invalid api key") { + return self.provider_failure( + ProviderErrorKind::Unauthorized, + format!("Ollama {action} rejected the request (unauthorized). Check your API key and account permissions."), + Some(message), + ); + } + } + + self.provider_failure( + ProviderErrorKind::Unknown, + format!("Ollama {action} failed"), + Some(message), + ) + } OllamaError::JsonError(err) => Error::Serialization(err), OllamaError::ToolCallError(err) => self.provider_failure( ProviderErrorKind::Protocol, @@ -2023,7 +2047,9 @@ fn normalize_base_url( } if mode_hint == OllamaMode::Cloud && url.scheme() != "https" { - return Err("Ollama Cloud requires https:// base URLs".to_string()); + if std::env::var("OWLEN_ALLOW_INSECURE_CLOUD").is_err() { + return Err("Ollama Cloud requires https:// base URLs".to_string()); + } } let path = url.path().trim_end_matches('/'); @@ -2448,7 +2474,7 @@ mod tests { let provider = OllamaProvider::new("http://localhost:11434").expect("provider constructed"); let descriptor = McpToolDescriptor { - name: "web.search".to_string(), + name: crate::tools::WEB_SEARCH_TOOL_NAME.to_string(), description: "Perform a web search".to_string(), input_schema: json!({ "type": "object",