mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
fix(mistral): keep async codestral file uploads working by not rewriting the caller's file blocks
This commit is contained in:
parent
e0c291e949
commit
4ef06b92a7
3 changed files with 57 additions and 14 deletions
|
|
@ -35,6 +35,7 @@ from litellm.router_utils.reasoning_effort_capability import (
|
|||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.llms.mistral import (
|
||||
MistralCacheKeyParams,
|
||||
MistralFilePart,
|
||||
MistralReasoningEffortParams,
|
||||
MistralRequestRouting,
|
||||
MistralThinkingBlock,
|
||||
|
|
@ -69,6 +70,7 @@ _NO_TOOL_CALL_TURN: Final[tuple[int, frozenset[str]]] = (-1, frozenset())
|
|||
_MISTRAL_REQUEST_ROUTING: Final = TypeAdapter(MistralRequestRouting)
|
||||
_MISTRAL_CACHE_KEY_PARAMS: Final = TypeAdapter(MistralCacheKeyParams)
|
||||
_MISTRAL_REASONING_EFFORT_PARAMS: Final = TypeAdapter(MistralReasoningEffortParams)
|
||||
_MISTRAL_FILE_PART: Final = TypeAdapter(MistralFilePart)
|
||||
|
||||
|
||||
def _reasoning_level_and_summary(reasoning_effort: object) -> tuple[object, object]:
|
||||
|
|
@ -438,24 +440,31 @@ class MistralConfig(OpenAIGPTConfig):
|
|||
messages = self._handle_message_with_file(messages)
|
||||
return messages
|
||||
|
||||
@staticmethod
|
||||
def _with_mistral_file_id(block: Mapping[str, object]) -> Mapping[str, object]:
|
||||
file_part: Final = block.get("file") if block.get("type") == "file" else None
|
||||
file_id: Final = (
|
||||
_MISTRAL_FILE_PART.validate_python(file_part).get("file_id") if isinstance(file_part, Mapping) else None
|
||||
)
|
||||
if not file_id:
|
||||
return block
|
||||
return {**{key: value for key, value in block.items() if key != "file"}, "file_id": file_id}
|
||||
|
||||
@classmethod
|
||||
def _with_mistral_file_ids(cls, message: AllMessageValues) -> AllMessageValues:
|
||||
content: Final = message.get("content")
|
||||
if not isinstance(content, list):
|
||||
return message
|
||||
blocks: Final[tuple[Mapping[str, object], ...]] = tuple(content)
|
||||
return cast( # cast-ok: only file blocks change, to the file_id shape the Mistral API takes
|
||||
AllMessageValues, {**message, "content": [cls._with_mistral_file_id(block) for block in blocks]}
|
||||
)
|
||||
|
||||
def _handle_message_with_file(self, messages: list[AllMessageValues]) -> list[AllMessageValues]:
|
||||
"""
|
||||
Mistral API supports only 'file_id' in message content with type 'file'.
|
||||
"""
|
||||
for m in messages:
|
||||
_content_block = m.get("content")
|
||||
if _content_block and isinstance(_content_block, list):
|
||||
if any(c.get("type") == "file" for c in _content_block):
|
||||
# If file content is present, we get file_id from 'file' attribute of content block
|
||||
# then replace 'file' with 'file_id' and assign the value of 'file_id' attribute to it.
|
||||
file_contents = [c for c in _content_block if c.get("type") == "file"]
|
||||
for file_content in file_contents:
|
||||
file_id = file_content.get("file", {}).get("file_id")
|
||||
if file_id:
|
||||
# Replace 'file' with 'file_id'
|
||||
file_content["file_id"] = file_id # pyright: ignore[reportGeneralTypeIssues] # legacy in-place rewrite of the block shape
|
||||
file_content.pop("file", None)
|
||||
return messages
|
||||
return [self._with_mistral_file_ids(m) for m in messages]
|
||||
|
||||
def _add_reasoning_system_prompt_if_needed(
|
||||
self, messages: list[AllMessageValues], optional_params: dict
|
||||
|
|
|
|||
|
|
@ -37,3 +37,7 @@ class MistralCacheKeyParams(TypedDict, total=False):
|
|||
|
||||
class MistralReasoningEffortParams(TypedDict, total=False):
|
||||
reasoning_effort: ReadOnly[object]
|
||||
|
||||
|
||||
class MistralFilePart(TypedDict, total=False):
|
||||
file_id: ReadOnly[object]
|
||||
|
|
|
|||
|
|
@ -1355,6 +1355,36 @@ async def test_prompt_cache_key_rejection_for_an_async_codestral_call_names_code
|
|||
assert not azure_route.called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_codestral_call_sends_an_uploaded_file_by_its_id(respx_mock: respx.MockRouter) -> None:
|
||||
import litellm
|
||||
|
||||
codestral_api_base: Final = "https://codestral.mistral.ai/v1"
|
||||
codestral_route: Final = respx_mock.post(f"{codestral_api_base}/chat/completions").mock(
|
||||
return_value=_chat_completion_reply()
|
||||
)
|
||||
|
||||
await litellm.acompletion(
|
||||
model="codestral/codestral-latest",
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Review this file"},
|
||||
{"type": "file", "file": {"file_id": "file-12345"}},
|
||||
],
|
||||
}
|
||||
],
|
||||
api_base=codestral_api_base,
|
||||
api_key="codestral-key",
|
||||
max_retries=0,
|
||||
client=AsyncOpenAI(api_key="codestral-key", base_url=codestral_api_base, http_client=httpx.AsyncClient()),
|
||||
)
|
||||
|
||||
sent_content: Final = json.loads(codestral_route.calls.last.request.content)["messages"][0]["content"]
|
||||
assert sent_content[1] == {"type": "file", "file_id": "file-12345"}
|
||||
|
||||
|
||||
def test_prompt_cache_key_rejection_for_a_sync_codestral_call_is_an_unsupported_params_error(
|
||||
respx_mock: respx.MockRouter,
|
||||
) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue