# Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License """LLMCompletion based on litellm.""" from collections.abc import AsyncIterator, Iterator from typing import TYPE_CHECKING, Any, Unpack import litellm from azure.identity import DefaultAzureCredential, get_bearer_token_provider from litellm import ModelResponse # type: ignore from graphrag_llm.completion.completion import LLMCompletion from graphrag_llm.config.types import AuthMethod from graphrag_llm.middleware import ( with_middleware_pipeline, ) from graphrag_llm.types import LLMCompletionChunk, LLMCompletionResponse from graphrag_llm.utils import ( structure_completion_response, ) if TYPE_CHECKING: from graphrag_cache import Cache, CacheKeyCreator from graphrag_llm.config import ModelConfig from graphrag_llm.metrics import MetricsProcessor, MetricsStore from graphrag_llm.rate_limit import RateLimiter from graphrag_llm.retry import Retry from graphrag_llm.tokenizer import Tokenizer from graphrag_llm.types import ( AsyncLLMCompletionFunction, LLMCompletionArgs, LLMCompletionFunction, LLMCompletionMessagesParam, Metrics, ResponseFormat, ) litellm.suppress_debug_info = True litellm.enable_json_schema_validation = True class LiteLLMCompletion(LLMCompletion): """LLMCompletion based on litellm.""" _model_config: "ModelConfig" _model_id: str _track_metrics: bool = False _metrics_store: "MetricsStore" _metrics_processor: "MetricsProcessor | None" _cache: "Cache | None" _cache_key_creator: "CacheKeyCreator" _tokenizer: "Tokenizer" _rate_limiter: "RateLimiter | None" _retrier: "Retry | None" def __init__( self, *, model_id: str, model_config: "ModelConfig", tokenizer: "Tokenizer", metrics_store: "MetricsStore", metrics_processor: "MetricsProcessor | None" = None, rate_limiter: "RateLimiter | None" = None, retrier: "Retry | None" = None, cache: "Cache | None" = None, cache_key_creator: "CacheKeyCreator", azure_cognitive_services_audience: str = "https://cognitiveservices.azure.com/.default", drop_unsupported_params: bool = True, **kwargs: Any, ) -> None: """Initialize LiteLLMCompletion. Args ---- model_id: str The LiteLLM model ID, e.g., "openai/gpt-4o" model_config: ModelConfig The configuration for the model. tokenizer: Tokenizer The tokenizer to use. metrics_store: MetricsStore | None (default: None) The metrics store to use. metrics_processor: MetricsProcessor | None (default: None) The metrics processor to use. cache: Cache | None (default: None) An optional cache instance. cache_key_prefix: str | None (default: "chat") The cache key prefix. Required if cache is provided. rate_limiter: RateLimiter | None (default: None) The rate limiter to use. retrier: Retry | None (default: None) The retry strategy to use. azure_cognitive_services_audience: str (default: "https://cognitiveservices.azure.com/.default") The audience for Azure Cognitive Services when using Managed Identity. drop_unsupported_params: bool (default: True) Whether to drop unsupported parameters for the model provider. """ self._model_id = model_id self._model_config = model_config self._tokenizer = tokenizer self._metrics_store = metrics_store self._metrics_processor = metrics_processor self._cache = cache self._track_metrics = metrics_processor is not None self._cache_key_creator = cache_key_creator self._rate_limiter = rate_limiter self._retrier = retrier self._completion, self._completion_async = _create_base_completions( model_config=model_config, drop_unsupported_params=drop_unsupported_params, azure_cognitive_services_audience=azure_cognitive_services_audience, ) self._completion, self._completion_async = with_middleware_pipeline( model_config=self._model_config, model_fn=self._completion, async_model_fn=self._completion_async, request_type="chat", cache=self._cache, cache_key_creator=self._cache_key_creator, tokenizer=self._tokenizer, metrics_processor=self._metrics_processor, rate_limiter=self._rate_limiter, retrier=self._retrier, ) def completion( self, /, **kwargs: Unpack["LLMCompletionArgs[ResponseFormat]"], ) -> "LLMCompletionResponse[ResponseFormat] | Iterator[LLMCompletionChunk]": """Sync completion method.""" messages: LLMCompletionMessagesParam = kwargs.pop("messages") response_format = kwargs.pop("response_format", None) is_streaming = kwargs.get("stream") or False if response_format is not None and is_streaming: msg = "response_format is not supported for streaming completions." raise ValueError(msg) request_metrics: Metrics | None = kwargs.pop("metrics", None) or {} if not self._track_metrics: request_metrics = None if isinstance(messages, str): messages = [{"role": "user", "content": messages}] try: response = self._completion( messages=messages, metrics=request_metrics, response_format=response_format, **kwargs, # type: ignore ) if response_format is not None: structured_response = structure_completion_response( response.content, response_format ) response.formatted_response = structured_response return response finally: if request_metrics is not None: self._metrics_store.update_metrics(metrics=request_metrics) async def completion_async( self, /, **kwargs: Unpack["LLMCompletionArgs[ResponseFormat]"], ) -> "LLMCompletionResponse[ResponseFormat] | AsyncIterator[LLMCompletionChunk]": """Async completion method.""" messages: LLMCompletionMessagesParam = kwargs.pop("messages") response_format = kwargs.pop("response_format", None) is_streaming = kwargs.get("stream") or False if response_format is not None and is_streaming: msg = "response_format is not supported for streaming completions." raise ValueError(msg) request_metrics: Metrics | None = kwargs.pop("metrics", None) or {} if not self._track_metrics: request_metrics = None if isinstance(messages, str): messages = [{"role": "user", "content": messages}] try: response = await self._completion_async( messages=messages, metrics=request_metrics, response_format=response_format, **kwargs, # type: ignore ) if response_format is not None: structured_response = structure_completion_response( response.content, response_format ) response.formatted_response = structured_response return response finally: if request_metrics is not None: self._metrics_store.update_metrics(metrics=request_metrics) @property def metrics_store(self) -> "MetricsStore": """Get metrics store.""" return self._metrics_store @property def tokenizer(self) -> "Tokenizer": """Get tokenizer.""" return self._tokenizer def _create_base_completions( *, model_config: "ModelConfig", drop_unsupported_params: bool, azure_cognitive_services_audience: str, ) -> tuple["LLMCompletionFunction", "AsyncLLMCompletionFunction"]: """Create base completions for LiteLLM. Convert litellm completion functions to graphrag_llm LLMCompletionFunction. LLMCompletionFunction is close to the litellm completion function signature, but uses a few extra params such as metrics. Remove graphrag_llm LLMCompletionFunction specific params before calling litellm completion functions. """ model_provider = model_config.model_provider model = model_config.azure_deployment_name or model_config.model base_args: dict[str, Any] = { "drop_params": drop_unsupported_params, "model": f"{model_provider}/{model}", "api_key": model_config.api_key, "api_base": model_config.api_base, "api_version": model_config.api_version, **model_config.call_args, } if model_config.auth_method == AuthMethod.AzureManagedIdentity: base_args["azure_ad_token_provider"] = get_bearer_token_provider( DefaultAzureCredential(), azure_cognitive_services_audience ) def _base_completion( **kwargs: Any, ) -> LLMCompletionResponse | Iterator[LLMCompletionChunk]: kwargs.pop("metrics", None) mock_response: str | None = kwargs.pop("mock_response", None) json_object: bool | None = kwargs.pop("response_format_json_object", None) new_args: dict[str, Any] = {**base_args, **kwargs} if model_config.mock_responses and mock_response is not None: new_args["mock_response"] = mock_response if json_object and "response_format" not in new_args: new_args["response_format"] = {"type": "json_object"} response = litellm.completion( **new_args, ) if isinstance(response, ModelResponse): return LLMCompletionResponse(**response.model_dump()) def _run_iterator() -> Iterator[LLMCompletionChunk]: for chunk in response: yield LLMCompletionChunk(**chunk.model_dump()) return _run_iterator() async def _base_completion_async( **kwargs: Any, ) -> LLMCompletionResponse | AsyncIterator[LLMCompletionChunk]: kwargs.pop("metrics", None) mock_response: str | None = kwargs.pop("mock_response", None) json_object: bool | None = kwargs.pop("response_format_json_object", None) new_args: dict[str, Any] = {**base_args, **kwargs} if model_config.mock_responses and mock_response is not None: new_args["mock_response"] = mock_response if json_object and "response_format" not in new_args: new_args["response_format"] = {"type": "json_object"} response = await litellm.acompletion( **new_args, ) if isinstance(response, ModelResponse): return LLMCompletionResponse(**response.model_dump()) async def _run_iterator() -> AsyncIterator[LLMCompletionChunk]: async for chunk in response: yield LLMCompletionChunk(**chunk.model_dump()) # type: ignore return _run_iterator() return (_base_completion, _base_completion_async)