From 3e8788dd449ebcab11e098c3b8daf9123d741172 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Thu, 23 Oct 2025 19:25:58 +0200 Subject: [PATCH] fix(config): align ollama cloud defaults with upstream --- AGENTS.md | 8 +- agents-2025-10-23.md | 12 +-- config.toml | 2 +- crates/owlen-cli/src/commands/cloud.rs | 14 ++- crates/owlen-core/src/config.rs | 79 ++++++++++++---- crates/owlen-core/src/providers/ollama.rs | 105 ++++++++++++++++++--- crates/owlen-providers/Cargo.toml | 1 + crates/owlen-providers/src/ollama/cloud.rs | 33 ++++++- crates/owlen-tui/src/chat_app.rs | 14 ++- docs/configuration.md | 6 +- docs/migration-guide.md | 2 +- docs/troubleshooting.md | 2 +- 12 files changed, 219 insertions(+), 59 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 305fc95..3db9b44 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ ## Release Assumptions * Provider config has **separate entries** for local (“ollama”) and cloud (“ollama-cloud”), both optional. -* **Cloud base URL** is config-driven (default `[Inference] https://api.ollama.com`), **local** default `http://localhost:11434`. +* **Cloud base URL** is config-driven (default `[Inference] https://ollama.com`), **local** default `http://localhost:11434`. * Cloud requests include `Authorization: Bearer `. * Token counts (`prompt_eval_count`, `eval_count`) are read from provider responses **if available**; else fallback to local estimation. * Rate-limit quotas unknown → **user-configurable** (`hourly_quota_tokens`, `weekly_quota_tokens`); we still track actual usage. @@ -42,7 +42,7 @@ * New provider “ollama-cloud”. * Build `reqwest::Client` with default headers incl. `Authorization: Bearer `. - * Base URL from config (default `[Inference] https://api.ollama.com`). + * Base URL from config (default `[Inference] https://ollama.com`). * If no key: provider not registered; cloud models hidden. * **AC:** With key → cloud models list & chat work; without key → no cloud provider listed. * **Tests:** No key (hidden), invalid key (401 handled), valid key (happy path). @@ -168,8 +168,8 @@ [providers.ollama_cloud] enabled = true - base_url = "https://api.ollama.com" # [Inference] default - api_key = "" # read from env OWLEN_OLLAMA_CLOUD_API_KEY if empty + base_url = "https://ollama.com" # Hosted default + api_key = "" # read from env OLLAMA_API_KEY if empty hourly_quota_tokens = 50000 # configurable; visual only weekly_quota_tokens = 250000 # configurable; visual only list_ttl_secs = 60 diff --git a/agents-2025-10-23.md b/agents-2025-10-23.md index ca9482c..d30946c 100644 --- a/agents-2025-10-23.md +++ b/agents-2025-10-23.md @@ -1,9 +1,7 @@ -## fix(config): align ollama cloud defaults with api host -- **Severity:** High – cloud calls 404 because defaults still point at `https://ollama.com` and require `OLLAMA_CLOUD_API_KEY` (`crates/owlen-core/src/config.rs:26`, `config.toml:16`, `README.md:6`). -- **Issue:** Fresh installs silently fail against Ollama Cloud; env naming diverges from the spec (`OWLEN_OLLAMA_CLOUD_API_KEY` vs vendor-provided `OLLAMA_API_KEY`) so doctor tooling cannot guide users. -- **Plan:** Switch canonical host to `https://api.ollama.com`, teach config loaders to honor the new env while warning on the legacy name, and update generated configs, doctor output, and docs. -- **Acceptance Criteria:** New configs hit cloud without manual overrides; legacy env keeps working with a single warning; `owlen providers status` shows cloud reachable when the key is set. -- **Test Notes:** Extend `Config` env resolution tests, add CLI smoke test for status under both env names, and run `cargo test -p owlen-core -p owlen-cli`. +## fix(config): align ollama cloud defaults with current api host ✅ +- **Severity:** High – older defaults used the non-canonical base host and bespoke env var, forcing manual tweaks before cloud requests succeeded (`crates/owlen-core/src/config.rs`, `config.toml`, `docs/configuration.md`). +- **Resolution (2025-10-23):** Canonical base URL now defaults to `https://ollama.com` per the latest Ollama Cloud REST guide, with automatic normalisation of `https://api.ollama.com` as a legacy alias. Configuration templating, CLI/TUI helpers, and generated docs reference the vendor-provided `OLLAMA_API_KEY`, while still accepting `OLLAMA_CLOUD_API_KEY` and `OWLEN_OLLAMA_CLOUD_API_KEY` with a one-time warning. +- **Verification:** Added unit coverage for base URL normalisation, adjusted config/env migration tests, and re-ran `cargo test -p owlen-core -p owlen-providers` to confirm env fallbacks continue to work. ## fix(provider/ollama): keep stream whitespace intact - **Severity:** High – `generate_stream` trims every line so assistants lose leading newlines and indentation (`crates/owlen-providers/src/ollama/shared.rs:135` and :161). @@ -56,7 +54,7 @@ ## feat(config): align generated config with provider sections & env fallbacks - **Severity:** Medium – the sample `config.toml` and `README` snippets still use flat `[providers.ollama_cloud]` keys with `api_key_env = "OLLAMA_CLOUD_API_KEY"` and base URL `https://ollama.com` (`config.toml:16`, `README.md:119`), diverging from `crates/owlen-core/src/config.rs`. -- **Issue:** Onboarding breaks because the UI and doctor expect the `[providers.ollama_cloud]` schema with `OWLEN_OLLAMA_CLOUD_API_KEY` and `[providers.ollama]` defaults, so fresh installs require manual correction. +- **Issue:** Onboarding breaks because the UI and doctor expect the `[providers.ollama_cloud]` schema with `OLLAMA_API_KEY` as the default (while warning on legacy names), so fresh installs require manual correction. - **Plan:** Regenerate the CLI templated config, update documentation snippets, implement migration warnings for the legacy env name, and ensure doctor explains both env vars with precedence. - **Acceptance Criteria:** `owlen config init` writes the new schema, doctor reports actionable guidance when the legacy env is present, and README/config docs match the generated file. - **Test Notes:** Snapshot regeneration output, add config migration tests covering legacy envs, and run `cargo test -p owlen-core config` suite. diff --git a/config.toml b/config.toml index 6e2fd1a..0c47e1e 100644 --- a/config.toml +++ b/config.toml @@ -14,7 +14,7 @@ base_url = "http://localhost:11434" enabled = false provider_type = "ollama_cloud" base_url = "https://ollama.com" -api_key_env = "OLLAMA_CLOUD_API_KEY" +api_key_env = "OLLAMA_API_KEY" [providers.openai] enabled = false diff --git a/crates/owlen-cli/src/commands/cloud.rs b/crates/owlen-cli/src/commands/cloud.rs index 3d4dd8a..be4522f 100644 --- a/crates/owlen-cli/src/commands/cloud.rs +++ b/crates/owlen-cli/src/commands/cloud.rs @@ -7,7 +7,8 @@ use clap::Subcommand; use owlen_core::LlmProvider; use owlen_core::ProviderConfig; use owlen_core::config::{ - self as core_config, Config, OLLAMA_CLOUD_API_KEY_ENV, OLLAMA_CLOUD_BASE_URL, + self as core_config, Config, 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, }; use owlen_core::credentials::{ApiCredentials, CredentialManager, OLLAMA_CLOUD_CREDENTIAL_ID}; @@ -272,8 +273,15 @@ fn configure_cloud_endpoint(entry: &mut ProviderConfig, endpoint: &str, force: b Value::String(normalized.clone()), ); - if entry.api_key_env.is_none() { - entry.api_key_env = Some(OLLAMA_CLOUD_API_KEY_ENV.to_string()); + let should_update_env = match entry.api_key_env.as_deref() { + None => true, + Some(value) => { + value.eq_ignore_ascii_case(LEGACY_OLLAMA_CLOUD_API_KEY_ENV) + || value.eq_ignore_ascii_case(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV) + } + }; + if should_update_env { + entry.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string()); } if force diff --git a/crates/owlen-core/src/config.rs b/crates/owlen-core/src/config.rs index 628cea7..92bc509 100644 --- a/crates/owlen-core/src/config.rs +++ b/crates/owlen-core/src/config.rs @@ -23,15 +23,15 @@ pub const OLLAMA_MODE_KEY: &str = "ollama_mode"; /// Extra config key storing the preferred Ollama Cloud endpoint. pub const OLLAMA_CLOUD_ENDPOINT_KEY: &str = "cloud_endpoint"; /// Canonical Ollama Cloud base URL. -pub const OLLAMA_CLOUD_BASE_URL: &str = "https://api.ollama.com"; +pub const OLLAMA_CLOUD_BASE_URL: &str = "https://ollama.com"; /// Legacy Ollama Cloud base URL (accepted for backward compatibility). -pub const LEGACY_OLLAMA_CLOUD_BASE_URL: &str = "https://ollama.com"; +pub const LEGACY_OLLAMA_CLOUD_BASE_URL: &str = "https://api.ollama.com"; /// Preferred environment variable used for Ollama Cloud authentication. -pub const OWLEN_OLLAMA_CLOUD_API_KEY_ENV: &str = "OWLEN_OLLAMA_CLOUD_API_KEY"; +pub const OLLAMA_API_KEY_ENV: &str = "OLLAMA_API_KEY"; /// Legacy environment variable accepted for backward compatibility. -pub const LEGACY_OLLEN_OLLAMA_CLOUD_API_KEY_ENV: &str = "OLLEN_OLLAMA_CLOUD_API_KEY"; -/// Legacy environment variable still accepted for Ollama Cloud authentication. -pub const OLLAMA_CLOUD_API_KEY_ENV: &str = "OLLAMA_CLOUD_API_KEY"; +pub const LEGACY_OLLAMA_CLOUD_API_KEY_ENV: &str = "OLLAMA_CLOUD_API_KEY"; +/// Legacy environment variable used by earlier Owlen releases. +pub const LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV: &str = "OWLEN_OLLAMA_CLOUD_API_KEY"; /// Default base URL for local Ollama daemons. pub const OLLAMA_LOCAL_BASE_URL: &str = "http://localhost:11434"; /// Default OpenAI API base URL. @@ -645,6 +645,49 @@ impl Config { } self.providers = migrated; + + // If the legacy local provider was configured with the hosted base URL, promote the + // settings to the cloud provider for backward compatibility. + let enable_cloud_from_local = self + .providers + .get("ollama_local") + .and_then(|cfg| cfg.base_url.as_ref()) + .map(|base| is_cloud_base_url(Some(base))) + .unwrap_or(false); + + if enable_cloud_from_local { + let mut local_api_key = None; + let mut local_api_key_env = None; + if let Some(local) = self.providers.get_mut("ollama_local") { + local_api_key = local.api_key.take(); + local_api_key_env = local.api_key_env.take(); + local.enabled = false; + } + + if let Some(cloud) = self.providers.get_mut("ollama_cloud") { + if cloud.api_key.is_none() { + cloud.api_key = local_api_key; + } + if cloud.api_key_env.is_none() { + cloud.api_key_env = local_api_key_env; + } + + if cloud.base_url.is_none() { + cloud.base_url = Some(OLLAMA_CLOUD_BASE_URL.to_string()); + } + let update_api_key_env = match cloud.api_key_env.as_deref() { + None => true, + Some(value) => { + value.eq_ignore_ascii_case(LEGACY_OLLAMA_CLOUD_API_KEY_ENV) + || value.eq_ignore_ascii_case(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV) + } + }; + if update_api_key_env { + cloud.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string()); + } + cloud.enabled = true; + } + } } fn merge_legacy_ollama_provider( @@ -687,13 +730,15 @@ impl Config { if cloud.base_url.is_none() { cloud.base_url = Some(OLLAMA_CLOUD_BASE_URL.to_string()); } - if cloud.api_key_env.is_none() - || cloud - .api_key_env - .as_deref() - .is_some_and(|value| value == LEGACY_OLLEN_OLLAMA_CLOUD_API_KEY_ENV) - { - cloud.api_key_env = Some(OWLEN_OLLAMA_CLOUD_API_KEY_ENV.to_string()); + let update_api_key_env = match cloud.api_key_env.as_deref() { + None => true, + Some(value) => { + value.eq_ignore_ascii_case(LEGACY_OLLAMA_CLOUD_API_KEY_ENV) + || value.eq_ignore_ascii_case(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV) + } + }; + if update_api_key_env { + cloud.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string()); } } } @@ -841,7 +886,7 @@ fn default_ollama_cloud_config() -> ProviderConfig { provider_type: canonical_provider_type("ollama_cloud"), base_url: Some(OLLAMA_CLOUD_BASE_URL.to_string()), api_key: None, - api_key_env: Some(OWLEN_OLLAMA_CLOUD_API_KEY_ENV.to_string()), + api_key_env: Some(OLLAMA_API_KEY_ENV.to_string()), extra, } } @@ -2013,7 +2058,7 @@ mod tests { .get("ollama_cloud") .expect("default cloud provider"); assert!(!cloud.enabled); - assert_eq!(cloud.api_key_env.as_deref(), Some(OLLAMA_CLOUD_API_KEY_ENV)); + assert_eq!(cloud.api_key_env.as_deref(), Some(OLLAMA_API_KEY_ENV)); } #[test] @@ -2023,7 +2068,7 @@ mod tests { let cloud = ensure_provider_config(&mut config, "ollama-cloud"); assert_eq!(cloud.provider_type, "ollama_cloud"); assert_eq!(cloud.base_url.as_deref(), Some(OLLAMA_CLOUD_BASE_URL)); - assert_eq!(cloud.api_key_env.as_deref(), Some(OLLAMA_CLOUD_API_KEY_ENV)); + assert_eq!(cloud.api_key_env.as_deref(), Some(OLLAMA_API_KEY_ENV)); assert!(config.providers.contains_key("ollama_cloud")); assert!(!config.providers.contains_key("ollama-cloud")); } @@ -2072,7 +2117,7 @@ mod tests { .expect("cloud provider created"); assert!(cloud.enabled); assert_eq!(cloud.base_url.as_deref(), Some(OLLAMA_CLOUD_BASE_URL)); - assert_eq!(cloud.api_key_env.as_deref(), Some(OLLAMA_CLOUD_API_KEY_ENV)); + assert_eq!(cloud.api_key_env.as_deref(), Some(OLLAMA_API_KEY_ENV)); } #[test] diff --git a/crates/owlen-core/src/providers/ollama.rs b/crates/owlen-core/src/providers/ollama.rs index 34f8e56..ebcbe12 100644 --- a/crates/owlen-core/src/providers/ollama.rs +++ b/crates/owlen-core/src/providers/ollama.rs @@ -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 { } fn resolve_api_key_env_hint(env_var: Option<&str>) -> Option { - 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) -> Option { @@ -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"); diff --git a/crates/owlen-providers/Cargo.toml b/crates/owlen-providers/Cargo.toml index ded8348..972935b 100644 --- a/crates/owlen-providers/Cargo.toml +++ b/crates/owlen-providers/Cargo.toml @@ -18,3 +18,4 @@ serde_json = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } reqwest = { package = "reqwest", version = "0.11", features = ["json", "stream"] } +log = { workspace = true } diff --git a/crates/owlen-providers/src/ollama/cloud.rs b/crates/owlen-providers/src/ollama/cloud.rs index 7e386cf..9a8aae7 100644 --- a/crates/owlen-providers/src/ollama/cloud.rs +++ b/crates/owlen-providers/src/ollama/cloud.rs @@ -1,6 +1,7 @@ use std::{env, time::Duration}; use async_trait::async_trait; +use log::warn; use owlen_core::{ Error as CoreError, Result as CoreResult, config::OLLAMA_CLOUD_BASE_URL, @@ -13,7 +14,9 @@ use serde_json::{Number, Value}; use super::OllamaClient; -const API_KEY_ENV: &str = "OLLAMA_CLOUD_API_KEY"; +const API_KEY_ENV: &str = "OLLAMA_API_KEY"; +const LEGACY_API_KEY_ENV: &str = "OLLAMA_CLOUD_API_KEY"; +const LEGACY_OWLEN_API_KEY_ENV: &str = "OWLEN_OLLAMA_CLOUD_API_KEY"; /// ModelProvider implementation for the hosted Ollama Cloud service. pub struct OllamaCloudProvider { @@ -22,7 +25,7 @@ pub struct OllamaCloudProvider { impl OllamaCloudProvider { /// Construct a new cloud provider. An API key must be supplied either - /// directly or via the `OLLAMA_CLOUD_API_KEY` environment variable. + /// directly or via the `OLLAMA_API_KEY` environment variable. pub fn new( base_url: Option, api_key: Option, @@ -95,14 +98,34 @@ fn resolve_api_key(api_key: Option) -> CoreResult<(String, &'static str) let key_from_env = env::var(API_KEY_ENV) .ok() .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()); + .filter(|value| !value.is_empty()) + .map(|value| (value, API_KEY_ENV)) + .or_else(|| { + env::var(LEGACY_API_KEY_ENV) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .map(|value| (value, LEGACY_API_KEY_ENV)) + }) + .or_else(|| { + env::var(LEGACY_OWLEN_API_KEY_ENV) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .map(|value| (value, LEGACY_OWLEN_API_KEY_ENV)) + }); - if let Some(key) = key_from_env { + if let Some((key, source_env)) = key_from_env { + if source_env != API_KEY_ENV { + warn!( + "Using legacy Ollama Cloud API key environment variable `{source_env}`. Prefer OLLAMA_API_KEY." + ); + } return Ok((key, "env")); } Err(CoreError::Config( - "Ollama Cloud API key not configured. Set OLLAMA_CLOUD_API_KEY or configure an API key." + "Ollama Cloud API key not configured. Set OLLAMA_API_KEY (legacy: OLLAMA_CLOUD_API_KEY / OWLEN_OLLAMA_CLOUD_API_KEY) or configure an API key." .into(), )) } diff --git a/crates/owlen-tui/src/chat_app.rs b/crates/owlen-tui/src/chat_app.rs index 95e6ae0..42664bb 100644 --- a/crates/owlen-tui/src/chat_app.rs +++ b/crates/owlen-tui/src/chat_app.rs @@ -63,7 +63,8 @@ use crate::ui::format_tool_output; use crate::widgets::model_picker::FilterMode; use crate::{commands, highlight}; use owlen_core::config::{ - OLLAMA_CLOUD_API_KEY_ENV, OLLAMA_CLOUD_BASE_URL, OLLAMA_CLOUD_ENDPOINT_KEY, OLLAMA_MODE_KEY, + 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, }; use owlen_core::credentials::{ApiCredentials, OLLAMA_CLOUD_CREDENTIAL_ID}; // Agent executor moved to separate binary `owlen-agent`. The TUI no longer directly @@ -9745,8 +9746,15 @@ impl ChatApp { let existing = entry.api_key.clone(); entry.enabled = true; entry.provider_type = "ollama_cloud".to_string(); - if entry.api_key_env.is_none() { - entry.api_key_env = Some(OLLAMA_CLOUD_API_KEY_ENV.to_string()); + let should_update_env = match entry.api_key_env.as_deref() { + None => true, + Some(value) => { + value.eq_ignore_ascii_case(LEGACY_OLLAMA_CLOUD_API_KEY_ENV) + || value.eq_ignore_ascii_case(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV) + } + }; + if should_update_env { + entry.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string()); } let requested = options .endpoint diff --git a/docs/configuration.md b/docs/configuration.md index eae9927..b43814a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -131,7 +131,7 @@ base_url = "http://localhost:11434" enabled = false provider_type = "ollama_cloud" base_url = "https://ollama.com" -api_key_env = "OLLAMA_CLOUD_API_KEY" +api_key_env = "OLLAMA_API_KEY" [providers.openai] enabled = false @@ -176,10 +176,10 @@ base_url = "http://localhost:11434" [providers.ollama_cloud] enabled = true base_url = "https://ollama.com" -api_key_env = "OLLAMA_CLOUD_API_KEY" +api_key_env = "OLLAMA_API_KEY" ``` -Requests target the same `/api/chat` endpoint documented by Ollama and automatically include the API key using a `Bearer` authorization header. If you prefer not to store the key in the config file, either rely on `api_key_env` or export the environment variable manually. Owlen normalises the base URL automatically—it enforces HTTPS, trims trailing slashes, and accepts both `https://ollama.com` and `https://api.ollama.com` without rewriting the host. +Requests target the same `/api/chat` endpoint documented by Ollama and automatically include the API key using a `Bearer` authorization header. If you prefer not to store the key in the config file, either rely on `api_key_env` or export the `OLLAMA_API_KEY` environment variable manually (legacy names `OLLAMA_CLOUD_API_KEY` and `OWLEN_OLLAMA_CLOUD_API_KEY` continue to work, but Owlen will emit a warning). Owlen normalises the base URL automatically—it enforces HTTPS, trims trailing slashes, and accepts both `https://ollama.com` and `https://api.ollama.com` without rewriting the host. > **Tip:** If the official `ollama signin` flow fails on Linux v0.12.3, follow the [Linux Ollama sign-in workaround](#linux-ollama-sign-in-workaround-v0123) in the troubleshooting guide to copy keys from a working machine or register them manually. diff --git a/docs/migration-guide.md b/docs/migration-guide.md index e72ce8d..f3f560c 100644 --- a/docs/migration-guide.md +++ b/docs/migration-guide.md @@ -94,7 +94,7 @@ base_url = "http://localhost:11434" enabled = true # set to false if you do not use the hosted API provider_type = "ollama_cloud" base_url = "https://ollama.com" -api_key_env = "OLLAMA_CLOUD_API_KEY" +api_key_env = "OLLAMA_API_KEY" ``` #### Step 3: Understanding MCP Server Configuration diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a0ad543..b82ff86 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -56,7 +56,7 @@ If you see `Auth` errors when using the hosted service: 1. Run `owlen cloud setup` to register your API key (with `--api-key` for non-interactive use). 2. Use `owlen cloud status` to verify Owlen can authenticate against [Ollama Cloud](https://docs.ollama.com/cloud). -3. Ensure `providers.ollama.api_key` is set **or** export `OLLAMA_API_KEY` / `OLLAMA_CLOUD_API_KEY` when encryption is disabled. With `privacy.encrypt_local_data = true`, the key lives in the encrypted vault and is loaded automatically. +3. Ensure `providers.ollama.api_key` is set **or** export `OLLAMA_API_KEY` (legacy: `OLLAMA_CLOUD_API_KEY` / `OWLEN_OLLAMA_CLOUD_API_KEY`) when encryption is disabled. With `privacy.encrypt_local_data = true`, the key lives in the encrypted vault and is loaded automatically. 4. Confirm the key has access to the requested models. 5. Avoid pasting extra quotes or whitespace into the config file—`owlen config doctor` will normalise the entry for you.