fix(mistral): reject file purposes Mistral lacks instead of mapping them to batch

The proxy runs batch-file validation and guardrails only for purpose=batch,
so a purpose such as assistants that was silently rewritten to batch on the
way to Mistral let an upload skip both. Only batch, fine-tune and ocr pass
through now; anything else is a 400.
This commit is contained in:
mubashir1osmani 2026-09-10 19:46:55 -04:00
parent 95fdefa390
commit e6bc4e47c7
2 changed files with 28 additions and 10 deletions

View file

@ -89,11 +89,14 @@ def _to_openai_purpose(purpose: MistralFilePurpose) -> OpenAIFilesPurpose:
def _to_mistral_purpose(purpose: str) -> MistralFilePurpose:
"""Only Mistral's own purposes pass through. Silently mapping anything else to ``batch``
would let an upload skip the proxy's batch-file validation and guardrails, which only
run when the caller says ``purpose=batch``."""
match purpose:
case "fine-tune" | "ocr":
case "batch" | "fine-tune" | "ocr":
return purpose
case _:
return "batch"
raise ValueError(f"Mistral does not support purpose={purpose!r}. Use one of: batch, fine-tune, ocr")
def _api_base_from(litellm_params: Mapping[str, object]) -> str:
@ -166,7 +169,7 @@ class MistralFilesConfig(BaseFilesConfig):
content_type: Final = extracted.get("content_type") or "application/octet-stream"
upload: Final = MistralMultipartUpload(
file=(filename, extracted["content"], content_type),
purpose=(None, _to_mistral_purpose(create_file_data.get("purpose", "batch"))),
purpose=(None, _to_mistral_purpose(create_file_data.get("purpose") or "batch")),
)
return dict(upload) # mutable-ok: BaseFilesConfig signature

View file

@ -91,18 +91,28 @@ def test_upload_request_is_multipart_with_batch_purpose(config):
}
@pytest.mark.parametrize(
"openai_purpose,mistral_purpose",
[("batch", "batch"), ("fine-tune", "fine-tune"), ("ocr", "ocr"), ("assistants", "batch"), ("user_data", "batch")],
)
def test_upload_request_maps_purpose_onto_mistral_enum(config, openai_purpose, mistral_purpose):
@pytest.mark.parametrize("purpose", ["batch", "fine-tune", "ocr"])
def test_upload_request_passes_mistral_purposes_through(config, purpose):
body = config.transform_create_file_request(
model="",
create_file_data=CreateFileRequest(file=("f.bin", b"x"), purpose=openai_purpose),
create_file_data=CreateFileRequest(file=("f.bin", b"x"), purpose=purpose),
optional_params={},
litellm_params={},
)
assert body["purpose"] == (None, mistral_purpose)
assert body["purpose"] == (None, purpose)
@pytest.mark.parametrize("purpose", ["assistants", "user_data", "vision", "evals"])
def test_upload_request_rejects_purposes_mistral_lacks(config, purpose):
"""Regression: these used to be silently rewritten to ``batch``, so an upload that skipped the
proxy's batch-only validation and guardrails still landed on Mistral as a batch input file."""
with pytest.raises(ValueError, match=f"purpose={purpose!r}"):
config.transform_create_file_request(
model="",
create_file_data=CreateFileRequest(file=("f.bin", b"x"), purpose=purpose),
optional_params={},
litellm_params={},
)
def test_upload_request_requires_file(config):
@ -181,6 +191,11 @@ def test_list_request_filters_by_mapped_purpose(config):
assert no_params == {}
def test_list_request_rejects_purposes_mistral_lacks(config):
with pytest.raises(ValueError, match="purpose='assistants'"):
config.transform_list_files_request(purpose="assistants", optional_params={}, litellm_params={})
def test_list_response(config):
out = config.transform_list_files_response(
raw_response=_response(