feat(config): separate Ollama into local/cloud providers, add OpenAI & Anthropic defaults, bump schema version to 1.6.0
This commit is contained in:
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user