mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-11 16:26:07 +00:00
[Bug Fix] Mistral Tool Calling - Grammar error: at 3(11): failed to compile JSON schema (#13389)
* test_claude_tool_use_with_gemini * add _remove_json_schema_refs * add _clean_tool_schema_for_mistral * fixes mistral tool calls * _remove_json_schema_refs * fix - vertex, remove hardcoded test
This commit is contained in:
@@ -9,6 +9,7 @@ Docs - https://docs.mistral.ai/api/
|
||||
from typing import Any, Coroutine, List, Literal, Optional, Tuple, Union, cast, overload
|
||||
|
||||
import httpx
|
||||
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
handle_messages_with_content_list_to_str_conversion,
|
||||
@@ -147,7 +148,8 @@ class MistralConfig(OpenAIGPTConfig):
|
||||
if param == "max_completion_tokens": # max_completion_tokens should take priority
|
||||
optional_params["max_tokens"] = value
|
||||
if param == "tools":
|
||||
optional_params["tools"] = value
|
||||
# Clean tools to remove problematic schema fields for Mistral API
|
||||
optional_params["tools"] = self._clean_tool_schema_for_mistral(value)
|
||||
if param == "stream" and value is True:
|
||||
optional_params["stream"] = value
|
||||
if param == "temperature":
|
||||
@@ -195,7 +197,8 @@ class MistralConfig(OpenAIGPTConfig):
|
||||
@overload
|
||||
def _transform_messages(
|
||||
self, messages: List[AllMessageValues], model: str, is_async: Literal[True]
|
||||
) -> Coroutine[Any, Any, List[AllMessageValues]]: ...
|
||||
) -> Coroutine[Any, Any, List[AllMessageValues]]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def _transform_messages(
|
||||
@@ -203,7 +206,8 @@ class MistralConfig(OpenAIGPTConfig):
|
||||
messages: List[AllMessageValues],
|
||||
model: str,
|
||||
is_async: Literal[False] = False,
|
||||
) -> List[AllMessageValues]: ...
|
||||
) -> List[AllMessageValues]:
|
||||
...
|
||||
|
||||
def _transform_messages(
|
||||
self, messages: List[AllMessageValues], model: str, is_async: bool = False
|
||||
@@ -286,6 +290,38 @@ class MistralConfig(OpenAIGPTConfig):
|
||||
optional_params.pop("_add_reasoning_prompt", None)
|
||||
return messages
|
||||
|
||||
@classmethod
|
||||
def _clean_tool_schema_for_mistral(cls, tools: list) -> list:
|
||||
"""
|
||||
Clean tool schemas to remove fields that cause issues with Mistral API.
|
||||
|
||||
Removes:
|
||||
- $id and $schema fields (cause grammar validation errors)
|
||||
- additionalProperties=False (causes OpenAI API schema errors)
|
||||
- strict field (not supported by Mistral)
|
||||
|
||||
Args:
|
||||
tools: List of tool definitions
|
||||
max_depth: Maximum recursion depth for schema cleaning (default: 10)
|
||||
|
||||
Returns:
|
||||
Cleaned tools list
|
||||
"""
|
||||
if not tools:
|
||||
return tools
|
||||
|
||||
import copy
|
||||
|
||||
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
|
||||
from litellm.utils import _remove_json_schema_refs
|
||||
|
||||
cleaned_tools = copy.deepcopy(tools)
|
||||
|
||||
# Apply all cleaning functions with max_depth protection
|
||||
cleaned_tools = _remove_json_schema_refs(cleaned_tools, max_depth=DEFAULT_MAX_RECURSE_DEPTH)
|
||||
|
||||
return cleaned_tools
|
||||
|
||||
@classmethod
|
||||
def _handle_name_in_message(cls, message: AllMessageValues) -> AllMessageValues:
|
||||
"""
|
||||
|
||||
@@ -2912,6 +2912,39 @@ def _remove_strict_from_schema(schema):
|
||||
return schema
|
||||
|
||||
|
||||
def _remove_json_schema_refs(schema, max_depth=10):
|
||||
"""
|
||||
Remove JSON schema reference fields like '$id' and '$schema' that can cause issues with some providers.
|
||||
|
||||
These fields are used for schema validation but can cause problems when the schema references
|
||||
are not accessible to the provider's validation system.
|
||||
|
||||
Args:
|
||||
schema: The schema object to clean (dict, list, or other)
|
||||
max_depth: Maximum recursion depth to prevent infinite loops (default: 10)
|
||||
|
||||
Relevant Issues: Mistral API grammar validation fails when schema contains $id and $schema references
|
||||
"""
|
||||
if max_depth <= 0:
|
||||
return schema
|
||||
|
||||
if isinstance(schema, dict):
|
||||
# Remove JSON schema reference fields
|
||||
schema.pop("$id", None)
|
||||
schema.pop("$schema", None)
|
||||
|
||||
# Recursively process all dictionary values
|
||||
for key, value in schema.items():
|
||||
_remove_json_schema_refs(value, max_depth - 1)
|
||||
|
||||
elif isinstance(schema, list):
|
||||
# Recursively process all items in the list
|
||||
for item in schema:
|
||||
_remove_json_schema_refs(item, max_depth - 1)
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def _remove_unsupported_params(
|
||||
non_default_params: dict, supported_openai_params: Optional[List[str]]
|
||||
) -> dict:
|
||||
|
||||
@@ -25,6 +25,7 @@ IGNORE_FUNCTIONS = [
|
||||
"filter_value_from_dict", # max depth set.
|
||||
"normalize_json_schema_types", # max depth set.
|
||||
"_extract_fields_recursive", # max depth set.
|
||||
"_remove_json_schema_refs", # max depth set.
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -430,7 +430,7 @@ def test_gemini_with_empty_function_call_arguments():
|
||||
async def test_claude_tool_use_with_gemini():
|
||||
response = await litellm.anthropic.messages.acreate(
|
||||
messages=[
|
||||
{"role": "user", "content": "Hello, can you tell me the weather in Boston?"}
|
||||
{"role": "user", "content": "Hello, can you tell me the weather in Boston. Please respond with a tool call?"}
|
||||
],
|
||||
model="gemini/gemini-2.5-flash",
|
||||
stream=True,
|
||||
|
||||
@@ -504,75 +504,6 @@ async def test_async_vertexai_streaming_response():
|
||||
pytest.fail(f"An exception occurred: {e}")
|
||||
|
||||
|
||||
# asyncio.run(test_async_vertexai_streaming_response())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ["vertex_ai"]) # "vertex_ai_beta"
|
||||
@pytest.mark.parametrize("sync_mode", [True, False])
|
||||
@pytest.mark.flaky(retries=3, delay=1)
|
||||
@pytest.mark.asyncio
|
||||
async def test_gemini_pro_vision(provider, sync_mode):
|
||||
try:
|
||||
load_vertex_ai_credentials()
|
||||
litellm.set_verbose = True
|
||||
litellm.num_retries = 3
|
||||
if sync_mode:
|
||||
resp = litellm.completion(
|
||||
model="{}/gemini-2.5-flash-lite".format(provider),
|
||||
messages=[
|
||||
{"role": "system", "content": "Be a good bot"},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Whats in this image?"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "gs://cloud-samples-data/generative-ai/image/boats.jpeg"
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
else:
|
||||
resp = await litellm.acompletion(
|
||||
model="{}/gemini-2.5-flash-lite".format(provider),
|
||||
messages=[
|
||||
{"role": "system", "content": "Be a good bot"},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Whats in this image?"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "gs://cloud-samples-data/generative-ai/image/boats.jpeg"
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
print(resp)
|
||||
|
||||
prompt_tokens = resp.usage.prompt_tokens
|
||||
|
||||
# DO Not DELETE this ASSERT
|
||||
# Google counts the prompt tokens for us, we should ensure we use the tokens from the orignal response
|
||||
assert prompt_tokens == 267 # the gemini api returns 267 to us
|
||||
|
||||
except litellm.RateLimitError as e:
|
||||
pass
|
||||
except Exception as e:
|
||||
if "500 Internal error encountered.'" in str(e):
|
||||
pass
|
||||
else:
|
||||
pytest.fail(f"An exception occurred - {str(e)}")
|
||||
|
||||
|
||||
# test_gemini_pro_vision()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("load_pdf", [False]) # True,
|
||||
@pytest.mark.flaky(retries=3, delay=1)
|
||||
|
||||
Reference in New Issue
Block a user