mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-02 07:13:02 +00:00
* feat(ocr): support outputContentFormat=markdown for Azure Document Intelligence * fix(ocr): map only span endpoints when slicing Azure Markdown pages * fix(ocr): accept any casing of outputContentFormat and report code point offsets Azure reads outputContentFormat case-insensitively, so Markdown and TEXT now map to the same values instead of returning 400. When the proxy asks Azure for code point offsets to split Markdown pages, the response now carries stringIndexType so callers know the unit of table and key-value spans. Tests use rstest and the enum derives VariantArray per the Rust workspace rules. * fix(ocr): ask Azure for code point offsets only on prebuilt-layout and keep line-joined pages when spans are malformed * fix(ocr): trim Azure DI outputContentFormat, accept output_content_format, and drop invalid values under drop_params * fix(ocr): fall back to output_content_format when outputContentFormat is empty --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
134 lines
4.9 KiB
Python
134 lines
4.9 KiB
Python
from io import BytesIO
|
|
from typing import Final
|
|
|
|
import pytest
|
|
|
|
import litellm
|
|
from tests.test_litellm_rust.ocr.data import (
|
|
OCR_DOCUMENT,
|
|
OCR_RESPONSE,
|
|
call_aocr,
|
|
call_ocr,
|
|
)
|
|
from tests.test_litellm_rust.support.recording_server import RecordingServer, ResponseSpec
|
|
from tests.test_litellm_rust.support.routing import assert_served_natively
|
|
|
|
pytestmark = pytest.mark.requires_rust_extension
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
|
|
@pytest.mark.parametrize("model", ["mistral/mistral-ocr-latest", "azure_ai/mistral-ocr-latest", "reducto/parse-v3"])
|
|
async def test_ocr_contract_native_format_supported(
|
|
server: RecordingServer,
|
|
asynchronous: bool,
|
|
model: str,
|
|
) -> None:
|
|
server.expected_requests = None
|
|
payload: Final = (
|
|
{"result": {"chunks": [{"content": "native OCR response"}]}, "usage": {"num_pages": 1}}
|
|
if model.startswith("reducto/")
|
|
else OCR_RESPONSE
|
|
)
|
|
server.default_response = ResponseSpec(body=payload)
|
|
arguments: Final = {
|
|
"model": model,
|
|
"req_format": "native",
|
|
"num_retries": 0,
|
|
"document": {"type": "document_url", "document_url": "reducto://ready.pdf"}
|
|
if model.startswith("reducto/")
|
|
else OCR_DOCUMENT,
|
|
}
|
|
response: Final = await call_aocr(server, **arguments) if asynchronous else call_ocr(server, **arguments)
|
|
assert response.pages[0].markdown == "native OCR response"
|
|
assert response.get_provider_native_response() == payload
|
|
assert len(server.requests) == 1
|
|
assert_served_natively(server, response)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
|
|
async def test_ocr_contract_unknown_reducto_model_reaches_provider(
|
|
server: RecordingServer,
|
|
asynchronous: bool,
|
|
) -> None:
|
|
server.default_response = ResponseSpec(body={"result": {"chunks": [{"content": "future model response"}]}})
|
|
arguments: Final = {
|
|
"model": "reducto/future-parse-model",
|
|
"document": {"type": "document_url", "document_url": "reducto://ready.pdf"},
|
|
"num_retries": 0,
|
|
}
|
|
response: Final = await call_aocr(server, **arguments) if asynchronous else call_ocr(server, **arguments)
|
|
assert response.model == "future-parse-model"
|
|
assert response.pages[0].markdown == "future model response"
|
|
assert len(server.requests) == 1
|
|
assert server.requests[0].path == "/parse"
|
|
assert server.requests[0].body == {"input": "reducto://ready.pdf"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
|
|
async def test_native_azure_ocr_uses_token_provider_result_as_bearer_token(
|
|
server: RecordingServer, isolated_azure_auth: None, asynchronous: bool
|
|
) -> None:
|
|
calls: Final = []
|
|
|
|
def token_provider() -> str:
|
|
calls.append("token")
|
|
return "callback-token"
|
|
|
|
arguments: Final = {
|
|
"model": "azure_ai/mistral-ocr-latest",
|
|
"api_key": None,
|
|
"azure_ad_token_provider": token_provider,
|
|
}
|
|
response: Final = await call_aocr(server, **arguments) if asynchronous else call_ocr(server, **arguments)
|
|
|
|
assert calls == ["token"]
|
|
assert response.pages[0].markdown == "native OCR response"
|
|
assert_served_natively(server, response)
|
|
assert server.requests[0].headers["authorization"] == "Bearer callback-token"
|
|
|
|
|
|
def test_native_ocr_encodes_python_file_input_and_drops_unknown_arguments(server: RecordingServer) -> None:
|
|
response: Final = call_ocr(
|
|
server,
|
|
document={"type": "file", "file": BytesIO(b"abc"), "mime_type": "image/png"},
|
|
opaque_extension=object(),
|
|
)
|
|
|
|
assert response.pages[0].markdown == "native OCR response"
|
|
assert_served_natively(server, response)
|
|
assert server.requests[0].body == {
|
|
"model": "mistral-ocr-latest",
|
|
"document": {"type": "image_url", "image_url": "data:image/png;base64,YWJj"},
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"drop_params,global_drop_params",
|
|
[(True, False), (" TRUE ", False), (None, True), (False, True)],
|
|
ids=["per_call", "per_call_spelled", "global", "global_wins"],
|
|
)
|
|
def test_azure_document_intelligence_drop_params_drops_an_unsupported_output_content_format(
|
|
server: RecordingServer,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
drop_params: object,
|
|
global_drop_params: bool,
|
|
) -> None:
|
|
monkeypatch.setattr(litellm, "drop_params", global_drop_params)
|
|
server.default_response = ResponseSpec(
|
|
body={"status": "succeeded", "analyzeResult": {"pages": [{"pageNumber": 1, "lines": [{"content": "hello"}]}]}}
|
|
)
|
|
|
|
response: Final = call_ocr(
|
|
server,
|
|
model="azure_ai/doc-intelligence/prebuilt-layout",
|
|
outputContentFormat="html",
|
|
drop_params=drop_params,
|
|
)
|
|
|
|
assert_served_natively(server, response)
|
|
assert response.pages[0].markdown == "hello"
|
|
assert "outputContentFormat" not in server.requests[0].path
|
|
assert "drop_params" not in server.requests[0].path
|