mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
fix(mistral): accept user_data as the OCR file purpose and keep OCR cost warnings single-line
This commit is contained in:
parent
fb76b67e78
commit
f3b198c1b7
4 changed files with 51 additions and 19 deletions
|
|
@ -2146,8 +2146,8 @@ def ocr_batch_cost(
|
|||
if resolved_info is None:
|
||||
verbose_logger.warning(
|
||||
"OCR batch cost: model=%s custom_llm_provider=%s has no pricing entry; returning 0.0 cost.",
|
||||
model,
|
||||
custom_llm_provider,
|
||||
_single_log_line(model),
|
||||
_single_log_line(custom_llm_provider),
|
||||
)
|
||||
return 0.0, 0.0
|
||||
|
||||
|
|
@ -2159,14 +2159,18 @@ def ocr_batch_cost(
|
|||
verbose_logger.warning(
|
||||
"OCR batch cost: model=%s custom_llm_provider=%s reported pages_processed=%s but no "
|
||||
"ocr_cost_per_page is configured; returning 0.0 cost for those pages.",
|
||||
model,
|
||||
custom_llm_provider,
|
||||
_single_log_line(model),
|
||||
_single_log_line(custom_llm_provider),
|
||||
pages_processed,
|
||||
)
|
||||
effective_annotation_rate: Final = annotation_rate if annotation_rate is not None else page_rate
|
||||
return (page_rate or 0.0) * pages_processed + (effective_annotation_rate or 0.0) * annotation_pages, 0.0
|
||||
|
||||
|
||||
def _single_log_line(value: str | None) -> str:
|
||||
return str(value).replace("\n", "").replace("\r", "")
|
||||
|
||||
|
||||
def _lookup_model_info_or_none(model: str, custom_llm_provider: str | None) -> ModelInfo | None:
|
||||
try:
|
||||
return litellm.get_model_info(model=model, custom_llm_provider=custom_llm_provider)
|
||||
|
|
|
|||
|
|
@ -307,7 +307,10 @@ def _mask_presigned_request_headers(transformed_request: bytes | str | dict) ->
|
|||
_get_masked_values, # pyright: ignore[reportPrivateUsage] # the shared header-masking helper has no public name
|
||||
)
|
||||
|
||||
return {**transformed_request, "headers": _get_masked_values(request_headers)}
|
||||
return { # mutable-ok: logging's curl and raw-request builders take dict
|
||||
**transformed_request,
|
||||
"headers": _get_masked_values(request_headers),
|
||||
}
|
||||
|
||||
|
||||
def _aws_signing_overrides(optional_params: Mapping[str, Any], litellm_params: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ Mistral only accepts ``fine-tune``, ``batch`` and ``ocr`` as upload purposes.
|
|||
|
||||
import time
|
||||
from collections.abc import Mapping, Sequence
|
||||
from types import MappingProxyType
|
||||
from typing import Final, Literal, TypeAlias
|
||||
|
||||
import httpx
|
||||
|
|
@ -33,6 +34,14 @@ 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(
|
||||
{"fine-tune": "fine-tune", "batch": "batch", "ocr": "user_data"}
|
||||
)
|
||||
_MISTRAL_PURPOSE_BY_OPENAI: Final[Mapping[str, MistralFilePurpose]] = MappingProxyType(
|
||||
{"fine-tune": "fine-tune", "batch": "batch", "ocr": "ocr", "user_data": "ocr"}
|
||||
)
|
||||
_SUPPORTED_PURPOSES: Final = ", ".join(_MISTRAL_PURPOSE_BY_OPENAI)
|
||||
|
||||
_NO_QUERY_PARAMS: Final[dict[str, str]] = {} # mutable-ok: BaseFilesConfig request transforms return tuple[str, dict]
|
||||
|
||||
|
||||
|
|
@ -81,22 +90,18 @@ def _to_openai_file_object(file: MistralFile) -> OpenAIFileObject:
|
|||
|
||||
|
||||
def _to_openai_purpose(purpose: MistralFilePurpose) -> OpenAIFilesPurpose:
|
||||
match purpose:
|
||||
case "fine-tune" | "batch":
|
||||
return purpose
|
||||
case "ocr":
|
||||
return "user_data"
|
||||
return _OPENAI_PURPOSE_BY_MISTRAL[purpose]
|
||||
|
||||
|
||||
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 "batch" | "fine-tune" | "ocr":
|
||||
return purpose
|
||||
case _:
|
||||
raise ValueError(f"Mistral does not support purpose={purpose!r}. Use one of: batch, fine-tune, ocr")
|
||||
"""``user_data`` is what an OCR file reads back as, since OpenAI's purpose literal has no ``ocr``,
|
||||
so it maps back onto ``ocr``. Every other purpose Mistral lacks is rejected: silently rewriting
|
||||
it to ``batch`` would let an upload skip the proxy's batch-file validation and guardrails, which
|
||||
only run when the caller says ``purpose=batch``."""
|
||||
mistral_purpose: Final = _MISTRAL_PURPOSE_BY_OPENAI.get(purpose)
|
||||
if mistral_purpose is None:
|
||||
raise ValueError(f"Mistral does not support purpose={purpose!r}. Use one of: {_SUPPORTED_PURPOSES}")
|
||||
return mistral_purpose
|
||||
|
||||
|
||||
def _api_base_from(litellm_params: Mapping[str, object]) -> str:
|
||||
|
|
|
|||
|
|
@ -102,7 +102,17 @@ def test_upload_request_passes_mistral_purposes_through(config, purpose):
|
|||
assert body["purpose"] == (None, purpose)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("purpose", ["assistants", "user_data", "vision", "evals"])
|
||||
def test_upload_request_maps_user_data_onto_ocr(config):
|
||||
body = config.transform_create_file_request(
|
||||
model="",
|
||||
create_file_data=CreateFileRequest(file=("scan.pdf", b"%PDF"), purpose="user_data"),
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
)
|
||||
assert body["purpose"] == (None, "ocr")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("purpose", ["assistants", "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."""
|
||||
|
|
@ -191,6 +201,16 @@ def test_list_request_filters_by_mapped_purpose(config):
|
|||
assert no_params == {}
|
||||
|
||||
|
||||
def test_list_request_accepts_the_purpose_an_ocr_file_reads_back_as(config):
|
||||
"""Regression: an OCR file reads back as ``purpose=user_data``, and listing with that purpose
|
||||
used to raise, so ``files.list(purpose=file.purpose)`` could never find OCR files."""
|
||||
ocr_file = config.transform_retrieve_file_response(
|
||||
raw_response=_response(_file(purpose="ocr")), logging_obj=None, litellm_params={}
|
||||
)
|
||||
_, params = config.transform_list_files_request(purpose=ocr_file.purpose, optional_params={}, litellm_params={})
|
||||
assert params == {"purpose": "ocr"}
|
||||
|
||||
|
||||
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={})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue