mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-10 22:24:51 +00:00
feat: Turn Mistral to use llm_http_handler (#12245)
* Enhance Mistral API: Add support for parallel tool calls and refine name handling in tool messages. Plus, introduce a new test for parallel tool calls in the Mistral model. * tests * make mypy happy * Refine name handling in Mistral chat transformation: clarify conditions for removing the 'name' field based on message role and content. * refactor: streamline Mistral integration by removing deprecated references and adding a new handler - Removed "mistral" from the list of compatible providers in constants. - Updated the completion function in main.py to utilize the new Mistral handler. - Deleted outdated Mistral chat and embedding files. - Introduced a new handler for Mistral chat completions, implementing the llm_http_handler pattern. - Added integration tests for the Mistral handler to ensure proper API base and key handling. * lint * fix: remove unneeded handler object * add tests * Addres PR comments
This commit is contained in:
+1
-1
@@ -1046,7 +1046,7 @@ from .llms.groq.chat.transformation import GroqChatConfig
|
||||
from .llms.voyage.embedding.transformation import VoyageEmbeddingConfig
|
||||
from .llms.infinity.embedding.transformation import InfinityEmbeddingConfig
|
||||
from .llms.azure_ai.chat.transformation import AzureAIStudioConfig
|
||||
from .llms.mistral.mistral_chat_transformation import MistralConfig
|
||||
from .llms.mistral.chat.transformation import MistralConfig
|
||||
from .llms.openai.responses.transformation import OpenAIResponsesAPIConfig
|
||||
from .llms.azure.responses.transformation import AzureOpenAIResponsesAPIConfig
|
||||
from .llms.openai.chat.o_series_transformation import (
|
||||
|
||||
@@ -397,7 +397,6 @@ openai_compatible_endpoints: List = [
|
||||
|
||||
openai_compatible_providers: List = [
|
||||
"anyscale",
|
||||
"mistral",
|
||||
"groq",
|
||||
"nvidia_nim",
|
||||
"cerebras",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
"""
|
||||
Calls handled in openai/
|
||||
|
||||
as mistral is an openai-compatible endpoint.
|
||||
"""
|
||||
+1
-1
@@ -174,7 +174,7 @@ class MistralConfig(OpenAIGPTConfig):
|
||||
|
||||
def _get_openai_compatible_provider_info(
|
||||
self, api_base: Optional[str], api_key: Optional[str]
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
) -> Tuple[str, Optional[str]]:
|
||||
# mistral is openai compatible, we just need to set this to custom_openai and have the api_base be https://api.mistral.ai
|
||||
api_base = (
|
||||
api_base
|
||||
@@ -1,5 +1,4 @@
|
||||
"""
|
||||
Calls handled in openai/
|
||||
|
||||
as mistral is an openai-compatible endpoint.
|
||||
"""
|
||||
"""
|
||||
+27
-1
@@ -1841,7 +1841,6 @@ def completion( # type: ignore # noqa: PLR0915
|
||||
or custom_llm_provider == "sambanova"
|
||||
or custom_llm_provider == "volcengine"
|
||||
or custom_llm_provider == "anyscale"
|
||||
or custom_llm_provider == "mistral"
|
||||
or custom_llm_provider == "openai"
|
||||
or custom_llm_provider == "together_ai"
|
||||
or custom_llm_provider == "nebius"
|
||||
@@ -1930,6 +1929,33 @@ def completion( # type: ignore # noqa: PLR0915
|
||||
additional_args={"headers": headers},
|
||||
)
|
||||
|
||||
elif custom_llm_provider == "mistral":
|
||||
api_key = api_key or litellm.api_key or get_secret("MISTRAL_API_KEY")
|
||||
api_base = (
|
||||
api_base
|
||||
or litellm.api_base
|
||||
or get_secret("MISTRAL_API_BASE")
|
||||
or "https://api.mistral.ai/v1"
|
||||
)
|
||||
|
||||
response = base_llm_http_handler.completion(
|
||||
model=model,
|
||||
messages=messages,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
model_response=model_response,
|
||||
encoding=encoding,
|
||||
logging_obj=logging,
|
||||
optional_params=optional_params,
|
||||
timeout=timeout,
|
||||
litellm_params=litellm_params,
|
||||
acompletion=acompletion,
|
||||
stream=stream,
|
||||
api_key=api_key,
|
||||
headers=headers,
|
||||
client=client,
|
||||
provider_config=provider_config,
|
||||
)
|
||||
elif (
|
||||
"replicate" in model
|
||||
or custom_llm_provider == "replicate"
|
||||
|
||||
@@ -8,7 +8,7 @@ sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
from litellm.llms.mistral.mistral_chat_transformation import MistralConfig
|
||||
from litellm.llms.mistral.chat.transformation import MistralConfig
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import pytest
|
||||
import litellm
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def add_mistral_api_key_to_env(monkeypatch):
|
||||
"""Add Mistral API key to environment for testing."""
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "fake-mistral-api-key-12345")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mistral_api_response():
|
||||
"""Mock response data for Mistral API calls."""
|
||||
return {
|
||||
"id": "chatcmpl-mistral-123",
|
||||
"object": "chat.completion",
|
||||
"created": 1677652288,
|
||||
"model": "mistral-medium-latest",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "Hello from Mistral! How can I help you today?",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {"prompt_tokens": 10, "completion_tokens": 15, "total_tokens": 25},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mistral_api_response_with_empty_content():
|
||||
"""Mock response data for Mistral API calls with empty content that should be converted to None."""
|
||||
return {
|
||||
"id": "chatcmpl-mistral-123",
|
||||
"object": "chat.completion",
|
||||
"created": 1677652288,
|
||||
"model": "mistral-medium-latest",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "", # Empty string that should be converted to None
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {"prompt_tokens": 10, "completion_tokens": 0, "total_tokens": 10},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sync_mode", [True, False])
|
||||
@pytest.mark.asyncio
|
||||
async def test_mistral_basic_completion(sync_mode, respx_mock, mistral_api_response):
|
||||
"""Test basic Mistral completion functionality."""
|
||||
litellm.disable_aiohttp_transport = True
|
||||
|
||||
model = "mistral/mistral-medium-latest"
|
||||
messages = [{"role": "user", "content": "Hello, how are you?"}]
|
||||
|
||||
# Mock the Mistral API endpoint
|
||||
respx_mock.post("https://api.mistral.ai/v1/chat/completions").respond(
|
||||
json=mistral_api_response
|
||||
)
|
||||
|
||||
if sync_mode:
|
||||
response = litellm.completion(model=model, messages=messages)
|
||||
else:
|
||||
response = await litellm.acompletion(model=model, messages=messages)
|
||||
|
||||
# Verify response
|
||||
assert response.choices[0].message.content == "Hello from Mistral! How can I help you today?"
|
||||
assert response.model == "mistral-medium-latest"
|
||||
assert response.usage.total_tokens == 25
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sync_mode", [True, False])
|
||||
@pytest.mark.asyncio
|
||||
async def test_mistral_transform_response_empty_content_conversion(sync_mode, respx_mock, mistral_api_response_with_empty_content):
|
||||
"""
|
||||
Test that Mistral's transform_response method is being called by verifying
|
||||
the specific behavior of converting empty string content to None.
|
||||
|
||||
This test verifies that the _handle_empty_content_response method in
|
||||
MistralConfig.transform_response is being applied.
|
||||
"""
|
||||
litellm.disable_aiohttp_transport = True
|
||||
|
||||
model = "mistral/mistral-medium-latest"
|
||||
messages = [{"role": "user", "content": "Generate an empty response"}]
|
||||
|
||||
# Mock the Mistral API endpoint with empty content
|
||||
respx_mock.post("https://api.mistral.ai/v1/chat/completions").respond(
|
||||
json=mistral_api_response_with_empty_content
|
||||
)
|
||||
|
||||
if sync_mode:
|
||||
response = litellm.completion(model=model, messages=messages)
|
||||
else:
|
||||
response = await litellm.acompletion(model=model, messages=messages)
|
||||
|
||||
# Verify that the transform_response method was called by checking that
|
||||
# empty string content was converted to None (Mistral-specific behavior)
|
||||
assert response.choices[0].message.content is None
|
||||
assert response.model == "mistral-medium-latest"
|
||||
assert response.usage.total_tokens == 10
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sync_mode", [True, False])
|
||||
@pytest.mark.asyncio
|
||||
async def test_mistral_transform_request_name_field_removal(sync_mode, respx_mock, mistral_api_response):
|
||||
"""
|
||||
Test that Mistral's transform_request method is being called by verifying
|
||||
the specific behavior of removing the 'name' field from non-tool messages.
|
||||
|
||||
This test verifies that the _handle_name_in_message method in
|
||||
MistralConfig._transform_messages is being applied.
|
||||
"""
|
||||
litellm.disable_aiohttp_transport = True
|
||||
|
||||
model = "mistral/mistral-medium-latest"
|
||||
# Include a message with 'name' field that should be removed for non-tool messages
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello", "name": "should_be_removed"},
|
||||
{"role": "assistant", "content": "Hi there!"},
|
||||
{"role": "user", "content": "How are you?"}
|
||||
]
|
||||
|
||||
# Mock the Mistral API endpoint
|
||||
respx_mock.post("https://api.mistral.ai/v1/chat/completions").respond(
|
||||
json=mistral_api_response
|
||||
)
|
||||
|
||||
if sync_mode:
|
||||
response = litellm.completion(model=model, messages=messages)
|
||||
else:
|
||||
response = await litellm.acompletion(model=model, messages=messages)
|
||||
|
||||
# Verify the response works (if transform_request wasn't called, the API would reject the request)
|
||||
assert response.choices[0].message.content == "Hello from Mistral! How can I help you today?"
|
||||
assert response.model == "mistral-medium-latest"
|
||||
|
||||
# Verify that the request was made (if transform_request failed, this would fail)
|
||||
assert len(respx_mock.calls) == 1
|
||||
|
||||
# Get the actual request that was made
|
||||
request = respx_mock.calls[0].request
|
||||
import json
|
||||
request_data = json.loads(request.content.decode('utf-8'))
|
||||
|
||||
# Verify that the 'name' field was removed from the user message
|
||||
# (Mistral API only supports 'name' in tool messages)
|
||||
user_message = request_data["messages"][0]
|
||||
assert user_message["role"] == "user"
|
||||
assert user_message["content"] == "Hello"
|
||||
assert "name" not in user_message # The 'name' field should have been removed
|
||||
|
||||
Reference in New Issue
Block a user