test(mistral): cover content-part flattening, get_models errors, and cost dispatch

Codecov flagged 16 uncovered lines across the PR. Each gap was a real
untested behavior rather than dead code, so this covers them with meaningful
assertions instead of coverage filler: OpenAI content-part message lists must
flatten to their text with non-text and malformed parts dropped, an assistant
turn with content=None plus tool_calls (the OpenAI SDK shape) must map to a
function.call entry without emitting an empty message entry, a message.output
with null content must transform gracefully, get_models must raise cleanly
without credentials and on non-2xx responses, the shared
get_cost_for_web_search_request dispatcher must route provider mistral to the
new cost calculator, and non-dict tool entries in a request body must not
crash allowlist extraction or shadow real tools that need checking. The two
mistral files codecov called out are now at 100% line coverage locally
This commit is contained in:
ElDjeee 2026-07-07 16:51:18 +02:00
parent 2b575169dc
commit 35fad0abf3
4 changed files with 108 additions and 0 deletions

View file

@ -213,6 +213,60 @@ def test_transform_request_maps_random_seed():
assert body["completion_args"] == {"random_seed": 7}
def test_transform_request_flattens_content_part_lists():
"""OpenAI content-parts messages must flatten to their text; non-text parts
and malformed entries are dropped, not crashed on."""
cfg = MistralConversationsConfig()
body = cfg.transform_request(
model="mistral-medium-latest",
messages=[
{"role": "system", "content": [{"type": "text", "text": "Be brief."}]},
{
"role": "user",
"content": [
{"type": "text", "text": "What is "},
{"type": "image_url", "image_url": {"url": "https://example.com/x.png"}},
"not-a-dict",
{"type": "text", "text": "the weather?"},
],
},
],
optional_params={"web_search_options": {}},
litellm_params={},
headers={},
)
assert body["instructions"] == "Be brief."
assert body["inputs"] == [{"role": "user", "content": "What is the weather?"}]
def test_transform_request_assistant_null_content_with_tool_calls():
"""The OpenAI SDK emits assistant tool-call turns with content=None; that must
map to a function.call entry with no empty assistant message entry."""
cfg = MistralConversationsConfig()
body = cfg.transform_request(
model="mistral-medium-latest",
messages=[
{"role": "user", "content": "weather?"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{"id": "call_1", "type": "function", "function": {"name": "get_weather", "arguments": "{}"}}
],
},
{"role": "tool", "tool_call_id": "call_1", "content": "18C"},
],
optional_params={"web_search_options": {}},
litellm_params={},
headers={},
)
assert body["inputs"] == [
{"role": "user", "content": "weather?"},
{"type": "function.call", "tool_call_id": "call_1", "name": "get_weather", "arguments": "{}"},
{"type": "function.result", "tool_call_id": "call_1", "result": "18C"},
]
def test_get_complete_url():
cfg = MistralConversationsConfig()
assert (
@ -347,6 +401,18 @@ def test_transform_response_without_message_output_is_graceful():
assert mr.choices[0].message.content is None
def test_transform_response_null_message_content_is_graceful():
mr = _transform(
{
"conversation_id": "conv_1",
"outputs": [{"type": "message.output", "role": "assistant", "content": None}],
"usage": {"prompt_tokens": 1, "completion_tokens": 0, "total_tokens": 1},
}
)
assert mr.choices[0].message.content is None
assert getattr(mr.choices[0].message, "annotations", None) is None
@pytest.mark.parametrize(
"web_search_kwargs",
[

View file

@ -79,3 +79,14 @@ class TestMistralModelInfo:
"mistral/mistral-medium-latest",
"mistral/mistral-large-latest",
]
def test_get_models_raises_without_api_key(self, monkeypatch):
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
with pytest.raises(ValueError, match="MISTRAL_API_BASE or MISTRAL_API_KEY"):
MistralModelInfo().get_models()
def test_get_models_raises_on_http_error(self, respx_mock):
litellm.disable_aiohttp_transport = True
respx_mock.get("https://api.mistral.ai/v1/models").respond(status_code=500, text="boom")
with pytest.raises(Exception, match="Status code: 500"):
MistralModelInfo().get_models()

View file

@ -36,6 +36,21 @@ def test_cost_per_web_search_request_no_details_is_zero():
assert cost_per_web_search_request(usage=usage, model_info=model_info) == 0.0
def test_get_cost_for_web_search_request_dispatches_for_mistral():
"""The shared dispatcher must route provider 'mistral' to this cost calculator;
without the mistral branch the surcharge silently becomes None."""
from litellm.llms import get_cost_for_web_search_request
usage = Usage(
prompt_tokens=10,
completion_tokens=5,
total_tokens=15,
prompt_tokens_details=PromptTokensDetailsWrapper(web_search_requests=2),
)
model_info = litellm.get_model_info("mistral/mistral-medium-latest")
assert get_cost_for_web_search_request("mistral", usage=usage, model_info=model_info) == pytest.approx(0.06)
def _conversation_response(connectors: dict = None, execution_names: list = None) -> ModelResponse:
"""Build a transformed ModelResponse from a Conversations payload.

View file

@ -260,6 +260,22 @@ class TestCheckToolsAllowlist:
assert exc_info.value.type == ProxyErrorTypes.tool_access_denied
assert "web_search" in str(exc_info.value.message)
@pytest.mark.asyncio
async def test_non_dict_tool_entries_skipped_without_masking_real_tools(self):
"""Malformed tool entries must not crash extraction or shadow the real
tools that still need allowlist checking."""
token = _token(metadata={"allowed_tools": ["get_weather"]})
body = {"tools": ["junk", 42, {"type": "web_search"}]}
with pytest.raises(ProxyException) as exc_info:
await check_tools_allowlist(
request_body=body,
valid_token=token,
team_object=None,
route="/v1/chat/completions",
)
assert exc_info.value.type == ProxyErrorTypes.tool_access_denied
assert "web_search" in str(exc_info.value.message)
@pytest.mark.asyncio
async def test_builtin_web_search_allowed_when_listed(self):
token = _token(metadata={"allowed_tools": ["web_search"]})