encode failing tests

This commit is contained in:
Yujong Lee 2026-09-17 08:04:52 -07:00
parent b063ffe883
commit 370cdaabf9
4 changed files with 290 additions and 1 deletions

View file

@ -419,4 +419,18 @@ mod tests {
model.split_once('/').unwrap().1
);
}
#[rstest]
#[case::prefix("not_a_provider/model", None)]
#[case::explicit("model", Some("not_a_provider"))]
fn ocr_contract_unknown_provider_is_bad_request(
#[case] model: &str,
#[case] provider: Option<&str>,
) {
let error = resolve_provider_config(model, provider).unwrap_err();
assert!(
matches!(&error, crate::ocr::Error::InvalidProvider(provider) if provider == "not_a_provider")
);
assert_eq!(error.http_status_code(), Some(400));
}
}

View file

@ -106,6 +106,35 @@ pub fn decode_document(value: Value) -> Result<OcrDocument, Error> {
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use serde_json::json;
#[rstest]
#[case::omitted(json!({"type":"document_url", "document_url":"https://example.com/a.pdf"}))]
#[case::null(json!({"type":"document_url", "document_url":"https://example.com/a.pdf", "document_name":null}))]
fn ocr_contract_optional_document_name(#[case] document: Value) {
let decoded = decode_document(document).unwrap();
assert_eq!(decoded.source(), "https://example.com/a.pdf");
}
#[rstest]
#[case::non_object(json!([]), "document")]
#[case::missing_type(json!({"document_url":"https://example.com/a.pdf"}), "document")]
#[case::unsupported_type(json!({"type":"text"}), "document")]
#[case::missing_document_url(json!({"type":"document_url"}), "Document URL")]
#[case::missing_image_url(json!({"type":"image_url"}), "Document URL")]
fn ocr_contract_malformed_document_is_bad_request(
#[case] document: Value,
#[case] field: &str,
) {
let error = decode_document(document).unwrap_err();
assert!(matches!(
error,
Error::RequestField { .. } | Error::MissingDocumentUrl
));
assert_eq!(error.http_status_code(), Some(400));
assert!(error.to_string().contains(field));
}
#[test]
fn option_projection_is_provider_specific_and_excludes_opaque_fields() {

View file

@ -1,5 +1,6 @@
use std::sync::{Arc, Mutex};
use rstest::rstest;
use serde_json::{Value, json};
use super::OcrClient;
@ -15,6 +16,59 @@ use super::{
};
use crate::call_lifecycle::{CallLifecycleContext, CallLifecycleTiming};
#[rstest]
#[case::mistral("mistral/model", json!({}))]
#[case::vertex("vertex_ai/mistral-ocr-latest", json!({"vertex_project":"test-project", "vertex_location":"us-central1"}))]
#[tokio::test]
async fn ocr_contract_upstream_error_preserves_status_body_and_headers(
#[case] model: &str,
#[case] options: Value,
) {
let payload = json!({"message": format!("{} END-OF-PROVIDER-BODY", "x".repeat(4096))});
let expected_body = serde_json::to_string(&payload).unwrap();
let (base, seen, server) = mock_server(vec![MockResponse {
status: 422,
headers: vec![
("Retry-After", "17".into()),
("X-Request-ID", "request-123".into()),
("X-Future-Header", "retained".into()),
],
body: payload,
}])
.await;
let error = perform_ocr(wire_request(model, &base, options))
.await
.unwrap_err();
server.await.unwrap();
assert_eq!(seen.lock().unwrap().len(), 1);
let super::Error::Provider {
status,
body,
headers,
} = error
else {
panic!("expected provider error, got {error:?}");
};
assert_eq!(status, 422);
for (name, value) in [
("retry-after", "17"),
("x-request-id", "request-123"),
("x-future-header", "retained"),
] {
assert!(
headers
.iter()
.any(|(key, actual)| key.eq_ignore_ascii_case(name) && actual == value)
);
}
assert_eq!(
body.len(),
expected_body.len(),
"provider error body was truncated"
);
assert_eq!(body, expected_body);
}
#[test]
fn request_boundary_selects_mistral_and_rejects_unknown_providers() {
let request = OcrWireRequest {

View file

@ -1,7 +1,10 @@
import json
from pathlib import Path
from typing import Final
import httpx
import pytest
from pydantic import JsonValue
import litellm
from litellm.llms.base_llm.ocr.transformation import OCRResponse
@ -17,6 +20,193 @@ from tests.test_litellm_rust.support.requests import (
pytestmark = pytest.mark.requires_rust_extension
@pytest.fixture(params=[False, True], ids=["python", "rust"])
def ocr_backend(request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch) -> bool:
enabled: Final = bool(request.param)
monkeypatch.setenv("LITELLM_RUST", "1" if enabled else "0")
return enabled
@pytest.mark.asyncio
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
async def test_ocr_contract_upstream_status(
ocr_server: RecordingServer,
ocr_backend: bool,
asynchronous: bool,
) -> None:
upstream: Final = ResponseSpec(body={"detail": "invalid provider option"}, status=422)
ocr_server.enqueue(upstream)
arguments: Final = {
"model": "vertex_ai/mistral-ocr-latest",
"vertex_project": "test-project",
"vertex_location": "us-central1",
"num_retries": 0,
}
with pytest.raises(litellm.BadRequestError) as caught:
if asynchronous:
await call_native_aocr(ocr_server, **arguments)
else:
call_native_ocr(ocr_server, **arguments)
assert caught.value.status_code == upstream.status
assert caught.value.response.status_code == upstream.status
@pytest.mark.asyncio
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
@pytest.mark.parametrize("preserved", ["body", "headers"])
async def test_ocr_contract_provider_error_details(
ocr_server: RecordingServer,
ocr_backend: bool,
asynchronous: bool,
preserved: str,
) -> None:
payload: Final = {"message": "rate limited"}
headers: Final = {"Retry-After": "17", "X-Request-ID": "ocr-request-123", "X-Future-Header": "retained"}
ocr_server.enqueue(ResponseSpec(body=payload, status=429, headers=headers))
with pytest.raises(litellm.RateLimitError) as caught:
if asynchronous:
await call_native_aocr(ocr_server, num_retries=0)
else:
call_native_ocr(ocr_server, num_retries=0)
response: Final = caught.value.response
assert isinstance(response, httpx.Response)
if preserved == "body":
assert response.content == json.dumps(payload).encode()
else:
for name, value in headers.items():
assert response.headers.get(name.lower()) == value
assert response.headers.get(name.upper()) == value
@pytest.mark.asyncio
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
async def test_ocr_contract_invalid_response_format(
ocr_server: RecordingServer,
ocr_backend: bool,
asynchronous: bool,
) -> None:
ocr_server.expected_requests = 0
with pytest.raises(litellm.UnsupportedParamsError) as caught:
if asynchronous:
await call_native_aocr(ocr_server, req_format="bogus", num_retries=0)
else:
call_native_ocr(ocr_server, req_format="bogus", num_retries=0)
assert caught.value.status_code == 400
for value in ("req_format", "bogus", "native", "litellm"):
assert value in str(caught.value)
assert ocr_server.requests == []
@pytest.mark.asyncio
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
@pytest.mark.parametrize(
"document,field",
[
([], "document"),
({"document_url": "https://example.com/a.pdf"}, "type"),
({"type": "text"}, "type"),
],
)
async def test_ocr_contract_malformed_document_is_actionable(
ocr_server: RecordingServer,
ocr_backend: bool,
asynchronous: bool,
document: JsonValue,
field: str,
) -> None:
ocr_server.expected_requests = None
with pytest.raises(litellm.BadRequestError) as caught:
if asynchronous:
await call_native_aocr(ocr_server, document=document, num_retries=0)
else:
call_native_ocr(ocr_server, document=document, num_retries=0)
assert caught.value.status_code == 400
assert field.lower() in str(caught.value).lower()
assert "NoneType: None" not in str(caught.value)
assert "indices must be" not in str(caught.value)
assert ocr_server.requests == []
@pytest.mark.asyncio
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
@pytest.mark.parametrize("option,value,field", [("pages", [-1], "pages"), ("features", [1], "features")])
async def test_ocr_contract_azure_invalid_options_are_bad_requests(
ocr_server: RecordingServer,
ocr_backend: bool,
asynchronous: bool,
option: str,
value: JsonValue,
field: str,
) -> None:
ocr_server.expected_requests = 0
arguments: Final = {"model": "azure_ai/doc-intelligence/prebuilt-read", option: value, "num_retries": 0}
with pytest.raises(litellm.BadRequestError) as caught:
if asynchronous:
await call_native_aocr(ocr_server, **arguments)
else:
call_native_ocr(ocr_server, **arguments)
assert caught.value.status_code == 400
assert field in str(caught.value)
assert ocr_server.requests == []
@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(
ocr_server: RecordingServer,
ocr_backend: bool,
asynchronous: bool,
model: str,
) -> None:
ocr_server.expected_requests = None
payload: Final = (
{"result": {"chunks": [{"content": "native OCR response"}]}, "usage": {"num_pages": 1}}
if model.startswith("reducto/")
else OCR_RESPONSE
)
ocr_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_native_aocr(ocr_server, **arguments) if asynchronous else call_native_ocr(ocr_server, **arguments)
)
assert response.pages[0].markdown == "native OCR response"
assert response.get_provider_native_response() == payload
assert len(ocr_server.requests) == 1
if ocr_backend:
assert_native_request(ocr_server)
@pytest.mark.asyncio
@pytest.mark.parametrize("asynchronous", [False, True], ids=["sync", "async"])
async def test_ocr_contract_unknown_reducto_model_reaches_provider(
ocr_server: RecordingServer,
ocr_backend: bool,
asynchronous: bool,
) -> None:
ocr_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_native_aocr(ocr_server, **arguments) if asynchronous else call_native_ocr(ocr_server, **arguments)
)
assert response.model == "future-parse-model"
assert response.pages[0].markdown == "future model response"
assert len(ocr_server.requests) == 1
assert ocr_server.requests[0].path == "/parse"
assert ocr_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(
@ -595,7 +785,9 @@ def test_native_file_preparation_rejects_unsupported_reader_results(ocr_server:
@pytest.mark.parametrize("kind", ["bytes", "path", "reader"])
def test_native_file_preparation_rejects_oversized_input(ocr_server: RecordingServer, kind: str, tmp_path: Path) -> None:
def test_native_file_preparation_rejects_oversized_input(
ocr_server: RecordingServer, kind: str, tmp_path: Path
) -> None:
ocr_server.expected_requests = 0
limit: Final = 50 * 1024 * 1024
path: Final = tmp_path / "large.pdf"