@@ -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.
0 commit comments