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:
@@ -155,7 +155,7 @@ OWLEN stores its configuration in the standard platform-specific config director
|
||||
| macOS | `~/Library/Application Support/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/`).
|
||||
|
||||
See the [themes/README.md](themes/README.md) for more details on theming.
|
||||
|
||||
@@ -6,17 +6,24 @@ mod bootstrap;
|
||||
mod commands;
|
||||
mod mcp;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, anyhow};
|
||||
use clap::{Parser, Subcommand};
|
||||
use commands::{
|
||||
cloud::{CloudCommand, run_cloud_command},
|
||||
providers::{ModelsArgs, ProvidersCommand, run_models_command, run_providers_command},
|
||||
};
|
||||
use mcp::{McpCommand, run_mcp_command};
|
||||
use owlen_core::config as core_config;
|
||||
use owlen_core::config::McpMode;
|
||||
use owlen_core::config::{
|
||||
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_tui::config;
|
||||
use serde_json::{Number as JsonNumber, Value as JsonValue};
|
||||
use std::env;
|
||||
|
||||
/// Owlen - Terminal UI for LLM chat
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -56,6 +63,12 @@ enum ConfigCommand {
|
||||
Doctor,
|
||||
/// Print the resolved configuration file 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<()> {
|
||||
@@ -85,15 +98,35 @@ fn run_config_command(command: ConfigCommand) -> Result<()> {
|
||||
println!("{}", path.display());
|
||||
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<()> {
|
||||
let config_path = core_config::default_config_path();
|
||||
let existed = config_path.exists();
|
||||
let mut config = config::try_load_config().unwrap_or_default();
|
||||
let _ = config.refresh_mcp_servers(None);
|
||||
let mut changes = Vec::new();
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
if !existed {
|
||||
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 entry.provider_type.trim().is_empty() || entry.provider_type != "ollama" {
|
||||
entry.provider_type = "ollama".to_string();
|
||||
if let Some(local) = config.providers.get_mut("ollama_local") {
|
||||
if ensure_numeric_extra_with_change(
|
||||
&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());
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
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")]
|
||||
async fn main() -> Result<()> {
|
||||
// Parse command-line arguments
|
||||
|
||||
@@ -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;
|
||||
/// Default weekly soft quota for Ollama Cloud usage visualization (tokens).
|
||||
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.
|
||||
pub const OLLAMA_LOCAL_BASE_URL: &str = "http://localhost:11434";
|
||||
/// Default OpenAI API base URL.
|
||||
@@ -392,6 +396,13 @@ impl Config {
|
||||
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() {
|
||||
self.schema_version = Self::default_schema_version();
|
||||
}
|
||||
@@ -868,13 +879,25 @@ fn default_provider_configs() -> HashMap<String, 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 {
|
||||
enabled: true,
|
||||
provider_type: canonical_provider_type("ollama_local"),
|
||||
base_url: Some(OLLAMA_LOCAL_BASE_URL.to_string()),
|
||||
api_key: None,
|
||||
api_key_env: None,
|
||||
extra: HashMap::new(),
|
||||
extra,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -892,6 +915,16 @@ fn default_ollama_cloud_config() -> ProviderConfig {
|
||||
"weekly_quota_tokens".to_string(),
|
||||
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 {
|
||||
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 {
|
||||
let normalized = name.trim().to_ascii_lowercase();
|
||||
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]
|
||||
fn test_storage_platform_specific_paths() {
|
||||
let config = Config::default();
|
||||
@@ -2110,7 +2412,7 @@ mod tests {
|
||||
.expect("migrated config");
|
||||
assert!(cloud.enabled);
|
||||
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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Owlen resolves the configuration path using the platform-specific config directo
|
||||
| macOS | `~/Library/Application Support/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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
Reference in New Issue
Block a user