mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
175 lines
6.1 KiB
Python
175 lines
6.1 KiB
Python
"""
|
|
Integration tests for the native Rust OCR engine (`litellm_rust`).
|
|
|
|
Mistral OCR is served by the `litellm_rust` extension: provider routing,
|
|
request/response translation and the upstream HTTP call all happen in Rust,
|
|
while auth / logging / spend stay in Python via the `@client` decorator.
|
|
|
|
These tests stand up a fake Mistral OCR server and drive `litellm.ocr` /
|
|
`litellm.aocr` through the real Rust path (no network egress, no API key). They
|
|
self-skip when the `litellm_rust` wheel is not built (e.g. in CI jobs that don't
|
|
compile the crate); build it with `maturin develop --features python` from
|
|
`litellm-rust/`.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import threading
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
import pytest
|
|
|
|
import litellm
|
|
|
|
# Skip the whole module if the native engine isn't installed.
|
|
pytest.importorskip("litellm_rust")
|
|
|
|
# Canonical Mistral OCR response shape (the standard LiteLLM OCR format).
|
|
GOLDEN_RESPONSE = {
|
|
"pages": [
|
|
{
|
|
"index": 0,
|
|
"markdown": "# Title\nSome extracted text.",
|
|
"images": [],
|
|
"dimensions": {"dpi": 200, "height": 2200, "width": 1700},
|
|
}
|
|
],
|
|
"model": "mistral-ocr-2505-completion",
|
|
"document_annotation": None,
|
|
"usage_info": {"pages_processed": 1, "doc_size_bytes": 12345},
|
|
}
|
|
|
|
DOCUMENT = {"type": "document_url", "document_url": "https://example.com/doc.pdf"}
|
|
|
|
|
|
class _FakeMistral:
|
|
"""A throwaway HTTP server that mimics Mistral's POST /v1/ocr endpoint."""
|
|
|
|
def __init__(self, status: int = 200):
|
|
self.status = status
|
|
self.captured: dict = {}
|
|
server = self
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def log_message(self, format, *args): # silence
|
|
pass
|
|
|
|
def do_POST(self):
|
|
length = int(self.headers.get("Content-Length", 0))
|
|
body = json.loads(self.rfile.read(length) or b"{}")
|
|
server.captured = {
|
|
"path": self.path,
|
|
"authorization": self.headers.get("Authorization"),
|
|
"body": body,
|
|
}
|
|
if server.status != 200:
|
|
payload = json.dumps({"error": "boom"}).encode()
|
|
else:
|
|
payload = json.dumps(GOLDEN_RESPONSE).encode()
|
|
self.send_response(server.status)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(payload)))
|
|
self.end_headers()
|
|
self.wfile.write(payload)
|
|
|
|
self._httpd = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
self.base = f"http://127.0.0.1:{self._httpd.server_address[1]}/v1"
|
|
|
|
def __enter__(self):
|
|
self._thread = threading.Thread(target=self._httpd.serve_forever, daemon=True)
|
|
self._thread.start()
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
self._httpd.shutdown()
|
|
|
|
|
|
def test_ocr_sync_routes_through_rust_engine():
|
|
"""litellm.ocr() for a mistral model goes through Rust and returns OCRResponse."""
|
|
with _FakeMistral() as server:
|
|
response = litellm.ocr(
|
|
model="mistral/mistral-ocr-latest",
|
|
document=DOCUMENT,
|
|
api_base=server.base,
|
|
api_key="fake-key-123",
|
|
include_image_base64=True,
|
|
pages=[0],
|
|
)
|
|
|
|
# Response parsed into the standard OCRResponse.
|
|
assert type(response).__name__ == "OCRResponse"
|
|
assert response.model == GOLDEN_RESPONSE["model"]
|
|
assert response.pages[0].index == 0
|
|
assert response.pages[0].markdown == GOLDEN_RESPONSE["pages"][0]["markdown"]
|
|
assert response.usage_info.pages_processed == 1
|
|
|
|
# The Rust engine built the request faithfully.
|
|
sent = server.captured
|
|
assert sent["path"] == "/v1/ocr"
|
|
assert sent["authorization"] == "Bearer fake-key-123"
|
|
assert sent["body"]["model"] == "mistral-ocr-latest" # provider stripped
|
|
assert sent["body"]["document"] == DOCUMENT
|
|
assert sent["body"]["include_image_base64"] is True
|
|
assert sent["body"]["pages"] == [0]
|
|
|
|
|
|
def test_aocr_async_routes_through_rust_engine():
|
|
"""The async path (run in executor) also returns a valid OCRResponse."""
|
|
with _FakeMistral() as server:
|
|
response = asyncio.run(
|
|
litellm.aocr(
|
|
model="mistral/mistral-ocr-latest",
|
|
document=DOCUMENT,
|
|
api_base=server.base,
|
|
api_key="fake-key-123",
|
|
)
|
|
)
|
|
assert type(response).__name__ == "OCRResponse"
|
|
assert response.model == GOLDEN_RESPONSE["model"]
|
|
assert len(response.pages) == 1
|
|
|
|
|
|
def test_unsupported_params_are_filtered_by_rust():
|
|
"""Params outside the supported OCR set are dropped before the upstream call."""
|
|
with _FakeMistral() as server:
|
|
litellm.ocr(
|
|
model="mistral/mistral-ocr-latest",
|
|
document=DOCUMENT,
|
|
api_base=server.base,
|
|
api_key="k",
|
|
pages=[1, 2],
|
|
not_a_real_param="should_be_dropped",
|
|
)
|
|
assert "not_a_real_param" not in server.captured["body"]
|
|
assert server.captured["body"]["pages"] == [1, 2]
|
|
|
|
|
|
def test_upstream_error_propagates():
|
|
"""A non-2xx upstream response surfaces as an exception, not a silent None."""
|
|
with _FakeMistral(status=500) as server:
|
|
with pytest.raises(Exception):
|
|
litellm.ocr(
|
|
model="mistral/mistral-ocr-latest",
|
|
document=DOCUMENT,
|
|
api_base=server.base,
|
|
api_key="k",
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not os.environ.get("RUN_LIVE_MISTRAL_OCR"),
|
|
reason="Set RUN_LIVE_MISTRAL_OCR=1 (with a valid MISTRAL_API_KEY) to run the live call",
|
|
)
|
|
def test_ocr_live_mistral():
|
|
"""Live end-to-end call against the real Mistral OCR API (opt-in)."""
|
|
response = litellm.ocr(
|
|
model="mistral/mistral-ocr-latest",
|
|
document={
|
|
"type": "document_url",
|
|
"document_url": "https://arxiv.org/pdf/2201.04234",
|
|
},
|
|
)
|
|
assert type(response).__name__ == "OCRResponse"
|
|
assert len(response.pages) > 0
|
|
assert response.pages[0].markdown
|