@@ -67,6 +67,8 @@ def route_chatbot(model: str) -> tuple[type, str]:
6767 return GPTBot , chatbot_model
6868 elif chatbot_type == "anthropic" :
6969 return ClaudeBot , chatbot_model
70+ elif chatbot_type == "litellm" :
71+ return LiteLLMBot , chatbot_model
7072 else :
7173 raise ValueError (f"Invalid chatbot type { chatbot_type } ." )
7274
@@ -260,7 +262,6 @@ def __init__(
260262 api_key : str | None = None ,
261263 extra_body : dict | None = None ,
262264 ):
263-
264265 # clamp temperature to 0-2
265266 temperature = max (0 , min (2 , temperature ))
266267
@@ -459,7 +460,6 @@ def __init__(
459460 api_key : str | None = None ,
460461 extra_body : dict | None = None ,
461462 ):
462-
463463 # clamp temperature to 0-1
464464 temperature = max (0 , min (1 , temperature ))
465465
@@ -774,4 +774,149 @@ def close(self):
774774 self .client .close ()
775775
776776
777- provider2chatbot = {ModelProvider .OPENAI : GPTBot , ModelProvider .ANTHROPIC : ClaudeBot , ModelProvider .GOOGLE : GeminiBot }
777+ class LiteLLMBot (ChatBot ):
778+ """ChatBot backed by the LiteLLM SDK, routing to 100+ LLM providers."""
779+
780+ def __init__ (
781+ self ,
782+ model_name : str = "openai/gpt-4o" ,
783+ temperature : float = 1 ,
784+ top_p : float = 1 ,
785+ retry : int = 8 ,
786+ max_async : int = 16 ,
787+ fee_limit : float = 0.8 ,
788+ proxy : str | None = None ,
789+ base_url_config : dict | None = None ,
790+ api_key : str | None = None ,
791+ extra_body : dict | None = None ,
792+ ):
793+ temperature = max (0 , min (2 , temperature ))
794+ super ().__init__ (model_name , temperature , top_p , retry , max_async , fee_limit )
795+ self .api_key = api_key
796+ self .api_base = (base_url_config or {}).get ("litellm" )
797+ self .extra_body = dict (extra_body ) if extra_body else {}
798+ if proxy :
799+ logger .warning (
800+ "LiteLLMBot does not support per-instance proxy. "
801+ "Set HTTP_PROXY/HTTPS_PROXY environment variables instead."
802+ )
803+
804+ def update_fee (self , response ):
805+ usage = getattr (response , "usage" , None )
806+ if usage is None :
807+ return
808+ prompt_tokens = getattr (usage , "prompt_tokens" , 0 ) or 0
809+ completion_tokens = getattr (usage , "completion_tokens" , 0 ) or 0
810+ self .api_fees [- 1 ] += (
811+ prompt_tokens * self .model_info .input_price + completion_tokens * self .model_info .output_price
812+ ) / 1000000
813+
814+ def get_content (self , response ):
815+ content = response .choices [0 ].message .content
816+ return content if content is not None else ""
817+
818+ def _create_chat (
819+ self ,
820+ messages : list [dict ],
821+ stop_sequences : list [str ] | None = None ,
822+ output_checker : Callable = lambda user_input , generated_content : True ,
823+ temperature : float | None = None ,
824+ top_p : float | None = None ,
825+ ):
826+ try :
827+ import litellm
828+ except ImportError :
829+ raise ImportError (
830+ "litellm is required for the litellm: provider. Install with: pip install 'openlrc[litellm]'"
831+ )
832+
833+ effective_temperature = temperature if temperature is not None else self .temperature
834+ effective_top_p = top_p if top_p is not None else self .top_p
835+
836+ completion_kwargs : dict = {
837+ "model" : self .model_name ,
838+ "messages" : messages ,
839+ "temperature" : effective_temperature ,
840+ "max_tokens" : self ._compute_max_tokens (messages ),
841+ "drop_params" : True ,
842+ }
843+ completion_kwargs ["top_p" ] = effective_top_p
844+ if stop_sequences :
845+ completion_kwargs ["stop" ] = stop_sequences
846+ if self .api_key :
847+ completion_kwargs ["api_key" ] = self .api_key
848+ if self .api_base :
849+ completion_kwargs ["api_base" ] = self .api_base
850+ completion_kwargs .update (self .extra_body )
851+
852+ response = None
853+ validated = False
854+ for i in range (self .retry ):
855+ try :
856+ response = litellm .completion (** completion_kwargs )
857+ self .update_fee (response )
858+
859+ if response .choices [0 ].finish_reason == "length" :
860+ usage = getattr (response , "usage" , None )
861+ raise LengthExceedException (
862+ prompt_tokens = getattr (usage , "prompt_tokens" , - 1 ) if usage else - 1 ,
863+ completion_tokens = getattr (usage , "completion_tokens" , - 1 ) if usage else - 1 ,
864+ total_tokens = getattr (usage , "total_tokens" , - 1 ) if usage else - 1 ,
865+ )
866+
867+ response_text = remove_stop (self .get_content (response ), stop_sequences )
868+
869+ if not output_checker (messages [- 1 ]["content" ], response_text ):
870+ logger .warning (f"Invalid response format. Retry num: { i + 1 } ." )
871+ continue
872+
873+ validated = True
874+ break
875+ except litellm .AuthenticationError as e :
876+ raise ChatBotException (f"Authentication failed: { e } " ) from e
877+ except (
878+ litellm .BadRequestError ,
879+ litellm .NotFoundError ,
880+ litellm .PermissionDeniedError ,
881+ litellm .UnprocessableEntityError ,
882+ ) as e :
883+ raise ChatBotException (f"Client error: { e } " ) from e
884+ except (
885+ litellm .RateLimitError ,
886+ litellm .APIConnectionError ,
887+ litellm .InternalServerError ,
888+ litellm .ServiceUnavailableError ,
889+ litellm .Timeout ,
890+ json .decoder .JSONDecodeError ,
891+ ) as e :
892+ sleep_time = self ._get_sleep_time (e )
893+ logger .warning (f"{ type (e ).__name__ } : { e } . Wait { sleep_time } s before retry. Retry num: { i + 1 } ." )
894+ time .sleep (sleep_time )
895+
896+ if not response :
897+ raise ChatBotException ("Failed to create a chat." )
898+
899+ if not validated :
900+ logger .warning ("Response format validation failed after all retries, returning best-effort response." )
901+
902+ return response
903+
904+ @staticmethod
905+ def _get_sleep_time (error ):
906+ qualname = type (error ).__name__
907+ if qualname == "RateLimitError" :
908+ return random .randint (30 , 60 )
909+ elif qualname in ("Timeout" , "APIConnectionError" ):
910+ return 3
911+ elif qualname == "JSONDecodeError" :
912+ return 1
913+ else :
914+ return 15
915+
916+
917+ provider2chatbot = {
918+ ModelProvider .OPENAI : GPTBot ,
919+ ModelProvider .ANTHROPIC : ClaudeBot ,
920+ ModelProvider .GOOGLE : GeminiBot ,
921+ ModelProvider .LITELLM : LiteLLMBot ,
922+ }
0 commit comments