feat(config): align defaults with provider sections

AC:\n- Config defaults include provider TTL/context extras and normalize cloud quotas/endpoints when missing.\n- owlen config init scaffolds the latest schema; config doctor updates legacy env names and issues warnings.\n- Documentation covers init/doctor usage and runtime env precedence.\n\nTests:\n- cargo test -p owlen-cli\n- cargo test -p owlen-core default_config_sets_provider_extras\n- cargo test -p owlen-core ensure_defaults_backfills_missing_provider_metadata
This commit is contained in:
2025-10-24 13:51:15 +02:00
parent e813736b47
commit 25628d1d58
5 changed files with 551 additions and 10 deletions

View File

@@ -155,7 +155,7 @@ OWLEN stores its configuration in the standard platform-specific config director
| macOS | `~/Library/Application Support/owlen/config.toml` | | macOS | `~/Library/Application Support/owlen/config.toml` |
| Windows | `%APPDATA%\owlen\config.toml` | | Windows | `%APPDATA%\owlen\config.toml` |
Use `owlen config path` to print the exact location on your machine and `owlen config doctor` to migrate a legacy config automatically. Use `owlen config init` to scaffold a fresh configuration (pass `--force` to overwrite an existing file), `owlen config path` to print the resolved location, and `owlen config doctor` to migrate legacy layouts automatically.
You can also add custom themes alongside the config directory (e.g., `~/.config/owlen/themes/`). You can also add custom themes alongside the config directory (e.g., `~/.config/owlen/themes/`).
See the [themes/README.md](themes/README.md) for more details on theming. See the [themes/README.md](themes/README.md) for more details on theming.

View File

