fix(config): align ollama cloud defaults with upstream

This commit is contained in:
2025-10-23 19:25:58 +02:00
parent 38a4c55eaa
commit 3e8788dd44
12 changed files with 219 additions and 59 deletions

View File

@@ -1,11 +1,12 @@
//! Ollama provider built on top of the `ollama-rs` crate.
use std::{
collections::{HashMap, HashSet},
convert::TryFrom,
env,
net::{SocketAddr, TcpStream},
pin::Pin,
process::Command,
sync::Arc,
sync::{Arc, OnceLock},
time::{Duration, Instant, SystemTime},
};
@@ -29,7 +30,7 @@ use serde_json::{Map as JsonMap, Value, json};
use tokio::{sync::RwLock, time::sleep};
#[cfg(test)]
use std::sync::{Mutex, MutexGuard, OnceLock};
use std::sync::{Mutex, MutexGuard};
#[cfg(test)]
use tokio_test::block_on;
use uuid::Uuid;
@@ -37,8 +38,8 @@ use uuid::Uuid;
use crate::{
Error, Result,
config::{
GeneralSettings, LEGACY_OLLEN_OLLAMA_CLOUD_API_KEY_ENV, OLLAMA_CLOUD_BASE_URL,
OLLAMA_CLOUD_ENDPOINT_KEY, OLLAMA_MODE_KEY, OWLEN_OLLAMA_CLOUD_API_KEY_ENV,
GeneralSettings, LEGACY_OLLAMA_CLOUD_API_KEY_ENV, LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV,
OLLAMA_API_KEY_ENV, OLLAMA_CLOUD_BASE_URL, OLLAMA_CLOUD_ENDPOINT_KEY, OLLAMA_MODE_KEY,
},
llm::{LlmProvider, ProviderConfig},
mcp::McpToolDescriptor,
@@ -57,6 +58,17 @@ const LOCAL_TAGS_TIMEOUT_STEPS_MS: [u64; 3] = [400, 800, 1_600];
const LOCAL_TAGS_RETRY_DELAYS_MS: [u64; 2] = [150, 300];
const HEALTHCHECK_TIMEOUT_MS: u64 = 1_000;
static LEGACY_CLOUD_ENV_WARNING: OnceLock<()> = OnceLock::new();
fn warn_legacy_cloud_env(var_name: &str) {
if LEGACY_CLOUD_ENV_WARNING.set(()).is_ok() {
warn!(
"Using legacy Ollama Cloud API key environment variable `{var_name}`. \
Prefer configuring OLLAMA_API_KEY; legacy names remain supported but may be removed."
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum OllamaMode {
Local,
@@ -390,10 +402,15 @@ impl OllamaProvider {
let mut api_key = resolve_api_key(config.api_key.clone())
.or_else(|| resolve_api_key_env_hint(config.api_key_env.as_deref()))
.or_else(|| env_var_non_empty(OWLEN_OLLAMA_CLOUD_API_KEY_ENV))
.or_else(|| env_var_non_empty(LEGACY_OLLEN_OLLAMA_CLOUD_API_KEY_ENV))
.or_else(|| env_var_non_empty("OLLAMA_API_KEY"))
.or_else(|| env_var_non_empty("OLLAMA_CLOUD_API_KEY"));
.or_else(|| env_var_non_empty(OLLAMA_API_KEY_ENV))
.or_else(|| {
warn_legacy_cloud_env(LEGACY_OLLAMA_CLOUD_API_KEY_ENV);
env_var_non_empty(LEGACY_OLLAMA_CLOUD_API_KEY_ENV)
})
.or_else(|| {
warn_legacy_cloud_env(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV);
env_var_non_empty(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV)
});
let api_key_present = api_key.is_some();
let configured_mode = configured_mode_from_extra(config);
@@ -429,7 +446,7 @@ impl OllamaProvider {
if matches!(variant, ProviderVariant::Cloud) {
if !api_key_present {
return Err(Error::Config(
"Ollama Cloud API key not configured. Set providers.ollama_cloud.api_key or OLLAMA_CLOUD_API_KEY."
"Ollama Cloud API key not configured. Set providers.ollama_cloud.api_key or export OLLAMA_API_KEY (legacy: OLLAMA_CLOUD_API_KEY / OWLEN_OLLAMA_CLOUD_API_KEY)."
.into(),
));
}
@@ -1247,12 +1264,20 @@ impl OllamaProvider {
let description = build_model_description(scope_tag, detail.as_ref());
let context_window = detail.as_ref().and_then(|info| {
pick_first_u64(
&info.model_info,
&["context_length", "num_ctx", "max_context"],
)
.and_then(|raw| u32::try_from(raw).ok())
});
ModelInfo {
id: name.clone(),
name,
description: Some(description),
provider: self.provider_name.clone(),
context_window: None,
context_window,
capabilities,
supports_tools: false,
}
@@ -1771,10 +1796,18 @@ fn env_var_non_empty(name: &str) -> Option<String> {
}
fn resolve_api_key_env_hint(env_var: Option<&str>) -> Option<String> {
env_var
.map(str::trim)
.filter(|value| !value.is_empty())
.and_then(env_var_non_empty)
let var = env_var?.trim();
if var.is_empty() {
return None;
}
if var.eq_ignore_ascii_case(LEGACY_OLLAMA_CLOUD_API_KEY_ENV)
|| var.eq_ignore_ascii_case(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV)
{
warn_legacy_cloud_env(var);
}
env_var_non_empty(var)
}
fn resolve_api_key(configured: Option<String>) -> Option<String> {
@@ -1838,6 +1871,15 @@ fn normalize_base_url(
return Err("Ollama base URLs must not include additional path segments".to_string());
}
if mode_hint == OllamaMode::Cloud {
if let Some(host) = url.host_str() {
if host.eq_ignore_ascii_case("api.ollama.com") {
url.set_host(Some("ollama.com"))
.map_err(|err| format!("Failed to normalise Ollama Cloud host: {err}"))?;
}
}
}
url.set_query(None);
url.set_fragment(None);
@@ -1900,6 +1942,7 @@ fn build_client_for_base(
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{Map as JsonMap, Value};
use std::collections::HashMap;
#[test]
@@ -1930,6 +1973,12 @@ mod tests {
assert_eq!(url, "https://ollama.com");
}
#[test]
fn normalize_base_url_canonicalises_api_hostname() {
let url = normalize_base_url(Some("https://api.ollama.com"), OllamaMode::Cloud).unwrap();
assert_eq!(url, "https://ollama.com");
}
#[test]
fn normalize_base_url_rejects_cloud_without_https() {
let err = normalize_base_url(Some("http://ollama.com"), OllamaMode::Cloud).unwrap_err();
@@ -2088,6 +2137,34 @@ mod tests {
assert_eq!(models[0].name, "llama3");
}
#[test]
fn convert_model_propagates_context_window_from_details() {
let provider = OllamaProvider::new("http://localhost:11434").expect("provider constructed");
let local = LocalModel {
name: "gemma3n:e4b".into(),
modified_at: "2024-01-01T00:00:00Z".into(),
size: 0,
};
let mut meta = JsonMap::new();
meta.insert(
"context_length".into(),
Value::Number(serde_json::Number::from(32_768)),
);
let detail = OllamaModelInfo {
license: String::new(),
modelfile: String::new(),
parameters: String::new(),
template: String::new(),
model_info: meta,
capabilities: vec![],
};
let info = provider.convert_model(OllamaMode::Local, local, Some(detail));
assert_eq!(info.context_window, Some(32_768));
}
#[test]
fn fetch_scope_tags_with_retry_retries_on_timeout_then_succeeds() {
let provider = OllamaProvider::new("http://localhost:11434").expect("provider constructed");