mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-20 00:11:50 +00:00
encode failing tests
This commit is contained in:
parent
b063ffe883
commit
370cdaabf9
4 changed files with 290 additions and 1 deletions
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue