@@ -53,6 +53,9 @@ class LLM:
5353 Anthropic, and others through LiteLLM.
5454 """
5555
56+ # Class-level flag for one-time logging configuration
57+ _logging_configured = False
58+
5659 # Default window sizes for different models (75% of actual to be safe)
5760 MODEL_WINDOWS = {
5861 # OpenAI
@@ -103,6 +106,57 @@ class LLM:
103106 # Ollama iteration threshold for summary generation
104107 OLLAMA_SUMMARY_ITERATION_THRESHOLD = 1
105108
109+ @classmethod
110+ def _configure_logging (cls ):
111+ """Configure logging settings once for all LLM instances."""
112+ try :
113+ import litellm
114+ # Disable telemetry
115+ litellm .telemetry = False
116+
117+ # Set litellm options globally
118+ litellm .set_verbose = False
119+ litellm .success_callback = []
120+ litellm ._async_success_callback = []
121+ litellm .callbacks = []
122+
123+ # Suppress all litellm debug info
124+ litellm .suppress_debug_info = True
125+ if hasattr (litellm , '_logging' ):
126+ litellm ._logging ._disable_debugging ()
127+
128+ # Always suppress litellm's internal debug messages
129+ logging .getLogger ("litellm.utils" ).setLevel (logging .WARNING )
130+ logging .getLogger ("litellm.main" ).setLevel (logging .WARNING )
131+ logging .getLogger ("litellm.litellm_logging" ).setLevel (logging .WARNING )
132+ logging .getLogger ("litellm.transformation" ).setLevel (logging .WARNING )
133+
134+ # Allow httpx logging when LOGLEVEL=debug, otherwise suppress it
135+ loglevel = os .environ .get ('LOGLEVEL' , 'INFO' ).upper ()
136+ if loglevel == 'DEBUG' :
137+ logging .getLogger ("litellm.llms.custom_httpx.http_handler" ).setLevel (logging .INFO )
138+ else :
139+ logging .getLogger ("litellm.llms.custom_httpx.http_handler" ).setLevel (logging .WARNING )
140+
141+ # Keep asyncio at WARNING unless explicitly in high debug mode
142+ logging .getLogger ("asyncio" ).setLevel (logging .WARNING )
143+ logging .getLogger ("selector_events" ).setLevel (logging .WARNING )
144+
145+ # Enable error dropping for cleaner output
146+ litellm .drop_params = True
147+ # Enable parameter modification for providers like Anthropic
148+ litellm .modify_params = True
149+
150+ if hasattr (litellm , '_logging' ):
151+ litellm ._logging ._disable_debugging ()
152+ warnings .filterwarnings ("ignore" , category = RuntimeWarning )
153+
154+ cls ._logging_configured = True
155+
156+ except ImportError :
157+ # If litellm not installed, we'll handle it in __init__
158+ pass
159+
106160 def _log_llm_config (self , method_name : str , ** config ):
107161 """Centralized debug logging for LLM configuration and parameters.
108162
@@ -186,47 +240,13 @@ def __init__(
186240 events : List [Any ] = [],
187241 ** extra_settings
188242 ):
243+ # Configure logging only once at the class level
244+ if not LLM ._logging_configured :
245+ LLM ._configure_logging ()
246+
247+ # Import litellm after logging is configured
189248 try :
190249 import litellm
191- # Disable telemetry
192- litellm .telemetry = False
193-
194- # Set litellm options globally
195- litellm .set_verbose = False
196- litellm .success_callback = []
197- litellm ._async_success_callback = []
198- litellm .callbacks = []
199-
200- # Suppress all litellm debug info
201- litellm .suppress_debug_info = True
202- if hasattr (litellm , '_logging' ):
203- litellm ._logging ._disable_debugging ()
204-
205- verbose = extra_settings .get ('verbose' , True )
206-
207- # Always suppress litellm's internal debug messages
208- # These are from external libraries and not useful for debugging user code
209- logging .getLogger ("litellm.utils" ).setLevel (logging .WARNING )
210- logging .getLogger ("litellm.main" ).setLevel (logging .WARNING )
211-
212- # Allow httpx logging when LOGLEVEL=debug, otherwise suppress it
213- loglevel = os .environ .get ('LOGLEVEL' , 'INFO' ).upper ()
214- if loglevel == 'DEBUG' :
215- logging .getLogger ("litellm.llms.custom_httpx.http_handler" ).setLevel (logging .INFO )
216- else :
217- logging .getLogger ("litellm.llms.custom_httpx.http_handler" ).setLevel (logging .WARNING )
218-
219- logging .getLogger ("litellm.litellm_logging" ).setLevel (logging .WARNING )
220- logging .getLogger ("litellm.transformation" ).setLevel (logging .WARNING )
221- litellm .suppress_debug_messages = True
222- if hasattr (litellm , '_logging' ):
223- litellm ._logging ._disable_debugging ()
224- warnings .filterwarnings ("ignore" , category = RuntimeWarning )
225-
226- # Keep asyncio at WARNING unless explicitly in high debug mode
227- logging .getLogger ("asyncio" ).setLevel (logging .WARNING )
228- logging .getLogger ("selector_events" ).setLevel (logging .WARNING )
229-
230250 except ImportError :
231251 raise ImportError (
232252 "LiteLLM is required but not installed. "
@@ -252,9 +272,9 @@ def __init__(
252272 self .base_url = base_url
253273 self .events = events
254274 self .extra_settings = extra_settings
255- self .console = Console ()
275+ self ._console = None # Lazy load console when needed
256276 self .chat_history = []
257- self .verbose = verbose
277+ self .verbose = extra_settings . get ( ' verbose' , True )
258278 self .markdown = extra_settings .get ('markdown' , True )
259279 self .self_reflect = extra_settings .get ('self_reflect' , False )
260280 self .max_reflect = extra_settings .get ('max_reflect' , 3 )
@@ -267,7 +287,12 @@ def __init__(
267287 self .session_token_metrics : Optional [TokenMetrics ] = None
268288 self .current_agent_name : Optional [str ] = None
269289
290+ # Cache for formatted tools and messages
291+ self ._formatted_tools_cache = {}
292+ self ._max_cache_size = 100
293+
270294 # Enable error dropping for cleaner output
295+ import litellm
271296 litellm .drop_params = True
272297 # Enable parameter modification for providers like Anthropic
273298 litellm .modify_params = True
@@ -301,6 +326,14 @@ def __init__(
301326 reasoning_steps = self .reasoning_steps ,
302327 extra_settings = self .extra_settings
303328 )
329+
330+ @property
331+ def console (self ):
332+ """Lazily initialize Rich Console only when needed."""
333+ if self ._console is None :
334+ from rich .console import Console
335+ self ._console = Console ()
336+ return self ._console
304337
305338 def _is_ollama_provider (self ) -> bool :
306339 """Detect if this is an Ollama provider regardless of naming convention"""
@@ -733,6 +766,29 @@ def _fix_array_schemas(self, schema: Dict) -> Dict:
733766
734767 return fixed_schema
735768
769+ def _get_tools_cache_key (self , tools ):
770+ """Generate a cache key for tools list."""
771+ if tools is None :
772+ return "none"
773+ if not tools :
774+ return "empty"
775+ # Create a simple hash based on tool names/content
776+ tool_parts = []
777+ for tool in tools :
778+ if isinstance (tool , dict ) and 'type' in tool and tool ['type' ] == 'function' :
779+ if 'function' in tool and isinstance (tool ['function' ], dict ) and 'name' in tool ['function' ]:
780+ tool_parts .append (f"openai:{ tool ['function' ]['name' ]} " )
781+ elif callable (tool ) and hasattr (tool , '__name__' ):
782+ tool_parts .append (f"callable:{ tool .__name__ } " )
783+ elif isinstance (tool , str ):
784+ tool_parts .append (f"string:{ tool } " )
785+ elif isinstance (tool , dict ) and len (tool ) == 1 :
786+ tool_name = next (iter (tool .keys ()))
787+ tool_parts .append (f"gemini:{ tool_name } " )
788+ else :
789+ tool_parts .append (f"other:{ id (tool )} " )
790+ return "|" .join (sorted (tool_parts ))
791+
736792 def _format_tools_for_litellm (self , tools : Optional [List [Any ]]) -> Optional [List [Dict ]]:
737793 """Format tools for LiteLLM - handles all tool formats.
738794
@@ -751,6 +807,11 @@ def _format_tools_for_litellm(self, tools: Optional[List[Any]]) -> Optional[List
751807 """
752808 if not tools :
753809 return None
810+
811+ # Check cache first
812+ tools_key = self ._get_tools_cache_key (tools )
813+ if tools_key in self ._formatted_tools_cache :
814+ return self ._formatted_tools_cache [tools_key ]
754815
755816 formatted_tools = []
756817 for tool in tools :
@@ -808,8 +869,12 @@ def _format_tools_for_litellm(self, tools: Optional[List[Any]]) -> Optional[List
808869 except (TypeError , ValueError ) as e :
809870 logging .error (f"Tools are not JSON serializable: { e } " )
810871 return None
811-
812- return formatted_tools if formatted_tools else None
872+
873+ # Cache the formatted tools
874+ result = formatted_tools if formatted_tools else None
875+ if len (self ._formatted_tools_cache ) < self ._max_cache_size :
876+ self ._formatted_tools_cache [tools_key ] = result
877+ return result
813878
814879 def get_response (
815880 self ,
@@ -956,7 +1021,7 @@ def get_response(
9561021
9571022 # Track token usage
9581023 if self .metrics :
959- self ._track_token_usage (final_response , model )
1024+ self ._track_token_usage (final_response , self . model )
9601025
9611026 # Execute callbacks and display based on verbose setting
9621027 generation_time_val = time .time () - current_time
0 commit comments