mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
test(ocr): generate bounded semantic provider fixtures
This commit is contained in:
parent
077d6e5e69
commit
bd860a2af6
13 changed files with 1507 additions and 178 deletions
|
|
@ -14,7 +14,7 @@ const AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT_ENV: &str = "AZURE_DOCUMENT_INTELLIGE
|
|||
const AZURE_DOCUMENT_INTELLIGENCE_API_VERSION: &str = "2024-11-30";
|
||||
const AZURE_DOCUMENT_INTELLIGENCE_DEFAULT_DPI: i64 = 96;
|
||||
|
||||
const AZURE_DOCUMENT_INTELLIGENCE_SUPPORTED_OCR_PARAMS: &[&str] = &["pages"];
|
||||
const AZURE_DOCUMENT_INTELLIGENCE_SUPPORTED_OCR_PARAMS: &[&str] = &["pages", "features"];
|
||||
|
||||
pub struct AzureAiOcrConfig;
|
||||
pub struct AzureDocumentIntelligenceOcrConfig;
|
||||
|
|
@ -192,6 +192,46 @@ fn normalize_pages_param(pages: &Value) -> CoreResult<Option<String>> {
|
|||
}
|
||||
}
|
||||
|
||||
fn feature_token_is_valid(token: &str) -> bool {
|
||||
let Some((first, rest)) = token.as_bytes().split_first() else {
|
||||
return false;
|
||||
};
|
||||
first.is_ascii_alphabetic() && rest.iter().all(u8::is_ascii_alphanumeric)
|
||||
}
|
||||
|
||||
fn invalid_features_error(features: &Value) -> CoreError {
|
||||
CoreError::InvalidRequest(format!(
|
||||
"Invalid `features` for Azure Document Intelligence: {features:?}. Expected a list of feature names or a comma-separated string like 'keyValuePairs' or 'keyValuePairs,languages'."
|
||||
))
|
||||
}
|
||||
|
||||
fn normalize_features_param(features: &Value) -> CoreResult<Option<String>> {
|
||||
let normalized = match features {
|
||||
Value::String(value) => value
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
Value::Array(values) if values.is_empty() => return Ok(None),
|
||||
Value::Array(values) => values
|
||||
.iter()
|
||||
.map(Value::as_str)
|
||||
.collect::<Option<Vec<_>>>()
|
||||
.ok_or_else(|| invalid_features_error(features))?
|
||||
.into_iter()
|
||||
.map(str::trim)
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
_ => return Err(invalid_features_error(features)),
|
||||
};
|
||||
|
||||
if normalized.split(',').all(feature_token_is_valid) {
|
||||
Ok(Some(normalized))
|
||||
} else {
|
||||
Err(invalid_features_error(features))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn complete_document_intelligence_url(
|
||||
api_base: Option<&str>,
|
||||
model: &str,
|
||||
|
|
@ -213,6 +253,13 @@ pub fn complete_document_intelligence_url(
|
|||
url.push_str(&normalized);
|
||||
}
|
||||
|
||||
if let Some(features) = optional_params.get("features")
|
||||
&& let Some(normalized) = normalize_features_param(features)?
|
||||
{
|
||||
url.push_str("&features=");
|
||||
url.push_str(&normalized);
|
||||
}
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
|
|
@ -475,6 +522,103 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_intelligence_url_normalizes_features() {
|
||||
let params = serde_json::Map::from_iter([(
|
||||
"features".to_string(),
|
||||
json!("keyValuePairs, languages"),
|
||||
)]);
|
||||
let url = complete_document_intelligence_url(
|
||||
Some("https://example.cognitiveservices.azure.com"),
|
||||
"prebuilt-layout",
|
||||
¶ms,
|
||||
&|_| None,
|
||||
)
|
||||
.expect("url builds");
|
||||
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://example.cognitiveservices.azure.com/documentintelligence/documentModels/prebuilt-layout:analyze?api-version=2024-11-30&features=keyValuePairs,languages"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_intelligence_url_combines_pages_and_feature_list() {
|
||||
let params = serde_json::Map::from_iter([
|
||||
("pages".to_string(), json!([0, 1, 2])),
|
||||
(
|
||||
"features".to_string(),
|
||||
json!([" keyValuePairs ", "languages"]),
|
||||
),
|
||||
]);
|
||||
let url = complete_document_intelligence_url(
|
||||
Some("https://example.cognitiveservices.azure.com"),
|
||||
"prebuilt-layout",
|
||||
¶ms,
|
||||
&|_| None,
|
||||
)
|
||||
.expect("url builds");
|
||||
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://example.cognitiveservices.azure.com/documentintelligence/documentModels/prebuilt-layout:analyze?api-version=2024-11-30&pages=1,2,3&features=keyValuePairs,languages"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_intelligence_url_omits_empty_feature_list() {
|
||||
let params = serde_json::Map::from_iter([("features".to_string(), json!([]))]);
|
||||
let url = complete_document_intelligence_url(
|
||||
Some("https://example.cognitiveservices.azure.com"),
|
||||
"prebuilt-layout",
|
||||
¶ms,
|
||||
&|_| None,
|
||||
)
|
||||
.expect("url builds");
|
||||
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://example.cognitiveservices.azure.com/documentintelligence/documentModels/prebuilt-layout:analyze?api-version=2024-11-30"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_intelligence_url_rejects_invalid_features() {
|
||||
for features in [
|
||||
json!("keyValuePairs&pages=9"),
|
||||
json!(""),
|
||||
json!(["keyValuePairs", 1]),
|
||||
json!({"feature": "keyValuePairs"}),
|
||||
] {
|
||||
let params = serde_json::Map::from_iter([("features".to_string(), features.clone())]);
|
||||
let error = complete_document_intelligence_url(
|
||||
Some("https://example.cognitiveservices.azure.com"),
|
||||
"prebuilt-layout",
|
||||
¶ms,
|
||||
&|_| None,
|
||||
)
|
||||
.expect_err("invalid features must fail");
|
||||
|
||||
assert!(
|
||||
matches!(error, CoreError::InvalidRequest(message) if message.contains("Invalid `features`")),
|
||||
"features={features:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_intelligence_maps_features() {
|
||||
let params = Map::from_iter([
|
||||
("features".to_string(), json!(["keyValuePairs"])),
|
||||
("unsupported".to_string(), json!(true)),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
AZURE_DOCUMENT_INTELLIGENCE_OCR_CONFIG.map_ocr_params(¶ms),
|
||||
Map::from_iter([("features".to_string(), json!(["keyValuePairs"]))])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_intelligence_request_uses_base64_source_for_data_uri() {
|
||||
let body = AZURE_DOCUMENT_INTELLIGENCE_OCR_CONFIG
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ litellm-proxy = "litellm.proxy.client.cli:cli"
|
|||
dev = [
|
||||
"diff-cover==9.7.2",
|
||||
"hypothesis==6.165.10",
|
||||
"reportlab==5.0.1",
|
||||
"basedpyright==1.39.7",
|
||||
"keyring==25.7.0",
|
||||
"pytest==9.0.3",
|
||||
|
|
|
|||
225
tests/route_parity/fixtures/media.py
Normal file
225
tests/route_parity/fixtures/media.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from functools import cache
|
||||
from io import BytesIO
|
||||
from typing import Final
|
||||
from urllib.parse import quote
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
from reportlab.graphics.barcode import code128 # pyright: ignore[reportMissingTypeStubs] # ReportLab has no stubs
|
||||
from reportlab.lib import colors # pyright: ignore[reportMissingTypeStubs] # ReportLab has no stubs
|
||||
from reportlab.lib.pagesizes import letter # pyright: ignore[reportMissingTypeStubs] # ReportLab has no stubs
|
||||
from reportlab.lib.utils import ImageReader # pyright: ignore[reportMissingTypeStubs] # ReportLab has no stubs
|
||||
from reportlab.pdfgen import canvas # pyright: ignore[reportMissingTypeStubs] # ReportLab has no stubs
|
||||
|
||||
|
||||
def dummy_image_url(text: str, font_size: int, width: int = 800, height: int = 300) -> str:
|
||||
return f"https://dummyjson.com/image/{width}x{height}/ffffff/000000?text={quote(text)}&fontSize={font_size}"
|
||||
|
||||
|
||||
_GLYPHS: Final = {
|
||||
"O": ("01110", "10001", "10001", "10001", "10001", "10001", "01110"),
|
||||
"C": ("01111", "10000", "10000", "10000", "10000", "10000", "01111"),
|
||||
"R": ("11110", "10001", "10001", "11110", "10100", "10010", "10001"),
|
||||
"1": ("00100", "01100", "00100", "00100", "00100", "00100", "01110"),
|
||||
"2": ("01110", "10001", "00001", "00010", "00100", "01000", "11111"),
|
||||
"3": ("11110", "00001", "00001", "01110", "00001", "00001", "11110"),
|
||||
}
|
||||
|
||||
|
||||
@cache
|
||||
def structured_image_bytes() -> bytes:
|
||||
image: Final = Image.new("RGB", (320, 80), "white")
|
||||
draw: Final = ImageDraw.Draw(image)
|
||||
scale: Final = 8
|
||||
cursor_x = 24
|
||||
for character in "OCR 123":
|
||||
if character == " ":
|
||||
cursor_x += scale * 3
|
||||
continue
|
||||
for glyph_y, row in enumerate(_GLYPHS[character]):
|
||||
for glyph_x, filled in enumerate(row):
|
||||
if filled == "1":
|
||||
x = cursor_x + glyph_x * scale
|
||||
y = 12 + glyph_y * scale
|
||||
draw.rectangle((x, y, x + scale - 1, y + scale - 1), fill="black")
|
||||
cursor_x += scale * 6
|
||||
output: Final = BytesIO()
|
||||
image.save(output, format="PNG")
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
@cache
|
||||
def structured_image_data_uri() -> str:
|
||||
encoded: Final = base64.b64encode(structured_image_bytes()).decode("ascii")
|
||||
return f"data:image/png;base64,{encoded}"
|
||||
|
||||
|
||||
def _draw_header(pdf: canvas.Canvas, title: str, page_number: int) -> None:
|
||||
pdf.setFillColor(colors.black)
|
||||
pdf.setFont("Helvetica", 11)
|
||||
pdf.drawString(45, 770, "Quarterly Operations Report")
|
||||
pdf.setFont("Helvetica-Bold", 16)
|
||||
pdf.drawString(45, 745, title)
|
||||
pdf.setFont("Helvetica", 9)
|
||||
pdf.drawString(45, 30, f"Confidential | Page {page_number} of 5")
|
||||
|
||||
|
||||
def _draw_body(pdf: canvas.Canvas, page_number: int) -> None:
|
||||
pdf.setFont("Helvetica", 10)
|
||||
for line_number in range(1, 9):
|
||||
pdf.drawString(
|
||||
45,
|
||||
500 - (line_number * 28),
|
||||
f"Section {page_number}.{line_number}: Invoice totals, regional revenue, and reconciliation notes.",
|
||||
)
|
||||
|
||||
|
||||
def _diagram_image(width: int, height: int, accent: tuple[int, int, int]) -> Image.Image:
|
||||
image: Final = Image.new("RGB", (width, height), (242, 246, 252))
|
||||
draw: Final = ImageDraw.Draw(image)
|
||||
for coordinate in range(0, max(width, height), 40):
|
||||
draw.line((coordinate, 0, coordinate, height), fill=(32, 32, 32), width=3)
|
||||
draw.line((0, coordinate, width, coordinate), fill=(32, 32, 32), width=3)
|
||||
draw.line((0, 0, width, height), fill=accent, width=8)
|
||||
draw.line((width, 0, 0, height), fill=accent, width=8)
|
||||
draw.rectangle((width // 4, height // 4, width * 3 // 4, height * 3 // 4), outline=accent, width=6)
|
||||
return image
|
||||
|
||||
|
||||
def _draw_embedded_images(pdf: canvas.Canvas) -> None:
|
||||
images: Final = (
|
||||
(_diagram_image(320, 320, (51, 115, 217)), 455, 655, 70, 70),
|
||||
(_diagram_image(360, 320, (38, 151, 92)), 455, 565, 70, 62),
|
||||
(_diagram_image(120, 120, (219, 68, 55)), 455, 500, 45, 45),
|
||||
)
|
||||
for image, x, y, width, height in images:
|
||||
pdf.drawImage( # pyright: ignore[reportUnknownMemberType] # ReportLab has no stubs
|
||||
ImageReader(image), x, y, width=width, height=height, mask="auto"
|
||||
)
|
||||
|
||||
|
||||
def _draw_table_page(pdf: canvas.Canvas) -> None:
|
||||
columns: Final = (45, 245, 405, 565)
|
||||
tables: Final = (
|
||||
(
|
||||
(730, 695, 660, 625),
|
||||
(
|
||||
("Item", "Quantity", "Amount", 707),
|
||||
("Document analysis", "2", "120.00", 672),
|
||||
("OCR verification", "1", "80.00", 637),
|
||||
),
|
||||
),
|
||||
(
|
||||
(600, 565, 530, 495),
|
||||
(
|
||||
("Item continued", "Quantity", "Amount", 577),
|
||||
("Fixture validation", "3", "45.00", 542),
|
||||
("Provider review", "1", "25.00", 507),
|
||||
),
|
||||
),
|
||||
)
|
||||
for rows, values in tables:
|
||||
for x in columns:
|
||||
pdf.line(x, rows[-1], x, rows[0])
|
||||
for y in rows:
|
||||
pdf.line(45, y, 565, y)
|
||||
for item, quantity, amount, y in values:
|
||||
pdf.drawString(55, y, item)
|
||||
pdf.drawString(255, y, quantity)
|
||||
pdf.drawString(415, y, amount)
|
||||
|
||||
|
||||
def _draw_chart_page(pdf: canvas.Canvas) -> None:
|
||||
bars: Final = ((70, 70), (170, 115), (270, 90), (370, 130))
|
||||
pdf.setFillColor(colors.HexColor("#3373D9"))
|
||||
for x, height in bars:
|
||||
pdf.rect(x, 610, 65, height, fill=1, stroke=0)
|
||||
pdf.setFillColor(colors.black)
|
||||
for quarter, x in zip(("Q1", "Q2", "Q3", "Q4"), (90, 190, 290, 390), strict=True):
|
||||
pdf.drawString(x, 590, quarter)
|
||||
pdf.drawString(45, 550, "Formula: gross margin = (revenue - cost) / revenue")
|
||||
_draw_embedded_images(pdf)
|
||||
|
||||
|
||||
def _draw_metadata_page(pdf: canvas.Canvas) -> None:
|
||||
pdf.setFont("Helvetica", 12)
|
||||
pdf.drawString(45, 700, "Invoice Number: INV-2048")
|
||||
pdf.drawString(45, 675, "Purchase Order: PO-4096")
|
||||
pdf.setFillColor(colors.HexColor("#F2E65A"))
|
||||
pdf.rect(40, 555, 500, 24, fill=1, stroke=0)
|
||||
pdf.setFillColor(colors.black)
|
||||
pdf.drawString(45, 560, "Highlighted total requiring review")
|
||||
pdf.drawString(45, 530, "Reviewer comment: verify the highlighted total before approval")
|
||||
pdf.setFillColor(colors.red)
|
||||
pdf.drawString(45, 495, "Revised total: 245.00")
|
||||
pdf.line(45, 501, 150, 501)
|
||||
pdf.setFillColor(colors.black)
|
||||
pdf.linkURL( # pyright: ignore[reportUnknownMemberType] # ReportLab has no stubs
|
||||
"https://example.com/invoices/INV-2048", (45, 575, 300, 590), relative=0
|
||||
)
|
||||
pdf.highlightAnnotation( # pyright: ignore[reportUnknownMemberType] # ReportLab has no stubs
|
||||
"Total highlighted for review",
|
||||
Rect=(40, 555, 540, 579),
|
||||
QuadPoints=(40, 579, 540, 579, 40, 555, 540, 555),
|
||||
)
|
||||
pdf.textAnnotation( # pyright: ignore[reportUnknownMemberType] # ReportLab has no stubs
|
||||
"Verify the highlighted total", Rect=(520, 525, 540, 545)
|
||||
)
|
||||
pdf.drawString(45, 575, "https://example.com/invoices/INV-2048")
|
||||
barcode: Final = code128.Code128("5901234123457", barHeight=70, barWidth=1.2)
|
||||
barcode.drawOn(pdf, 90, 130)
|
||||
|
||||
|
||||
def _draw_signature_page(pdf: canvas.Canvas) -> None:
|
||||
pdf.saveState()
|
||||
pdf.setFillColor(colors.lightgrey)
|
||||
pdf.setFont("Helvetica-Bold", 54)
|
||||
pdf.translate(110, 390)
|
||||
pdf.rotate(25)
|
||||
pdf.drawString(0, 0, "DRAFT")
|
||||
pdf.restoreState()
|
||||
pdf.setFillColor(colors.black)
|
||||
pdf.setFont("Helvetica", 12)
|
||||
pdf.drawString(45, 635, "Approved by: Jordan Lee")
|
||||
pdf.line(45, 610, 310, 610)
|
||||
pdf.bezier(55, 595, 75, 625, 112, 602, 155, 600)
|
||||
pdf.drawString(45, 580, "Signature")
|
||||
|
||||
|
||||
def _draw_appendix_page(pdf: canvas.Canvas) -> None:
|
||||
pdf.setFont("Helvetica-Bold", 14)
|
||||
pdf.drawString(45, 700, "1. Scope")
|
||||
pdf.drawString(45, 650, "2. Findings")
|
||||
pdf.drawString(45, 600, "3. Recommendations")
|
||||
|
||||
|
||||
@cache
|
||||
def structured_pdf_bytes() -> bytes:
|
||||
output: Final = BytesIO()
|
||||
pdf: Final = canvas.Canvas(output, pagesize=letter, pageCompression=0, invariant=1)
|
||||
pdf.setTitle("Quarterly Operations Report")
|
||||
pdf.setAuthor("LiteLLM OCR fixture generator")
|
||||
pdf.setSubject("Semantic OCR coverage for tables, figures, annotations, and metadata")
|
||||
pdf.setKeywords("OCR, invoice, table, figure, annotation")
|
||||
pages: Final = (
|
||||
("Invoice Summary and Line Items", _draw_table_page),
|
||||
("Revenue Chart and Formula Review", _draw_chart_page),
|
||||
("Key Values, Link, Highlight, and Comment", _draw_metadata_page),
|
||||
("Approval Signature and Watermark", _draw_signature_page),
|
||||
("Appendix with Section Boundaries", _draw_appendix_page),
|
||||
)
|
||||
for page_number, (title, draw_page) in enumerate(pages, start=1):
|
||||
_draw_header(pdf, title, page_number)
|
||||
draw_page(pdf)
|
||||
_draw_body(pdf, page_number)
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
@cache
|
||||
def structured_pdf_data_uri() -> str:
|
||||
encoded: Final = base64.b64encode(structured_pdf_bytes()).decode("ascii")
|
||||
return f"data:application/pdf;base64,{encoded}"
|
||||
27
tests/route_parity/fixtures/test_media.py
Normal file
27
tests/route_parity/fixtures/test_media.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from typing import Final, cast
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from tests.route_parity.fixtures.media import dummy_image_url, structured_image_bytes, structured_image_data_uri
|
||||
|
||||
|
||||
def test_dummy_image_url_encodes_text_and_dimensions() -> None:
|
||||
assert dummy_image_url("invoice 123", 24, width=320, height=80) == (
|
||||
"https://dummyjson.com/image/320x80/ffffff/000000?text=invoice%20123&fontSize=24"
|
||||
)
|
||||
|
||||
|
||||
def test_structured_image_is_local_content_bearing_png() -> None:
|
||||
png: Final = structured_image_bytes()
|
||||
encoded: Final = structured_image_data_uri().partition(",")[2]
|
||||
image: Final = Image.open(BytesIO(png))
|
||||
colors: Final = cast(list[tuple[int, tuple[int, int, int]]], image.getcolors(maxcolors=2))
|
||||
|
||||
assert png.startswith(b"\x89PNG\r\n\x1a\n")
|
||||
assert base64.b64decode(encoded, validate=True) == png
|
||||
assert image.size == (320, 80)
|
||||
assert {color for _, color in colors} == {(0, 0, 0), (255, 255, 255)}
|
||||
|
|
@ -5,25 +5,24 @@ from typing import Final, Literal, cast
|
|||
|
||||
from hypothesis import strategies as st
|
||||
from hypothesis.strategies import SearchStrategy
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic import Field, StrictInt, StrictStr, field_validator
|
||||
|
||||
from tests.route_parity.fixtures.recording import ProviderSpec
|
||||
from tests.test_litellm.ocr.fixtures.base import OcrDocument, OcrSdkInputBase
|
||||
from tests.test_litellm.ocr.fixtures.common import (
|
||||
OcrFixtureClient,
|
||||
OcrRecordingTarget,
|
||||
image_document,
|
||||
invoke_with_api_key,
|
||||
parameter_strategy,
|
||||
pdf_document,
|
||||
public_document_strategy,
|
||||
sampled_list_strategy,
|
||||
sampled_parameter_group_strategy,
|
||||
sampled_scalar_strategy,
|
||||
)
|
||||
from tests.test_litellm.ocr.fixtures.mistral import (
|
||||
MISTRAL_MODEL,
|
||||
MistralCompatibleOcrSdkInput,
|
||||
MistralOcrSdkInput,
|
||||
mistral_input_strategy,
|
||||
mistral_input_values_strategy,
|
||||
)
|
||||
|
||||
AzureMistralModel = Literal["azure_ai/mistral-document-ai-2512",]
|
||||
|
|
@ -39,6 +38,12 @@ AZURE_DOCUMENT_INTELLIGENCE_MODELS: Final[tuple[AzureDocumentIntelligenceModel,
|
|||
"azure_ai/doc-intelligence/prebuilt-layout",
|
||||
"azure_ai/doc-intelligence/prebuilt-document",
|
||||
)
|
||||
# API v4 replaces prebuilt-document with prebuilt-layout plus keyValuePairs. Keep
|
||||
# the broader fixture model above so existing recordings remain loadable.
|
||||
AZURE_DOCUMENT_INTELLIGENCE_RECORDING_MODELS: Final[tuple[AzureDocumentIntelligenceModel, ...]] = (
|
||||
"azure_ai/doc-intelligence/prebuilt-read",
|
||||
"azure_ai/doc-intelligence/prebuilt-layout",
|
||||
)
|
||||
|
||||
|
||||
class AzureMistralOcrSdkInput(MistralCompatibleOcrSdkInput):
|
||||
|
|
@ -59,26 +64,28 @@ class AzureDocumentIntelligenceOcrSdkInput(OcrSdkInputBase):
|
|||
model: AzureDocumentIntelligenceModel
|
||||
document: OcrDocument
|
||||
custom_llm_provider: Literal["azure_ai"] | None = None
|
||||
pages: str | list[int] | None = None
|
||||
pages: str | list[StrictInt] | list[StrictStr] | None = None
|
||||
features: str | list[str] | None = None
|
||||
req_format: Literal["litellm"] = "litellm"
|
||||
|
||||
|
||||
def _as_azure_mistral(case_input: MistralOcrSdkInput, model: AzureMistralModel) -> AzureMistralOcrSdkInput:
|
||||
values: Final = case_input.model_dump(
|
||||
mode="python",
|
||||
exclude={"boundary", "model", "custom_llm_provider"},
|
||||
exclude_unset=True,
|
||||
)
|
||||
def _azure_mistral_input(values: dict[str, object], model: AzureMistralModel) -> AzureMistralOcrSdkInput:
|
||||
return AzureMistralOcrSdkInput.model_validate({**values, "model": model})
|
||||
|
||||
|
||||
def azure_mistral_input_strategy(inline_image_data_uri: str) -> SearchStrategy[AzureMistralOcrSdkInput]:
|
||||
# Foundry's active gateway schema rejects 2512-only controls and
|
||||
# document_annotation_prompt, even though native Mistral accepts them.
|
||||
return st.builds(
|
||||
_azure_mistral_input,
|
||||
values=mistral_input_values_strategy("2505", inline_image_data_uri, include_document_annotation_prompt=False),
|
||||
model=sampled_scalar_strategy(AZURE_MISTRAL_MODELS),
|
||||
)
|
||||
|
||||
|
||||
_AZURE_DOCUMENT_INTELLIGENCE_CANONICAL_MODEL: Final[AzureDocumentIntelligenceModel] = (
|
||||
"azure_ai/doc-intelligence/prebuilt-layout"
|
||||
)
|
||||
_AZURE_DOCUMENT_INTELLIGENCE_DOCUMENT_MODEL: Final[AzureDocumentIntelligenceModel] = (
|
||||
"azure_ai/doc-intelligence/prebuilt-document"
|
||||
)
|
||||
|
||||
|
||||
def _document_intelligence_input(
|
||||
|
|
@ -93,26 +100,41 @@ def _document_intelligence_input(
|
|||
|
||||
def azure_document_intelligence_input_strategy() -> SearchStrategy[AzureDocumentIntelligenceOcrSdkInput]:
|
||||
document: Final = pdf_document()
|
||||
pages: Final = st.one_of(
|
||||
parameter_strategy("pages", sampled_list_strategy(((0,), (0, 1)))),
|
||||
parameter_strategy("pages", sampled_scalar_strategy(("1", "1,2", "1-2"))),
|
||||
pages: Final = parameter_strategy(
|
||||
"pages",
|
||||
st.one_of(
|
||||
sampled_list_strategy(((0,), (2, 0, 0, 1))),
|
||||
sampled_list_strategy((("1", "2-4"),)),
|
||||
sampled_scalar_strategy(("1-4, 5",)),
|
||||
),
|
||||
)
|
||||
common_features: Final = parameter_strategy(
|
||||
features: Final = parameter_strategy(
|
||||
"features",
|
||||
st.one_of(
|
||||
sampled_list_strategy(
|
||||
(("languages",), ("ocrHighResolution",), ("barcodes",), ("formulas",), ("styleFont",))
|
||||
(
|
||||
("languages",),
|
||||
("ocrHighResolution",),
|
||||
("barcodes",),
|
||||
("formulas",),
|
||||
("styleFont",),
|
||||
("keyValuePairs",),
|
||||
)
|
||||
),
|
||||
sampled_scalar_strategy(("languages,styleFont",)),
|
||||
sampled_scalar_strategy(("languages, styleFont",)),
|
||||
),
|
||||
)
|
||||
combined_query: Final = sampled_parameter_group_strategy(
|
||||
((("pages", (0, 1)), ("features", ("languages", "styleFont"))),)
|
||||
)
|
||||
return st.one_of(
|
||||
st.sampled_from(AZURE_DOCUMENT_INTELLIGENCE_MODELS).map(
|
||||
st.sampled_from(AZURE_DOCUMENT_INTELLIGENCE_RECORDING_MODELS).map(
|
||||
lambda model: _document_intelligence_input(model, document)
|
||||
),
|
||||
public_document_strategy().map(
|
||||
lambda selected_document: _document_intelligence_input(
|
||||
_AZURE_DOCUMENT_INTELLIGENCE_CANONICAL_MODEL, selected_document
|
||||
st.just(
|
||||
_document_intelligence_input(
|
||||
_AZURE_DOCUMENT_INTELLIGENCE_CANONICAL_MODEL,
|
||||
image_document("invoice 123", 24),
|
||||
)
|
||||
),
|
||||
pages.map(
|
||||
|
|
@ -120,16 +142,14 @@ def azure_document_intelligence_input_strategy() -> SearchStrategy[AzureDocument
|
|||
_AZURE_DOCUMENT_INTELLIGENCE_CANONICAL_MODEL, document, optional_params
|
||||
)
|
||||
),
|
||||
common_features.map(
|
||||
features.map(
|
||||
lambda optional_params: _document_intelligence_input(
|
||||
_AZURE_DOCUMENT_INTELLIGENCE_CANONICAL_MODEL, document, optional_params
|
||||
)
|
||||
),
|
||||
st.just(
|
||||
_document_intelligence_input(
|
||||
_AZURE_DOCUMENT_INTELLIGENCE_DOCUMENT_MODEL,
|
||||
document,
|
||||
{"features": ["keyValuePairs"]},
|
||||
combined_query.map(
|
||||
lambda optional_params: _document_intelligence_input(
|
||||
_AZURE_DOCUMENT_INTELLIGENCE_CANONICAL_MODEL, document, optional_params
|
||||
)
|
||||
),
|
||||
st.just(
|
||||
|
|
@ -143,7 +163,7 @@ def azure_document_intelligence_input_strategy() -> SearchStrategy[AzureDocument
|
|||
|
||||
|
||||
def azure_mistral_recording_targets(
|
||||
environ: Mapping[str, str], client: OcrFixtureClient
|
||||
environ: Mapping[str, str], client: OcrFixtureClient, inline_image_data_uri: str
|
||||
) -> tuple[OcrRecordingTarget, ...]:
|
||||
api_key: Final = environ.get("AZURE_AI_API_KEY")
|
||||
upstream_base: Final = environ.get("AZURE_AI_API_BASE")
|
||||
|
|
@ -155,11 +175,7 @@ def azure_mistral_recording_targets(
|
|||
provider_spec=ProviderSpec(upstream_base=upstream_base.rstrip("/")),
|
||||
strategy=cast(
|
||||
SearchStrategy[OcrSdkInputBase],
|
||||
st.sampled_from(AZURE_MISTRAL_MODELS).flatmap(
|
||||
lambda model: mistral_input_strategy(MISTRAL_MODEL, feature_level="2512").map(
|
||||
lambda case_input: _as_azure_mistral(case_input, model)
|
||||
)
|
||||
),
|
||||
azure_mistral_input_strategy(inline_image_data_uri),
|
||||
),
|
||||
invocation=invoke_with_api_key(client, api_key),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Final, Protocol, TypeVar
|
||||
from urllib.parse import quote
|
||||
from functools import cache
|
||||
from typing import Final, Literal, Protocol, TypeVar
|
||||
|
||||
from hypothesis import strategies as st
|
||||
from hypothesis.strategies import SearchStrategy
|
||||
|
||||
from tests.route_parity.fixtures.media import dummy_image_url, structured_pdf_data_uri
|
||||
from tests.route_parity.fixtures.pipeline import RecordingTarget
|
||||
from tests.test_litellm.ocr.fixtures.base import (
|
||||
DocumentUrlDocument,
|
||||
|
|
@ -40,22 +39,46 @@ class ApiKeyOcrInvocation:
|
|||
|
||||
|
||||
def image_document(text: str, font_size: int) -> ImageUrlDocument:
|
||||
url: Final = f"https://dummyjson.com/image/800x300/ffffff/000000?text={quote(text)}&fontSize={font_size}"
|
||||
return ImageUrlDocument(type="image_url", image_url=url)
|
||||
return ImageUrlDocument(type="image_url", image_url=dummy_image_url(text, font_size))
|
||||
|
||||
|
||||
def fixture_pdf_data_uri() -> str:
|
||||
fixture: Final = Path(__file__).resolve().parents[3] / "llm_translation" / "fixtures" / "dummy.pdf"
|
||||
encoded: Final = base64.b64encode(fixture.read_bytes()).decode("ascii")
|
||||
return f"data:application/pdf;base64,{encoded}"
|
||||
def image_data_document(data_uri: str) -> ImageUrlDocument:
|
||||
return ImageUrlDocument(type="image_url", image_url=data_uri)
|
||||
|
||||
|
||||
@cache
|
||||
def remote_pdf_document() -> DocumentUrlDocument:
|
||||
return DocumentUrlDocument(
|
||||
type="document_url",
|
||||
document_url="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
|
||||
)
|
||||
|
||||
|
||||
@cache
|
||||
def pdf_document() -> DocumentUrlDocument:
|
||||
return DocumentUrlDocument(type="document_url", document_url=fixture_pdf_data_uri())
|
||||
return DocumentUrlDocument(type="document_url", document_url=structured_pdf_data_uri())
|
||||
|
||||
|
||||
def public_document_strategy() -> SearchStrategy[ImageUrlDocument | DocumentUrlDocument]:
|
||||
return st.sampled_from((image_document("invoice 123", 24), pdf_document()))
|
||||
def document_transport_strategy(inline_image_data_uri: str) -> SearchStrategy[ImageUrlDocument | DocumentUrlDocument]:
|
||||
transports: Final[tuple[Literal["remote_image", "inline_image", "remote_pdf", "inline_pdf"], ...]] = (
|
||||
"remote_image",
|
||||
"inline_image",
|
||||
"remote_pdf",
|
||||
"inline_pdf",
|
||||
)
|
||||
|
||||
def as_document(
|
||||
transport: Literal["remote_image", "inline_image", "remote_pdf", "inline_pdf"],
|
||||
) -> ImageUrlDocument | DocumentUrlDocument:
|
||||
if transport == "remote_image":
|
||||
return image_document("invoice 123", 24)
|
||||
if transport == "inline_image":
|
||||
return image_data_document(inline_image_data_uri)
|
||||
if transport == "remote_pdf":
|
||||
return remote_pdf_document()
|
||||
return pdf_document()
|
||||
|
||||
return st.sampled_from(transports).map(as_document)
|
||||
|
||||
|
||||
def sampled_scalar_strategy(values: tuple[ValueT, ...]) -> SearchStrategy[ValueT]:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from collections.abc import Mapping
|
|||
from typing import Final, Literal, cast
|
||||
|
||||
from hypothesis import strategies as st
|
||||
from hypothesis.strategies import DrawFn, SearchStrategy
|
||||
from hypothesis.strategies import SearchStrategy
|
||||
from pydantic import Field, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
|
|
@ -18,10 +18,10 @@ from tests.test_litellm.ocr.fixtures.common import (
|
|||
OcrFixtureClient,
|
||||
OcrRecordingTarget,
|
||||
annotation_format,
|
||||
image_document,
|
||||
document_transport_strategy,
|
||||
invoke_with_api_key,
|
||||
parameter_strategy,
|
||||
public_document_strategy,
|
||||
pdf_document,
|
||||
sampled_list_strategy,
|
||||
sampled_parameter_group_strategy,
|
||||
sampled_scalar_strategy,
|
||||
|
|
@ -113,7 +113,14 @@ def _feature_level(model: str) -> MistralFeatureLevel:
|
|||
return "2505"
|
||||
|
||||
|
||||
def mistral_optional_params_strategy(feature_level: MistralFeatureLevel) -> SearchStrategy[dict[str, object]]:
|
||||
def _optional_param_strategies(
|
||||
*,
|
||||
include_document_annotation_prompt: bool = True,
|
||||
) -> tuple[
|
||||
tuple[SearchStrategy[dict[str, object]], ...],
|
||||
tuple[SearchStrategy[dict[str, object]], ...],
|
||||
tuple[SearchStrategy[dict[str, object]], ...],
|
||||
]:
|
||||
annotation: Final = annotation_format("document_title")
|
||||
common: Final[tuple[SearchStrategy[dict[str, object]], ...]] = (
|
||||
parameter_strategy("pages", sampled_list_strategy(((0,), (0, 1)))),
|
||||
|
|
@ -125,16 +132,21 @@ def mistral_optional_params_strategy(feature_level: MistralFeatureLevel) -> Sear
|
|||
sampled_scalar_strategy((annotation_format("bounding_boxes"),)),
|
||||
),
|
||||
parameter_strategy("document_annotation_format", sampled_scalar_strategy((annotation,))),
|
||||
sampled_parameter_group_strategy(
|
||||
*(
|
||||
(
|
||||
(
|
||||
("document_annotation_format", annotation),
|
||||
("document_annotation_prompt", "Extract the visible title"),
|
||||
sampled_parameter_group_strategy(
|
||||
(
|
||||
(
|
||||
("document_annotation_format", annotation),
|
||||
("document_annotation_prompt", "Extract the visible title"),
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
if include_document_annotation_prompt
|
||||
else ()
|
||||
),
|
||||
parameter_strategy("confidence_scores_granularity", sampled_scalar_strategy(("page", "word"))),
|
||||
parameter_strategy("id", sampled_scalar_strategy(("case-1",))),
|
||||
)
|
||||
feature_2512: Final[tuple[SearchStrategy[dict[str, object]], ...]] = (
|
||||
parameter_strategy("extract_header", sampled_scalar_strategy((False, True))),
|
||||
|
|
@ -142,9 +154,21 @@ def mistral_optional_params_strategy(feature_level: MistralFeatureLevel) -> Sear
|
|||
parameter_strategy("table_format", sampled_scalar_strategy(("markdown", "html"))),
|
||||
)
|
||||
feature_4: Final[tuple[SearchStrategy[dict[str, object]], ...]] = (
|
||||
parameter_strategy("pages", sampled_scalar_strategy(("0-2",))),
|
||||
parameter_strategy("include_blocks", sampled_scalar_strategy((False, True))),
|
||||
sampled_parameter_group_strategy(((("include_blocks", True), ("confidence_scores_granularity", "block")),)),
|
||||
)
|
||||
return common, feature_2512, feature_4
|
||||
|
||||
|
||||
def mistral_optional_params_strategy(
|
||||
feature_level: MistralFeatureLevel,
|
||||
*,
|
||||
include_document_annotation_prompt: bool = True,
|
||||
) -> SearchStrategy[dict[str, object]]:
|
||||
common, feature_2512, feature_4 = _optional_param_strategies(
|
||||
include_document_annotation_prompt=include_document_annotation_prompt
|
||||
)
|
||||
return st.one_of(
|
||||
*common,
|
||||
*(feature_2512 if feature_level in {"2512", "4"} else ()),
|
||||
|
|
@ -152,25 +176,72 @@ def mistral_optional_params_strategy(feature_level: MistralFeatureLevel) -> Sear
|
|||
)
|
||||
|
||||
|
||||
@st.composite
|
||||
def mistral_input_strategy(
|
||||
draw: DrawFn,
|
||||
def _mistral_input_values(
|
||||
document: OcrDocument,
|
||||
optional_params: dict[str, object] | None = None,
|
||||
) -> dict[str, object]:
|
||||
return {"document": document, **(optional_params or {})}
|
||||
|
||||
|
||||
def _mistral_input(
|
||||
model: str,
|
||||
feature_level: MistralFeatureLevel | None = None,
|
||||
document: OcrDocument,
|
||||
optional_params: dict[str, object] | None = None,
|
||||
) -> MistralOcrSdkInput:
|
||||
canonical_document: Final = image_document("invoice 123", 24)
|
||||
values: Final = draw(
|
||||
st.one_of(
|
||||
public_document_strategy().map(lambda document: {"document": document}),
|
||||
mistral_optional_params_strategy(feature_level or _feature_level(model)).map(
|
||||
lambda optional_params: {"document": canonical_document, **optional_params}
|
||||
),
|
||||
)
|
||||
return MistralOcrSdkInput.model_validate({"model": model, **_mistral_input_values(document, optional_params)})
|
||||
|
||||
|
||||
def mistral_input_values_strategy(
|
||||
feature_level: MistralFeatureLevel,
|
||||
inline_image_data_uri: str,
|
||||
*,
|
||||
include_document_annotation_prompt: bool = True,
|
||||
) -> SearchStrategy[dict[str, object]]:
|
||||
option_document: Final = pdf_document()
|
||||
return st.one_of(
|
||||
document_transport_strategy(inline_image_data_uri).map(_mistral_input_values),
|
||||
mistral_optional_params_strategy(
|
||||
feature_level,
|
||||
include_document_annotation_prompt=include_document_annotation_prompt,
|
||||
).map(lambda optional_params: _mistral_input_values(option_document, optional_params)),
|
||||
)
|
||||
return MistralOcrSdkInput.model_validate({"model": model, **values})
|
||||
|
||||
|
||||
def mistral_recording_targets(environ: Mapping[str, str], client: OcrFixtureClient) -> tuple[OcrRecordingTarget, ...]:
|
||||
def mistral_input_strategy(
|
||||
model: str,
|
||||
inline_image_data_uri: str,
|
||||
feature_level: MistralFeatureLevel | None = None,
|
||||
) -> SearchStrategy[MistralOcrSdkInput]:
|
||||
return mistral_input_values_strategy(feature_level or _feature_level(model), inline_image_data_uri).map(
|
||||
lambda values: MistralOcrSdkInput.model_validate({"model": model, **values})
|
||||
)
|
||||
|
||||
|
||||
def _mistral_recording_strategy(inline_image_data_uri: str) -> SearchStrategy[MistralOcrSdkInput]:
|
||||
document: Final = pdf_document()
|
||||
baseline_models: Final = tuple(model for model in MISTRAL_MODELS if model != MISTRAL_MODEL)
|
||||
common, feature_2512, feature_4 = _optional_param_strategies()
|
||||
common_options: Final[SearchStrategy[dict[str, object]]] = st.one_of(*common)
|
||||
feature_2512_options: Final[SearchStrategy[dict[str, object]]] = st.one_of(*feature_2512)
|
||||
feature_4_options: Final[SearchStrategy[dict[str, object]]] = st.one_of(*feature_4)
|
||||
return st.one_of(
|
||||
sampled_scalar_strategy(baseline_models).map(lambda model: _mistral_input(model, document)),
|
||||
document_transport_strategy(inline_image_data_uri).map(
|
||||
lambda selected_document: _mistral_input(MISTRAL_MODEL, selected_document)
|
||||
),
|
||||
common_options.map(lambda optional_params: _mistral_input(MISTRAL_MODEL, document, optional_params)),
|
||||
feature_2512_options.map(
|
||||
lambda optional_params: _mistral_input("mistral/mistral-ocr-2512", document, optional_params)
|
||||
),
|
||||
feature_4_options.map(
|
||||
lambda optional_params: _mistral_input("mistral/mistral-ocr-4-1", document, optional_params)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def mistral_recording_targets(
|
||||
environ: Mapping[str, str], client: OcrFixtureClient, inline_image_data_uri: str
|
||||
) -> tuple[OcrRecordingTarget, ...]:
|
||||
api_key: Final = environ.get("MISTRAL_API_KEY")
|
||||
if not api_key:
|
||||
return ()
|
||||
|
|
@ -182,7 +253,7 @@ def mistral_recording_targets(environ: Mapping[str, str], client: OcrFixtureClie
|
|||
provider_spec=ProviderSpec(upstream_base=upstream_base),
|
||||
strategy=cast(
|
||||
SearchStrategy[OcrSdkInputBase],
|
||||
sampled_scalar_strategy(MISTRAL_MODELS).flatmap(mistral_input_strategy),
|
||||
_mistral_recording_strategy(inline_image_data_uri),
|
||||
),
|
||||
invocation=invoke_with_api_key(client, api_key),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from dotenv import load_dotenv
|
|||
|
||||
import litellm
|
||||
from litellm.rust_bridge.ocr import use_litellm_rust
|
||||
from tests.route_parity.fixtures.media import structured_image_data_uri
|
||||
from tests.route_parity.fixtures.pipeline import parse_recording_args, record_fixtures
|
||||
from tests.route_parity.fixtures.store import fixture_directory
|
||||
from tests.test_litellm.ocr.fixtures.azure import (
|
||||
|
|
@ -37,13 +38,14 @@ class LiteLLMOcrFixtureClient:
|
|||
def discover_targets(
|
||||
environ: Mapping[str, str],
|
||||
client: OcrFixtureClient,
|
||||
inline_image_data_uri: str,
|
||||
) -> tuple[OcrRecordingTarget, ...]:
|
||||
return (
|
||||
*mistral_recording_targets(environ, client),
|
||||
*azure_mistral_recording_targets(environ, client),
|
||||
*mistral_recording_targets(environ, client, inline_image_data_uri),
|
||||
*azure_mistral_recording_targets(environ, client, inline_image_data_uri),
|
||||
*azure_document_intelligence_recording_targets(environ, client),
|
||||
*vertex_recording_targets(environ, client),
|
||||
*reducto_recording_targets(environ, client),
|
||||
*vertex_recording_targets(environ, client, inline_image_data_uri),
|
||||
*reducto_recording_targets(environ, client, inline_image_data_uri),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -58,7 +60,8 @@ def main() -> int:
|
|||
load_dotenv()
|
||||
args: Final = parse_recording_args()
|
||||
client: Final = LiteLLMOcrFixtureClient(cast(OcrSdkCall, litellm.ocr))
|
||||
targets: Final = require_targets(discover_targets(os.environ, client))
|
||||
inline_image_data_uri: Final = structured_image_data_uri()
|
||||
targets: Final = require_targets(discover_targets(os.environ, client, inline_image_data_uri))
|
||||
root: Final = fixture_directory(
|
||||
args.fixture_dir,
|
||||
os.environ.get(FIXTURE_DIR_ENV),
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ from pydantic import Field, field_validator, model_validator
|
|||
from typing_extensions import Self
|
||||
|
||||
from tests.route_parity.fixture_models import FixtureModel, JsonObject
|
||||
from tests.route_parity.fixtures.media import structured_pdf_data_uri
|
||||
from tests.route_parity.fixtures.recording import ProviderSpec
|
||||
from tests.test_litellm.ocr.fixtures.base import OcrSdkInputBase
|
||||
from tests.test_litellm.ocr.fixtures.common import (
|
||||
OcrFixtureClient,
|
||||
OcrRecordingTarget,
|
||||
fixture_pdf_data_uri,
|
||||
image_data_document,
|
||||
invoke_with_api_key,
|
||||
parameter_strategy,
|
||||
sampled_list_strategy,
|
||||
|
|
@ -102,7 +103,7 @@ _REDUCTO_RETURN_IMAGE_GROUPS: Final[tuple[tuple[ReductoReturnImage, ...], ...]]
|
|||
("figure",),
|
||||
("table",),
|
||||
("page",),
|
||||
("figure", "table", "page"),
|
||||
("figure", "table"),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -170,14 +171,17 @@ class ReductoHybridVpcSettings(FixtureModel):
|
|||
ReductoPageSelection = ReductoPageRange | list[ReductoPageRange] | list[int] | list[str]
|
||||
ReductoV3Model = Literal["reducto/parse-v3", "parse-v3"]
|
||||
ReductoLegacyModel = Literal["reducto/parse-legacy", "parse-legacy"]
|
||||
_ReductoV3Route = Literal["qualified", "image", "unqualified"]
|
||||
_ReductoLegacyRoute = Literal["qualified", "unqualified"]
|
||||
|
||||
REDUCTO_V3_MODELS: Final[tuple[Literal["reducto/parse-v3"], ...]] = ("reducto/parse-v3",)
|
||||
REDUCTO_LEGACY_MODELS: Final[tuple[Literal["reducto/parse-legacy"], ...]] = ("reducto/parse-legacy",)
|
||||
|
||||
|
||||
class ReductoSettings(FixtureModel):
|
||||
model: Literal["r-1"] | None = None
|
||||
ocr_system: Literal["standard", "legacy"] = "standard"
|
||||
extraction_mode: Literal["ocr", "hybrid"] = "hybrid"
|
||||
extraction_mode: Literal["ocr", "hybrid", "metadata"] = "hybrid"
|
||||
force_url_result: bool = False
|
||||
force_file_extension: str | None = None
|
||||
return_ocr_data: bool = False
|
||||
|
|
@ -285,6 +289,8 @@ def _retrieval_strategy() -> SearchStrategy[ReductoRetrieval]:
|
|||
|
||||
|
||||
def _settings_strategy() -> SearchStrategy[ReductoSettings]:
|
||||
# force_url_result stays model-compatible but is not recorded until the
|
||||
# response transform follows and downloads result.url.
|
||||
return_images: Final[SearchStrategy[list[ReductoReturnImage]]] = sampled_list_strategy(_REDUCTO_RETURN_IMAGE_GROUPS)
|
||||
page_ranges: Final = st.one_of(
|
||||
st.just(ReductoPageRange(start=1, end=1)),
|
||||
|
|
@ -299,33 +305,50 @@ def _settings_strategy() -> SearchStrategy[ReductoSettings]:
|
|||
),
|
||||
)
|
||||
return st.one_of(
|
||||
st.just(ReductoSettings(model="r-1")),
|
||||
st.sampled_from(("standard", "legacy")).map(lambda value: ReductoSettings(ocr_system=value)),
|
||||
st.sampled_from(("hybrid", "ocr")).map(lambda value: ReductoSettings(extraction_mode=value)),
|
||||
st.just(ReductoSettings(force_url_result=True)),
|
||||
st.sampled_from(("hybrid", "ocr", "metadata")).map(lambda value: ReductoSettings(extraction_mode=value)),
|
||||
st.just(ReductoSettings(return_ocr_data=True)),
|
||||
return_images.map(lambda selected_images: ReductoSettings(return_images=selected_images)),
|
||||
st.just(ReductoSettings(embed_pdf_metadata=True)),
|
||||
sampled_scalar_strategy((50, 100, 250)).map(
|
||||
lambda dpi: ReductoSettings(embed_pdf_metadata=True, embed_pdf_metadata_dpi=dpi)
|
||||
),
|
||||
sampled_scalar_strategy((300.0, 900.0)).map(lambda timeout: ReductoSettings(timeout=timeout)),
|
||||
sampled_scalar_strategy((300.0,)).map(lambda timeout: ReductoSettings(timeout=timeout)),
|
||||
page_ranges.map(lambda page_range: ReductoSettings(page_range=page_range)),
|
||||
)
|
||||
|
||||
|
||||
def _reducto_v3_baseline(
|
||||
route: _ReductoV3Route,
|
||||
document: ReductoDocument,
|
||||
inline_image_data_uri: str,
|
||||
) -> ReductoParseV3SdkInput:
|
||||
if route == "image":
|
||||
inline_image: Final = ReductoImageUrlDocument.model_validate(
|
||||
image_data_document(inline_image_data_uri).model_dump(mode="json")
|
||||
)
|
||||
return ReductoParseV3SdkInput(model="reducto/parse-v3", document=inline_image)
|
||||
if route == "unqualified":
|
||||
return ReductoParseV3SdkInput(
|
||||
model="parse-v3",
|
||||
custom_llm_provider="reducto",
|
||||
document=document,
|
||||
)
|
||||
return ReductoParseV3SdkInput(model="reducto/parse-v3", document=document)
|
||||
|
||||
|
||||
def reducto_v3_input_strategy(
|
||||
document: ReductoDocumentUrlDocument | None = None,
|
||||
inline_image_data_uri: str,
|
||||
document: ReductoDocument | None = None,
|
||||
) -> SearchStrategy[ReductoParseV3SdkInput]:
|
||||
selected_document: Final = document or ReductoDocumentUrlDocument(
|
||||
type="document_url", document_url="reducto://fixture-document.pdf"
|
||||
)
|
||||
baseline_routes: Final[tuple[_ReductoV3Route, ...]] = ("qualified", "image", "unqualified")
|
||||
return st.one_of(
|
||||
st.just(ReductoParseV3SdkInput(model="reducto/parse-v3", document=selected_document)),
|
||||
st.just(
|
||||
ReductoParseV3SdkInput(
|
||||
model="parse-v3",
|
||||
custom_llm_provider="reducto",
|
||||
document=selected_document,
|
||||
)
|
||||
st.sampled_from(baseline_routes).map(
|
||||
lambda route: _reducto_v3_baseline(route, selected_document, inline_image_data_uri)
|
||||
),
|
||||
_formatting_strategy().map(
|
||||
lambda formatting: ReductoParseV3SdkInput(
|
||||
|
|
@ -351,33 +374,43 @@ def reducto_v3_input_strategy(
|
|||
)
|
||||
|
||||
|
||||
def _reducto_legacy_input(
|
||||
route: _ReductoLegacyRoute,
|
||||
document: ReductoDocument,
|
||||
) -> ReductoParseLegacySdkInput:
|
||||
if route == "unqualified":
|
||||
return ReductoParseLegacySdkInput(
|
||||
model="parse-legacy",
|
||||
custom_llm_provider="reducto",
|
||||
document=document,
|
||||
)
|
||||
return ReductoParseLegacySdkInput(model="reducto/parse-legacy", document=document)
|
||||
|
||||
|
||||
def reducto_legacy_input_strategy(
|
||||
document: ReductoDocumentUrlDocument | None = None,
|
||||
document: ReductoDocument | None = None,
|
||||
) -> SearchStrategy[ReductoParseLegacySdkInput]:
|
||||
selected_document: Final = document or ReductoDocumentUrlDocument(
|
||||
type="document_url", document_url="reducto://fixture-document.pdf"
|
||||
)
|
||||
return st.sampled_from(
|
||||
(
|
||||
ReductoParseLegacySdkInput(model="reducto/parse-legacy", document=selected_document),
|
||||
ReductoParseLegacySdkInput(model="parse-legacy", custom_llm_provider="reducto", document=selected_document),
|
||||
ReductoParseLegacySdkInput(model="reducto/parse-legacy", document=selected_document, enhance={}),
|
||||
)
|
||||
)
|
||||
routes: Final[tuple[_ReductoLegacyRoute, ...]] = ("qualified", "unqualified")
|
||||
return st.sampled_from(routes).map(lambda route: _reducto_legacy_input(route, selected_document))
|
||||
|
||||
|
||||
def reducto_recording_targets(environ: Mapping[str, str], client: OcrFixtureClient) -> tuple[OcrRecordingTarget, ...]:
|
||||
def reducto_recording_targets(
|
||||
environ: Mapping[str, str], client: OcrFixtureClient, inline_image_data_uri: str
|
||||
) -> tuple[OcrRecordingTarget, ...]:
|
||||
api_key: Final = environ.get("REDUCTO_API_KEY")
|
||||
if not api_key:
|
||||
return ()
|
||||
upstream_base: Final = environ.get("REDUCTO_API_BASE", _REDUCTO_API_BASE).rstrip("/")
|
||||
document: Final = ReductoDocumentUrlDocument(type="document_url", document_url=fixture_pdf_data_uri())
|
||||
document: Final = ReductoDocumentUrlDocument(type="document_url", document_url=structured_pdf_data_uri())
|
||||
invocation: Final = invoke_with_api_key(client, api_key)
|
||||
return (
|
||||
OcrRecordingTarget(
|
||||
name="reducto-v3",
|
||||
provider_spec=ProviderSpec(upstream_base=upstream_base),
|
||||
strategy=cast(SearchStrategy[OcrSdkInputBase], reducto_v3_input_strategy(document)),
|
||||
strategy=cast(SearchStrategy[OcrSdkInputBase], reducto_v3_input_strategy(inline_image_data_uri, document)),
|
||||
invocation=invocation,
|
||||
),
|
||||
OcrRecordingTarget(
|
||||
|
|
|
|||
|
|
@ -12,15 +12,13 @@ from tests.test_litellm.ocr.fixtures.base import OcrDocument, OcrSdkInputBase
|
|||
from tests.test_litellm.ocr.fixtures.common import (
|
||||
OcrFixtureClient,
|
||||
OcrRecordingTarget,
|
||||
image_data_document,
|
||||
invoke_with_api_key,
|
||||
public_document_strategy,
|
||||
sampled_scalar_strategy,
|
||||
)
|
||||
from tests.test_litellm.ocr.fixtures.mistral import (
|
||||
MISTRAL_MODEL,
|
||||
MistralCompatibleOcrSdkInput,
|
||||
MistralOcrSdkInput,
|
||||
mistral_input_strategy,
|
||||
mistral_input_values_strategy,
|
||||
)
|
||||
|
||||
VertexMistralModel = Literal["vertex_ai/mistral-ocr-2505"]
|
||||
|
|
@ -48,34 +46,50 @@ class VertexDeepSeekOcrSdkInput(OcrSdkInputBase):
|
|||
|
||||
|
||||
def _as_vertex_mistral(
|
||||
case_input: MistralOcrSdkInput,
|
||||
values: dict[str, object],
|
||||
project: str,
|
||||
location: str,
|
||||
model: VertexMistralModel,
|
||||
) -> VertexMistralOcrSdkInput:
|
||||
values: Final = case_input.model_dump(
|
||||
mode="python",
|
||||
exclude={"boundary", "model", "custom_llm_provider"},
|
||||
exclude_unset=True,
|
||||
)
|
||||
return VertexMistralOcrSdkInput.model_validate(
|
||||
{**values, "model": model, "vertex_project": project, "vertex_location": location}
|
||||
)
|
||||
|
||||
|
||||
def vertex_mistral_input_strategy(
|
||||
project: str,
|
||||
location: str,
|
||||
inline_image_data_uri: str,
|
||||
) -> SearchStrategy[VertexMistralOcrSdkInput]:
|
||||
return st.builds(
|
||||
_as_vertex_mistral,
|
||||
project=st.just(project),
|
||||
location=st.just(location),
|
||||
model=sampled_scalar_strategy(VERTEX_MISTRAL_MODELS),
|
||||
values=mistral_input_values_strategy("2505", inline_image_data_uri),
|
||||
)
|
||||
|
||||
|
||||
@st.composite
|
||||
def vertex_deepseek_input_strategy(draw: DrawFn, project: str, location: str) -> VertexDeepSeekOcrSdkInput:
|
||||
def vertex_deepseek_input_strategy(
|
||||
draw: DrawFn, project: str, location: str, inline_image_data_uri: str
|
||||
) -> VertexDeepSeekOcrSdkInput:
|
||||
return VertexDeepSeekOcrSdkInput.model_validate(
|
||||
{
|
||||
"model": draw(sampled_scalar_strategy(VERTEX_DEEPSEEK_MODELS)),
|
||||
"document": draw(public_document_strategy()),
|
||||
# The current Vertex model card documents image input only. Keep
|
||||
# the broader fixture model for existing recordings, but do not
|
||||
# spend a paid request on the transform's unsupported PDF branch.
|
||||
"document": image_data_document(inline_image_data_uri),
|
||||
"vertex_project": project,
|
||||
"vertex_location": location,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def vertex_recording_targets(environ: Mapping[str, str], client: OcrFixtureClient) -> tuple[OcrRecordingTarget, ...]:
|
||||
def vertex_recording_targets(
|
||||
environ: Mapping[str, str], client: OcrFixtureClient, inline_image_data_uri: str
|
||||
) -> tuple[OcrRecordingTarget, ...]:
|
||||
api_key: Final = environ.get("VERTEX_AI_API_KEY")
|
||||
project: Final = environ.get("VERTEXAI_PROJECT") or environ.get("VERTEX_PROJECT")
|
||||
location: Final = environ.get("VERTEXAI_LOCATION") or environ.get("VERTEX_LOCATION") or "us-central1"
|
||||
|
|
@ -89,20 +103,17 @@ def vertex_recording_targets(environ: Mapping[str, str], client: OcrFixtureClien
|
|||
provider_spec=ProviderSpec(upstream_base=upstream_base.rstrip("/")),
|
||||
strategy=cast(
|
||||
SearchStrategy[OcrSdkInputBase],
|
||||
st.builds(
|
||||
_as_vertex_mistral,
|
||||
project=st.just(project),
|
||||
location=st.just(location),
|
||||
model=sampled_scalar_strategy(VERTEX_MISTRAL_MODELS),
|
||||
case_input=mistral_input_strategy(MISTRAL_MODEL, feature_level="2505"),
|
||||
),
|
||||
vertex_mistral_input_strategy(project, location, inline_image_data_uri),
|
||||
),
|
||||
invocation=invocation,
|
||||
),
|
||||
OcrRecordingTarget(
|
||||
name="vertex-deepseek",
|
||||
provider_spec=ProviderSpec(upstream_base=upstream_base.rstrip("/")),
|
||||
strategy=cast(SearchStrategy[OcrSdkInputBase], vertex_deepseek_input_strategy(project, location)),
|
||||
strategy=cast(
|
||||
SearchStrategy[OcrSdkInputBase],
|
||||
vertex_deepseek_input_strategy(project, location, inline_image_data_uri),
|
||||
),
|
||||
invocation=invocation,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from collections.abc import Callable
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Final, TypeVar, cast
|
||||
from unittest.mock import patch
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
from hypothesis import find, given, settings
|
||||
from hypothesis import strategies as st
|
||||
from hypothesis.strategies import DataObject, SearchStrategy
|
||||
|
|
@ -13,18 +18,25 @@ from pydantic import BaseModel, ConfigDict, JsonValue, TypeAdapter, ValidationEr
|
|||
|
||||
from litellm.llms.azure_ai.ocr.document_intelligence.transformation import AzureDocumentIntelligenceOCRConfig
|
||||
from litellm.llms.azure_ai.ocr.transformation import AzureAIOCRConfig
|
||||
from litellm.llms.base_llm.ocr.transformation import BaseOCRConfig
|
||||
from litellm.llms.base_llm.ocr.transformation import (
|
||||
BaseOCRConfig,
|
||||
DocumentType,
|
||||
OCRRequestData,
|
||||
)
|
||||
from litellm.llms.mistral.ocr.transformation import MistralOCRConfig
|
||||
from litellm.llms.reducto.ocr.transformation import ReductoParseLegacyConfig, ReductoParseV3Config
|
||||
from litellm.llms.vertex_ai.ocr.deepseek_transformation import VertexAIDeepSeekOCRConfig
|
||||
from litellm.llms.vertex_ai.ocr.transformation import VertexAIOCRConfig
|
||||
from tests.route_parity.fixtures.media import structured_pdf_data_uri
|
||||
from tests.test_litellm.ocr.conftest import ocr_fixture_marks
|
||||
from tests.test_litellm.ocr.fixtures.azure import (
|
||||
AZURE_DOCUMENT_INTELLIGENCE_MODELS,
|
||||
AZURE_DOCUMENT_INTELLIGENCE_RECORDING_MODELS,
|
||||
AZURE_MISTRAL_MODELS,
|
||||
AzureDocumentIntelligenceOcrSdkInput,
|
||||
AzureMistralOcrSdkInput,
|
||||
azure_document_intelligence_input_strategy,
|
||||
azure_mistral_input_strategy,
|
||||
)
|
||||
from tests.test_litellm.ocr.fixtures.base import (
|
||||
DocumentUrlDocument,
|
||||
|
|
@ -42,6 +54,7 @@ from tests.test_litellm.ocr.fixtures.reducto import (
|
|||
ReductoChunking,
|
||||
ReductoDocumentUrlDocument,
|
||||
ReductoFormatting,
|
||||
ReductoImageUrlDocument,
|
||||
ReductoPageRange,
|
||||
ReductoParseLegacySdkInput,
|
||||
ReductoParseV3SdkInput,
|
||||
|
|
@ -56,6 +69,7 @@ from tests.test_litellm.ocr.fixtures.vertex import (
|
|||
VertexDeepSeekOcrSdkInput,
|
||||
VertexMistralOcrSdkInput,
|
||||
vertex_deepseek_input_strategy,
|
||||
vertex_mistral_input_strategy,
|
||||
)
|
||||
|
||||
COMMON_FIELDS: Final = frozenset(
|
||||
|
|
@ -109,15 +123,79 @@ _MISTRAL_OPTION_GROUPS: Final = frozenset(
|
|||
"table_format",
|
||||
"confidence_scores_granularity",
|
||||
"include_blocks",
|
||||
"id",
|
||||
)
|
||||
),
|
||||
frozenset({"document_annotation_format", "document_annotation_prompt"}),
|
||||
frozenset({"include_blocks", "confidence_scores_granularity"}),
|
||||
}
|
||||
)
|
||||
_MISTRAL_2505_OPTION_GROUPS: Final = frozenset(
|
||||
{
|
||||
frozenset[str](),
|
||||
*(
|
||||
frozenset({field})
|
||||
for field in (
|
||||
"pages",
|
||||
"include_image_base64",
|
||||
"image_limit",
|
||||
"image_min_size",
|
||||
"bbox_annotation_format",
|
||||
"document_annotation_format",
|
||||
"confidence_scores_granularity",
|
||||
)
|
||||
),
|
||||
frozenset({"document_annotation_format", "document_annotation_prompt"}),
|
||||
}
|
||||
)
|
||||
_AZURE_MISTRAL_OPTION_GROUPS: Final = _MISTRAL_2505_OPTION_GROUPS - {
|
||||
frozenset({"document_annotation_format", "document_annotation_prompt"})
|
||||
}
|
||||
_REDUCTO_FORMATTING_INCLUDE_GROUPS: Final = (
|
||||
(),
|
||||
("hyperlinks",),
|
||||
("change_tracking", "highlight", "comments"),
|
||||
("signatures", "ignore_watermarks"),
|
||||
)
|
||||
_REDUCTO_FILTER_BLOCK_GROUPS: Final = (
|
||||
(),
|
||||
("Header",),
|
||||
("Header", "Footer", "Page Number"),
|
||||
("Figure", "Table", "Key Value"),
|
||||
)
|
||||
_REDUCTO_RETURN_IMAGE_GROUPS: Final = (
|
||||
(),
|
||||
("figure",),
|
||||
("table",),
|
||||
("page",),
|
||||
("figure", "table"),
|
||||
)
|
||||
_FIND_SETTINGS: Final = settings(max_examples=2_000, deadline=None, derandomize=True, database=None)
|
||||
_FixtureInputT = TypeVar("_FixtureInputT")
|
||||
INLINE_IMAGE_DATA_URI: Final = "data:image/png;base64,dGVzdA=="
|
||||
_MapOcrParams = Callable[[dict[str, object], dict[str, object], str], dict[str, object]]
|
||||
_TransformOcrRequest = Callable[
|
||||
[str, DocumentType, dict[str, object], dict[str, object]],
|
||||
OCRRequestData,
|
||||
]
|
||||
_GetCompleteUrl = Callable[[str | None, str, dict[str, object]], str]
|
||||
|
||||
|
||||
def _transform_with_stubbed_download(
|
||||
transform_request: _TransformOcrRequest,
|
||||
model: str,
|
||||
document: DocumentType,
|
||||
mapped: dict[str, object],
|
||||
) -> OCRRequestData:
|
||||
source_key: Final = "image_url" if document["type"] == "image_url" else "document_url"
|
||||
source: Final = document[source_key]
|
||||
if source.startswith("data:"):
|
||||
return transform_request(model, document, mapped, {})
|
||||
media_type: Final = "image/png" if document["type"] == "image_url" else "application/pdf"
|
||||
with respx.mock(assert_all_called=False) as router:
|
||||
router.route(method="GET").mock(
|
||||
return_value=httpx.Response(200, content=b"\x00", headers={"content-type": media_type})
|
||||
)
|
||||
return transform_request(model, document, mapped, {})
|
||||
|
||||
|
||||
def _find_fixture(
|
||||
|
|
@ -127,6 +205,49 @@ def _find_fixture(
|
|||
return find(strategy, predicate, settings=_FIND_SETTINGS)
|
||||
|
||||
|
||||
def _document_transport(document: ImageUrlDocument | DocumentUrlDocument) -> tuple[str, str]:
|
||||
if isinstance(document, ImageUrlDocument):
|
||||
source: Final = document.image_url.url if isinstance(document.image_url, ImageUrlValue) else document.image_url
|
||||
return document.type, "data" if source.startswith("data:") else "remote"
|
||||
return document.type, "data" if document.document_url.startswith("data:") else "remote"
|
||||
|
||||
|
||||
def _normalized_azure_pages(pages: object) -> str:
|
||||
if isinstance(pages, str):
|
||||
return pages.replace(" ", "")
|
||||
assert isinstance(pages, list)
|
||||
raw_pages: Final = cast(list[object], pages)
|
||||
if all(isinstance(page, int) for page in raw_pages):
|
||||
integer_pages: Final = cast(list[int], raw_pages)
|
||||
return ",".join(str(page + 1) for page in sorted(set(integer_pages)))
|
||||
string_pages: Final = cast(list[str], raw_pages)
|
||||
return ",".join(page.strip() for page in string_pages)
|
||||
|
||||
|
||||
def test_structured_pdf_exercises_semantic_ocr_features() -> None:
|
||||
encoded: Final = structured_pdf_data_uri().partition(",")[2]
|
||||
pdf: Final = base64.b64decode(encoded, validate=True)
|
||||
|
||||
assert pdf.startswith(b"%PDF-1.")
|
||||
assert b"/Count 5" in pdf
|
||||
assert pdf.count(b"/Subtype /Image") == 3
|
||||
assert all(
|
||||
marker in pdf
|
||||
for marker in (
|
||||
b"/Width 120",
|
||||
b"/Width 320",
|
||||
b"/Width 360",
|
||||
b"/Subtype /Highlight",
|
||||
b"/Subtype /Link",
|
||||
b"/Subtype /Text",
|
||||
b"/Title (Quarterly Operations Report)",
|
||||
)
|
||||
)
|
||||
assert b"Invoice Number: INV-2048" in pdf
|
||||
assert b"Formula: gross margin" in pdf
|
||||
assert b"Approved by: Jordan Lee" in pdf
|
||||
|
||||
|
||||
class _ModelRegistryEntry(BaseModel):
|
||||
model_config = ConfigDict(frozen=True, extra="ignore")
|
||||
|
||||
|
|
@ -349,6 +470,30 @@ def test_vertex_deepseek_request_uses_single_provider_namespace(model: str) -> N
|
|||
assert data["model"] == "deepseek-ai/deepseek-ocr-maas"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"document",
|
||||
(
|
||||
{"type": "image_url", "image_url": "data:image/png;base64,AA=="},
|
||||
{"type": "document_url", "document_url": "data:application/pdf;base64,AA=="},
|
||||
),
|
||||
)
|
||||
def test_vertex_deepseek_request_maps_both_document_types_to_image_content(
|
||||
document: DocumentType,
|
||||
) -> None:
|
||||
request: Final = VertexAIDeepSeekOCRConfig().transform_ocr_request( # pyright: ignore[reportUnknownMemberType]
|
||||
model="deepseek-ai/deepseek-ocr-maas",
|
||||
document=document,
|
||||
optional_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
source_key: Final = "image_url" if document["type"] == "image_url" else "document_url"
|
||||
data: Final = cast(dict[str, object], request.data)
|
||||
messages: Final = cast(list[dict[str, object]], data["messages"])
|
||||
content: Final = cast(list[dict[str, object]], messages[0]["content"])
|
||||
assert content == [{"type": "image_url", "image_url": document[source_key]}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sdk_input",
|
||||
(
|
||||
|
|
@ -434,12 +579,12 @@ def test_reducto_nested_constraints() -> None:
|
|||
@settings(max_examples=100, deadline=None)
|
||||
@given(model=st.sampled_from(MISTRAL_MODELS), data=st.data())
|
||||
def test_mistral_strategy_only_generates_bounded_valid_sdk_inputs(model: str, data: DataObject) -> None:
|
||||
sdk_input: Final = data.draw(mistral_input_strategy(model))
|
||||
sdk_input: Final = data.draw(mistral_input_strategy(model, INLINE_IMAGE_DATA_URI))
|
||||
assert MistralOcrSdkInput.model_validate(sdk_input.canonical_input()) == sdk_input
|
||||
optional_fields: Final = frozenset(sdk_input.model_fields_set) - {"model", "document"}
|
||||
assert optional_fields in _MISTRAL_OPTION_GROUPS
|
||||
if sdk_input.pages is not None:
|
||||
assert sdk_input.pages in ([0], [0, 1])
|
||||
assert sdk_input.pages in ([0], [0, 1], "0-2")
|
||||
if sdk_input.image_limit is not None:
|
||||
assert sdk_input.image_limit == 1
|
||||
if sdk_input.image_min_size is not None:
|
||||
|
|
@ -454,6 +599,46 @@ def test_mistral_strategy_only_generates_bounded_valid_sdk_inputs(model: str, da
|
|||
assert optional_fields.isdisjoint({"extract_header", "extract_footer", "table_format"})
|
||||
if model not in _MISTRAL_4_OR_NEWER:
|
||||
assert "include_blocks" not in optional_fields
|
||||
assert not isinstance(sdk_input.pages, str)
|
||||
if optional_fields:
|
||||
assert isinstance(sdk_input.document, DocumentUrlDocument)
|
||||
assert sdk_input.document.document_url == structured_pdf_data_uri()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"transport",
|
||||
(
|
||||
("image_url", "remote"),
|
||||
("image_url", "data"),
|
||||
("document_url", "remote"),
|
||||
("document_url", "data"),
|
||||
),
|
||||
)
|
||||
def test_mistral_strategy_reaches_every_document_transform_branch(transport: tuple[str, str]) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
mistral_input_strategy("mistral/mistral-ocr-4-1", INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: _document_transport(candidate.document) == transport,
|
||||
)
|
||||
|
||||
assert _document_transport(sdk_input.document) == transport
|
||||
|
||||
|
||||
@settings(max_examples=100, deadline=None)
|
||||
@given(sdk_input=mistral_input_strategy("mistral/mistral-ocr-4-1", INLINE_IMAGE_DATA_URI))
|
||||
def test_mistral_strategy_values_survive_the_request_transform(sdk_input: MistralOcrSdkInput) -> None:
|
||||
sdk_kwargs: Final = sdk_input.as_sdk_kwargs()
|
||||
model: Final = cast(str, sdk_kwargs["model"])
|
||||
document: Final = cast(DocumentType, sdk_kwargs["document"])
|
||||
optional_params: Final = {name: value for name, value in sdk_kwargs.items() if name not in {"model", "document"}}
|
||||
config: Final = MistralOCRConfig()
|
||||
map_params: Final = cast(_MapOcrParams, config.map_ocr_params)
|
||||
transform_request: Final = cast(_TransformOcrRequest, config.transform_ocr_request)
|
||||
mapped: Final = map_params(optional_params, {}, model)
|
||||
request: Final = transform_request(model, document, mapped, {})
|
||||
request_data: Final = cast(dict[str, object], request.data)
|
||||
|
||||
assert mapped == optional_params
|
||||
assert request_data == {"model": model, "document": document, **optional_params}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -461,6 +646,7 @@ def test_mistral_strategy_only_generates_bounded_valid_sdk_inputs(model: str, da
|
|||
(
|
||||
("pages", [0]),
|
||||
("pages", [0, 1]),
|
||||
("pages", "0-2"),
|
||||
("include_image_base64", False),
|
||||
("include_image_base64", True),
|
||||
("image_limit", 1),
|
||||
|
|
@ -476,12 +662,11 @@ def test_mistral_strategy_only_generates_bounded_valid_sdk_inputs(model: str, da
|
|||
("confidence_scores_granularity", "block"),
|
||||
("include_blocks", False),
|
||||
("include_blocks", True),
|
||||
("id", "case-1"),
|
||||
),
|
||||
)
|
||||
def test_mistral_strategy_reaches_every_finite_scalar_value(field: str, value: object) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
mistral_input_strategy("mistral/mistral-ocr-4-1"),
|
||||
mistral_input_strategy("mistral/mistral-ocr-4-1", INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: field in candidate.model_fields_set and getattr(candidate, field) == value,
|
||||
)
|
||||
|
||||
|
|
@ -489,20 +674,22 @@ def test_mistral_strategy_reaches_every_finite_scalar_value(field: str, value: o
|
|||
|
||||
|
||||
@settings(max_examples=50, deadline=None)
|
||||
@given(sdk_input=reducto_v3_input_strategy())
|
||||
@given(sdk_input=reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI))
|
||||
def test_reducto_v3_strategy_only_generates_bounded_valid_sdk_inputs(sdk_input: ReductoParseV3SdkInput) -> None:
|
||||
assert ReductoParseV3SdkInput.model_validate(sdk_input.canonical_input()) == sdk_input
|
||||
option_groups: Final = frozenset(sdk_input.model_fields_set) & {"formatting", "retrieval", "settings"}
|
||||
assert len(option_groups) <= 1
|
||||
if "formatting" in option_groups:
|
||||
assert len(sdk_input.formatting.model_fields_set) == 1
|
||||
assert sdk_input.formatting.table_output_format in {"dynamic", "html", "md", "json", "csv", "jsonbbox"}
|
||||
assert tuple(sdk_input.formatting.include) in {
|
||||
(),
|
||||
("hyperlinks",),
|
||||
("change_tracking", "highlight", "comments"),
|
||||
("signatures", "ignore_watermarks"),
|
||||
}
|
||||
formatting_fields: Final = frozenset(sdk_input.formatting.model_fields_set)
|
||||
assert len(formatting_fields) == 1
|
||||
if "table_output_format" in formatting_fields:
|
||||
assert sdk_input.formatting.table_output_format in {"dynamic", "html", "md", "json", "csv", "jsonbbox"}
|
||||
if "add_page_markers" in formatting_fields:
|
||||
assert sdk_input.formatting.add_page_markers in {False, True}
|
||||
if "merge_tables" in formatting_fields:
|
||||
assert sdk_input.formatting.merge_tables in {False, True}
|
||||
if "include" in formatting_fields:
|
||||
assert tuple(sdk_input.formatting.include) in _REDUCTO_FORMATTING_INCLUDE_GROUPS
|
||||
if "retrieval" in option_groups:
|
||||
retrieval_fields: Final = frozenset(sdk_input.retrieval.model_fields_set)
|
||||
assert retrieval_fields in {
|
||||
|
|
@ -511,39 +698,123 @@ def test_reducto_v3_strategy_only_generates_bounded_valid_sdk_inputs(sdk_input:
|
|||
frozenset({"chunking", "embedding_optimized"}),
|
||||
}
|
||||
chunking: Final = sdk_input.retrieval.chunking
|
||||
if chunking.chunk_size is not None or chunking.chunk_overlap != 0:
|
||||
if "chunking" in retrieval_fields:
|
||||
assert chunking.chunk_mode in {"variable", "section", "page", "disabled", "block", "page_sections"}
|
||||
assert chunking.chunk_size in {None, 250, 1000, 1500}
|
||||
assert chunking.chunk_overlap in {0, 32, 128}
|
||||
if chunking.chunk_size is not None or chunking.chunk_overlap:
|
||||
assert chunking.chunk_mode == "variable"
|
||||
if chunking.chunk_overlap:
|
||||
assert chunking.chunk_size == 1000
|
||||
if "filter_blocks" in retrieval_fields:
|
||||
assert tuple(sdk_input.retrieval.filter_blocks) in _REDUCTO_FILTER_BLOCK_GROUPS
|
||||
if "embedding_optimized" in retrieval_fields:
|
||||
assert chunking.chunk_mode == "variable"
|
||||
assert chunking.chunk_size is None
|
||||
assert chunking.chunk_overlap == 0
|
||||
assert sdk_input.retrieval.embedding_optimized in {False, True}
|
||||
if "settings" in option_groups:
|
||||
settings_fields: Final = frozenset(sdk_input.settings.model_fields_set)
|
||||
assert settings_fields in {
|
||||
frozenset({"model"}),
|
||||
frozenset({"ocr_system"}),
|
||||
frozenset({"extraction_mode"}),
|
||||
frozenset({"force_url_result"}),
|
||||
frozenset({"return_ocr_data"}),
|
||||
frozenset({"return_images"}),
|
||||
frozenset({"embed_pdf_metadata"}),
|
||||
frozenset({"embed_pdf_metadata", "embed_pdf_metadata_dpi"}),
|
||||
frozenset({"timeout"}),
|
||||
frozenset({"page_range"}),
|
||||
}
|
||||
assert "persist_results" not in settings_fields
|
||||
assert settings_fields.isdisjoint(
|
||||
{
|
||||
"force_url_result",
|
||||
"force_file_extension",
|
||||
"persist_results",
|
||||
"tenant_throttling",
|
||||
"document_password",
|
||||
"hybrid_vpc",
|
||||
}
|
||||
)
|
||||
if "model" in settings_fields:
|
||||
assert sdk_input.settings.model == "r-1"
|
||||
if "ocr_system" in settings_fields:
|
||||
assert sdk_input.settings.ocr_system in {"standard", "legacy"}
|
||||
if "extraction_mode" in settings_fields:
|
||||
assert sdk_input.settings.extraction_mode in {"hybrid", "ocr", "metadata"}
|
||||
if "return_ocr_data" in settings_fields:
|
||||
assert sdk_input.settings.return_ocr_data is True
|
||||
if "return_images" in settings_fields:
|
||||
assert tuple(sdk_input.settings.return_images) in _REDUCTO_RETURN_IMAGE_GROUPS
|
||||
if "embed_pdf_metadata_dpi" in settings_fields:
|
||||
assert sdk_input.settings.embed_pdf_metadata is True
|
||||
assert sdk_input.settings.embed_pdf_metadata_dpi in {50, 100, 250}
|
||||
if "timeout" in settings_fields:
|
||||
assert sdk_input.settings.timeout == 300.0
|
||||
if sdk_input.settings.page_range is not None:
|
||||
ranges: Final = (
|
||||
sdk_input.settings.page_range
|
||||
if isinstance(sdk_input.settings.page_range, list)
|
||||
else [sdk_input.settings.page_range]
|
||||
dumped_range: Final = cast(
|
||||
dict[str, object], sdk_input.settings.model_dump(mode="json", exclude_unset=True)
|
||||
)["page_range"]
|
||||
assert dumped_range in (
|
||||
{"start": 1, "end": 1},
|
||||
{"start": 1, "end": 3},
|
||||
[{"start": 1, "end": 2}, {"start": 4, "end": 5}],
|
||||
)
|
||||
assert all(isinstance(page_range, ReductoPageRange) for page_range in ranges)
|
||||
|
||||
|
||||
@settings(max_examples=60, deadline=None)
|
||||
@given(sdk_input=reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI))
|
||||
def test_reducto_v3_strategy_values_survive_the_request_transform(sdk_input: ReductoParseV3SdkInput) -> None:
|
||||
sdk_kwargs: Final = sdk_input.as_sdk_kwargs()
|
||||
model: Final = cast(str, sdk_kwargs["model"])
|
||||
document: Final = cast(DocumentType, sdk_kwargs["document"])
|
||||
optional_params: Final = {
|
||||
name: value for name, value in sdk_kwargs.items() if name not in {"model", "document", "custom_llm_provider"}
|
||||
}
|
||||
config: Final = ReductoParseV3Config()
|
||||
map_params: Final = cast(_MapOcrParams, config.map_ocr_params)
|
||||
transform_request: Final = cast(_TransformOcrRequest, config.transform_ocr_request)
|
||||
mapped: Final = map_params(optional_params, {}, model)
|
||||
|
||||
with patch.object(config, "_ensure_file_id_sync", return_value="reducto://fixture-document.pdf"):
|
||||
request: Final = transform_request(model, document, mapped, {})
|
||||
|
||||
assert mapped == optional_params
|
||||
assert cast(dict[str, object], request.data) == {
|
||||
"input": "reducto://fixture-document.pdf",
|
||||
**optional_params,
|
||||
}
|
||||
|
||||
|
||||
def test_reducto_v3_strategy_reaches_image_upload_branch_without_options() -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: isinstance(candidate.document, ReductoImageUrlDocument),
|
||||
)
|
||||
|
||||
assert isinstance(sdk_input.document, ReductoImageUrlDocument)
|
||||
assert sdk_input.document.image_url.startswith("data:image/")
|
||||
assert sdk_input.model_fields_set == {"model", "document"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "provider"),
|
||||
(("reducto/parse-v3", None), ("parse-v3", "reducto")),
|
||||
)
|
||||
def test_reducto_v3_strategy_reaches_every_routing_form(model: str, provider: str | None) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: candidate.model == model and candidate.custom_llm_provider == provider,
|
||||
)
|
||||
|
||||
assert sdk_input.model == model
|
||||
assert sdk_input.custom_llm_provider == provider
|
||||
|
||||
|
||||
@pytest.mark.parametrize("table_format", ("dynamic", "html", "md", "json", "csv", "jsonbbox"))
|
||||
def test_reducto_v3_strategy_reaches_every_table_format(table_format: str) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(),
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: (
|
||||
"formatting" in candidate.model_fields_set
|
||||
and "table_output_format" in candidate.formatting.model_fields_set
|
||||
|
|
@ -554,10 +825,46 @@ def test_reducto_v3_strategy_reaches_every_table_format(table_format: str) -> No
|
|||
assert sdk_input.formatting.table_output_format == table_format
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field", "value"),
|
||||
(
|
||||
("add_page_markers", False),
|
||||
("add_page_markers", True),
|
||||
("merge_tables", False),
|
||||
("merge_tables", True),
|
||||
),
|
||||
)
|
||||
def test_reducto_v3_strategy_reaches_every_formatting_boolean(field: str, value: bool) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: (
|
||||
"formatting" in candidate.model_fields_set
|
||||
and field in candidate.formatting.model_fields_set
|
||||
and getattr(candidate.formatting, field) is value
|
||||
),
|
||||
)
|
||||
|
||||
assert getattr(sdk_input.formatting, field) is value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("include", _REDUCTO_FORMATTING_INCLUDE_GROUPS)
|
||||
def test_reducto_v3_strategy_reaches_every_formatting_include(include: tuple[str, ...]) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: (
|
||||
"formatting" in candidate.model_fields_set
|
||||
and "include" in candidate.formatting.model_fields_set
|
||||
and tuple(candidate.formatting.include) == include
|
||||
),
|
||||
)
|
||||
|
||||
assert tuple(sdk_input.formatting.include) == include
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chunk_mode", ("variable", "section", "page", "disabled", "block", "page_sections"))
|
||||
def test_reducto_v3_strategy_reaches_every_chunk_mode(chunk_mode: str) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(),
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: (
|
||||
"retrieval" in candidate.model_fields_set
|
||||
and "chunking" in candidate.retrieval.model_fields_set
|
||||
|
|
@ -571,7 +878,7 @@ def test_reducto_v3_strategy_reaches_every_chunk_mode(chunk_mode: str) -> None:
|
|||
@pytest.mark.parametrize("chunk_size", (250, 1000, 1500))
|
||||
def test_reducto_v3_strategy_reaches_every_chunk_size(chunk_size: int) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(),
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: candidate.retrieval.chunking.chunk_size == chunk_size,
|
||||
)
|
||||
|
||||
|
|
@ -579,10 +886,51 @@ def test_reducto_v3_strategy_reaches_every_chunk_size(chunk_size: int) -> None:
|
|||
assert sdk_input.retrieval.chunking.chunk_size == chunk_size
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chunk_overlap", (32, 128))
|
||||
def test_reducto_v3_strategy_reaches_every_chunk_overlap(chunk_overlap: int) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: candidate.retrieval.chunking.chunk_overlap == chunk_overlap,
|
||||
)
|
||||
|
||||
assert sdk_input.retrieval.chunking.chunk_mode == "variable"
|
||||
assert sdk_input.retrieval.chunking.chunk_size == 1000
|
||||
assert sdk_input.retrieval.chunking.chunk_overlap == chunk_overlap
|
||||
|
||||
|
||||
@pytest.mark.parametrize("filter_blocks", _REDUCTO_FILTER_BLOCK_GROUPS)
|
||||
def test_reducto_v3_strategy_reaches_every_filter_block_group(filter_blocks: tuple[str, ...]) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: (
|
||||
"retrieval" in candidate.model_fields_set
|
||||
and "filter_blocks" in candidate.retrieval.model_fields_set
|
||||
and tuple(candidate.retrieval.filter_blocks) == filter_blocks
|
||||
),
|
||||
)
|
||||
|
||||
assert tuple(sdk_input.retrieval.filter_blocks) == filter_blocks
|
||||
|
||||
|
||||
@pytest.mark.parametrize("embedding_optimized", (False, True))
|
||||
def test_reducto_v3_strategy_reaches_every_embedding_setting(embedding_optimized: bool) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: (
|
||||
"retrieval" in candidate.model_fields_set
|
||||
and "embedding_optimized" in candidate.retrieval.model_fields_set
|
||||
and candidate.retrieval.embedding_optimized is embedding_optimized
|
||||
),
|
||||
)
|
||||
|
||||
assert sdk_input.retrieval.chunking.chunk_mode == "variable"
|
||||
assert sdk_input.retrieval.embedding_optimized is embedding_optimized
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dpi", (50, 100, 250))
|
||||
def test_reducto_v3_strategy_reaches_every_metadata_dpi(dpi: int) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(),
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: (
|
||||
"settings" in candidate.model_fields_set
|
||||
and "embed_pdf_metadata_dpi" in candidate.settings.model_fields_set
|
||||
|
|
@ -594,6 +942,58 @@ def test_reducto_v3_strategy_reaches_every_metadata_dpi(dpi: int) -> None:
|
|||
assert sdk_input.settings.embed_pdf_metadata_dpi == dpi
|
||||
|
||||
|
||||
def test_reducto_v3_strategy_reaches_metadata_with_default_dpi_omitted() -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: (
|
||||
"settings" in candidate.model_fields_set and candidate.settings.model_fields_set == {"embed_pdf_metadata"}
|
||||
),
|
||||
)
|
||||
|
||||
assert sdk_input.settings.embed_pdf_metadata is True
|
||||
assert "embed_pdf_metadata_dpi" not in sdk_input.settings.model_fields_set
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field", "value"),
|
||||
(
|
||||
("model", "r-1"),
|
||||
("ocr_system", "standard"),
|
||||
("ocr_system", "legacy"),
|
||||
("extraction_mode", "hybrid"),
|
||||
("extraction_mode", "ocr"),
|
||||
("extraction_mode", "metadata"),
|
||||
("return_ocr_data", True),
|
||||
("timeout", 300.0),
|
||||
),
|
||||
)
|
||||
def test_reducto_v3_strategy_reaches_every_scalar_setting(field: str, value: object) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: (
|
||||
"settings" in candidate.model_fields_set
|
||||
and field in candidate.settings.model_fields_set
|
||||
and getattr(candidate.settings, field) == value
|
||||
),
|
||||
)
|
||||
|
||||
assert getattr(sdk_input.settings, field) == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("return_images", _REDUCTO_RETURN_IMAGE_GROUPS)
|
||||
def test_reducto_v3_strategy_reaches_every_return_image_group(return_images: tuple[str, ...]) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: (
|
||||
"settings" in candidate.model_fields_set
|
||||
and "return_images" in candidate.settings.model_fields_set
|
||||
and tuple(candidate.settings.return_images) == return_images
|
||||
),
|
||||
)
|
||||
|
||||
assert tuple(sdk_input.settings.return_images) == return_images
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"page_range",
|
||||
(
|
||||
|
|
@ -604,7 +1004,7 @@ def test_reducto_v3_strategy_reaches_every_metadata_dpi(dpi: int) -> None:
|
|||
)
|
||||
def test_reducto_v3_strategy_reaches_every_page_range_shape(page_range: object) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_v3_input_strategy(),
|
||||
reducto_v3_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: (
|
||||
cast(
|
||||
dict[str, object],
|
||||
|
|
@ -621,6 +1021,128 @@ def test_reducto_v3_strategy_reaches_every_page_range_shape(page_range: object)
|
|||
@given(sdk_input=reducto_legacy_input_strategy())
|
||||
def test_reducto_legacy_strategy_generates_valid_litellm_inputs(sdk_input: ReductoParseLegacySdkInput) -> None:
|
||||
assert ReductoParseLegacySdkInput.model_validate(sdk_input.canonical_input()) == sdk_input
|
||||
assert "enhance" not in sdk_input.model_fields_set
|
||||
|
||||
|
||||
@settings(max_examples=10, deadline=None)
|
||||
@given(sdk_input=reducto_legacy_input_strategy())
|
||||
def test_reducto_legacy_strategy_values_survive_the_request_transform(
|
||||
sdk_input: ReductoParseLegacySdkInput,
|
||||
) -> None:
|
||||
sdk_kwargs: Final = sdk_input.as_sdk_kwargs()
|
||||
model: Final = cast(str, sdk_kwargs["model"])
|
||||
document: Final = cast(DocumentType, sdk_kwargs["document"])
|
||||
config: Final = ReductoParseLegacyConfig()
|
||||
transform_request: Final = cast(_TransformOcrRequest, config.transform_ocr_request)
|
||||
|
||||
with patch.object(config, "_ensure_file_id_sync", return_value="reducto://fixture-document.pdf"):
|
||||
request: Final = transform_request(model, document, {}, {})
|
||||
|
||||
assert cast(dict[str, object], request.data) == {
|
||||
"document_url": "reducto://fixture-document.pdf",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "provider"),
|
||||
(("reducto/parse-legacy", None), ("parse-legacy", "reducto")),
|
||||
)
|
||||
def test_reducto_legacy_strategy_reaches_every_routing_form(model: str, provider: str | None) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
reducto_legacy_input_strategy(),
|
||||
lambda candidate: candidate.model == model and candidate.custom_llm_provider == provider,
|
||||
)
|
||||
|
||||
assert sdk_input.model == model
|
||||
assert sdk_input.custom_llm_provider == provider
|
||||
|
||||
|
||||
@settings(max_examples=50, deadline=None)
|
||||
@given(sdk_input=azure_mistral_input_strategy(INLINE_IMAGE_DATA_URI))
|
||||
def test_azure_mistral_strategy_is_contained_to_gateway_capabilities(
|
||||
sdk_input: AzureMistralOcrSdkInput,
|
||||
) -> None:
|
||||
optional_fields: Final = frozenset(sdk_input.model_fields_set) - {"model", "document"}
|
||||
|
||||
assert optional_fields in _AZURE_MISTRAL_OPTION_GROUPS
|
||||
assert optional_fields.isdisjoint(
|
||||
{
|
||||
"document_annotation_prompt",
|
||||
"extract_header",
|
||||
"extract_footer",
|
||||
"table_format",
|
||||
"include_blocks",
|
||||
"id",
|
||||
}
|
||||
)
|
||||
assert not isinstance(sdk_input.pages, str)
|
||||
assert sdk_input.confidence_scores_granularity in {None, "page", "word"}
|
||||
if optional_fields:
|
||||
assert isinstance(sdk_input.document, DocumentUrlDocument)
|
||||
assert sdk_input.document.document_url == structured_pdf_data_uri()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field", "value"),
|
||||
(
|
||||
("pages", [0]),
|
||||
("pages", [0, 1]),
|
||||
("include_image_base64", False),
|
||||
("include_image_base64", True),
|
||||
("image_limit", 1),
|
||||
("image_min_size", 300),
|
||||
("confidence_scores_granularity", "page"),
|
||||
("confidence_scores_granularity", "word"),
|
||||
),
|
||||
)
|
||||
def test_azure_mistral_strategy_reaches_every_gateway_scalar(field: str, value: object) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
azure_mistral_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: field in candidate.model_fields_set and getattr(candidate, field) == value,
|
||||
)
|
||||
|
||||
assert getattr(sdk_input, field) == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ("bbox_annotation_format", "document_annotation_format"))
|
||||
def test_azure_mistral_strategy_reaches_every_gateway_schema(field: str) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
azure_mistral_input_strategy(INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: frozenset(candidate.model_fields_set) - {"model", "document"} == frozenset({field}),
|
||||
)
|
||||
|
||||
assert frozenset(sdk_input.model_fields_set) - {"model", "document"} == {field}
|
||||
|
||||
|
||||
@settings(max_examples=50, deadline=None)
|
||||
@given(sdk_input=azure_mistral_input_strategy(INLINE_IMAGE_DATA_URI))
|
||||
def test_azure_mistral_strategy_exercises_url_conversion_and_inline_bypass(
|
||||
sdk_input: AzureMistralOcrSdkInput,
|
||||
) -> None:
|
||||
sdk_kwargs: Final = sdk_input.as_sdk_kwargs()
|
||||
model: Final = cast(str, sdk_kwargs["model"])
|
||||
document: Final = cast(DocumentType, sdk_kwargs["document"])
|
||||
optional_params: Final = {name: value for name, value in sdk_kwargs.items() if name not in {"model", "document"}}
|
||||
config: Final = AzureAIOCRConfig()
|
||||
map_params: Final = cast(_MapOcrParams, config.map_ocr_params)
|
||||
transform_request: Final = cast(_TransformOcrRequest, config.transform_ocr_request)
|
||||
mapped: Final = map_params(optional_params, {}, model)
|
||||
|
||||
request: Final = _transform_with_stubbed_download(transform_request, model, document, mapped)
|
||||
|
||||
source_key: Final = "image_url" if document["type"] == "image_url" else "document_url"
|
||||
source: Final = document[source_key]
|
||||
expected_document: Final = dict(document)
|
||||
if not source.startswith("data:"):
|
||||
media_type: Final = "image/png" if document["type"] == "image_url" else "application/pdf"
|
||||
expected_document[source_key] = f"data:{media_type};base64,AA=="
|
||||
|
||||
assert mapped == optional_params
|
||||
assert cast(dict[str, object], request.data) == {
|
||||
"model": model,
|
||||
"document": expected_document,
|
||||
**optional_params,
|
||||
}
|
||||
|
||||
|
||||
@settings(max_examples=30, deadline=None)
|
||||
|
|
@ -629,16 +1151,18 @@ def test_azure_document_intelligence_strategy_only_generates_litellm_inputs(
|
|||
sdk_input: AzureDocumentIntelligenceOcrSdkInput,
|
||||
) -> None:
|
||||
assert sdk_input.req_format == "litellm"
|
||||
assert sdk_input.model in AZURE_DOCUMENT_INTELLIGENCE_RECORDING_MODELS
|
||||
assert "boundary" not in sdk_input.as_sdk_kwargs()
|
||||
optional_fields: Final = frozenset(sdk_input.model_fields_set) - {"model", "document"}
|
||||
assert optional_fields in {
|
||||
frozenset[str](),
|
||||
frozenset({"pages"}),
|
||||
frozenset({"features"}),
|
||||
frozenset({"pages", "features"}),
|
||||
frozenset({"req_format"}),
|
||||
}
|
||||
if sdk_input.pages is not None:
|
||||
assert sdk_input.pages in ([0], [0, 1], "1", "1,2", "1-2")
|
||||
assert sdk_input.pages in ([0], [2, 0, 0, 1], ["1", "2-4"], "1-4, 5", [0, 1])
|
||||
if isinstance(sdk_input.features, list):
|
||||
assert tuple(sdk_input.features) in {
|
||||
("languages",),
|
||||
|
|
@ -647,27 +1171,70 @@ def test_azure_document_intelligence_strategy_only_generates_litellm_inputs(
|
|||
("formulas",),
|
||||
("styleFont",),
|
||||
("keyValuePairs",),
|
||||
("languages", "styleFont"),
|
||||
}
|
||||
if isinstance(sdk_input.features, str):
|
||||
assert sdk_input.features == "languages,styleFont"
|
||||
assert sdk_input.features == "languages, styleFont"
|
||||
|
||||
|
||||
@settings(max_examples=50, deadline=None)
|
||||
@given(sdk_input=azure_document_intelligence_input_strategy())
|
||||
def test_azure_document_intelligence_strategy_exercises_request_transform(
|
||||
sdk_input: AzureDocumentIntelligenceOcrSdkInput,
|
||||
) -> None:
|
||||
sdk_kwargs: Final = sdk_input.as_sdk_kwargs()
|
||||
model: Final = cast(str, sdk_kwargs["model"])
|
||||
document: Final = cast(DocumentType, sdk_kwargs["document"])
|
||||
optional_params: Final = {name: value for name, value in sdk_kwargs.items() if name not in {"model", "document"}}
|
||||
config: Final = AzureDocumentIntelligenceOCRConfig()
|
||||
map_params: Final = cast(_MapOcrParams, config.map_ocr_params)
|
||||
get_complete_url: Final = cast(_GetCompleteUrl, config.get_complete_url)
|
||||
transform_request: Final = cast(_TransformOcrRequest, config.transform_ocr_request)
|
||||
mapped: Final = map_params(optional_params, {}, model)
|
||||
url: Final = get_complete_url("https://document.example", model, mapped)
|
||||
query: Final = parse_qs(urlparse(url).query)
|
||||
request: Final = transform_request(model, document, mapped, {})
|
||||
|
||||
if sdk_input.pages is None:
|
||||
assert "pages" not in mapped
|
||||
assert "pages" not in query
|
||||
else:
|
||||
expected_pages: Final = _normalized_azure_pages(sdk_input.pages)
|
||||
assert mapped["pages"] == expected_pages
|
||||
assert query["pages"] == [expected_pages]
|
||||
if sdk_input.features is None:
|
||||
assert "features" not in mapped
|
||||
assert "features" not in query
|
||||
else:
|
||||
raw_features: Final = (
|
||||
sdk_input.features.split(",") if isinstance(sdk_input.features, str) else sdk_input.features
|
||||
)
|
||||
expected_features: Final = ",".join(feature.strip() for feature in raw_features)
|
||||
assert mapped["features"] == expected_features
|
||||
assert query["features"] == [expected_features]
|
||||
|
||||
source: Final = document["document_url"] if document["type"] == "document_url" else document["image_url"]
|
||||
assert isinstance(source, str)
|
||||
expected_body: Final = (
|
||||
{"base64Source": source.partition(",")[2]} if source.startswith("data:") else {"urlSource": source}
|
||||
)
|
||||
assert cast(dict[str, object], request.data) == expected_body
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field", "value"),
|
||||
(
|
||||
("pages", [0]),
|
||||
("pages", [0, 1]),
|
||||
("pages", "1"),
|
||||
("pages", "1,2"),
|
||||
("pages", "1-2"),
|
||||
("pages", [2, 0, 0, 1]),
|
||||
("pages", ["1", "2-4"]),
|
||||
("pages", "1-4, 5"),
|
||||
("features", ["languages"]),
|
||||
("features", ["ocrHighResolution"]),
|
||||
("features", ["barcodes"]),
|
||||
("features", ["formulas"]),
|
||||
("features", ["styleFont"]),
|
||||
("features", ["keyValuePairs"]),
|
||||
("features", "languages,styleFont"),
|
||||
("req_format", "litellm"),
|
||||
("features", "languages, styleFont"),
|
||||
),
|
||||
)
|
||||
def test_azure_document_intelligence_strategy_reaches_every_finite_value(field: str, value: object) -> None:
|
||||
|
|
@ -679,10 +1246,142 @@ def test_azure_document_intelligence_strategy_reaches_every_finite_value(field:
|
|||
assert getattr(sdk_input, field) == value
|
||||
|
||||
|
||||
def test_azure_document_intelligence_strategy_reaches_combined_query_branch() -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
azure_document_intelligence_input_strategy(),
|
||||
lambda candidate: {"pages", "features"}.issubset(candidate.model_fields_set),
|
||||
)
|
||||
|
||||
assert sdk_input.pages == [0, 1]
|
||||
assert sdk_input.features == ["languages", "styleFont"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"transport",
|
||||
(("document_url", "data"), ("image_url", "remote")),
|
||||
)
|
||||
def test_azure_document_intelligence_strategy_reaches_body_source_branches(
|
||||
transport: tuple[str, str],
|
||||
) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
azure_document_intelligence_input_strategy(),
|
||||
lambda candidate: _document_transport(candidate.document) == transport,
|
||||
)
|
||||
|
||||
assert _document_transport(sdk_input.document) == transport
|
||||
|
||||
|
||||
@settings(max_examples=50, deadline=None)
|
||||
@given(sdk_input=vertex_mistral_input_strategy("project-1", "us-central1", INLINE_IMAGE_DATA_URI))
|
||||
def test_vertex_mistral_strategy_is_contained_to_2505_capabilities(
|
||||
sdk_input: VertexMistralOcrSdkInput,
|
||||
) -> None:
|
||||
optional_fields: Final = frozenset(sdk_input.model_fields_set) - {
|
||||
"model",
|
||||
"document",
|
||||
"vertex_project",
|
||||
"vertex_location",
|
||||
}
|
||||
|
||||
assert optional_fields in _MISTRAL_2505_OPTION_GROUPS
|
||||
assert optional_fields.isdisjoint({"extract_header", "extract_footer", "table_format", "include_blocks", "id"})
|
||||
assert not isinstance(sdk_input.pages, str)
|
||||
assert sdk_input.confidence_scores_granularity in {None, "page", "word"}
|
||||
if optional_fields:
|
||||
assert isinstance(sdk_input.document, DocumentUrlDocument)
|
||||
assert sdk_input.document.document_url == structured_pdf_data_uri()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field", "value"),
|
||||
(
|
||||
("pages", [0]),
|
||||
("pages", [0, 1]),
|
||||
("include_image_base64", False),
|
||||
("include_image_base64", True),
|
||||
("image_limit", 1),
|
||||
("image_min_size", 300),
|
||||
("confidence_scores_granularity", "page"),
|
||||
("confidence_scores_granularity", "word"),
|
||||
),
|
||||
)
|
||||
def test_vertex_mistral_strategy_reaches_every_2505_scalar(field: str, value: object) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
vertex_mistral_input_strategy("project-1", "us-central1", INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: field in candidate.model_fields_set and getattr(candidate, field) == value,
|
||||
)
|
||||
|
||||
assert getattr(sdk_input, field) == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fields",
|
||||
(
|
||||
frozenset({"bbox_annotation_format"}),
|
||||
frozenset({"document_annotation_format"}),
|
||||
frozenset({"document_annotation_format", "document_annotation_prompt"}),
|
||||
),
|
||||
)
|
||||
def test_vertex_mistral_strategy_reaches_every_2505_schema_group(fields: frozenset[str]) -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
vertex_mistral_input_strategy("project-1", "us-central1", INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: (
|
||||
frozenset(candidate.model_fields_set) - {"model", "document", "vertex_project", "vertex_location"} == fields
|
||||
),
|
||||
)
|
||||
|
||||
assert frozenset(sdk_input.model_fields_set) - {"model", "document", "vertex_project", "vertex_location"} == fields
|
||||
|
||||
|
||||
@settings(max_examples=50, deadline=None)
|
||||
@given(sdk_input=vertex_mistral_input_strategy("project-1", "us-central1", INLINE_IMAGE_DATA_URI))
|
||||
def test_vertex_mistral_strategy_exercises_url_conversion_and_inline_bypass(
|
||||
sdk_input: VertexMistralOcrSdkInput,
|
||||
) -> None:
|
||||
sdk_kwargs: Final = sdk_input.as_sdk_kwargs()
|
||||
model: Final = cast(str, sdk_kwargs["model"])
|
||||
document: Final = cast(DocumentType, sdk_kwargs["document"])
|
||||
optional_params: Final = {
|
||||
name: value
|
||||
for name, value in sdk_kwargs.items()
|
||||
if name not in {"model", "document", "vertex_project", "vertex_location"}
|
||||
}
|
||||
config: Final = VertexAIOCRConfig()
|
||||
map_params: Final = cast(_MapOcrParams, config.map_ocr_params)
|
||||
transform_request: Final = cast(_TransformOcrRequest, config.transform_ocr_request)
|
||||
mapped: Final = map_params(optional_params, {}, model)
|
||||
|
||||
request: Final = _transform_with_stubbed_download(transform_request, model, document, mapped)
|
||||
|
||||
source_key: Final = "image_url" if document["type"] == "image_url" else "document_url"
|
||||
source: Final = document[source_key]
|
||||
expected_document: Final = dict(document)
|
||||
if not source.startswith("data:"):
|
||||
media_type: Final = "image/png" if document["type"] == "image_url" else "application/pdf"
|
||||
expected_document[source_key] = f"data:{media_type};base64,AA=="
|
||||
|
||||
assert mapped == optional_params
|
||||
assert cast(dict[str, object], request.data) == {
|
||||
"model": model,
|
||||
"document": expected_document,
|
||||
**optional_params,
|
||||
}
|
||||
|
||||
|
||||
@settings(max_examples=30, deadline=None)
|
||||
@given(sdk_input=vertex_deepseek_input_strategy("project-1", "us-central1"))
|
||||
@given(sdk_input=vertex_deepseek_input_strategy("project-1", "us-central1", INLINE_IMAGE_DATA_URI))
|
||||
def test_vertex_deepseek_strategy_only_generates_litellm_inputs(
|
||||
sdk_input: VertexDeepSeekOcrSdkInput,
|
||||
) -> None:
|
||||
assert sdk_input.vertex_project == "project-1"
|
||||
assert "boundary" not in sdk_input.as_sdk_kwargs()
|
||||
assert _document_transport(sdk_input.document) == ("image_url", "data")
|
||||
|
||||
|
||||
def test_vertex_deepseek_strategy_reaches_documented_image_branch() -> None:
|
||||
sdk_input: Final = _find_fixture(
|
||||
vertex_deepseek_input_strategy("project-1", "us-central1", INLINE_IMAGE_DATA_URI),
|
||||
lambda candidate: _document_transport(candidate.document) == ("image_url", "data"),
|
||||
)
|
||||
|
||||
assert _document_transport(sdk_input.document) == ("image_url", "data")
|
||||
|
|
|
|||
|
|
@ -1,25 +1,29 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import queue
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
from typing import Final, cast
|
||||
|
||||
import pytest
|
||||
from hypothesis import find, settings
|
||||
from hypothesis.strategies import SearchStrategy
|
||||
|
||||
from tests.route_parity.fixtures.inputs import generate_case_inputs
|
||||
from tests.route_parity.fixtures.media import structured_pdf_data_uri
|
||||
from tests.route_parity.fixtures.pipeline import parse_recording_args
|
||||
from tests.test_litellm.ocr.fixtures.azure import (
|
||||
AZURE_DOCUMENT_INTELLIGENCE_MODELS,
|
||||
AZURE_DOCUMENT_INTELLIGENCE_RECORDING_MODELS,
|
||||
AZURE_MISTRAL_MODELS,
|
||||
)
|
||||
from tests.test_litellm.ocr.fixtures.base import OcrSdkInputBase
|
||||
from tests.test_litellm.ocr.fixtures.common import OcrFixtureClient, OcrRecordingTarget
|
||||
from tests.test_litellm.ocr.fixtures.mistral import MISTRAL_MODELS
|
||||
from tests.test_litellm.ocr.fixtures.record import (
|
||||
discover_targets,
|
||||
discover_targets as discover_targets_with_media,
|
||||
)
|
||||
from tests.test_litellm.ocr.fixtures.record import (
|
||||
require_targets,
|
||||
)
|
||||
from tests.test_litellm.ocr.fixtures.reducto import REDUCTO_LEGACY_MODELS, REDUCTO_V3_MODELS
|
||||
|
|
@ -54,12 +58,17 @@ _MISTRAL_PARAMS: Final = frozenset(
|
|||
"table_format",
|
||||
"confidence_scores_granularity",
|
||||
"include_blocks",
|
||||
"id",
|
||||
}
|
||||
)
|
||||
_MISTRAL_2512_PARAMS: Final = _MISTRAL_PARAMS - {"include_blocks"}
|
||||
_MISTRAL_2505_PARAMS: Final = _MISTRAL_2512_PARAMS - {"extract_header", "extract_footer", "table_format"}
|
||||
_AZURE_MISTRAL_PARAMS: Final = _MISTRAL_2505_PARAMS - {"document_annotation_prompt"}
|
||||
_FIND_SETTINGS: Final = settings(max_examples=2_000, deadline=None, derandomize=True, database=None)
|
||||
_INLINE_IMAGE_DATA_URI: Final = "data:image/png;base64,dGVzdA=="
|
||||
|
||||
|
||||
def discover_targets(environ: Mapping[str, str], client: OcrFixtureClient) -> tuple[OcrRecordingTarget, ...]:
|
||||
return discover_targets_with_media(environ, client, _INLINE_IMAGE_DATA_URI)
|
||||
|
||||
|
||||
def _model(case_input: OcrSdkInputBase) -> str:
|
||||
|
|
@ -75,6 +84,14 @@ def _find_input(
|
|||
return find(strategy, predicate, settings=_FIND_SETTINGS)
|
||||
|
||||
|
||||
def _document_transport(case_input: OcrSdkInputBase) -> tuple[str, str]:
|
||||
document: Final = cast(dict[str, object], case_input.canonical_input()["document"])
|
||||
document_type: Final = cast(str, document["type"])
|
||||
source: Final = document["image_url"] if document_type == "image_url" else document["document_url"]
|
||||
assert isinstance(source, str)
|
||||
return document_type, "data" if source.startswith("data:") else "remote"
|
||||
|
||||
|
||||
def test_parse_args_has_no_model_selection() -> None:
|
||||
args: Final = parse_recording_args(["--examples", "2", "--concurrency", "3", "--fixture-dir", "/tmp/ocr"])
|
||||
|
||||
|
|
@ -191,7 +208,7 @@ def test_mistral_target_invocation_forwards_discovered_credentials() -> None:
|
|||
assert kwargs["model"] in MISTRAL_MODELS
|
||||
|
||||
|
||||
def test_every_target_strategy_reaches_every_model_and_coverage_param() -> None:
|
||||
def test_every_target_strategy_reaches_every_recording_model_and_coverage_param() -> None:
|
||||
targets: Final = discover_targets(
|
||||
{
|
||||
"MISTRAL_API_KEY": "mistral-secret",
|
||||
|
|
@ -207,15 +224,15 @@ def test_every_target_strategy_reaches_every_model_and_coverage_param() -> None:
|
|||
)
|
||||
expected: Final[dict[str, tuple[tuple[str, ...], frozenset[str]]]] = {
|
||||
"mistral-ocr": (MISTRAL_MODELS, _MISTRAL_PARAMS),
|
||||
"azure-mistral": (AZURE_MISTRAL_MODELS, _MISTRAL_2512_PARAMS),
|
||||
"azure-mistral": (AZURE_MISTRAL_MODELS, _AZURE_MISTRAL_PARAMS),
|
||||
"azure-document-intelligence": (
|
||||
AZURE_DOCUMENT_INTELLIGENCE_MODELS,
|
||||
AZURE_DOCUMENT_INTELLIGENCE_RECORDING_MODELS,
|
||||
frozenset({"pages", "features", "req_format"}),
|
||||
),
|
||||
"vertex-mistral": (VERTEX_MISTRAL_MODELS, _MISTRAL_2505_PARAMS),
|
||||
"vertex-deepseek": (VERTEX_DEEPSEEK_MODELS, frozenset[str]()),
|
||||
"reducto-v3": (REDUCTO_V3_MODELS, frozenset({"formatting", "retrieval", "settings"})),
|
||||
"reducto-legacy": (REDUCTO_LEGACY_MODELS, frozenset({"enhance"})),
|
||||
"reducto-legacy": (REDUCTO_LEGACY_MODELS, frozenset[str]()),
|
||||
}
|
||||
|
||||
for target in targets:
|
||||
|
|
@ -231,13 +248,57 @@ def test_every_target_strategy_reaches_every_model_and_coverage_param() -> None:
|
|||
== model
|
||||
)
|
||||
for param in expected_params:
|
||||
assert (
|
||||
param
|
||||
in _find_input(
|
||||
target.strategy,
|
||||
lambda case_input, expected_param=param: expected_param in case_input.as_sdk_kwargs(),
|
||||
).as_sdk_kwargs()
|
||||
reached = _find_input(
|
||||
target.strategy,
|
||||
lambda case_input, expected_param=param: expected_param in case_input.as_sdk_kwargs(),
|
||||
)
|
||||
assert param in reached.as_sdk_kwargs()
|
||||
document = cast(dict[str, object], reached.canonical_input()["document"])
|
||||
assert document == {"type": "document_url", "document_url": structured_pdf_data_uri()}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target_name", ("mistral-ocr", "azure-mistral", "vertex-mistral"))
|
||||
def test_mistral_recording_targets_reach_every_transport_branch(target_name: str) -> None:
|
||||
targets: Final = discover_targets(
|
||||
{
|
||||
"MISTRAL_API_KEY": "mistral-secret",
|
||||
"AZURE_AI_API_KEY": "azure-secret",
|
||||
"AZURE_AI_API_BASE": "https://azure.example",
|
||||
"VERTEX_AI_API_KEY": "vertex-secret",
|
||||
"VERTEXAI_PROJECT": "project-1",
|
||||
},
|
||||
_UNUSED_OCR_CLIENT,
|
||||
)
|
||||
target: Final = next(candidate for candidate in targets if candidate.name == target_name)
|
||||
|
||||
for transport in (
|
||||
("image_url", "remote"),
|
||||
("image_url", "data"),
|
||||
("document_url", "remote"),
|
||||
("document_url", "data"),
|
||||
):
|
||||
reached = _find_input(
|
||||
target.strategy,
|
||||
lambda case_input, expected=transport: _document_transport(case_input) == expected,
|
||||
)
|
||||
assert _document_transport(reached) == transport
|
||||
|
||||
|
||||
def test_vertex_deepseek_recording_reaches_documented_image_branch() -> None:
|
||||
targets: Final = discover_targets(
|
||||
{
|
||||
"VERTEX_AI_API_KEY": "vertex-secret",
|
||||
"VERTEXAI_PROJECT": "project-1",
|
||||
},
|
||||
_UNUSED_OCR_CLIENT,
|
||||
)
|
||||
target: Final = next(candidate for candidate in targets if candidate.name == "vertex-deepseek")
|
||||
case_input: Final = _find_input(
|
||||
target.strategy,
|
||||
lambda candidate: _document_transport(candidate) == ("image_url", "data"),
|
||||
)
|
||||
|
||||
assert _document_transport(case_input) == ("image_url", "data")
|
||||
|
||||
|
||||
def test_ocr_targets_have_no_hardcoded_required_inputs() -> None:
|
||||
|
|
|
|||
15
uv.lock
generated
15
uv.lock
generated
|
|
@ -4542,6 +4542,7 @@ dev = [
|
|||
{ name = "pytest-rerunfailures" },
|
||||
{ name = "pytest-timeout" },
|
||||
{ name = "pytest-xdist" },
|
||||
{ name = "reportlab" },
|
||||
{ name = "requests-mock" },
|
||||
{ name = "responses" },
|
||||
{ name = "respx" },
|
||||
|
|
@ -4728,6 +4729,7 @@ dev = [
|
|||
{ name = "pytest-rerunfailures", specifier = "==15.1" },
|
||||
{ name = "pytest-timeout", specifier = "==2.4.0" },
|
||||
{ name = "pytest-xdist", specifier = "==3.8.0" },
|
||||
{ name = "reportlab", specifier = "==5.0.1" },
|
||||
{ name = "requests-mock", specifier = "==1.12.1" },
|
||||
{ name = "responses", specifier = "==0.26.0" },
|
||||
{ name = "respx", specifier = "==0.22.0" },
|
||||
|
|
@ -8283,6 +8285,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reportlab"
|
||||
version = "5.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "pillow" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4a/51/dbe28534ae12c852f61be91f039f343305fd1f34f1c66b8de75afae7a525/reportlab-5.0.1.tar.gz", hash = "sha256:ebd13154be1c8515e665de70bd2d303ae9ddc3ef47e44afd5116441ca0283a26", size = 3945711, upload-time = "2026-08-20T13:48:16.461Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/cb/dacbc268cb68d0428ea2cbd85266195a9ab3e677449589ddae59bd7542ac/reportlab-5.0.1-py3-none-any.whl", hash = "sha256:1c36e6bb0e71780c72331eba60da7f602e8d4389a8723825af71342e49d791e8", size = 1957258, upload-time = "2026-08-20T13:48:14.026Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.0"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue