feat(config): separate Ollama into local/cloud providers, add OpenAI & Anthropic defaults, bump schema version to 1.6.0
This commit is contained in:
@@ -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_BASE_URL, OLLAMA_CLOUD_ENDPOINT_KEY, OLLAMA_MODE_KEY,
|
||||
self as core_config, Config, OLLAMA_CLOUD_API_KEY_ENV, OLLAMA_CLOUD_BASE_URL,
|
||||
OLLAMA_CLOUD_ENDPOINT_KEY, OLLAMA_MODE_KEY,
|
||||
};
|
||||
use owlen_core::credentials::{ApiCredentials, CredentialManager, OLLAMA_CLOUD_CREDENTIAL_ID};
|
||||
use owlen_core::encryption;
|
||||
@@ -17,6 +18,7 @@ use serde_json::Value;
|
||||
|
||||
const DEFAULT_CLOUD_ENDPOINT: &str = OLLAMA_CLOUD_BASE_URL;
|
||||
const CLOUD_ENDPOINT_KEY: &str = OLLAMA_CLOUD_ENDPOINT_KEY;
|
||||
const CLOUD_PROVIDER_KEY: &str = "ollama_cloud";
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum CloudCommand {
|
||||
@@ -28,8 +30,8 @@ pub enum CloudCommand {
|
||||
/// Override the cloud endpoint (default: https://ollama.com)
|
||||
#[arg(long)]
|
||||
endpoint: Option<String>,
|
||||
/// Provider name to configure (default: ollama)
|
||||
#[arg(long, default_value = "ollama")]
|
||||
/// Provider name to configure (default: ollama_cloud)
|
||||
#[arg(long, default_value = "ollama_cloud")]
|
||||
provider: String,
|
||||
/// Overwrite the provider base URL with the cloud endpoint
|
||||
#[arg(long)]
|
||||
@@ -37,20 +39,20 @@ pub enum CloudCommand {
|
||||
},
|
||||
/// Check connectivity to Ollama Cloud
|
||||
Status {
|
||||
/// Provider name to check (default: ollama)
|
||||
#[arg(long, default_value = "ollama")]
|
||||
/// Provider name to check (default: ollama_cloud)
|
||||
#[arg(long, default_value = "ollama_cloud")]
|
||||
provider: String,
|
||||
},
|
||||
/// List available cloud-hosted models
|
||||
Models {
|
||||
/// Provider name to query (default: ollama)
|
||||
#[arg(long, default_value = "ollama")]
|
||||
/// Provider name to query (default: ollama_cloud)
|
||||
#[arg(long, default_value = "ollama_cloud")]
|
||||
provider: String,
|
||||
},
|
||||
/// Remove stored Ollama Cloud credentials
|
||||
Logout {
|
||||
/// Provider name to clear (default: ollama)
|
||||
#[arg(long, default_value = "ollama")]
|
||||
/// Provider name to clear (default: ollama_cloud)
|
||||
#[arg(long, default_value = "ollama_cloud")]
|
||||
provider: String,
|
||||
},
|
||||
}
|
||||
@@ -82,6 +84,7 @@ async fn setup(
|
||||
|
||||
let base_changed = {
|
||||
let entry = ensure_provider_entry(&mut config, &provider);
|
||||
entry.enabled = true;
|
||||
configure_cloud_endpoint(entry, &endpoint, force_cloud_base_url)
|
||||
};
|
||||
|
||||
@@ -140,6 +143,7 @@ async fn status(provider: String) -> Result<()> {
|
||||
let api_key = hydrate_api_key(&mut config, manager.as_ref()).await?;
|
||||
{
|
||||
let entry = ensure_provider_entry(&mut config, &provider);
|
||||
entry.enabled = true;
|
||||
configure_cloud_endpoint(entry, DEFAULT_CLOUD_ENDPOINT, false);
|
||||
}
|
||||
|
||||
@@ -190,6 +194,7 @@ async fn models(provider: String) -> Result<()> {
|
||||
|
||||
{
|
||||
let entry = ensure_provider_entry(&mut config, &provider);
|
||||
entry.enabled = true;
|
||||
configure_cloud_endpoint(entry, DEFAULT_CLOUD_ENDPOINT, false);
|
||||
}
|
||||
|
||||
@@ -245,8 +250,9 @@ async fn logout(provider: String) -> Result<()> {
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(entry) = provider_entry_mut(&mut config) {
|
||||
if let Some(entry) = config.providers.get_mut(&provider) {
|
||||
entry.api_key = None;
|
||||
entry.enabled = false;
|
||||
}
|
||||
|
||||
crate::config::save_config(&config)?;
|
||||
@@ -255,28 +261,7 @@ async fn logout(provider: String) -> Result<()> {
|
||||
}
|
||||
|
||||
fn ensure_provider_entry<'a>(config: &'a mut Config, provider: &str) -> &'a mut ProviderConfig {
|
||||
if provider == "ollama"
|
||||
&& config.providers.contains_key("ollama-cloud")
|
||||
&& !config.providers.contains_key("ollama")
|
||||
{
|
||||
if let Some(mut legacy) = config.providers.remove("ollama-cloud") {
|
||||
legacy.provider_type = "ollama".to_string();
|
||||
config.providers.insert("ollama".to_string(), legacy);
|
||||
}
|
||||
}
|
||||
|
||||
core_config::ensure_provider_config(config, provider);
|
||||
|
||||
let entry = config
|
||||
.providers
|
||||
.get_mut(provider)
|
||||
.expect("provider entry must exist");
|
||||
|
||||
if entry.provider_type != "ollama" {
|
||||
entry.provider_type = "ollama".to_string();
|
||||
}
|
||||
|
||||
entry
|
||||
core_config::ensure_provider_config_mut(config, provider)
|
||||
}
|
||||
|
||||
fn configure_cloud_endpoint(entry: &mut ProviderConfig, endpoint: &str, force: bool) -> bool {
|
||||
@@ -287,6 +272,10 @@ 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());
|
||||
}
|
||||
|
||||
if force
|
||||
|| entry
|
||||
.base_url
|
||||
@@ -298,10 +287,7 @@ fn configure_cloud_endpoint(entry: &mut ProviderConfig, endpoint: &str, force: b
|
||||
}
|
||||
|
||||
if force {
|
||||
entry.extra.insert(
|
||||
OLLAMA_MODE_KEY.to_string(),
|
||||
Value::String("cloud".to_string()),
|
||||
);
|
||||
entry.enabled = true;
|
||||
}
|
||||
|
||||
entry.base_url != previous_base
|
||||
@@ -333,10 +319,11 @@ fn normalize_endpoint(endpoint: &str) -> String {
|
||||
}
|
||||
|
||||
fn canonical_provider_name(provider: &str) -> String {
|
||||
let normalized = provider.trim().replace('_', "-").to_ascii_lowercase();
|
||||
let normalized = provider.trim().to_ascii_lowercase().replace('-', "_");
|
||||
match normalized.as_str() {
|
||||
"" => "ollama".to_string(),
|
||||
"ollama-cloud" => "ollama".to_string(),
|
||||
"" => CLOUD_PROVIDER_KEY.to_string(),
|
||||
"ollama" => CLOUD_PROVIDER_KEY.to_string(),
|
||||
"ollama_cloud" => CLOUD_PROVIDER_KEY.to_string(),
|
||||
value => value.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -362,21 +349,6 @@ fn set_env_if_missing(var: &str, value: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_entry_mut(config: &mut Config) -> Option<&mut ProviderConfig> {
|
||||
if config.providers.contains_key("ollama") {
|
||||
config.providers.get_mut("ollama")
|
||||
} else {
|
||||
config.providers.get_mut("ollama-cloud")
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_entry(config: &Config) -> Option<&ProviderConfig> {
|
||||
if let Some(entry) = config.providers.get("ollama") {
|
||||
return Some(entry);
|
||||
}
|
||||
config.providers.get("ollama-cloud")
|
||||
}
|
||||
|
||||
fn unlock_credential_manager(
|
||||
config: &Config,
|
||||
storage: Arc<StorageManager>,
|
||||
@@ -463,14 +435,13 @@ async fn hydrate_api_key(
|
||||
set_env_if_missing("OLLAMA_CLOUD_API_KEY", &key);
|
||||
}
|
||||
|
||||
let Some(cfg) = provider_entry_mut(config) else {
|
||||
return Ok(Some(key));
|
||||
};
|
||||
let cfg = core_config::ensure_provider_config_mut(config, CLOUD_PROVIDER_KEY);
|
||||
configure_cloud_endpoint(cfg, &credentials.endpoint, false);
|
||||
return Ok(Some(key));
|
||||
}
|
||||
|
||||
if let Some(key) = provider_entry(config)
|
||||
if let Some(key) = config
|
||||
.provider(CLOUD_PROVIDER_KEY)
|
||||
.and_then(|cfg| cfg.api_key.as_ref())
|
||||
.map(|value| value.trim())
|
||||
.filter(|value| !value.is_empty())
|
||||
@@ -501,8 +472,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn canonicalises_provider_names() {
|
||||
assert_eq!(canonical_provider_name("OLLAMA_CLOUD"), "ollama");
|
||||
assert_eq!(canonical_provider_name(" ollama-cloud"), "ollama");
|
||||
assert_eq!(canonical_provider_name(""), "ollama");
|
||||
assert_eq!(canonical_provider_name("OLLAMA_CLOUD"), CLOUD_PROVIDER_KEY);
|
||||
assert_eq!(canonical_provider_name(" ollama-cloud"), CLOUD_PROVIDER_KEY);
|
||||
assert_eq!(canonical_provider_name(""), CLOUD_PROVIDER_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ fn build_local_provider(cfg: &Config) -> anyhow::Result<Arc<dyn Provider>> {
|
||||
})?;
|
||||
|
||||
match provider_cfg.provider_type.as_str() {
|
||||
"ollama" | "ollama-cloud" => {
|
||||
"ollama" | "ollama_cloud" => {
|
||||
let provider = OllamaProvider::from_config(provider_cfg, Some(&cfg.general))?;
|
||||
Ok(Arc::new(provider) as Arc<dyn Provider>)
|
||||
}
|
||||
@@ -172,40 +172,16 @@ fn run_config_doctor() -> Result<()> {
|
||||
changes.push("created configuration file from defaults".to_string());
|
||||
}
|
||||
|
||||
if !config
|
||||
.providers
|
||||
.contains_key(&config.general.default_provider)
|
||||
{
|
||||
config.general.default_provider = "ollama".to_string();
|
||||
changes.push("default provider missing; reset to 'ollama'".to_string());
|
||||
if config.provider(&config.general.default_provider).is_none() {
|
||||
config.general.default_provider = "ollama_local".to_string();
|
||||
changes.push("default provider missing; reset to 'ollama_local'".to_string());
|
||||
}
|
||||
|
||||
if let Some(mut legacy) = config.providers.remove("ollama-cloud") {
|
||||
legacy.provider_type = "ollama".to_string();
|
||||
use std::collections::hash_map::Entry;
|
||||
match config.providers.entry("ollama".to_string()) {
|
||||
Entry::Occupied(mut existing) => {
|
||||
let entry = existing.get_mut();
|
||||
if entry.api_key.is_none() {
|
||||
entry.api_key = legacy.api_key.take();
|
||||
}
|
||||
if entry.base_url.is_none() && legacy.base_url.is_some() {
|
||||
entry.base_url = legacy.base_url.take();
|
||||
}
|
||||
entry.extra.extend(legacy.extra);
|
||||
}
|
||||
Entry::Vacant(slot) => {
|
||||
slot.insert(legacy);
|
||||
}
|
||||
for key in ["ollama_local", "ollama_cloud", "openai", "anthropic"] {
|
||||
if !config.providers.contains_key(key) {
|
||||
core_config::ensure_provider_config_mut(&mut config, key);
|
||||
changes.push(format!("added default configuration for provider '{key}'"));
|
||||
}
|
||||
changes.push(
|
||||
"migrated legacy 'ollama-cloud' provider into unified 'ollama' entry".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if !config.providers.contains_key("ollama") {
|
||||
core_config::ensure_provider_config(&mut config, "ollama");
|
||||
changes.push("added default ollama provider configuration".to_string());
|
||||
}
|
||||
|
||||
match config.mcp.mode {
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::time::Duration;
|
||||
pub const DEFAULT_CONFIG_PATH: &str = "~/.config/owlen/config.toml";
|
||||
|
||||
/// Current schema version written to `config.toml`.
|
||||
pub const CONFIG_SCHEMA_VERSION: &str = "1.5.0";
|
||||
pub const CONFIG_SCHEMA_VERSION: &str = "1.6.0";
|
||||
|
||||
/// Provider config key for forcing Ollama provider mode.
|
||||
pub const OLLAMA_MODE_KEY: &str = "ollama_mode";
|
||||
@@ -24,6 +24,18 @@ pub const OLLAMA_MODE_KEY: &str = "ollama_mode";
|
||||
pub const OLLAMA_CLOUD_ENDPOINT_KEY: &str = "cloud_endpoint";
|
||||
/// Canonical Ollama Cloud base URL.
|
||||
pub const OLLAMA_CLOUD_BASE_URL: &str = "https://ollama.com";
|
||||
/// Environment variable used for Ollama Cloud authentication.
|
||||
pub const OLLAMA_CLOUD_API_KEY_ENV: &str = "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.
|
||||
pub const OPENAI_DEFAULT_BASE_URL: &str = "https://api.openai.com/v1";
|
||||
/// Environment variable name used for OpenAI API keys.
|
||||
pub const OPENAI_API_KEY_ENV: &str = "OPENAI_API_KEY";
|
||||
/// Default Anthropic API base URL.
|
||||
pub const ANTHROPIC_DEFAULT_BASE_URL: &str = "https://api.anthropic.com/v1";
|
||||
/// Environment variable name used for Anthropic API keys.
|
||||
pub const ANTHROPIC_API_KEY_ENV: &str = "ANTHROPIC_API_KEY";
|
||||
|
||||
/// Core configuration shared by all OWLEN clients
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -82,8 +94,7 @@ pub struct Config {
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let mut providers = HashMap::new();
|
||||
providers.insert("ollama".to_string(), default_ollama_provider_config());
|
||||
let providers = default_provider_configs();
|
||||
|
||||
Self {
|
||||
schema_version: Self::default_schema_version(),
|
||||
@@ -270,6 +281,8 @@ impl Config {
|
||||
let content = fs::read_to_string(&path)?;
|
||||
let parsed: toml::Value =
|
||||
toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))?;
|
||||
let mut parsed = parsed;
|
||||
migrate_legacy_provider_tables(&mut parsed);
|
||||
let previous_version = parsed
|
||||
.get("schema_version")
|
||||
.and_then(|value| value.as_str())
|
||||
@@ -326,12 +339,19 @@ impl Config {
|
||||
|
||||
/// Get provider configuration by provider name
|
||||
pub fn provider(&self, name: &str) -> Option<&ProviderConfig> {
|
||||
self.providers.get(name)
|
||||
let key = normalize_provider_key(name);
|
||||
self.providers.get(&key)
|
||||
}
|
||||
|
||||
/// Update or insert a provider configuration
|
||||
pub fn upsert_provider(&mut self, name: impl Into<String>, config: ProviderConfig) {
|
||||
self.providers.insert(name.into(), config);
|
||||
let raw = name.into();
|
||||
let key = normalize_provider_key(&raw);
|
||||
let mut config = config;
|
||||
if config.provider_type.is_empty() {
|
||||
config.provider_type = key.clone();
|
||||
}
|
||||
self.providers.insert(key, config);
|
||||
}
|
||||
|
||||
/// Resolve default model in order of priority: explicit default, first cached model, provider fallback
|
||||
@@ -353,11 +373,15 @@ impl Config {
|
||||
}
|
||||
|
||||
fn ensure_defaults(&mut self) {
|
||||
if self.general.default_provider.is_empty() {
|
||||
self.general.default_provider = "ollama".to_string();
|
||||
if self.general.default_provider.is_empty() || self.general.default_provider == "ollama" {
|
||||
self.general.default_provider = "ollama_local".to_string();
|
||||
}
|
||||
|
||||
let mut defaults = default_provider_configs();
|
||||
for (name, default_cfg) in defaults.drain() {
|
||||
self.providers.entry(name).or_insert(default_cfg);
|
||||
}
|
||||
|
||||
ensure_provider_config(self, "ollama");
|
||||
if self.schema_version.is_empty() {
|
||||
self.schema_version = Self::default_schema_version();
|
||||
}
|
||||
@@ -561,6 +585,7 @@ impl Config {
|
||||
self.validate_default_provider()?;
|
||||
self.validate_mcp_settings()?;
|
||||
self.validate_mcp_servers()?;
|
||||
self.validate_providers()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -573,57 +598,92 @@ impl Config {
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(legacy_cloud) = self.providers.remove("ollama_cloud") {
|
||||
self.merge_legacy_ollama_provider(legacy_cloud);
|
||||
self.migrate_provider_entries();
|
||||
if self.general.default_provider == "ollama" {
|
||||
self.general.default_provider = "ollama_local".to_string();
|
||||
}
|
||||
|
||||
if let Some(legacy_cloud) = self.providers.remove("ollama-cloud") {
|
||||
self.merge_legacy_ollama_provider(legacy_cloud);
|
||||
}
|
||||
|
||||
if let Some(ollama) = self.providers.get_mut("ollama") {
|
||||
let previous_mode = ollama
|
||||
.extra
|
||||
.get(OLLAMA_MODE_KEY)
|
||||
.and_then(|value| value.as_str())
|
||||
.map(|value| value.to_ascii_lowercase());
|
||||
ensure_ollama_mode_extra(ollama);
|
||||
if previous_mode.as_deref().unwrap_or("auto") == "auto"
|
||||
&& is_cloud_base_url(ollama.base_url.as_ref())
|
||||
{
|
||||
ollama.extra.insert(
|
||||
OLLAMA_MODE_KEY.to_string(),
|
||||
serde_json::Value::String("cloud".to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.ensure_defaults();
|
||||
self.schema_version = CONFIG_SCHEMA_VERSION.to_string();
|
||||
}
|
||||
|
||||
fn merge_legacy_ollama_provider(&mut self, mut legacy_cloud: ProviderConfig) {
|
||||
use std::collections::hash_map::Entry;
|
||||
fn migrate_provider_entries(&mut self) {
|
||||
let mut migrated = default_provider_configs();
|
||||
let legacy_entries = std::mem::take(&mut self.providers);
|
||||
|
||||
legacy_cloud.provider_type = "ollama".to_string();
|
||||
|
||||
match self.providers.entry("ollama".to_string()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
let target = entry.get_mut();
|
||||
if target.base_url.is_none() {
|
||||
target.base_url = legacy_cloud.base_url.take();
|
||||
}
|
||||
if target.api_key.is_none() {
|
||||
target.api_key = legacy_cloud.api_key.take();
|
||||
}
|
||||
if target.extra.is_empty() && !legacy_cloud.extra.is_empty() {
|
||||
target.extra = legacy_cloud.extra;
|
||||
}
|
||||
ensure_ollama_mode_extra(target);
|
||||
for (original_key, mut legacy) in legacy_entries {
|
||||
if original_key == "ollama" {
|
||||
Self::merge_legacy_ollama_provider(legacy, &mut migrated);
|
||||
continue;
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let mut inserted = legacy_cloud;
|
||||
ensure_ollama_mode_extra(&mut inserted);
|
||||
entry.insert(inserted);
|
||||
|
||||
let normalized = normalize_provider_key(&original_key);
|
||||
let entry = migrated
|
||||
.entry(normalized.clone())
|
||||
.or_insert_with(|| ProviderConfig {
|
||||
enabled: true,
|
||||
provider_type: normalized.clone(),
|
||||
base_url: None,
|
||||
api_key: None,
|
||||
api_key_env: None,
|
||||
extra: HashMap::new(),
|
||||
});
|
||||
|
||||
if legacy.provider_type.is_empty() {
|
||||
legacy.provider_type = normalized.clone();
|
||||
}
|
||||
|
||||
entry.merge_from(legacy);
|
||||
if entry.provider_type.is_empty() {
|
||||
entry.provider_type = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
self.providers = migrated;
|
||||
}
|
||||
|
||||
fn merge_legacy_ollama_provider(
|
||||
mut legacy: ProviderConfig,
|
||||
targets: &mut HashMap<String, ProviderConfig>,
|
||||
) {
|
||||
let mode = legacy
|
||||
.extra
|
||||
.remove(OLLAMA_MODE_KEY)
|
||||
.and_then(|value| value.as_str().map(|s| s.trim().to_ascii_lowercase()));
|
||||
|
||||
let api_key_present = legacy
|
||||
.api_key
|
||||
.as_ref()
|
||||
.map(|value| !value.trim().is_empty())
|
||||
.unwrap_or(false);
|
||||
let cloud_candidate =
|
||||
matches!(mode.as_deref(), Some("cloud")) || is_cloud_base_url(legacy.base_url.as_ref());
|
||||
let should_enable_cloud = cloud_candidate || api_key_present;
|
||||
|
||||
if matches!(mode.as_deref(), Some("local")) || !should_enable_cloud {
|
||||
if let Some(local) = targets.get_mut("ollama_local") {
|
||||
let mut copy = legacy.clone();
|
||||
copy.api_key = None;
|
||||
copy.api_key_env = None;
|
||||
copy.enabled = true;
|
||||
local.merge_from(copy);
|
||||
local.enabled = true;
|
||||
if local.base_url.is_none() {
|
||||
local.base_url = Some(OLLAMA_LOCAL_BASE_URL.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_enable_cloud || matches!(mode.as_deref(), Some("cloud")) {
|
||||
if let Some(cloud) = targets.get_mut("ollama_cloud") {
|
||||
legacy.enabled = true;
|
||||
cloud.merge_from(legacy);
|
||||
cloud.enabled = true;
|
||||
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 = Some(OLLAMA_CLOUD_API_KEY_ENV.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -693,40 +753,164 @@ impl Config {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_providers(&self) -> Result<()> {
|
||||
for (name, provider) in &self.providers {
|
||||
if !provider.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
match name.as_str() {
|
||||
"ollama_local" => {
|
||||
if is_blank(&provider.base_url) {
|
||||
return Err(Error::Config(
|
||||
"providers.ollama_local.base_url must be set when enabled".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
"ollama_cloud" => {
|
||||
if is_blank(&provider.base_url) {
|
||||
return Err(Error::Config(
|
||||
"providers.ollama_cloud.base_url must be set when enabled".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if is_blank(&provider.api_key) && is_blank(&provider.api_key_env) {
|
||||
return Err(Error::Config(
|
||||
"providers.ollama_cloud requires `api_key` or `api_key_env` when enabled"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
"openai" | "anthropic" => {
|
||||
if is_blank(&provider.api_key) && is_blank(&provider.api_key_env) {
|
||||
return Err(Error::Config(format!(
|
||||
"providers.{name} requires `api_key` or `api_key_env` when enabled"
|
||||
)));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_ollama_provider_config() -> ProviderConfig {
|
||||
let mut config = ProviderConfig {
|
||||
provider_type: "ollama".to_string(),
|
||||
base_url: Some("http://localhost:11434".to_string()),
|
||||
fn default_provider_configs() -> HashMap<String, ProviderConfig> {
|
||||
let mut providers = HashMap::new();
|
||||
for name in ["ollama_local", "ollama_cloud", "openai", "anthropic"] {
|
||||
if let Some(config) = default_provider_config_for(name) {
|
||||
providers.insert(name.to_string(), config);
|
||||
}
|
||||
}
|
||||
providers
|
||||
}
|
||||
|
||||
fn default_ollama_local_config() -> ProviderConfig {
|
||||
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(),
|
||||
};
|
||||
ensure_ollama_mode_extra(&mut config);
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_ollama_mode_extra(provider: &mut ProviderConfig) {
|
||||
if provider.provider_type != "ollama" {
|
||||
fn default_ollama_cloud_config() -> ProviderConfig {
|
||||
let mut extra = HashMap::new();
|
||||
extra.insert(
|
||||
OLLAMA_CLOUD_ENDPOINT_KEY.to_string(),
|
||||
serde_json::Value::String(OLLAMA_CLOUD_BASE_URL.to_string()),
|
||||
);
|
||||
|
||||
ProviderConfig {
|
||||
enabled: false,
|
||||
provider_type: canonical_provider_type("ollama_cloud"),
|
||||
base_url: Some(OLLAMA_CLOUD_BASE_URL.to_string()),
|
||||
api_key: None,
|
||||
api_key_env: Some(OLLAMA_CLOUD_API_KEY_ENV.to_string()),
|
||||
extra,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_openai_config() -> ProviderConfig {
|
||||
ProviderConfig {
|
||||
enabled: false,
|
||||
provider_type: canonical_provider_type("openai"),
|
||||
base_url: Some(OPENAI_DEFAULT_BASE_URL.to_string()),
|
||||
api_key: None,
|
||||
api_key_env: Some(OPENAI_API_KEY_ENV.to_string()),
|
||||
extra: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_anthropic_config() -> ProviderConfig {
|
||||
ProviderConfig {
|
||||
enabled: false,
|
||||
provider_type: canonical_provider_type("anthropic"),
|
||||
base_url: Some(ANTHROPIC_DEFAULT_BASE_URL.to_string()),
|
||||
api_key: None,
|
||||
api_key_env: Some(ANTHROPIC_API_KEY_ENV.to_string()),
|
||||
extra: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_provider_config_for(name: &str) -> Option<ProviderConfig> {
|
||||
match name {
|
||||
"ollama_local" => Some(default_ollama_local_config()),
|
||||
"ollama_cloud" => Some(default_ollama_cloud_config()),
|
||||
"openai" => Some(default_openai_config()),
|
||||
"anthropic" => Some(default_anthropic_config()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_provider_key(name: &str) -> String {
|
||||
let normalized = name.trim().to_ascii_lowercase();
|
||||
match normalized.as_str() {
|
||||
"ollama" | "ollama-local" => "ollama_local".to_string(),
|
||||
"ollama_cloud" | "ollama-cloud" => "ollama_cloud".to_string(),
|
||||
other => other.replace('-', "_"),
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_provider_type(key: &str) -> String {
|
||||
match key {
|
||||
"ollama_local" => "ollama".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_blank(value: &Option<String>) -> bool {
|
||||
value.as_ref().map(|s| s.trim().is_empty()).unwrap_or(true)
|
||||
}
|
||||
|
||||
fn migrate_legacy_provider_tables(document: &mut toml::Value) {
|
||||
let Some(table) = document.as_table_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut legacy = Vec::new();
|
||||
for key in ["ollama", "ollama_cloud", "ollama-cloud"] {
|
||||
if let Some(entry) = table.remove(key) {
|
||||
legacy.push((key.to_string(), entry));
|
||||
}
|
||||
}
|
||||
|
||||
if legacy.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let entry = provider
|
||||
.extra
|
||||
.entry(OLLAMA_MODE_KEY.to_string())
|
||||
.or_insert_with(|| serde_json::Value::String("auto".to_string()));
|
||||
let providers_entry = table
|
||||
.entry("providers".to_string())
|
||||
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
|
||||
|
||||
if let Some(value) = entry.as_str() {
|
||||
let normalized = value.trim().to_ascii_lowercase();
|
||||
if matches!(normalized.as_str(), "auto" | "local" | "cloud") {
|
||||
if normalized != value {
|
||||
*entry = serde_json::Value::String(normalized);
|
||||
}
|
||||
} else {
|
||||
*entry = serde_json::Value::String("auto".to_string());
|
||||
if let Some(providers_table) = providers_entry.as_table_mut() {
|
||||
for (key, value) in legacy {
|
||||
providers_table.insert(key, value);
|
||||
}
|
||||
} else {
|
||||
*entry = serde_json::Value::String("auto".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1117,7 +1301,7 @@ impl GeneralSettings {
|
||||
impl Default for GeneralSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_provider: "ollama".to_string(),
|
||||
default_provider: "ollama_local".to_string(),
|
||||
default_model: Some("llama3.2:latest".to_string()),
|
||||
enable_streaming: Self::default_streaming(),
|
||||
project_context_file: Some("OWLEN.md".to_string()),
|
||||
@@ -1650,7 +1834,35 @@ impl Default for InputSettings {
|
||||
|
||||
/// Convenience accessor for an Ollama provider entry, creating a default if missing
|
||||
pub fn ensure_ollama_config(config: &mut Config) -> &ProviderConfig {
|
||||
ensure_provider_config(config, "ollama")
|
||||
ensure_provider_config(config, "ollama_local")
|
||||
}
|
||||
|
||||
/// Ensure a provider configuration exists for the requested provider name and return a mutable reference.
|
||||
pub fn ensure_provider_config_mut<'a>(
|
||||
config: &'a mut Config,
|
||||
provider_name: &str,
|
||||
) -> &'a mut ProviderConfig {
|
||||
let key = normalize_provider_key(provider_name);
|
||||
let entry = config.providers.entry(key.clone()).or_insert_with(|| {
|
||||
let mut default = default_provider_config_for(&key).unwrap_or_else(|| ProviderConfig {
|
||||
enabled: true,
|
||||
provider_type: canonical_provider_type(&key),
|
||||
base_url: None,
|
||||
api_key: None,
|
||||
api_key_env: None,
|
||||
extra: HashMap::new(),
|
||||
});
|
||||
if default.provider_type.is_empty() {
|
||||
default.provider_type = canonical_provider_type(&key);
|
||||
}
|
||||
default
|
||||
});
|
||||
|
||||
if entry.provider_type.is_empty() {
|
||||
entry.provider_type = canonical_provider_type(&key);
|
||||
}
|
||||
|
||||
entry
|
||||
}
|
||||
|
||||
/// Ensure a provider configuration exists for the requested provider name
|
||||
@@ -1658,35 +1870,8 @@ pub fn ensure_provider_config<'a>(
|
||||
config: &'a mut Config,
|
||||
provider_name: &str,
|
||||
) -> &'a ProviderConfig {
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
if matches!(provider_name, "ollama_cloud" | "ollama-cloud") {
|
||||
return ensure_provider_config(config, "ollama");
|
||||
}
|
||||
|
||||
match config.providers.entry(provider_name.to_string()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
ensure_ollama_mode_extra(entry.get_mut());
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let mut default = match provider_name {
|
||||
"ollama" => default_ollama_provider_config(),
|
||||
other => ProviderConfig {
|
||||
provider_type: other.to_string(),
|
||||
base_url: None,
|
||||
api_key: None,
|
||||
extra: HashMap::new(),
|
||||
},
|
||||
};
|
||||
ensure_ollama_mode_extra(&mut default);
|
||||
entry.insert(default);
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
.providers
|
||||
.get(provider_name)
|
||||
.expect("provider entry must exist")
|
||||
let entry = ensure_provider_config_mut(config, provider_name);
|
||||
&*entry
|
||||
}
|
||||
|
||||
/// Calculate absolute timeout for session data based on configuration
|
||||
@@ -1705,8 +1890,8 @@ mod tests {
|
||||
}
|
||||
|
||||
let mut config = Config::default();
|
||||
if let Some(ollama) = config.providers.get_mut("ollama") {
|
||||
ollama.api_key = Some("${OWLEN_TEST_API_KEY}".to_string());
|
||||
if let Some(ollama_local) = config.providers.get_mut("ollama_local") {
|
||||
ollama_local.api_key = Some("${OWLEN_TEST_API_KEY}".to_string());
|
||||
}
|
||||
|
||||
config
|
||||
@@ -1714,7 +1899,7 @@ mod tests {
|
||||
.expect("environment expansion succeeded");
|
||||
|
||||
assert_eq!(
|
||||
config.providers["ollama"].api_key.as_deref(),
|
||||
config.providers["ollama_local"].api_key.as_deref(),
|
||||
Some("super-secret")
|
||||
);
|
||||
|
||||
@@ -1730,8 +1915,8 @@ mod tests {
|
||||
}
|
||||
|
||||
let mut config = Config::default();
|
||||
if let Some(ollama) = config.providers.get_mut("ollama") {
|
||||
ollama.api_key = Some("${OWLEN_TEST_MISSING}".to_string());
|
||||
if let Some(ollama_local) = config.providers.get_mut("ollama_local") {
|
||||
ollama_local.api_key = Some("${OWLEN_TEST_MISSING}".to_string());
|
||||
}
|
||||
|
||||
let error = config
|
||||
@@ -1792,15 +1977,19 @@ mod tests {
|
||||
#[test]
|
||||
fn default_config_contains_local_provider() {
|
||||
let config = Config::default();
|
||||
assert!(config.providers.contains_key("ollama"));
|
||||
let provider = config.providers.get("ollama").unwrap();
|
||||
assert_eq!(
|
||||
provider
|
||||
.extra
|
||||
.get(OLLAMA_MODE_KEY)
|
||||
.and_then(|value| value.as_str()),
|
||||
Some("auto")
|
||||
);
|
||||
let local = config
|
||||
.providers
|
||||
.get("ollama_local")
|
||||
.expect("default local provider");
|
||||
assert!(local.enabled);
|
||||
assert_eq!(local.base_url.as_deref(), Some(OLLAMA_LOCAL_BASE_URL));
|
||||
|
||||
let cloud = config
|
||||
.providers
|
||||
.get("ollama_cloud")
|
||||
.expect("default cloud provider");
|
||||
assert!(!cloud.enabled);
|
||||
assert_eq!(cloud.api_key_env.as_deref(), Some(OLLAMA_CLOUD_API_KEY_ENV));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1808,16 +1997,10 @@ mod tests {
|
||||
let mut config = Config::default();
|
||||
config.providers.clear();
|
||||
let cloud = ensure_provider_config(&mut config, "ollama-cloud");
|
||||
assert_eq!(cloud.provider_type, "ollama");
|
||||
assert_eq!(cloud.base_url.as_deref(), Some("http://localhost:11434"));
|
||||
assert_eq!(
|
||||
cloud
|
||||
.extra
|
||||
.get(OLLAMA_MODE_KEY)
|
||||
.and_then(|value| value.as_str()),
|
||||
Some("auto")
|
||||
);
|
||||
assert!(config.providers.contains_key("ollama"));
|
||||
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!(config.providers.contains_key("ollama_cloud"));
|
||||
assert!(!config.providers.contains_key("ollama-cloud"));
|
||||
}
|
||||
|
||||
@@ -1828,48 +2011,100 @@ mod tests {
|
||||
config.providers.insert(
|
||||
"ollama_cloud".to_string(),
|
||||
ProviderConfig {
|
||||
enabled: true,
|
||||
provider_type: "ollama_cloud".to_string(),
|
||||
base_url: Some("https://api.ollama.com".to_string()),
|
||||
api_key: Some("secret".to_string()),
|
||||
api_key_env: None,
|
||||
extra: HashMap::new(),
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_schema_migrations("1.0.0");
|
||||
|
||||
assert!(config.providers.get("ollama_cloud").is_none());
|
||||
assert!(config.providers.get("ollama-cloud").is_none());
|
||||
let cloud = config.providers.get("ollama").expect("migrated config");
|
||||
assert_eq!(cloud.provider_type, "ollama");
|
||||
assert!(config.providers.get("ollama_cloud").is_some());
|
||||
let cloud = config
|
||||
.providers
|
||||
.get("ollama_cloud")
|
||||
.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.api_key.as_deref(), Some("secret"));
|
||||
assert_eq!(
|
||||
cloud
|
||||
.extra
|
||||
.get(OLLAMA_MODE_KEY)
|
||||
.and_then(|value| value.as_str()),
|
||||
Some("auto")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_sets_cloud_mode_for_cloud_base() {
|
||||
let mut config = Config::default();
|
||||
if let Some(ollama) = config.providers.get_mut("ollama") {
|
||||
if let Some(ollama) = config.providers.get_mut("ollama_local") {
|
||||
ollama.base_url = Some(OLLAMA_CLOUD_BASE_URL.to_string());
|
||||
ollama.extra.remove(OLLAMA_MODE_KEY);
|
||||
}
|
||||
|
||||
config.apply_schema_migrations("1.4.0");
|
||||
|
||||
let provider = config.providers.get("ollama").expect("ollama provider");
|
||||
assert_eq!(
|
||||
provider
|
||||
.extra
|
||||
.get(OLLAMA_MODE_KEY)
|
||||
.and_then(|value| value.as_str()),
|
||||
Some("cloud")
|
||||
let cloud = config
|
||||
.providers
|
||||
.get("ollama_cloud")
|
||||
.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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrate_legacy_monolithic_ollama_entry() {
|
||||
let mut config = Config::default();
|
||||
config.providers.clear();
|
||||
config.providers.insert(
|
||||
"ollama".to_string(),
|
||||
ProviderConfig {
|
||||
enabled: true,
|
||||
provider_type: "ollama".to_string(),
|
||||
base_url: Some(OLLAMA_LOCAL_BASE_URL.to_string()),
|
||||
api_key: None,
|
||||
api_key_env: None,
|
||||
extra: HashMap::new(),
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_schema_migrations("1.2.0");
|
||||
|
||||
let local = config
|
||||
.providers
|
||||
.get("ollama_local")
|
||||
.expect("local provider migrated");
|
||||
assert!(local.enabled);
|
||||
assert_eq!(local.base_url.as_deref(), Some(OLLAMA_LOCAL_BASE_URL));
|
||||
|
||||
let cloud = config
|
||||
.providers
|
||||
.get("ollama_cloud")
|
||||
.expect("cloud provider placeholder");
|
||||
assert!(!cloud.enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrate_legacy_provider_tables_moves_top_level_entries() {
|
||||
let mut document: toml::Value = toml::from_str(
|
||||
r#"
|
||||
[ollama]
|
||||
base_url = "http://localhost:11434"
|
||||
|
||||
[general]
|
||||
default_provider = "ollama"
|
||||
"#,
|
||||
)
|
||||
.expect("valid inline config");
|
||||
|
||||
migrate_legacy_provider_tables(&mut document);
|
||||
|
||||
let providers = document
|
||||
.get("providers")
|
||||
.and_then(|value| value.as_table())
|
||||
.expect("providers table present");
|
||||
assert!(providers.contains_key("ollama"));
|
||||
assert!(providers["ollama"].get("base_url").is_some());
|
||||
assert!(document.get("ollama").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -144,17 +144,57 @@ where
|
||||
/// Runtime configuration for a provider instance.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProviderConfig {
|
||||
/// Provider type identifier.
|
||||
/// Whether this provider should be activated.
|
||||
#[serde(default = "ProviderConfig::default_enabled")]
|
||||
pub enabled: bool,
|
||||
/// Provider type identifier used to resolve implementations.
|
||||
#[serde(default)]
|
||||
pub provider_type: String,
|
||||
/// Base URL for API calls.
|
||||
#[serde(default)]
|
||||
pub base_url: Option<String>,
|
||||
/// API key or token material.
|
||||
#[serde(default)]
|
||||
pub api_key: Option<String>,
|
||||
/// Environment variable holding the API key.
|
||||
#[serde(default)]
|
||||
pub api_key_env: Option<String>,
|
||||
/// Additional provider-specific configuration.
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl ProviderConfig {
|
||||
const fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Merge the current configuration with overrides from `other`.
|
||||
pub fn merge_from(&mut self, mut other: ProviderConfig) {
|
||||
self.enabled = other.enabled;
|
||||
|
||||
if !other.provider_type.is_empty() {
|
||||
self.provider_type = other.provider_type;
|
||||
}
|
||||
|
||||
if let Some(base_url) = other.base_url.take() {
|
||||
self.base_url = Some(base_url);
|
||||
}
|
||||
|
||||
if let Some(api_key) = other.api_key.take() {
|
||||
self.api_key = Some(api_key);
|
||||
}
|
||||
|
||||
if let Some(api_key_env) = other.api_key_env.take() {
|
||||
self.api_key_env = Some(api_key_env);
|
||||
}
|
||||
|
||||
if !other.extra.is_empty() {
|
||||
self.extra.extend(other.extra);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Static registry of providers available to the application.
|
||||
pub struct ProviderRegistry {
|
||||
providers: HashMap<String, Arc<dyn Provider>>,
|
||||
|
||||
@@ -1467,9 +1467,11 @@ mod tests {
|
||||
#[test]
|
||||
fn explicit_local_mode_overrides_api_key() {
|
||||
let mut config = ProviderConfig {
|
||||
enabled: true,
|
||||
provider_type: "ollama".to_string(),
|
||||
base_url: Some("http://localhost:11434".to_string()),
|
||||
api_key: Some("secret-key".to_string()),
|
||||
api_key_env: None,
|
||||
extra: HashMap::new(),
|
||||
};
|
||||
config.extra.insert(
|
||||
@@ -1486,9 +1488,11 @@ mod tests {
|
||||
#[test]
|
||||
fn auto_mode_prefers_explicit_local_base() {
|
||||
let config = ProviderConfig {
|
||||
enabled: true,
|
||||
provider_type: "ollama".to_string(),
|
||||
base_url: Some("http://localhost:11434".to_string()),
|
||||
api_key: Some("secret-key".to_string()),
|
||||
api_key_env: None,
|
||||
extra: HashMap::new(),
|
||||
};
|
||||
// simulate missing explicit mode; defaults to auto
|
||||
@@ -1502,9 +1506,11 @@ mod tests {
|
||||
#[test]
|
||||
fn auto_mode_with_api_key_and_no_local_probe_switches_to_cloud() {
|
||||
let mut config = ProviderConfig {
|
||||
enabled: true,
|
||||
provider_type: "ollama".to_string(),
|
||||
base_url: None,
|
||||
api_key: Some("secret-key".to_string()),
|
||||
api_key_env: None,
|
||||
extra: HashMap::new(),
|
||||
};
|
||||
config.extra.insert(
|
||||
@@ -1580,9 +1586,11 @@ fn auto_mode_with_api_key_and_successful_probe_prefers_local() {
|
||||
let _guard = ProbeOverrideGuard::set(Some(true));
|
||||
|
||||
let mut config = ProviderConfig {
|
||||
enabled: true,
|
||||
provider_type: "ollama".to_string(),
|
||||
base_url: None,
|
||||
api_key: Some("secret-key".to_string()),
|
||||
api_key_env: None,
|
||||
extra: HashMap::new(),
|
||||
};
|
||||
config.extra.insert(
|
||||
@@ -1603,9 +1611,11 @@ fn auto_mode_with_api_key_and_failed_probe_prefers_cloud() {
|
||||
let _guard = ProbeOverrideGuard::set(Some(false));
|
||||
|
||||
let mut config = ProviderConfig {
|
||||
enabled: true,
|
||||
provider_type: "ollama".to_string(),
|
||||
base_url: None,
|
||||
api_key: Some("secret-key".to_string()),
|
||||
api_key_env: None,
|
||||
extra: HashMap::new(),
|
||||
};
|
||||
config.extra.insert(
|
||||
@@ -1622,9 +1632,11 @@ fn auto_mode_with_api_key_and_failed_probe_prefers_cloud() {
|
||||
#[test]
|
||||
fn annotate_scope_status_adds_capabilities_for_unavailable_scopes() {
|
||||
let config = ProviderConfig {
|
||||
enabled: true,
|
||||
provider_type: "ollama".to_string(),
|
||||
base_url: Some("http://localhost:11434".to_string()),
|
||||
api_key: None,
|
||||
api_key_env: None,
|
||||
extra: HashMap::new(),
|
||||
};
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ fn provider_from_config() -> Result<Arc<dyn Provider>, RpcError> {
|
||||
})?;
|
||||
|
||||
match provider_cfg.provider_type.as_str() {
|
||||
"ollama" | "ollama-cloud" => {
|
||||
"ollama" | "ollama_cloud" => {
|
||||
let provider = OllamaProvider::from_config(&provider_cfg, Some(&config.general))
|
||||
.map_err(|e| {
|
||||
RpcError::internal_error(format!(
|
||||
@@ -153,10 +153,12 @@ fn create_provider() -> Result<Arc<dyn Provider>, RpcError> {
|
||||
}
|
||||
|
||||
fn canonical_provider_name(name: &str) -> String {
|
||||
if name.eq_ignore_ascii_case("ollama-cloud") {
|
||||
"ollama".to_string()
|
||||
} else {
|
||||
name.to_string()
|
||||
let normalized = name.trim().to_ascii_lowercase().replace('-', "_");
|
||||
match normalized.as_str() {
|
||||
"" => "ollama_local".to_string(),
|
||||
"ollama" | "ollama_local" => "ollama_local".to_string(),
|
||||
"ollama_cloud" => "ollama_cloud".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,9 @@ use crate::state::{
|
||||
use crate::toast::{Toast, ToastLevel, ToastManager};
|
||||
use crate::ui::format_tool_output;
|
||||
use crate::{commands, highlight};
|
||||
use owlen_core::config::{OLLAMA_CLOUD_BASE_URL, OLLAMA_CLOUD_ENDPOINT_KEY, OLLAMA_MODE_KEY};
|
||||
use owlen_core::config::{
|
||||
OLLAMA_CLOUD_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
|
||||
// imports `AgentExecutor` to avoid a circular dependency on `owlen-cli`.
|
||||
@@ -500,7 +502,7 @@ impl ChatApp {
|
||||
models: Vec::new(),
|
||||
provider_scope_status: HashMap::new(),
|
||||
available_providers: Vec::new(),
|
||||
selected_provider: "ollama".to_string(), // Default, will be updated in initialize_models
|
||||
selected_provider: "ollama_local".to_string(), // Default, will be updated in initialize_models
|
||||
selected_provider_index: 0,
|
||||
selected_model_item: None,
|
||||
model_selector_items: Vec::new(),
|
||||
@@ -4297,7 +4299,7 @@ impl ChatApp {
|
||||
self.recompute_available_providers();
|
||||
|
||||
if self.available_providers.is_empty() {
|
||||
self.available_providers.push("ollama".to_string());
|
||||
self.available_providers.push("ollama_local".to_string());
|
||||
}
|
||||
|
||||
if !config_model_provider.is_empty() {
|
||||
@@ -7419,14 +7421,14 @@ impl ChatApp {
|
||||
|
||||
for (name, provider_cfg) in provider_entries {
|
||||
let provider_type = provider_cfg.provider_type.to_ascii_lowercase();
|
||||
if provider_type != "ollama" && provider_type != "ollama-cloud" {
|
||||
if provider_type != "ollama" && provider_type != "ollama_cloud" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let canonical_name = if name.eq_ignore_ascii_case("ollama-cloud") {
|
||||
"ollama".to_string()
|
||||
} else {
|
||||
name.clone()
|
||||
let canonical_name = match name.trim().to_ascii_lowercase().as_str() {
|
||||
"ollama" | "ollama_local" => "ollama_local".to_string(),
|
||||
"ollama-cloud" | "ollama_cloud" => "ollama_cloud".to_string(),
|
||||
other => other.to_string(),
|
||||
};
|
||||
|
||||
// All providers communicate via MCP LLM server (Phase 10).
|
||||
@@ -7599,7 +7601,10 @@ impl ChatApp {
|
||||
for (idx, model) in entries {
|
||||
let canonical = model.id.to_string();
|
||||
let is_cloud_id = model.id.ends_with("-cloud");
|
||||
let priority = if matches!(provider_lower, "ollama" | "ollama-cloud") {
|
||||
let priority = if matches!(
|
||||
provider_lower,
|
||||
"ollama" | "ollama_local" | "ollama-cloud" | "ollama_cloud"
|
||||
) {
|
||||
match scope {
|
||||
ModelScope::Local => {
|
||||
if is_cloud_id {
|
||||
@@ -7651,7 +7656,7 @@ impl ChatApp {
|
||||
}
|
||||
|
||||
if providers.is_empty() {
|
||||
providers.insert("ollama".to_string());
|
||||
providers.insert("ollama_local".to_string());
|
||||
}
|
||||
|
||||
self.available_providers = providers.into_iter().collect();
|
||||
@@ -7693,7 +7698,7 @@ impl ChatApp {
|
||||
let mut items = Vec::new();
|
||||
|
||||
if self.available_providers.is_empty() {
|
||||
items.push(ModelSelectorItem::header("ollama", false));
|
||||
items.push(ModelSelectorItem::header("ollama_local", false));
|
||||
self.model_selector_items = items;
|
||||
return;
|
||||
}
|
||||
@@ -8074,7 +8079,7 @@ impl ChatApp {
|
||||
self.set_model_info_visible(false);
|
||||
self.recompute_available_providers();
|
||||
if self.available_providers.is_empty() {
|
||||
self.available_providers.push("ollama".to_string());
|
||||
self.available_providers.push("ollama_local".to_string());
|
||||
}
|
||||
self.rebuild_model_selector_items();
|
||||
self.selected_model_item = None;
|
||||
@@ -8093,7 +8098,7 @@ impl ChatApp {
|
||||
self.recompute_available_providers();
|
||||
|
||||
if self.available_providers.is_empty() {
|
||||
self.available_providers.push("ollama".to_string());
|
||||
self.available_providers.push("ollama_local".to_string());
|
||||
}
|
||||
|
||||
if !config_model_provider.is_empty() {
|
||||
@@ -8248,8 +8253,10 @@ impl ChatApp {
|
||||
let (existing_plain_api_key, normalized_endpoint_local, base_overridden_local) =
|
||||
if let Some(entry) = config.providers.get_mut(&options.provider) {
|
||||
let existing = entry.api_key.clone();
|
||||
if entry.provider_type != "ollama" {
|
||||
entry.provider_type = "ollama".to_string();
|
||||
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 requested = options
|
||||
.endpoint
|
||||
@@ -8271,12 +8278,6 @@ impl ChatApp {
|
||||
entry.base_url = Some(normalized_endpoint_local.clone());
|
||||
base_overridden_local = true;
|
||||
}
|
||||
if options.force_cloud_base_url {
|
||||
entry.extra.insert(
|
||||
OLLAMA_MODE_KEY.to_string(),
|
||||
Value::String("cloud".to_string()),
|
||||
);
|
||||
}
|
||||
(existing, normalized_endpoint_local, base_overridden_local)
|
||||
} else {
|
||||
return Err(anyhow!("Provider '{}' is not configured", options.provider));
|
||||
@@ -10666,7 +10667,7 @@ struct CloudSetupOptions {
|
||||
impl CloudSetupOptions {
|
||||
fn parse(args: &[&str]) -> Result<Self> {
|
||||
let mut options = CloudSetupOptions {
|
||||
provider: "ollama".to_string(),
|
||||
provider: "ollama_cloud".to_string(),
|
||||
endpoint: None,
|
||||
api_key: None,
|
||||
force_cloud_base_url: false,
|
||||
@@ -10712,7 +10713,7 @@ impl CloudSetupOptions {
|
||||
}
|
||||
|
||||
if options.provider.trim().is_empty() {
|
||||
options.provider = "ollama".to_string();
|
||||
options.provider = "ollama_cloud".to_string();
|
||||
}
|
||||
|
||||
options.provider = canonical_provider_name(&options.provider);
|
||||
@@ -10722,10 +10723,11 @@ impl CloudSetupOptions {
|
||||
}
|
||||
|
||||
fn canonical_provider_name(provider: &str) -> String {
|
||||
let normalized = provider.trim().replace('_', "-").to_ascii_lowercase();
|
||||
let normalized = provider.trim().to_ascii_lowercase().replace('-', "_");
|
||||
match normalized.as_str() {
|
||||
"" => "ollama".to_string(),
|
||||
"ollama-cloud" => "ollama".to_string(),
|
||||
"" => "ollama_cloud".to_string(),
|
||||
"ollama" => "ollama_cloud".to_string(),
|
||||
"ollama_cloud" => "ollama_cloud".to_string(),
|
||||
value => value.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user