""" Dynamic configuration class generator for JSON-based providers. """ from typing import Any, Coroutine, List, Literal, Optional, Tuple, Union, overload from litellm._logging import verbose_logger from litellm.litellm_core_utils.prompt_templates.common_utils import ( handle_messages_with_content_list_to_str_conversion, ) from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig from litellm.llms.openai_like.chat.transformation import OpenAILikeChatConfig from litellm.secret_managers.main import get_secret_str from litellm.types.llms.openai import AllMessageValues from .json_loader import SimpleProviderConfig def create_config_class(provider: SimpleProviderConfig): """Generate config class dynamically from JSON configuration""" # Choose base class base_class: type = ( OpenAIGPTConfig if provider.base_class == "openai_gpt" else OpenAILikeChatConfig ) class JSONProviderConfig(base_class): # type: ignore[valid-type,misc] @overload def _transform_messages( self, messages: List[AllMessageValues], model: str, is_async: Literal[True] ) -> Coroutine[Any, Any, List[AllMessageValues]]: ... @overload def _transform_messages( self, messages: List[AllMessageValues], model: str, is_async: Literal[False] = False, ) -> List[AllMessageValues]: ... def _transform_messages( self, messages: List[AllMessageValues], model: str, is_async: bool = False ) -> Union[List[AllMessageValues], Coroutine[Any, Any, List[AllMessageValues]]]: """Transform messages based on special_handling config""" # Handle content list to string conversion if configured if provider.special_handling.get("convert_content_list_to_string"): messages = handle_messages_with_content_list_to_str_conversion(messages) if is_async: return super()._transform_messages( messages=messages, model=model, is_async=True ) else: return super()._transform_messages( messages=messages, model=model, is_async=False ) def _get_openai_compatible_provider_info( self, api_base: Optional[str], api_key: Optional[str] ) -> Tuple[Optional[str], Optional[str]]: """Get API base and key from JSON config""" # Resolve base URL resolved_base = api_base if not resolved_base and provider.api_base_env: resolved_base = get_secret_str(provider.api_base_env) if not resolved_base: resolved_base = provider.base_url # Resolve API key resolved_key = api_key or get_secret_str(provider.api_key_env) return resolved_base, resolved_key def get_complete_url( self, api_base: Optional[str], api_key: Optional[str], model: str, optional_params: dict, litellm_params: dict, stream: Optional[bool] = None, ) -> str: """Build complete URL for the API endpoint""" if not api_base: api_base = provider.base_url if api_base is None: raise ValueError(f"api_base is required for provider {provider.slug}") if not api_base.endswith("/chat/completions"): api_base = f"{api_base}/chat/completions" return api_base def get_supported_openai_params(self, model: str) -> list: """Get supported OpenAI params, excluding tool-related params for models that don't support function calling.""" from litellm.utils import supports_function_calling supported_params = super().get_supported_openai_params(model=model) _supports_fc = supports_function_calling( model=model, custom_llm_provider=provider.slug ) if not _supports_fc: tool_params = [ "tools", "tool_choice", "function_call", "functions", "parallel_tool_calls", ] for param in tool_params: if param in supported_params: supported_params.remove(param) verbose_logger.debug( f"Model {model} on provider {provider.slug} does not support " f"function calling — removed tool-related params from supported params." ) return supported_params def map_openai_params( self, non_default_params: dict, optional_params: dict, model: str, drop_params: bool, ) -> dict: """Apply parameter mappings and constraints""" supported_params = self.get_supported_openai_params(model) # Apply supported params for param, value in non_default_params.items(): # Check parameter mappings first if param in provider.param_mappings: optional_params[provider.param_mappings[param]] = value elif param in supported_params: optional_params[param] = value # Apply temperature constraints if present if "temperature" in optional_params: temp = optional_params["temperature"] constraints = provider.constraints # Clamp to max if "temperature_max" in constraints: temp = min(temp, constraints["temperature_max"]) # Clamp to min if "temperature_min" in constraints: temp = max(temp, constraints["temperature_min"]) # Special case: temperature_min_with_n_gt_1 if "temperature_min_with_n_gt_1" in constraints: n = optional_params.get("n", 1) if n > 1 and temp < constraints["temperature_min_with_n_gt_1"]: temp = constraints["temperature_min_with_n_gt_1"] optional_params["temperature"] = temp return optional_params @property def custom_llm_provider(self) -> Optional[str]: return provider.slug return JSONProviderConfig _responses_config_cache: dict = {} def create_responses_config_class(provider: SimpleProviderConfig): """Generate a Responses API config class dynamically from JSON configuration. Parallel to create_config_class() but for /v1/responses endpoints. Classes are cached per provider slug to avoid regeneration on every request. """ if provider.slug in _responses_config_cache: return _responses_config_cache[provider.slug] from litellm.llms.openai_like.responses.transformation import ( OpenAILikeResponsesConfig, ) from litellm.types.router import GenericLiteLLMParams class JSONProviderResponsesConfig(OpenAILikeResponsesConfig): @property def custom_llm_provider(self): # type: ignore[override] return provider.slug def validate_environment( self, headers: dict, model: str, litellm_params: Optional[GenericLiteLLMParams], ) -> dict: litellm_params = litellm_params or GenericLiteLLMParams() api_key = litellm_params.api_key or get_secret_str(provider.api_key_env) if api_key: headers["Authorization"] = f"Bearer {api_key}" return headers def get_complete_url( self, api_base: Optional[str], litellm_params: dict, ) -> str: if not api_base: if provider.api_base_env: api_base = get_secret_str(provider.api_base_env) if not api_base: api_base = provider.base_url if api_base is None: raise ValueError(f"api_base is required for provider {provider.slug}") api_base = api_base.rstrip("/") return f"{api_base}/responses" _responses_config_cache[provider.slug] = JSONProviderResponsesConfig return JSONProviderResponsesConfig