Add support for multiple providers of same type
Features: - Multiple OpenAI instances (e.g., OpenAI + local AI proxy) - Multiple Ollama instances (e.g., local + remote) - API key not required if at least one provider works without it - Provider instances can have optional names for reference Config format (TOML arrays): [[providers.openai]] name = "openai-main" api_key = "sk-..." base_url = "https://api.openai.com/v1" [[providers.openai]] name = "local-ai" base_url = "http://localhost:8080/v1" [[providers.ollama]] name = "local" base_url = "http://localhost:11434" [[providers.ollama]] name = "remote" base_url = "http://192.168.1.100:11434" bearer_token = "token" Changes: - config/types.rs: Changed providers from Option<T> to Vec<T> - config/loader.rs: Updated merge logic for provider arrays - providers/registry.rs: Register all provider instances, skip those without API keys if alternatives exist Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
use super::{Config, ConfigError, Result, OpenAiConfig, OllamaConfig, AgentConfig, AgentModels};
|
use super::{Config, ConfigError, Result, OpenAiConfig, OllamaConfig, AgentConfig, AgentModels};
|
||||||
use super::types::{default_openai_base_url, default_openai_model, default_ollama_base_url, default_ollama_model};
|
use super::types::{default_openai_base_url, default_openai_model, default_ollama_base_url, default_ollama_model};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
/// Configuration loader
|
/// Configuration loader
|
||||||
@@ -138,17 +137,20 @@ fn find_local_config() -> Option<PathBuf> {
|
|||||||
/// Merge environment variables into config
|
/// Merge environment variables into config
|
||||||
impl Config {
|
impl Config {
|
||||||
fn merge_env(&mut self) {
|
fn merge_env(&mut self) {
|
||||||
// OpenAI API key from environment
|
// OpenAI API key from environment (applies to first instance without key)
|
||||||
if let Some(ref mut openai) = self.providers.openai {
|
for openai in &mut self.providers.openai {
|
||||||
if openai.api_key.is_none() {
|
if openai.api_key.is_none() {
|
||||||
openai.api_key = std::env::var("OPENAI_API_KEY").ok();
|
if let Ok(key) = std::env::var("OPENAI_API_KEY") {
|
||||||
|
openai.api_key = Some(key);
|
||||||
|
break; // Only apply to first one
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ollama base URL from environment
|
// Ollama base URL from environment (applies to all instances)
|
||||||
if let Some(ref mut ollama) = self.providers.ollama {
|
for ollama in &mut self.providers.ollama {
|
||||||
if let Ok(url) = std::env::var("OLLAMA_HOST") {
|
if let Ok(url) = std::env::var("OLLAMA_HOST") {
|
||||||
if !url.is_empty() {
|
if !url.is_empty() && ollama.base_url == default_ollama_base_url() {
|
||||||
ollama.base_url = url;
|
ollama.base_url = url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,10 +159,16 @@ impl Config {
|
|||||||
|
|
||||||
/// Merge another config into this one (other overrides self)
|
/// Merge another config into this one (other overrides self)
|
||||||
fn merge(&mut self, other: Config) {
|
fn merge(&mut self, other: Config) {
|
||||||
// Merge providers
|
// Merge OpenAI providers (extend list)
|
||||||
if let Some(other_openai) = other.providers.openai {
|
for other_openai in other.providers.openai {
|
||||||
if let Some(ref mut self_openai) = self.providers.openai {
|
// Check if we should merge with existing or add new
|
||||||
// Merge OpenAI configs
|
let existing = self.providers.openai.iter_mut().find(|p| {
|
||||||
|
p.name == other_openai.name ||
|
||||||
|
(p.name.is_none() && other_openai.name.is_none())
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(self_openai) = existing {
|
||||||
|
// Merge with existing provider
|
||||||
if other_openai.api_key.is_some() {
|
if other_openai.api_key.is_some() {
|
||||||
self_openai.api_key = other_openai.api_key;
|
self_openai.api_key = other_openai.api_key;
|
||||||
}
|
}
|
||||||
@@ -171,13 +179,20 @@ impl Config {
|
|||||||
self_openai.default_model = other_openai.default_model;
|
self_openai.default_model = other_openai.default_model;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.providers.openai = Some(other_openai);
|
// Add as new provider
|
||||||
|
self.providers.openai.push(other_openai);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(other_ollama) = other.providers.ollama {
|
// Merge Ollama providers (extend list)
|
||||||
if let Some(ref mut self_ollama) = self.providers.ollama {
|
for other_ollama in other.providers.ollama {
|
||||||
// Merge Ollama configs
|
let existing = self.providers.ollama.iter_mut().find(|p| {
|
||||||
|
p.name == other_ollama.name ||
|
||||||
|
(p.name.is_none() && other_ollama.name.is_none())
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(self_ollama) = existing {
|
||||||
|
// Merge with existing provider
|
||||||
if other_ollama.base_url != default_ollama_base_url() {
|
if other_ollama.base_url != default_ollama_base_url() {
|
||||||
self_ollama.base_url = other_ollama.base_url;
|
self_ollama.base_url = other_ollama.base_url;
|
||||||
}
|
}
|
||||||
@@ -194,7 +209,8 @@ impl Config {
|
|||||||
self_ollama.password = other_ollama.password;
|
self_ollama.password = other_ollama.password;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.providers.ollama = Some(other_ollama);
|
// Add as new provider
|
||||||
|
self.providers.ollama.push(other_ollama);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,13 +265,17 @@ fn default_agent_config() -> AgentConfig {
|
|||||||
pub fn create_default_config() -> Result<Config> {
|
pub fn create_default_config() -> Result<Config> {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
|
|
||||||
config.providers.openai = Some(OpenAiConfig {
|
// Add default OpenAI provider
|
||||||
api_key: None, // User must set this
|
config.providers.openai.push(OpenAiConfig {
|
||||||
|
name: Some("default".to_string()),
|
||||||
|
api_key: None, // User must set this or use env var
|
||||||
base_url: default_openai_base_url(),
|
base_url: default_openai_base_url(),
|
||||||
default_model: default_openai_model(),
|
default_model: default_openai_model(),
|
||||||
});
|
});
|
||||||
|
|
||||||
config.providers.ollama = Some(OllamaConfig {
|
// Add default Ollama provider
|
||||||
|
config.providers.ollama.push(OllamaConfig {
|
||||||
|
name: Some("default".to_string()),
|
||||||
base_url: default_ollama_base_url(),
|
base_url: default_ollama_base_url(),
|
||||||
default_model: default_ollama_model(),
|
default_model: default_ollama_model(),
|
||||||
bearer_token: None,
|
bearer_token: None,
|
||||||
|
|||||||
@@ -28,18 +28,22 @@ fn default_agent() -> String {
|
|||||||
/// Provider configurations
|
/// Provider configurations
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct ProvidersConfig {
|
pub struct ProvidersConfig {
|
||||||
/// OpenAI provider config
|
/// OpenAI provider instances (can have multiple)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub openai: Option<OpenAiConfig>,
|
pub openai: Vec<OpenAiConfig>,
|
||||||
|
|
||||||
/// Ollama provider config
|
/// Ollama provider instances (can have multiple)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ollama: Option<OllamaConfig>,
|
pub ollama: Vec<OllamaConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// OpenAI configuration
|
/// Named OpenAI configuration
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct OpenAiConfig {
|
pub struct OpenAiConfig {
|
||||||
|
/// Name for this provider instance (optional, for reference)
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
|
||||||
/// API key (can also be set via OPENAI_API_KEY env var)
|
/// API key (can also be set via OPENAI_API_KEY env var)
|
||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
|
|
||||||
@@ -63,6 +67,7 @@ pub fn default_openai_model() -> String {
|
|||||||
impl Default for OpenAiConfig {
|
impl Default for OpenAiConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
name: None,
|
||||||
api_key: None,
|
api_key: None,
|
||||||
base_url: default_openai_base_url(),
|
base_url: default_openai_base_url(),
|
||||||
default_model: default_openai_model(),
|
default_model: default_openai_model(),
|
||||||
@@ -70,9 +75,13 @@ impl Default for OpenAiConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ollama configuration
|
/// Named Ollama configuration
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct OllamaConfig {
|
pub struct OllamaConfig {
|
||||||
|
/// Name for this provider instance (optional, for reference)
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
|
||||||
/// Base URL
|
/// Base URL
|
||||||
#[serde(default = "default_ollama_base_url")]
|
#[serde(default = "default_ollama_base_url")]
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
@@ -105,6 +114,7 @@ pub fn default_ollama_model() -> String {
|
|||||||
impl Default for OllamaConfig {
|
impl Default for OllamaConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
name: None,
|
||||||
base_url: default_ollama_base_url(),
|
base_url: default_ollama_base_url(),
|
||||||
default_model: default_ollama_model(),
|
default_model: default_ollama_model(),
|
||||||
bearer_token: None,
|
bearer_token: None,
|
||||||
|
|||||||
@@ -16,27 +16,50 @@ impl ProviderRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a registry from configuration
|
/// Create a registry from configuration
|
||||||
|
/// Does not require API key if at least one provider is configured
|
||||||
pub fn from_config(config: &Config) -> Result<Self> {
|
pub fn from_config(config: &Config) -> Result<Self> {
|
||||||
let mut registry = Self::new();
|
let mut registry = Self::new();
|
||||||
|
let mut has_any_provider = false;
|
||||||
|
|
||||||
// Register OpenAI provider if configured
|
// Register all OpenAI provider instances
|
||||||
if let Some(ref openai_config) = config.providers.openai {
|
for (i, openai_config) in config.providers.openai.iter().enumerate() {
|
||||||
|
let name = openai_config
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| format!("openai-{}", i + 1));
|
||||||
|
let registry_name = if i == 0 { "openai".to_string() } else { name.clone() };
|
||||||
|
|
||||||
|
// Get API key from config or environment
|
||||||
let api_key = openai_config
|
let api_key = openai_config
|
||||||
.api_key
|
.api_key
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or_else(|| ProviderError::AuthError("OpenAI API key not configured".to_string()))?;
|
.or_else(|| std::env::var("OPENAI_API_KEY").ok());
|
||||||
|
|
||||||
let provider = OpenAiProvider::new(
|
if let Some(key) = api_key {
|
||||||
&openai_config.base_url,
|
let provider = OpenAiProvider::new(
|
||||||
&api_key,
|
&openai_config.base_url,
|
||||||
&openai_config.default_model,
|
&key,
|
||||||
);
|
&openai_config.default_model,
|
||||||
|
);
|
||||||
registry.register("openai", Arc::new(provider));
|
registry.register(®istry_name, Arc::new(provider));
|
||||||
|
has_any_provider = true;
|
||||||
|
} else if config.providers.ollama.is_empty() {
|
||||||
|
// Only error if no other providers are available
|
||||||
|
return Err(ProviderError::AuthError(
|
||||||
|
"OpenAI API key not configured. Set api_key in config or OPENAI_API_KEY env var, or configure Ollama provider.".to_string()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// If no API key but Ollama is available, just skip this OpenAI instance
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register Ollama provider if configured
|
// Register all Ollama provider instances
|
||||||
if let Some(ref ollama_config) = config.providers.ollama {
|
for (i, ollama_config) in config.providers.ollama.iter().enumerate() {
|
||||||
|
let name = ollama_config
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| format!("ollama-{}", i + 1));
|
||||||
|
let registry_name = if i == 0 { "ollama".to_string() } else { name.clone() };
|
||||||
|
|
||||||
let mut provider = OllamaProvider::new(
|
let mut provider = OllamaProvider::new(
|
||||||
&ollama_config.base_url,
|
&ollama_config.base_url,
|
||||||
&ollama_config.default_model,
|
&ollama_config.default_model,
|
||||||
@@ -50,7 +73,14 @@ impl ProviderRegistry {
|
|||||||
provider = provider.with_basic_auth(username, password);
|
provider = provider.with_basic_auth(username, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.register("ollama", Arc::new(provider));
|
registry.register(®istry_name, Arc::new(provider));
|
||||||
|
has_any_provider = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_any_provider && config.providers.openai.is_empty() && config.providers.ollama.is_empty() {
|
||||||
|
return Err(ProviderError::AuthError(
|
||||||
|
"No providers configured. Add at least one OpenAI or Ollama provider.".to_string()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(registry)
|
Ok(registry)
|
||||||
@@ -80,11 +110,11 @@ impl ProviderRegistry {
|
|||||||
Ok((provider, model_name.to_string()))
|
Ok((provider, model_name.to_string()))
|
||||||
} else {
|
} else {
|
||||||
// Try to find a provider that has this model as default
|
// Try to find a provider that has this model as default
|
||||||
// For now, default to openai if available
|
// Prefer Ollama for local-first
|
||||||
if let Some(provider) = self.get("openai") {
|
if let Some(provider) = self.get("ollama") {
|
||||||
return Ok((provider, model_spec.to_string()));
|
return Ok((provider, model_spec.to_string()));
|
||||||
}
|
}
|
||||||
if let Some(provider) = self.get("ollama") {
|
if let Some(provider) = self.get("openai") {
|
||||||
return Ok((provider, model_spec.to_string()));
|
return Ok((provider, model_spec.to_string()));
|
||||||
}
|
}
|
||||||
Err(ProviderError::UnknownProvider(
|
Err(ProviderError::UnknownProvider(
|
||||||
@@ -109,6 +139,11 @@ impl ProviderRegistry {
|
|||||||
|
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if any provider is available
|
||||||
|
pub fn has_providers(&self) -> bool {
|
||||||
|
!self.providers.is_empty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProviderRegistry {
|
impl Default for ProviderRegistry {
|
||||||
|
|||||||
Reference in New Issue
Block a user