fix(mistral): read back files whose purpose Mistral never lets us upload as user_data

This commit is contained in:
mateo-berri 2026-09-19 03:04:45 -07:00
parent dd79c1f77d
commit 217ff78ae7
2 changed files with 24 additions and 5 deletions

View file

@ -3,7 +3,8 @@ Mistral Files API. Reference: https://docs.mistral.ai/api/#tag/files
Mistral's file objects already carry the OpenAI field names (id, bytes, created_at,
filename, purpose), so this config is URL routing, auth, and a purpose mapping:
Mistral only accepts ``fine-tune``, ``batch`` and ``ocr`` as upload purposes.
Mistral only accepts ``fine-tune``, ``batch`` and ``ocr`` as upload purposes, while files
other Mistral products created read back with purposes outside that set and map onto ``user_data``.
"""
import time
@ -34,9 +35,10 @@ from ..common_utils import get_mistral_api_base, get_mistral_auth_headers, mistr
MistralFilePurpose: TypeAlias = Literal["fine-tune", "batch", "ocr"]
_OPENAI_PURPOSE_BY_MISTRAL: Final[Mapping[MistralFilePurpose, OpenAIFilesPurpose]] = MappingProxyType(
_OPENAI_PURPOSE_BY_MISTRAL: Final[Mapping[str, OpenAIFilesPurpose]] = MappingProxyType(
{"fine-tune": "fine-tune", "batch": "batch", "ocr": "user_data"}
)
_OPENAI_PURPOSE_FOR_UNMAPPED: Final[OpenAIFilesPurpose] = "user_data"
_MISTRAL_PURPOSE_BY_OPENAI: Final[Mapping[str, MistralFilePurpose]] = MappingProxyType(
{"fine-tune": "fine-tune", "batch": "batch", "ocr": "ocr", "user_data": "ocr"}
)
@ -59,7 +61,7 @@ class MistralFile(BaseModel):
bytes: int = 0
created_at: int | None = None
filename: str = ""
purpose: MistralFilePurpose = "batch"
purpose: str = "batch"
expires_at: int | None = None
@ -89,8 +91,8 @@ def _to_openai_file_object(file: MistralFile) -> OpenAIFileObject:
)
def _to_openai_purpose(purpose: MistralFilePurpose) -> OpenAIFilesPurpose:
return _OPENAI_PURPOSE_BY_MISTRAL[purpose]
def _to_openai_purpose(purpose: str) -> OpenAIFilesPurpose:
return _OPENAI_PURPOSE_BY_MISTRAL.get(purpose, _OPENAI_PURPOSE_FOR_UNMAPPED)
def _to_mistral_purpose(purpose: str) -> MistralFilePurpose:

View file

@ -158,6 +158,23 @@ def test_file_response_with_ocr_purpose_maps_onto_user_data(config):
assert obj.expires_at == 1_800_000_000
@pytest.mark.parametrize("purpose", ["playground", "audio", "code_interpreter"])
def test_files_with_purposes_mistral_never_lets_us_upload_still_read_back(config, purpose):
"""Regression: Mistral's live API returns purposes its upload endpoint rejects for files
other Mistral products created, and both the unfiltered list and a retrieve of such a file
used to fail validation, so one playground file 500'd ``GET /v1/files`` for the whole key."""
retrieved = config.transform_retrieve_file_response(
raw_response=_response(_file(purpose=purpose)), logging_obj=None, litellm_params={}
)
assert retrieved.purpose == "user_data"
listed = config.transform_list_files_response(
raw_response=_response({"data": [_file(purpose=purpose), _file(id="second")], "object": "list", "total": 2}),
logging_obj=None,
litellm_params={},
)
assert [(f.id, f.purpose) for f in listed] == [(FILE_ID, "user_data"), ("second", "batch")]
@pytest.mark.parametrize(
"method,suffix",
[