@@ -6,17 +6,24 @@ mod bootstrap;
mod commands; mod commands;
mod mcp; mod mcp;
use anyhow::Result; use anyhow::{Result, anyhow};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use commands::{ use commands::{
cloud::{CloudCommand, run_cloud_command}, cloud::{CloudCommand, run_cloud_command},
providers::{ModelsArgs, ProvidersCommand, run_models_command, run_providers_command}, providers::{ModelsArgs, ProvidersCommand, run_models_command, run_providers_command},
}; };
use mcp::{McpCommand, run_mcp_command}; use mcp::{McpCommand, run_mcp_command};
use owlen_core::config as core_config; use owlen_core::config::{
use owlen_core::config::McpMode; self as core_config, Config, DEFAULT_OLLAMA_CLOUD_HOURLY_QUOTA,
DEFAULT_OLLAMA_CLOUD_WEEKLY_QUOTA, DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS,
DEFAULT_PROVIDER_LIST_TTL_SECS, LEGACY_OLLAMA_CLOUD_API_KEY_ENV, LEGACY_OLLAMA_CLOUD_BASE_URL,
LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV, McpMode, OLLAMA_API_KEY_ENV, OLLAMA_CLOUD_BASE_URL,
OLLAMA_CLOUD_ENDPOINT_KEY,
};
use owlen_core::mode::Mode; use owlen_core::mode::Mode;
use owlen_tui::config; use owlen_tui::config;
use serde_json::{Number as JsonNumber, Value as JsonValue};
use std::env;
/// Owlen - Terminal UI for LLM chat /// Owlen - Terminal UI for LLM chat
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -56,6 +63,12 @@ enum ConfigCommand {
Doctor, Doctor,
/// Print the resolved configuration file path /// Print the resolved configuration file path
Path, Path,
/// Create a fresh configuration file using the latest defaults
Init {
/// Overwrite the existing configuration if present.
#[arg(long)]
force: bool,
},
} }
async fn run_command(command: OwlenCommand) -> Result<()> { async fn run_command(command: OwlenCommand) -> Result<()> {
@@ -85,15 +98,35 @@ fn run_config_command(command: ConfigCommand) -> Result<()> {
println!("{}", path.display()); println!("{}", path.display());
Ok(()) Ok(())
} }
ConfigCommand::Init { force } => run_config_init(force),
} }
} }
fn run_config_init(force: bool) -> Result<()> {
let config_path = core_config::default_config_path();
if config_path.exists() && !force {
return Err(anyhow!(
"Configuration already exists at {}. Re-run with --force to overwrite.",
config_path.display()
));
}
let mut config = Config::default();
let _ = config.refresh_mcp_servers(None);
config.validate()?;
config::save_config(&config)?;
println!("Wrote default configuration to {}.", config_path.display());
Ok(())
}
fn run_config_doctor() -> Result<()> { fn run_config_doctor() -> Result<()> {
let config_path = core_config::default_config_path(); let config_path = core_config::default_config_path();
let existed = config_path.exists(); let existed = config_path.exists();
let mut config = config::try_load_config().unwrap_or_default(); let mut config = config::try_load_config().unwrap_or_default();
let _ = config.refresh_mcp_servers(None); let _ = config.refresh_mcp_servers(None);
let mut changes = Vec::new(); let mut changes = Vec::new();
let mut warnings = Vec::new();
if !existed { if !existed {
changes.push("created configuration file from defaults".to_string()); changes.push("created configuration file from defaults".to_string());
@@ -111,13 +144,164 @@ fn run_config_doctor() -> Result<()> {
} }
} }
if let Some(entry) = config.providers.get_mut("ollama_local") { if let Some(local) = config.providers.get_mut("ollama_local") {
if entry.provider_type.trim().is_empty() || entry.provider_type != "ollama" { if ensure_numeric_extra_with_change(
entry.provider_type = "ollama".to_string(); &mut local.extra,
"list_ttl_secs",
DEFAULT_PROVIDER_LIST_TTL_SECS,
) {
changes.push("added providers.ollama_local.list_ttl_secs (default 60)".to_string());
}
if ensure_numeric_extra_with_change(
&mut local.extra,
"default_context_window",
u64::from(DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS),
) {
changes.push(format!(
"added providers.ollama_local.default_context_window (default {})",
DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS
));
}
if local.provider_type.trim().is_empty() || local.provider_type != "ollama" {
local.provider_type = "ollama".to_string();
changes.push("normalised providers.ollama_local.provider_type to 'ollama'".to_string()); changes.push("normalised providers.ollama_local.provider_type to 'ollama'".to_string());
} }
} }
if let Some(cloud) = config.providers.get_mut("ollama_cloud") {
if cloud.provider_type.trim().is_empty()
|| !cloud.provider_type.eq_ignore_ascii_case("ollama_cloud")
{
cloud.provider_type = "ollama_cloud".to_string();
changes.push(
"normalised providers.ollama_cloud.provider_type to 'ollama_cloud'".to_string(),
);
}
let previous_base_url = cloud.base_url.clone();
match cloud
.base_url
.as_ref()
.map(|value| value.trim_end_matches('/'))
{
None => {
cloud.base_url = Some(OLLAMA_CLOUD_BASE_URL.to_string());
}
Some(current) if current.eq_ignore_ascii_case(LEGACY_OLLAMA_CLOUD_BASE_URL) => {
cloud.base_url = Some(OLLAMA_CLOUD_BASE_URL.to_string());
}
_ => {}
}
if cloud.base_url != previous_base_url {
changes.push(
"normalised providers.ollama_cloud.base_url to https://ollama.com".to_string(),
);
}
let original_api_key_env = cloud.api_key_env.clone();
let needs_env_update = cloud
.api_key_env
.as_ref()
.map(|value| value.trim().is_empty())
.unwrap_or(true);
if needs_env_update {
cloud.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string());
}
if let Some(ref value) = original_api_key_env {
if value.eq_ignore_ascii_case(LEGACY_OLLAMA_CLOUD_API_KEY_ENV)
|| value.eq_ignore_ascii_case(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV)
{
cloud.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string());
}
}
if cloud.api_key_env != original_api_key_env {
changes
.push("updated providers.ollama_cloud.api_key_env to 'OLLAMA_API_KEY'".to_string());
}
if ensure_string_extra_with_change(
&mut cloud.extra,
OLLAMA_CLOUD_ENDPOINT_KEY,
OLLAMA_CLOUD_BASE_URL,
) {
changes.push(
"added providers.ollama_cloud.extra.cloud_endpoint (default https://ollama.com)"
.to_string(),
);
}
if ensure_numeric_extra_with_change(
&mut cloud.extra,
"hourly_quota_tokens",
DEFAULT_OLLAMA_CLOUD_HOURLY_QUOTA,
) {
changes.push(format!(
"added providers.ollama_cloud.hourly_quota_tokens (default {})",
DEFAULT_OLLAMA_CLOUD_HOURLY_QUOTA
));
}
if ensure_numeric_extra_with_change(
&mut cloud.extra,
"weekly_quota_tokens",
DEFAULT_OLLAMA_CLOUD_WEEKLY_QUOTA,
) {
changes.push(format!(
"added providers.ollama_cloud.weekly_quota_tokens (default {})",
DEFAULT_OLLAMA_CLOUD_WEEKLY_QUOTA
));
}
if ensure_numeric_extra_with_change(
&mut cloud.extra,
"list_ttl_secs",
DEFAULT_PROVIDER_LIST_TTL_SECS,
) {
changes.push("added providers.ollama_cloud.list_ttl_secs (default 60)".to_string());
}
if ensure_numeric_extra_with_change(
&mut cloud.extra,
"default_context_window",
u64::from(DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS),
) {
changes.push(format!(
"added providers.ollama_cloud.default_context_window (default {})",
DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS
));
}
}
let canonical_env = env::var(OLLAMA_API_KEY_ENV)
.ok()
.filter(|value| !value.trim().is_empty());
let legacy_env = env::var(LEGACY_OLLAMA_CLOUD_API_KEY_ENV)
.ok()
.filter(|value| !value.trim().is_empty());
let legacy_alt_env = env::var(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV)
.ok()
.filter(|value| !value.trim().is_empty());
if canonical_env.is_some() {
if legacy_env.is_some() {
warnings.push(format!(
"Both {OLLAMA_API_KEY_ENV} and {LEGACY_OLLAMA_CLOUD_API_KEY_ENV} are set; Owlen will prefer {OLLAMA_API_KEY_ENV}."
));
}
if legacy_alt_env.is_some() {
warnings.push(format!(
"Both {OLLAMA_API_KEY_ENV} and {LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV} are set; Owlen will prefer {OLLAMA_API_KEY_ENV}."
));
}
} else {
if legacy_env.is_some() {
warnings.push(format!(
"Legacy environment variable {LEGACY_OLLAMA_CLOUD_API_KEY_ENV} is set. Rename it to {OLLAMA_API_KEY_ENV} to match the latest configuration schema."
));
}
if legacy_alt_env.is_some() {
warnings.push(format!(
"Legacy environment variable {LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV} is set. Rename it to {OLLAMA_API_KEY_ENV} to match the latest configuration schema."
));
}
}
let mut ensure_default_enabled = true; let mut ensure_default_enabled = true;
if !config.providers.values().any(|cfg| cfg.enabled) { if !config.providers.values().any(|cfg| cfg.enabled) {
@@ -213,9 +397,63 @@ fn run_config_doctor() -> Result<()> {
} }
} }
if !warnings.is_empty() {
println!("Warnings:");
for warning in warnings {
println!(" - {warning}");
}
}
Ok(()) Ok(())
} }
fn ensure_numeric_extra_with_change(
extra: &mut std::collections::HashMap<String, JsonValue>,
key: &str,
default_value: u64,
) -> bool {
match extra.get_mut(key) {
Some(existing) => {
if existing.as_u64().is_some() {
false
} else {
*existing = JsonValue::Number(JsonNumber::from(default_value));
true
}
}
None => {
extra.insert(
key.to_string(),
JsonValue::Number(JsonNumber::from(default_value)),
);
true
}
}
}
fn ensure_string_extra_with_change(
extra: &mut std::collections::HashMap<String, JsonValue>,
key: &str,
default_value: &str,
) -> bool {
match extra.get_mut(key) {
Some(existing) => match existing.as_str() {
Some(value) if !value.trim().is_empty() => false,
_ => {
*existing = JsonValue::String(default_value.to_string());
true
}
},
None => {
extra.insert(
key.to_string(),
JsonValue::String(default_value.to_string()),
);
true
}
}
}
#[tokio::main(flavor = "multi_thread")] #[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> { async fn main() -> Result<()> {
// Parse command-line arguments // Parse command-line arguments

View File

@@ -36,6 +36,10 @@ pub const LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV: &str = "OWLEN_OLLAMA_CLOUD_API_
pub const DEFAULT_OLLAMA_CLOUD_HOURLY_QUOTA: u64 = 50_000; pub const DEFAULT_OLLAMA_CLOUD_HOURLY_QUOTA: u64 = 50_000;
/// Default weekly soft quota for Ollama Cloud usage visualization (tokens). /// Default weekly soft quota for Ollama Cloud usage visualization (tokens).
pub const DEFAULT_OLLAMA_CLOUD_WEEKLY_QUOTA: u64 = 250_000; pub const DEFAULT_OLLAMA_CLOUD_WEEKLY_QUOTA: u64 = 250_000;
/// Default TTL (seconds) for cached model listings per provider.
pub const DEFAULT_PROVIDER_LIST_TTL_SECS: u64 = 60;
/// Default context window (tokens) assumed when provider metadata is absent.
pub const DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS: u32 = 8_192;
/// 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.
@@ -392,6 +396,13 @@ impl Config {
self.providers.entry(name).or_insert(default_cfg); self.providers.entry(name).or_insert(default_cfg);
} }
if let Some(local) = self.providers.get_mut("ollama_local") {
normalize_local_provider_config(local);
}
if let Some(cloud) = self.providers.get_mut("ollama_cloud") {
normalize_cloud_provider_config(cloud);
}
if self.schema_version.is_empty() { if self.schema_version.is_empty() {
self.schema_version = Self::default_schema_version(); self.schema_version = Self::default_schema_version();
} }
@@ -868,13 +879,25 @@ fn default_provider_configs() -> HashMap<String, ProviderConfig> {
} }
fn default_ollama_local_config() -> ProviderConfig { fn default_ollama_local_config() -> ProviderConfig {
let mut extra = HashMap::new();
extra.insert(
"list_ttl_secs".to_string(),
serde_json::Value::Number(serde_json::Number::from(DEFAULT_PROVIDER_LIST_TTL_SECS)),
);
extra.insert(
"default_context_window".to_string(),
serde_json::Value::Number(serde_json::Number::from(u64::from(
DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS,
))),
);
ProviderConfig { ProviderConfig {
enabled: true, enabled: true,
provider_type: canonical_provider_type("ollama_local"), provider_type: canonical_provider_type("ollama_local"),
base_url: Some(OLLAMA_LOCAL_BASE_URL.to_string()), base_url: Some(OLLAMA_LOCAL_BASE_URL.to_string()),
api_key: None, api_key: None,
api_key_env: None, api_key_env: None,
extra: HashMap::new(), extra,
} }
} }
@@ -892,6 +915,16 @@ fn default_ollama_cloud_config() -> ProviderConfig {
"weekly_quota_tokens".to_string(), "weekly_quota_tokens".to_string(),
serde_json::Value::Number(serde_json::Number::from(DEFAULT_OLLAMA_CLOUD_WEEKLY_QUOTA)), serde_json::Value::Number(serde_json::Number::from(DEFAULT_OLLAMA_CLOUD_WEEKLY_QUOTA)),
); );
extra.insert(
"list_ttl_secs".to_string(),
serde_json::Value::Number(serde_json::Number::from(DEFAULT_PROVIDER_LIST_TTL_SECS)),
);
extra.insert(
"default_context_window".to_string(),
serde_json::Value::Number(serde_json::Number::from(u64::from(
DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS,
))),
);
ProviderConfig { ProviderConfig {
enabled: false, enabled: false,
@@ -935,6 +968,119 @@ fn default_provider_config_for(name: &str) -> Option<ProviderConfig> {
} }
} }
fn ensure_numeric_extra(
extra: &mut HashMap<String, serde_json::Value>,
key: &str,
default_value: u64,
) {
let needs_update = match extra.get(key) {
Some(existing) => existing.as_u64().is_none(),
None => true,
};
if needs_update {
extra.insert(
key.to_string(),
serde_json::Value::Number(serde_json::Number::from(default_value)),
);
}
}
fn ensure_string_extra(
extra: &mut HashMap<String, serde_json::Value>,
key: &str,
default_value: &str,
) {
let needs_update = match extra.get(key) {
Some(existing) => existing
.as_str()
.map(|value| value.trim().is_empty())
.unwrap_or(true),
None => true,
};
if needs_update {
extra.insert(
key.to_string(),
serde_json::Value::String(default_value.to_string()),
);
}
}
fn normalize_local_provider_config(provider: &mut ProviderConfig) {
if provider.provider_type.trim().is_empty() || provider.provider_type != "ollama" {
provider.provider_type = "ollama".to_string();
}
ensure_numeric_extra(
&mut provider.extra,
"list_ttl_secs",
DEFAULT_PROVIDER_LIST_TTL_SECS,
);
ensure_numeric_extra(
&mut provider.extra,
"default_context_window",
u64::from(DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS),
);
}
fn normalize_cloud_provider_config(provider: &mut ProviderConfig) {
if provider.provider_type.trim().is_empty()
|| !provider.provider_type.eq_ignore_ascii_case("ollama_cloud")
{
provider.provider_type = "ollama_cloud".to_string();
}
match provider
.base_url
.as_ref()
.map(|value| value.trim_end_matches('/'))
{
None => {
provider.base_url = Some(OLLAMA_CLOUD_BASE_URL.to_string());
}
Some(current) if current.eq_ignore_ascii_case(LEGACY_OLLAMA_CLOUD_BASE_URL) => {
provider.base_url = Some(OLLAMA_CLOUD_BASE_URL.to_string());
}
_ => {}
}
if provider
.api_key_env
.as_ref()
.map(|value| value.trim().is_empty())
.unwrap_or(true)
{
provider.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string());
}
ensure_string_extra(
&mut provider.extra,
OLLAMA_CLOUD_ENDPOINT_KEY,
OLLAMA_CLOUD_BASE_URL,
);
ensure_numeric_extra(
&mut provider.extra,
"hourly_quota_tokens",
DEFAULT_OLLAMA_CLOUD_HOURLY_QUOTA,
);
ensure_numeric_extra(
&mut provider.extra,
"weekly_quota_tokens",
DEFAULT_OLLAMA_CLOUD_WEEKLY_QUOTA,
);
ensure_numeric_extra(
&mut provider.extra,
"list_ttl_secs",
DEFAULT_PROVIDER_LIST_TTL_SECS,
);
ensure_numeric_extra(
&mut provider.extra,
"default_context_window",
u64::from(DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS),
);
}
fn normalize_provider_key(name: &str) -> String { fn normalize_provider_key(name: &str) -> String {
let normalized = name.trim().to_ascii_lowercase(); let normalized = name.trim().to_ascii_lowercase();
match normalized.as_str() { match normalized.as_str() {
@@ -2012,6 +2158,162 @@ mod tests {
} }
} }
#[test]
fn default_config_sets_provider_extras() {
let config = Config::default();
let local = config
.providers
.get("ollama_local")
.expect("local provider");
assert_eq!(
local
.extra
.get("list_ttl_secs")
.and_then(|value| value.as_u64()),
Some(DEFAULT_PROVIDER_LIST_TTL_SECS)
);
assert_eq!(
local
.extra
.get("default_context_window")
.and_then(|value| value.as_u64()),
Some(u64::from(DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS))
);
let cloud = config
.providers
.get("ollama_cloud")
.expect("cloud provider");
assert_eq!(
cloud
.extra
.get("list_ttl_secs")
.and_then(|value| value.as_u64()),
Some(DEFAULT_PROVIDER_LIST_TTL_SECS)
);
assert_eq!(
cloud
.extra
.get("default_context_window")
.and_then(|value| value.as_u64()),
Some(u64::from(DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS))
);
assert_eq!(
cloud
.extra
.get("hourly_quota_tokens")
.and_then(|value| value.as_u64()),
Some(DEFAULT_OLLAMA_CLOUD_HOURLY_QUOTA)
);
assert_eq!(
cloud
.extra
.get("weekly_quota_tokens")
.and_then(|value| value.as_u64()),
Some(DEFAULT_OLLAMA_CLOUD_WEEKLY_QUOTA)
);
assert_eq!(
cloud
.extra
.get(OLLAMA_CLOUD_ENDPOINT_KEY)
.and_then(|value| value.as_str()),
Some(OLLAMA_CLOUD_BASE_URL)
);
}
#[test]
fn ensure_defaults_backfills_missing_provider_metadata() {
let mut config = Config::default();
if let Some(local) = config.providers.get_mut("ollama_local") {
local.extra.remove("list_ttl_secs");
local.extra.insert(
"default_context_window".into(),
serde_json::Value::String("".into()),
);
local.provider_type.clear();
}
if let Some(cloud) = config.providers.get_mut("ollama_cloud") {
cloud.extra.remove("list_ttl_secs");
cloud.extra.insert(
"default_context_window".into(),
serde_json::Value::String("invalid".into()),
);
cloud.extra.remove("hourly_quota_tokens");
cloud.extra.remove("weekly_quota_tokens");
cloud.extra.remove(OLLAMA_CLOUD_ENDPOINT_KEY);
cloud.api_key_env = None;
cloud.provider_type.clear();
cloud.base_url = Some(LEGACY_OLLAMA_CLOUD_BASE_URL.to_string());
}
config.ensure_defaults();
let local = config
.providers
.get("ollama_local")
.expect("local provider");
assert_eq!(local.provider_type, "ollama");
assert_eq!(
local
.extra
.get("list_ttl_secs")
.and_then(|value| value.as_u64()),
Some(DEFAULT_PROVIDER_LIST_TTL_SECS)
);
assert_eq!(
local
.extra
.get("default_context_window")
.and_then(|value| value.as_u64()),
Some(u64::from(DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS))
);
let cloud = config
.providers
.get("ollama_cloud")
.expect("cloud provider");
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_API_KEY_ENV));
assert_eq!(
cloud
.extra
.get("list_ttl_secs")
.and_then(|value| value.as_u64()),
Some(DEFAULT_PROVIDER_LIST_TTL_SECS)
);
assert_eq!(
cloud
.extra
.get("default_context_window")
.and_then(|value| value.as_u64()),
Some(u64::from(DEFAULT_PROVIDER_CONTEXT_WINDOW_TOKENS))
);
assert_eq!(
cloud
.extra
.get("hourly_quota_tokens")
.and_then(|value| value.as_u64()),
Some(DEFAULT_OLLAMA_CLOUD_HOURLY_QUOTA)
);
assert_eq!(
cloud
.extra
.get("weekly_quota_tokens")
.and_then(|value| value.as_u64()),
Some(DEFAULT_OLLAMA_CLOUD_WEEKLY_QUOTA)
);
assert_eq!(
cloud
.extra
.get(OLLAMA_CLOUD_ENDPOINT_KEY)
.and_then(|value| value.as_str()),
Some(OLLAMA_CLOUD_BASE_URL)
);
}
#[test] #[test]
fn test_storage_platform_specific_paths() { fn test_storage_platform_specific_paths() {
let config = Config::default(); let config = Config::default();
@@ -2110,7 +2412,7 @@ mod tests {
.expect("migrated config"); .expect("migrated config");
assert!(cloud.enabled); assert!(cloud.enabled);
assert_eq!(cloud.provider_type, "ollama_cloud"); assert_eq!(cloud.provider_type, "ollama_cloud");
assert_eq!(cloud.base_url.as_deref(), Some("https://api.ollama.com")); assert_eq!(cloud.base_url.as_deref(), Some(OLLAMA_CLOUD_BASE_URL));
assert_eq!(cloud.api_key.as_deref(), Some("secret")); assert_eq!(cloud.api_key.as_deref(), Some("secret"));
} }

View File

@@ -12,7 +12,7 @@ Owlen resolves the configuration path using the platform-specific config directo
| macOS | `~/Library/Application Support/owlen/config.toml` | | macOS | `~/Library/Application Support/owlen/config.toml` |
| Windows | `%APPDATA%\owlen\config.toml` | | Windows | `%APPDATA%\owlen\config.toml` |
Run `owlen config path` to print the exact location on your machine. A default configuration file is created on the first run if one doesn't exist, and `owlen config doctor` can migrate/repair legacy files automatically. Use `owlen config init` to scaffold the latest default configuration (pass `--force` to overwrite an existing file), `owlen config path` to print the resolved location, and `owlen config doctor` to migrate or repair legacy files automatically.
## Configuration Precedence ## Configuration Precedence

View File

@@ -46,6 +46,7 @@ Owlen is built with `ratatui`, which supports most modern terminals. However, if
If Owlen is not behaving as you expect, there might be an issue with your configuration file. If Owlen is not behaving as you expect, there might be an issue with your configuration file.
- **Location:** Run `owlen config path` to print the exact location (Linux, macOS, or Windows). Owlen now follows platform defaults instead of hard-coding `~/.config`. - **Location:** Run `owlen config path` to print the exact location (Linux, macOS, or Windows). Owlen now follows platform defaults instead of hard-coding `~/.config`.
- **Init:** Run `owlen config init` to recreate the latest default file (`--force` will overwrite an existing configuration).
- **Syntax:** The configuration file is in TOML format. Make sure the syntax is correct. - **Syntax:** The configuration file is in TOML format. Make sure the syntax is correct.
- **Values:** Check that the values for your models, providers, and other settings are correct. - **Values:** Check that the values for your models, providers, and other settings are correct.
- **Automation:** Run `owlen config doctor` to migrate legacy settings (`mode = "legacy"`, missing providers) and validate the file before launching the TUI. - **Automation:** Run `owlen config doctor` to migrate legacy settings (`mode = "legacy"`, missing providers) and validate the file before launching the TUI.