fix(config): align ollama cloud defaults with upstream
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user