This commit is contained in:
JingHao-Leon 2026-10-17 08:01:39 -07:00 • committed by GitHub
commit 62cb7893d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 134 additions and 5 deletions

View file

@ -535,22 +535,31 @@ class MistralConfig(OpenAIGPTConfig):
# Only process if content is a list
if isinstance(content, list):
thinking_content = ""
text_content = ""
text_segments: list[str] = [] # mutable-ok: accumulated across this choice's blocks
has_reference_blocks = False
# Process each content block
for block in content:
if block.get("type") == "thinking":
block_type = block.get("type") # pyright: ignore[reportUnknownVariableType] # provider content blocks are untyped JSON
if block_type == "thinking":
thinking_blocks = block.get("thinking", [])
thinking_texts = []
for thinking_block in thinking_blocks:
if thinking_block.get("type") == "text":
thinking_texts.append(thinking_block.get("text", ""))
thinking_content = "\n".join(thinking_texts)
elif block.get("type") == "text":
text_content = block.get("text", "")
elif block_type == "text":
text_segments.append(block.get("text", "")) # pyright: ignore[reportUnknownArgumentType] # provider content blocks are untyped JSON
elif block_type == "reference":
has_reference_blocks = True
# Set the extracted content
choice["message"]["content"] = text_content
choice["message"]["content"] = "".join(text_segments)
if has_reference_blocks:
# The flattened content string cannot carry Mistral
# citation chunks; keep the original block list
# reachable instead of dropping them silently.
choice["message"]["mistral_content_blocks"] = content
if thinking_content:
choice["message"]["reasoning_content"] = thinking_content
@ -657,6 +666,13 @@ class MistralChatResponseIterator(OpenAIChatCompletionStreamingHandler):
else:
delta.pop("thinking_blocks", None)
delta.pop("reasoning_content", None)
if any(
block.get("type") == "reference"
for block in content # pyright: ignore[reportUnknownArgumentType, reportUnknownVariableType] # provider content blocks are untyped JSON
):
# citation chunks carry no text; expose them on
# provider_specific_fields instead of dropping them
delta["provider_specific_fields"] = {"mistral_content_blocks": content}
except Exception:
# Fall back to default parsing if custom handling fails
return super().chunk_parser(chunk)

View file

@ -957,3 +957,116 @@ def test_mistral_transform_request_hoists_tool_message_image():
{"type": "text", "text": TOOL_RESULT_IMAGE_BOUNDARY},
{"type": "image_url", "image_url": {"url": data_uri}},
]
class TestMistralContentListConversion:
"""
Mistral content-as-list must keep every text chunk and expose citation chunks.
Issue #45378: the non-streaming conversion overwrote the content per text
block (only the last chunk survived — an answer closed by a citation was
flattened to "."), and reference chunks were dropped on both the
non-streaming and streaming paths.
"""
CITATION_CONTENT = [
{"type": "text", "text": "The sky is blue during the day"},
{"type": "reference", "reference_ids": ["doc1"]},
{"type": "text", "text": "."},
]
def test_non_streaming_concatenates_all_text_chunks(self):
response_data = {"choices": [{"message": {"role": "assistant", "content": self.CITATION_CONTENT}}]}
MistralConfig._handle_content_list_to_str_conversion(response_data)
message = response_data["choices"][0]["message"]
assert message["content"] == "The sky is blue during the day."
def test_non_streaming_exposes_reference_chunks_in_provider_specific_fields(self):
response_data = {"choices": [{"message": {"role": "assistant", "content": self.CITATION_CONTENT}}]}
MistralConfig._handle_content_list_to_str_conversion(response_data)
message = response_data["choices"][0]["message"]
assert message["mistral_content_blocks"] == self.CITATION_CONTENT
def test_non_streaming_adds_no_content_blocks_key_without_citations(self):
content = [{"type": "text", "text": "plain answer"}]
response_data = {"choices": [{"message": {"role": "assistant", "content": content}}]}
MistralConfig._handle_content_list_to_str_conversion(response_data)
message = response_data["choices"][0]["message"]
assert message["content"] == "plain answer"
assert "mistral_content_blocks" not in message
def test_non_streaming_thinking_blocks_still_extracted(self):
response_data = {
"choices": [
{
"message": {
"role": "assistant",
"content": [
{"type": "thinking", "thinking": [{"type": "text", "text": "reasoning"}]},
{"type": "text", "text": "answer part one"},
{"type": "text", "text": " and two"},
],
}
}
]
}
MistralConfig._handle_content_list_to_str_conversion(response_data)
message = response_data["choices"][0]["message"]
assert message["content"] == "answer part one and two"
assert message["reasoning_content"] == "reasoning"
def test_non_streaming_reference_blocks_reach_model_response_provider_specific_fields(self):
# the exposed key must survive convert_to_model_response_object so
# callers actually receive the citation chunks
from litellm.utils import convert_to_model_response_object
response_data = {
"id": "chatcmpl-test",
"object": "chat.completion",
"created": 1,
"model": "mistral-medium-2604",
"choices": [
{
"index": 0,
"finish_reason": "stop",
"message": {"role": "assistant", "content": self.CITATION_CONTENT},
}
],
}
response_data = MistralConfig._handle_content_list_to_str_conversion(response_data)
model_response = convert_to_model_response_object(
response_object=response_data, model_response_object=ModelResponse()
)
provider_specific_fields = model_response.choices[0].message.provider_specific_fields
assert provider_specific_fields["mistral_content_blocks"] == self.CITATION_CONTENT
def test_streaming_mixed_blocks_concatenate_and_expose_references(self):
iterator = MistralChatResponseIterator(streaming_response=iter([]), sync_stream=True)
chunk = {"id": "x", "choices": [{"delta": {"content": self.CITATION_CONTENT}}]}
result = iterator.chunk_parser(chunk)
delta = result.choices[0].delta
assert delta.content == "The sky is blue during the day."
assert delta.provider_specific_fields["mistral_content_blocks"] == self.CITATION_CONTENT
def test_streaming_reference_only_delta_exposes_references(self):
iterator = MistralChatResponseIterator(streaming_response=iter([]), sync_stream=True)
content = [{"type": "reference", "reference_ids": ["doc1"]}]
chunk = {"id": "x", "choices": [{"delta": {"content": content}}]}
result = iterator.chunk_parser(chunk)
delta = result.choices[0].delta
assert not delta.content
assert delta.provider_specific_fields["mistral_content_blocks"] == content