|
""" |
|
Support for OpenAI's `/v1/chat/completions` endpoint. |
|
|
|
Calls done in OpenAI/openai.py as OpenRouter is openai-compatible. |
|
|
|
Docs: https://openrouter.ai/docs/parameters |
|
""" |
|
|
|
from typing import Any, AsyncIterator, Iterator, List, Optional, Union |
|
|
|
import httpx |
|
|
|
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator |
|
from litellm.llms.base_llm.chat.transformation import BaseLLMException |
|
from litellm.types.llms.openai import AllMessageValues |
|
from litellm.types.llms.openrouter import OpenRouterErrorMessage |
|
from litellm.types.utils import ModelResponse, ModelResponseStream |
|
|
|
from ...openai.chat.gpt_transformation import OpenAIGPTConfig |
|
from ..common_utils import OpenRouterException |
|
|
|
|
|
class OpenrouterConfig(OpenAIGPTConfig): |
|
def map_openai_params( |
|
self, |
|
non_default_params: dict, |
|
optional_params: dict, |
|
model: str, |
|
drop_params: bool, |
|
) -> dict: |
|
mapped_openai_params = super().map_openai_params( |
|
non_default_params, optional_params, model, drop_params |
|
) |
|
|
|
|
|
extra_body = {} |
|
transforms = non_default_params.pop("transforms", None) |
|
models = non_default_params.pop("models", None) |
|
route = non_default_params.pop("route", None) |
|
if transforms is not None: |
|
extra_body["transforms"] = transforms |
|
if models is not None: |
|
extra_body["models"] = models |
|
if route is not None: |
|
extra_body["route"] = route |
|
mapped_openai_params[ |
|
"extra_body" |
|
] = extra_body |
|
return mapped_openai_params |
|
|
|
def transform_request( |
|
self, |
|
model: str, |
|
messages: List[AllMessageValues], |
|
optional_params: dict, |
|
litellm_params: dict, |
|
headers: dict, |
|
) -> dict: |
|
""" |
|
Transform the overall request to be sent to the API. |
|
|
|
Returns: |
|
dict: The transformed request. Sent as the body of the API call. |
|
""" |
|
extra_body = optional_params.pop("extra_body", {}) |
|
response = super().transform_request( |
|
model, messages, optional_params, litellm_params, headers |
|
) |
|
response.update(extra_body) |
|
return response |
|
|
|
def get_error_class( |
|
self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] |
|
) -> BaseLLMException: |
|
return OpenRouterException( |
|
message=error_message, |
|
status_code=status_code, |
|
headers=headers, |
|
) |
|
|
|
def get_model_response_iterator( |
|
self, |
|
streaming_response: Union[Iterator[str], AsyncIterator[str], ModelResponse], |
|
sync_stream: bool, |
|
json_mode: Optional[bool] = False, |
|
) -> Any: |
|
return OpenRouterChatCompletionStreamingHandler( |
|
streaming_response=streaming_response, |
|
sync_stream=sync_stream, |
|
json_mode=json_mode, |
|
) |
|
|
|
|
|
class OpenRouterChatCompletionStreamingHandler(BaseModelResponseIterator): |
|
def chunk_parser(self, chunk: dict) -> ModelResponseStream: |
|
try: |
|
|
|
if "error" in chunk: |
|
error_chunk = chunk["error"] |
|
error_message = OpenRouterErrorMessage( |
|
message="Message: {}, Metadata: {}, User ID: {}".format( |
|
error_chunk["message"], |
|
error_chunk.get("metadata", {}), |
|
error_chunk.get("user_id", ""), |
|
), |
|
code=error_chunk["code"], |
|
metadata=error_chunk.get("metadata", {}), |
|
) |
|
raise OpenRouterException( |
|
message=error_message["message"], |
|
status_code=error_message["code"], |
|
headers=error_message["metadata"].get("headers", {}), |
|
) |
|
|
|
new_choices = [] |
|
for choice in chunk["choices"]: |
|
choice["delta"]["reasoning_content"] = choice["delta"].get("reasoning") |
|
new_choices.append(choice) |
|
return ModelResponseStream( |
|
id=chunk["id"], |
|
object="chat.completion.chunk", |
|
created=chunk["created"], |
|
model=chunk["model"], |
|
choices=new_choices, |
|
) |
|
except KeyError as e: |
|
raise OpenRouterException( |
|
message=f"KeyError: {e}, Got unexpected response from OpenRouter: {chunk}", |
|
status_code=400, |
|
headers={"Content-Type": "application/json"}, |
|
) |
|
except Exception as e: |
|
raise e |
|
|