Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions litellm/llms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ def get_cost_for_web_search_request(custom_llm_provider: str, usage: "Usage", mo
)

return groq_cost_per_web_search_request(usage=usage, model_info=model_info)
elif custom_llm_provider == "mistral":
from .mistral.cost_calculator import cost_per_web_search_request

return cost_per_web_search_request(usage=usage, model_info=model_info)
else:
return None

Expand Down
10 changes: 10 additions & 0 deletions litellm/llms/base_llm/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,16 @@ def supports_stream_param_in_request_body(self) -> bool:
"""
return True

@property
def reserved_request_body_keys(self) -> frozenset[str]:
"""
Request-body keys that ``extra_body`` may not override after
``transform_request`` has sanitized them (e.g. an allowlisted ``tools``
list). Empty by default so ``extra_body`` stays a full escape hatch for
providers that do not sanitize their body.
"""
return frozenset()

def post_stream_processing(self, stream: Any) -> Any:
"""Hook for providers to post-process streaming responses. Default: pass-through."""
return stream
Expand Down
3 changes: 2 additions & 1 deletion litellm/llms/custom_httpx/llm_http_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,8 @@ def completion(
)

if extra_body is not None:
data = {**data, **extra_body}
reserved_keys: Final = provider_config.reserved_request_body_keys
data.update((key, value) for key, value in extra_body.items() if key not in reserved_keys)

headers, signed_json_body = provider_config.sign_request(
headers=headers,
Expand Down
18 changes: 7 additions & 11 deletions litellm/llms/mistral/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def get_supported_openai_params(self, model: str) -> list[str]:
"stop",
"response_format",
"parallel_tool_calls",
"web_search_options",
]

# Add reasoning support for magistral models
Expand Down Expand Up @@ -149,7 +150,12 @@ def map_openai_params(
model: str,
drop_params: bool,
) -> dict:
direct_passthrough: Final = frozenset(
{"temperature", "top_p", "stop", "response_format", "parallel_tool_calls", "web_search_options"}
)
for param, value in non_default_params.items():
if param in direct_passthrough:
optional_params[param] = value
if param == "max_tokens":
optional_params["max_tokens"] = value
if param == "max_completion_tokens": # max_completion_tokens should take priority
Expand All @@ -159,26 +165,16 @@ def map_openai_params(
optional_params["tools"] = self._clean_tool_schema_for_mistral(value)
if param == "stream" and value is True:
optional_params["stream"] = value
if param == "temperature":
optional_params["temperature"] = value
if param == "top_p":
optional_params["top_p"] = value
if param == "stop":
optional_params["stop"] = value
if param == "tool_choice" and isinstance(value, str):
optional_params["tool_choice"] = self._map_tool_choice(tool_choice=value)
if param == "seed":
optional_params["extra_body"] = {"random_seed": value}
if param == "response_format":
optional_params["response_format"] = value
optional_params["random_seed"] = value
if param == "reasoning_effort" and "magistral" in model.lower():
# Flag that we need to add reasoning system prompt
optional_params["_add_reasoning_prompt"] = True
if param == "thinking" and "magistral" in model.lower():
# Flag that we need to add reasoning system prompt
optional_params["_add_reasoning_prompt"] = True
if param == "parallel_tool_calls":
optional_params["parallel_tool_calls"] = value
return optional_params

def _get_openai_compatible_provider_info(self, api_base: str | None, api_key: str | None) -> tuple[str, str | None]:
Expand Down
85 changes: 85 additions & 0 deletions litellm/llms/mistral/common_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from collections.abc import Mapping, Sequence
from typing import Final

import httpx
from pydantic import TypeAdapter

import litellm
from litellm.llms.base_llm.base_utils import BaseLLMModelInfo
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues

WEB_SEARCH_TOOL_TYPES: Final[tuple[str, ...]] = ("web_search", "web_search_premium")

STR_OBJ_DICT: Final[TypeAdapter[Mapping[str, object]]] = TypeAdapter(Mapping[str, object])
OBJ_LIST: Final[TypeAdapter[Sequence[object]]] = TypeAdapter(Sequence[object])


def is_web_search_request(optional_params: Mapping[str, object]) -> bool:
"""True when a Mistral request should route to the Conversations API for web search."""
params: Final = STR_OBJ_DICT.validate_python(optional_params)
if params.get("web_search_options") is not None:
return True
tools: Final = params.get("tools")
if isinstance(tools, list):
return any(
isinstance(tool, dict) and STR_OBJ_DICT.validate_python(tool).get("type") in WEB_SEARCH_TOOL_TYPES
for tool in OBJ_LIST.validate_python(tools)
)
return False


class MistralModelInfo(BaseLLMModelInfo):
def validate_environment(
self,
headers: Mapping[str, str],
model: str,
messages: Sequence[AllMessageValues],
optional_params: Mapping[str, object],
litellm_params: Mapping[str, object],
api_key: str | None = None,
api_base: str | None = None,
) -> dict: # mutable-ok: BaseLLMModelInfo contract returns the headers dict
auth: Final = (("Authorization", f"Bearer {api_key}"),) if api_key is not None else ()
has_content_type: Final = "content-type" in headers or "Content-Type" in headers
content_type: Final = () if has_content_type else (("Content-Type", "application/json"),)
return dict( # mutable-ok: BaseLLMModelInfo contract returns the headers dict
(*headers.items(), *auth, *content_type)
)

@staticmethod
def get_api_base(api_base: str | None = None) -> str | None:
return api_base or get_secret_str("MISTRAL_API_BASE") or "https://api.mistral.ai"

@staticmethod
def get_api_key(api_key: str | None = None) -> str | None:
return api_key or get_secret_str("MISTRAL_API_KEY")

@staticmethod
def get_base_model(model: str) -> str | None:
return model.replace("mistral/", "")

def get_models(
self, api_key: str | None = None, api_base: str | None = None
) -> list[str]: # mutable-ok: BaseLLMModelInfo contract returns List[str]
resolved_api_base: Final = self.get_api_base(api_base)
resolved_api_key: Final = self.get_api_key(api_key)
if resolved_api_base is None or resolved_api_key is None:
raise ValueError(
"MISTRAL_API_BASE or MISTRAL_API_KEY is not set. Set them in the environment or pass them in."
)
response: Final = litellm.module_level_client.get(
url=f"{resolved_api_base}/v1/models",
headers={ # mutable-ok: forwarded to get(headers: dict | None)
"Authorization": f"Bearer {resolved_api_key}"
},
)
try:
response.raise_for_status()
except httpx.HTTPStatusError:
raise Exception(
f"Failed to fetch models from Mistral. Status code: {response.status_code}, Response: {response.text}"
)
return [ # mutable-ok: BaseLLMModelInfo contract returns List[str]
f"mistral/{model['id']}" for model in response.json()["data"]
]
5 changes: 5 additions & 0 deletions litellm/llms/mistral/conversations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from litellm.llms.mistral.conversations.transformation import (
MistralConversationsConfig,
)

__all__ = ("MistralConversationsConfig",)
Loading
Loading