fix(config): align ollama cloud defaults with upstream
This commit is contained in:
@@ -15,7 +15,7 @@
|
|||||||
## Release Assumptions
|
## Release Assumptions
|
||||||
|
|
||||||
* Provider config has **separate entries** for local (“ollama”) and cloud (“ollama-cloud”), both optional.
|
* 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 <API_KEY>`.
|
* Cloud requests include `Authorization: Bearer <API_KEY>`.
|
||||||
* Token counts (`prompt_eval_count`, `eval_count`) are read from provider responses **if available**; else fallback to local estimation.
|
* 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.
|
* 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”.
|
* New provider “ollama-cloud”.
|
||||||
* Build `reqwest::Client` with default headers incl. `Authorization: Bearer <API_KEY>`.
|
* Build `reqwest::Client` with default headers incl. `Authorization: Bearer <API_KEY>`.
|
||||||
* 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.
|
* If no key: provider not registered; cloud models hidden.
|
||||||
* **AC:** With key → cloud models list & chat work; without key → no cloud provider listed.
|
* **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).
|
* **Tests:** No key (hidden), invalid key (401 handled), valid key (happy path).
|
||||||
@@ -168,8 +168,8 @@
|
|||||||
|
|
||||||
[providers.ollama_cloud]
|
[providers.ollama_cloud]
|
||||||
enabled = true
|
enabled = true
|
||||||
base_url = "https://api.ollama.com" # [Inference] default
|
base_url = "https://ollama.com" # Hosted default
|
||||||
api_key = "" # read from env OWLEN_OLLAMA_CLOUD_API_KEY if empty
|
api_key = "" # read from env OLLAMA_API_KEY if empty
|
||||||
hourly_quota_tokens = 50000 # configurable; visual only
|
hourly_quota_tokens = 50000 # configurable; visual only
|
||||||
weekly_quota_tokens = 250000 # configurable; visual only
|
weekly_quota_tokens = 250000 # configurable; visual only
|
||||||
list_ttl_secs = 60
|
list_ttl_secs = 60
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
## fix(config): align ollama cloud defaults with api host
|
## fix(config): align ollama cloud defaults with current 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`).
|
- **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`).
|
||||||
- **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.
|
- **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.
|
||||||
- **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.
|
- **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.
|
||||||
- **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(provider/ollama): keep stream whitespace intact
|
## 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).
|
- **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
|
## 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`.
|
- **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.
|
- **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.
|
- **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.
|
- **Test Notes:** Snapshot regeneration output, add config migration tests covering legacy envs, and run `cargo test -p owlen-core config` suite.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ base_url = "http://localhost:11434"
|
|||||||
enabled = false
|
enabled = false
|
||||||
provider_type = "ollama_cloud"
|
provider_type = "ollama_cloud"
|
||||||
base_url = "https://ollama.com"
|
base_url = "https://ollama.com"
|
||||||
api_key_env = "OLLAMA_CLOUD_API_KEY"
|
api_key_env = "OLLAMA_API_KEY"
|
||||||
|
|
||||||
[providers.openai]
|
[providers.openai]
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use clap::Subcommand;
|
|||||||
use owlen_core::LlmProvider;
|
use owlen_core::LlmProvider;
|
||||||
use owlen_core::ProviderConfig;
|
use owlen_core::ProviderConfig;
|
||||||
use owlen_core::config::{
|
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,
|
OLLAMA_CLOUD_ENDPOINT_KEY, OLLAMA_MODE_KEY,
|
||||||
};
|
};
|
||||||
use owlen_core::credentials::{ApiCredentials, CredentialManager, OLLAMA_CLOUD_CREDENTIAL_ID};
|
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()),
|
Value::String(normalized.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if entry.api_key_env.is_none() {
|
let should_update_env = match entry.api_key_env.as_deref() {
|
||||||
entry.api_key_env = Some(OLLAMA_CLOUD_API_KEY_ENV.to_string());
|
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
|
if force
|
||||||
|
|||||||
@@ -23,15 +23,15 @@ pub const OLLAMA_MODE_KEY: &str = "ollama_mode";
|
|||||||
/// Extra config key storing the preferred Ollama Cloud endpoint.
|
/// Extra config key storing the preferred Ollama Cloud endpoint.
|
||||||
pub const OLLAMA_CLOUD_ENDPOINT_KEY: &str = "cloud_endpoint";
|
pub const OLLAMA_CLOUD_ENDPOINT_KEY: &str = "cloud_endpoint";
|
||||||
/// Canonical Ollama Cloud base URL.
|
/// 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).
|
/// 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.
|
/// 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.
|
/// Legacy environment variable accepted for backward compatibility.
|
||||||
pub const LEGACY_OLLEN_OLLAMA_CLOUD_API_KEY_ENV: &str = "OLLEN_OLLAMA_CLOUD_API_KEY";
|
pub const LEGACY_OLLAMA_CLOUD_API_KEY_ENV: &str = "OLLAMA_CLOUD_API_KEY";
|
||||||
/// Legacy environment variable still accepted for Ollama Cloud authentication.
|
/// Legacy environment variable used by earlier Owlen releases.
|
||||||
pub const OLLAMA_CLOUD_API_KEY_ENV: &str = "OLLAMA_CLOUD_API_KEY";
|
pub const LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV: &str = "OWLEN_OLLAMA_CLOUD_API_KEY";
|
||||||
/// Default base URL for local Ollama daemons.
|
/// Default base URL for local Ollama daemons.
|
||||||
pub const OLLAMA_LOCAL_BASE_URL: &str = "http://localhost:11434";
|
pub const OLLAMA_LOCAL_BASE_URL: &str = "http://localhost:11434";
|
||||||
/// Default OpenAI API base URL.
|
/// Default OpenAI API base URL.
|
||||||
@@ -645,6 +645,49 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.providers = migrated;
|
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(
|
fn merge_legacy_ollama_provider(
|
||||||
@@ -687,13 +730,15 @@ impl Config {
|
|||||||
if cloud.base_url.is_none() {
|
if cloud.base_url.is_none() {
|
||||||
cloud.base_url = Some(OLLAMA_CLOUD_BASE_URL.to_string());
|
cloud.base_url = Some(OLLAMA_CLOUD_BASE_URL.to_string());
|
||||||
}
|
}
|
||||||
if cloud.api_key_env.is_none()
|
let update_api_key_env = match cloud.api_key_env.as_deref() {
|
||||||
|| cloud
|
None => true,
|
||||||
.api_key_env
|
Some(value) => {
|
||||||
.as_deref()
|
value.eq_ignore_ascii_case(LEGACY_OLLAMA_CLOUD_API_KEY_ENV)
|
||||||
.is_some_and(|value| value == LEGACY_OLLEN_OLLAMA_CLOUD_API_KEY_ENV)
|
|| value.eq_ignore_ascii_case(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV)
|
||||||
{
|
}
|
||||||
cloud.api_key_env = Some(OWLEN_OLLAMA_CLOUD_API_KEY_ENV.to_string());
|
};
|
||||||
|
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"),
|
provider_type: canonical_provider_type("ollama_cloud"),
|
||||||
base_url: Some(OLLAMA_CLOUD_BASE_URL.to_string()),
|
base_url: Some(OLLAMA_CLOUD_BASE_URL.to_string()),
|
||||||
api_key: None,
|
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,
|
extra,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2013,7 +2058,7 @@ mod tests {
|
|||||||
.get("ollama_cloud")
|
.get("ollama_cloud")
|
||||||
.expect("default cloud provider");
|
.expect("default cloud provider");
|
||||||
assert!(!cloud.enabled);
|
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]
|
#[test]
|
||||||
@@ -2023,7 +2068,7 @@ mod tests {
|
|||||||
let cloud = ensure_provider_config(&mut config, "ollama-cloud");
|
let cloud = ensure_provider_config(&mut config, "ollama-cloud");
|
||||||
assert_eq!(cloud.provider_type, "ollama_cloud");
|
assert_eq!(cloud.provider_type, "ollama_cloud");
|
||||||
assert_eq!(cloud.base_url.as_deref(), Some(OLLAMA_CLOUD_BASE_URL));
|
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"));
|
||||||
assert!(!config.providers.contains_key("ollama-cloud"));
|
assert!(!config.providers.contains_key("ollama-cloud"));
|
||||||
}
|
}
|
||||||
@@ -2072,7 +2117,7 @@ mod tests {
|
|||||||
.expect("cloud provider created");
|
.expect("cloud provider created");
|
||||||
assert!(cloud.enabled);
|
assert!(cloud.enabled);
|
||||||
assert_eq!(cloud.base_url.as_deref(), Some(OLLAMA_CLOUD_BASE_URL));
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
//! Ollama provider built on top of the `ollama-rs` crate.
|
//! Ollama provider built on top of the `ollama-rs` crate.
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
|
convert::TryFrom,
|
||||||
env,
|
env,
|
||||||
net::{SocketAddr, TcpStream},
|
net::{SocketAddr, TcpStream},
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
process::Command,
|
process::Command,
|
||||||
sync::Arc,
|
sync::{Arc, OnceLock},
|
||||||
time::{Duration, Instant, SystemTime},
|
time::{Duration, Instant, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ use serde_json::{Map as JsonMap, Value, json};
|
|||||||
use tokio::{sync::RwLock, time::sleep};
|
use tokio::{sync::RwLock, time::sleep};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use std::sync::{Mutex, MutexGuard, OnceLock};
|
use std::sync::{Mutex, MutexGuard};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use tokio_test::block_on;
|
use tokio_test::block_on;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -37,8 +38,8 @@ use uuid::Uuid;
|
|||||||
use crate::{
|
use crate::{
|
||||||
Error, Result,
|
Error, Result,
|
||||||
config::{
|
config::{
|
||||||
GeneralSettings, LEGACY_OLLEN_OLLAMA_CLOUD_API_KEY_ENV, OLLAMA_CLOUD_BASE_URL,
|
GeneralSettings, LEGACY_OLLAMA_CLOUD_API_KEY_ENV, LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV,
|
||||||
OLLAMA_CLOUD_ENDPOINT_KEY, OLLAMA_MODE_KEY, OWLEN_OLLAMA_CLOUD_API_KEY_ENV,
|
OLLAMA_API_KEY_ENV, OLLAMA_CLOUD_BASE_URL, OLLAMA_CLOUD_ENDPOINT_KEY, OLLAMA_MODE_KEY,
|
||||||
},
|
},
|
||||||
llm::{LlmProvider, ProviderConfig},
|
llm::{LlmProvider, ProviderConfig},
|
||||||
mcp::McpToolDescriptor,
|
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 LOCAL_TAGS_RETRY_DELAYS_MS: [u64; 2] = [150, 300];
|
||||||
const HEALTHCHECK_TIMEOUT_MS: u64 = 1_000;
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
enum OllamaMode {
|
enum OllamaMode {
|
||||||
Local,
|
Local,
|
||||||
@@ -390,10 +402,15 @@ impl OllamaProvider {
|
|||||||
|
|
||||||
let mut api_key = resolve_api_key(config.api_key.clone())
|
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(|| 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(OLLAMA_API_KEY_ENV))
|
||||||
.or_else(|| env_var_non_empty(LEGACY_OLLEN_OLLAMA_CLOUD_API_KEY_ENV))
|
.or_else(|| {
|
||||||
.or_else(|| env_var_non_empty("OLLAMA_API_KEY"))
|
warn_legacy_cloud_env(LEGACY_OLLAMA_CLOUD_API_KEY_ENV);
|
||||||
.or_else(|| env_var_non_empty("OLLAMA_CLOUD_API_KEY"));
|
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 api_key_present = api_key.is_some();
|
||||||
|
|
||||||
let configured_mode = configured_mode_from_extra(config);
|
let configured_mode = configured_mode_from_extra(config);
|
||||||
@@ -429,7 +446,7 @@ impl OllamaProvider {
|
|||||||
if matches!(variant, ProviderVariant::Cloud) {
|
if matches!(variant, ProviderVariant::Cloud) {
|
||||||
if !api_key_present {
|
if !api_key_present {
|
||||||
return Err(Error::Config(
|
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(),
|
.into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -1247,12 +1264,20 @@ impl OllamaProvider {
|
|||||||
|
|
||||||
let description = build_model_description(scope_tag, detail.as_ref());
|
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 {
|
ModelInfo {
|
||||||
id: name.clone(),
|
id: name.clone(),
|
||||||
name,
|
name,
|
||||||
description: Some(description),
|
description: Some(description),
|
||||||
provider: self.provider_name.clone(),
|
provider: self.provider_name.clone(),
|
||||||
context_window: None,
|
context_window,
|
||||||
capabilities,
|
capabilities,
|
||||||
supports_tools: false,
|
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> {
|
fn resolve_api_key_env_hint(env_var: Option<&str>) -> Option<String> {
|
||||||
env_var
|
let var = env_var?.trim();
|
||||||
.map(str::trim)
|
if var.is_empty() {
|
||||||
.filter(|value| !value.is_empty())
|
return None;
|
||||||
.and_then(env_var_non_empty)
|
}
|
||||||
|
|
||||||
|
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> {
|
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());
|
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_query(None);
|
||||||
url.set_fragment(None);
|
url.set_fragment(None);
|
||||||
|
|
||||||
@@ -1900,6 +1942,7 @@ fn build_client_for_base(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use serde_json::{Map as JsonMap, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1930,6 +1973,12 @@ mod tests {
|
|||||||
assert_eq!(url, "https://ollama.com");
|
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]
|
#[test]
|
||||||
fn normalize_base_url_rejects_cloud_without_https() {
|
fn normalize_base_url_rejects_cloud_without_https() {
|
||||||
let err = normalize_base_url(Some("http://ollama.com"), OllamaMode::Cloud).unwrap_err();
|
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");
|
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]
|
#[test]
|
||||||
fn fetch_scope_tags_with_retry_retries_on_timeout_then_succeeds() {
|
fn fetch_scope_tags_with_retry_retries_on_timeout_then_succeeds() {
|
||||||
let provider = OllamaProvider::new("http://localhost:11434").expect("provider constructed");
|
let provider = OllamaProvider::new("http://localhost:11434").expect("provider constructed");
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ serde_json = { workspace = true }
|
|||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tokio-stream = { workspace = true }
|
tokio-stream = { workspace = true }
|
||||||
reqwest = { package = "reqwest", version = "0.11", features = ["json", "stream"] }
|
reqwest = { package = "reqwest", version = "0.11", features = ["json", "stream"] }
|
||||||
|
log = { workspace = true }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::{env, time::Duration};
|
use std::{env, time::Duration};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use log::warn;
|
||||||
use owlen_core::{
|
use owlen_core::{
|
||||||
Error as CoreError, Result as CoreResult,
|
Error as CoreError, Result as CoreResult,
|
||||||
config::OLLAMA_CLOUD_BASE_URL,
|
config::OLLAMA_CLOUD_BASE_URL,
|
||||||
@@ -13,7 +14,9 @@ use serde_json::{Number, Value};
|
|||||||
|
|
||||||
use super::OllamaClient;
|
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.
|
/// ModelProvider implementation for the hosted Ollama Cloud service.
|
||||||
pub struct OllamaCloudProvider {
|
pub struct OllamaCloudProvider {
|
||||||
@@ -22,7 +25,7 @@ pub struct OllamaCloudProvider {
|
|||||||
|
|
||||||
impl OllamaCloudProvider {
|
impl OllamaCloudProvider {
|
||||||
/// Construct a new cloud provider. An API key must be supplied either
|
/// 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(
|
pub fn new(
|
||||||
base_url: Option<String>,
|
base_url: Option<String>,
|
||||||
api_key: Option<String>,
|
api_key: Option<String>,
|
||||||
@@ -95,14 +98,34 @@ fn resolve_api_key(api_key: Option<String>) -> CoreResult<(String, &'static str)
|
|||||||
let key_from_env = env::var(API_KEY_ENV)
|
let key_from_env = env::var(API_KEY_ENV)
|
||||||
.ok()
|
.ok()
|
||||||
.map(|value| value.trim().to_string())
|
.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"));
|
return Ok((key, "env"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(CoreError::Config(
|
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(),
|
.into(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ use crate::ui::format_tool_output;
|
|||||||
use crate::widgets::model_picker::FilterMode;
|
use crate::widgets::model_picker::FilterMode;
|
||||||
use crate::{commands, highlight};
|
use crate::{commands, highlight};
|
||||||
use owlen_core::config::{
|
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};
|
use owlen_core::credentials::{ApiCredentials, OLLAMA_CLOUD_CREDENTIAL_ID};
|
||||||
// Agent executor moved to separate binary `owlen-agent`. The TUI no longer directly
|
// 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();
|
let existing = entry.api_key.clone();
|
||||||
entry.enabled = true;
|
entry.enabled = true;
|
||||||
entry.provider_type = "ollama_cloud".to_string();
|
entry.provider_type = "ollama_cloud".to_string();
|
||||||
if entry.api_key_env.is_none() {
|
let should_update_env = match entry.api_key_env.as_deref() {
|
||||||
entry.api_key_env = Some(OLLAMA_CLOUD_API_KEY_ENV.to_string());
|
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
|
let requested = options
|
||||||
.endpoint
|
.endpoint
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ base_url = "http://localhost:11434"
|
|||||||
enabled = false
|
enabled = false
|
||||||
provider_type = "ollama_cloud"
|
provider_type = "ollama_cloud"
|
||||||
base_url = "https://ollama.com"
|
base_url = "https://ollama.com"
|
||||||
api_key_env = "OLLAMA_CLOUD_API_KEY"
|
api_key_env = "OLLAMA_API_KEY"
|
||||||
|
|
||||||
[providers.openai]
|
[providers.openai]
|
||||||
enabled = false
|
enabled = false
|
||||||
@@ -176,10 +176,10 @@ base_url = "http://localhost:11434"
|
|||||||
[providers.ollama_cloud]
|
[providers.ollama_cloud]
|
||||||
enabled = true
|
enabled = true
|
||||||
base_url = "https://ollama.com"
|
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.
|
> **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.
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ base_url = "http://localhost:11434"
|
|||||||
enabled = true # set to false if you do not use the hosted API
|
enabled = true # set to false if you do not use the hosted API
|
||||||
provider_type = "ollama_cloud"
|
provider_type = "ollama_cloud"
|
||||||
base_url = "https://ollama.com"
|
base_url = "https://ollama.com"
|
||||||
api_key_env = "OLLAMA_CLOUD_API_KEY"
|
api_key_env = "OLLAMA_API_KEY"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Step 3: Understanding MCP Server Configuration
|
#### Step 3: Understanding MCP Server Configuration
|
||||||
|
|||||||
@@ -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).
|
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).
|
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.
|
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.
|
5. Avoid pasting extra quotes or whitespace into the config file—`owlen config doctor` will normalise the entry for you.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user