Skip to content

Commit 4b7866f

Browse files
committed
refactor(llm): enhance provider configuration handling
Improve the ConfigurationManager by adding deduplication and filtering logic for fallback chains and configured providers. Update environment variable handling to support both LLM_PROVIDER and DEFAULT_LLM_PROVIDER. Ensure only valid providers with proper API keys are considered configured, streamlining the provider selection process.
1 parent c254b71 commit 4b7866f

File tree

3 files changed

+96
-23
lines changed

3 files changed

+96
-23
lines changed

spoon_ai/llm/config.py

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -400,10 +400,15 @@ def get_default_provider(self) -> str:
400400
return provider
401401

402402
# 2. Check environment variable for explicit preference
403-
env_provider = os.getenv('DEFAULT_LLM_PROVIDER')
403+
env_provider = os.getenv("LLM_PROVIDER") or os.getenv("DEFAULT_LLM_PROVIDER")
404404
if env_provider:
405-
logger.info(f"Using provider from DEFAULT_LLM_PROVIDER: {env_provider}")
406-
return env_provider
405+
normalized = env_provider.strip().lower()
406+
if normalized:
407+
logger.info(
408+
"Using provider from environment (LLM_PROVIDER/DEFAULT_LLM_PROVIDER): %s",
409+
normalized,
410+
)
411+
return normalized
407412

408413
# 3. Intelligent selection based on available API keys and quality
409414
# Priority order: openai (GPT-4.1 default) -> anthropic -> openrouter -> deepseek -> gemini
@@ -435,20 +440,70 @@ def get_fallback_chain(self) -> List[str]:
435440
Returns:
436441
List[str]: List of provider names in fallback order
437442
"""
443+
def _dedupe_preserve_order(items: List[str]) -> List[str]:
444+
seen = set()
445+
result: List[str] = []
446+
for item in items:
447+
if item in seen:
448+
continue
449+
seen.add(item)
450+
result.append(item)
451+
return result
452+
453+
def _filter_available_providers(items: List[str]) -> List[str]:
454+
usable: List[str] = []
455+
for provider in items:
456+
try:
457+
config = self._get_provider_config_dict(provider)
458+
except Exception:
459+
# Invalid config (e.g. placeholder BASE_URL). Treat as unavailable.
460+
continue
461+
462+
api_key = config.get("api_key")
463+
if not api_key:
464+
continue
465+
if isinstance(api_key, str) and self._is_placeholder_value(api_key):
466+
continue
467+
usable.append(provider)
468+
return usable
469+
438470
# Check environment variable override first
439471
env_chain = os.getenv("LLM_FALLBACK_CHAIN")
440472
if env_chain:
441-
chain = [provider.strip() for provider in env_chain.split(",") if provider.strip()]
442-
if chain:
443-
logger.info(f"Using fallback chain from environment: {chain}")
444-
return chain
473+
raw = [provider.strip().lower() for provider in env_chain.split(",") if provider.strip()]
474+
chain = _dedupe_preserve_order(raw)
475+
filtered = _filter_available_providers(chain)
476+
if filtered:
477+
if filtered != chain:
478+
logger.info(
479+
"Filtered fallback chain to configured providers. requested=%s usable=%s",
480+
chain,
481+
filtered,
482+
)
483+
else:
484+
logger.info("Using fallback chain from environment: %s", filtered)
485+
return filtered
445486

446487
# Support programmatic cache injections (used by higher-level tooling)
447488
if self._config_cache and 'llm_settings' in self._config_cache:
448489
fallback_chain = self._config_cache['llm_settings'].get('fallback_chain')
449490
if isinstance(fallback_chain, list) and fallback_chain:
450-
logger.info(f"Using fallback chain from injected configuration: {fallback_chain}")
451-
return fallback_chain
491+
raw = [str(provider).strip().lower() for provider in fallback_chain if str(provider).strip()]
492+
chain = _dedupe_preserve_order(raw)
493+
filtered = _filter_available_providers(chain)
494+
if filtered:
495+
if filtered != chain:
496+
logger.info(
497+
"Filtered injected fallback chain to configured providers. requested=%s usable=%s",
498+
chain,
499+
filtered,
500+
)
501+
else:
502+
logger.info(
503+
"Using fallback chain from injected configuration: %s",
504+
filtered,
505+
)
506+
return filtered
452507

453508
# Fallback to intelligent selection based on available providers
454509
available_providers = self.get_available_providers_by_priority()
@@ -466,21 +521,40 @@ def list_configured_providers(self) -> List[str]:
466521
Returns:
467522
List[str]: List of provider names that have configuration
468523
"""
469-
providers = set()
524+
configured: set[str] = set()
470525

471-
# From injected configuration data
526+
# Candidate providers from injected configuration data (if present).
527+
candidates: set[str] = set()
472528
if self._config_cache:
473529
if 'providers' in self._config_cache:
474-
providers.update(self._config_cache['providers'].keys())
530+
candidates.update(str(k).strip().lower() for k in self._config_cache['providers'].keys())
475531
if 'api_keys' in self._config_cache:
476-
providers.update(self._config_cache['api_keys'].keys())
532+
candidates.update(str(k).strip().lower() for k in self._config_cache['api_keys'].keys())
477533

478-
# From environment variables
534+
# Candidate providers from environment variables.
479535
for provider in ['openai', 'anthropic', 'openrouter', 'deepseek', 'gemini']:
480-
if os.getenv(f'{provider.upper()}_API_KEY'):
481-
providers.add(provider)
536+
env_value = os.getenv(f'{provider.upper()}_API_KEY')
537+
if not env_value:
538+
continue
539+
if self._is_placeholder_value(env_value):
540+
continue
541+
candidates.add(provider)
542+
543+
# Only consider a provider "configured" if it can supply a valid api_key
544+
# after applying the full resolution logic (config cache -> env -> defaults).
545+
for provider in candidates:
546+
try:
547+
config = self._get_provider_config_dict(provider)
548+
except Exception:
549+
continue
550+
api_key = config.get("api_key")
551+
if not api_key:
552+
continue
553+
if isinstance(api_key, str) and self._is_placeholder_value(api_key):
554+
continue
555+
configured.add(provider)
482556

483-
return list(providers)
557+
return list(configured)
484558

485559
def get_available_providers_by_priority(self) -> List[str]:
486560
"""Get available providers ordered by priority and quality.

spoon_ai/llm/providers/ollama_provider.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,4 @@ async def cleanup(self) -> None:
298298
if self.client is not None:
299299
await self.client.aclose()
300300
self.client = None
301+

spoon_ai/rag/embeddings.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,13 +237,11 @@ def _derive_openrouter_embedding_model(base_model: str) -> str:
237237
from spoon_ai.llm.config import ConfigurationManager
238238

239239
cm = ConfigurationManager()
240+
available = set(cm.list_configured_providers())
240241
for p in ("openai", "openrouter", "gemini"):
241-
try:
242-
cm.load_provider_config(p)
243-
except Exception:
244-
continue
245-
provider_norm = p
246-
break
242+
if p in available:
243+
provider_norm = p
244+
break
247245

248246
# Finally, allow a custom OpenAI-compatible embeddings endpoint if explicitly configured.
249247
# This is checked after Gemini to match the desired priority.

0 commit comments

Comments
 (0)