feat(rust): scaffold OCR compiler contracts

This commit is contained in:
Yujong Lee 2026-09-01 12:25:10 -07:00
parent 2395450714
commit 82e6caf87f
25 changed files with 1008 additions and 0 deletions

View file

@ -1428,7 +1428,9 @@ dependencies = [
"aws-sigv4",
"aws-smithy-runtime-api",
"aws-types",
"bytes",
"futures-util",
"mime",
"rand 0.8.7",
"reqwest",
"serde",
@ -1436,6 +1438,7 @@ dependencies = [
"sha2 0.10.9",
"thiserror 2.0.19",
"tokio",
"url",
]
[[package]]
@ -2720,6 +2723,7 @@ dependencies = [
"idna",
"percent-encoding",
"serde",
"serde_derive",
]
[[package]]

View file

@ -33,6 +33,9 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "net"]
tokio-tungstenite = { version = "0.24", default-features = false, features = ["connect", "rustls-tls-native-roots"] }
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
base64 = "0.22"
bytes = "1"
mime = "0.3"
url = { version = "2", features = ["serde"] }
[profile.release]
opt-level = 3

View file

@ -7,11 +7,14 @@ repository.workspace = true
[dependencies]
futures-util.workspace = true
bytes.workspace = true
mime.workspace = true
rand.workspace = true
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
url.workspace = true
sha2.workspace = true
aws-config = { version = "1.9.0", default-features = false, features = ["rustls", "rt-tokio"], optional = true }
aws-credential-types = { version = "1.3.0", features = ["hardcoded-credentials"], optional = true }

View file

@ -0,0 +1,240 @@
use std::collections::BTreeMap;
use bytes::Bytes;
use mime::Mime;
use serde_json::Value;
use url::Url;
use super::compiler::CompileError;
use super::policy::OcrCanonicalField;
use super::types::{Field, OcrDialectId};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DocumentKind {
Image,
Pdf,
}
#[derive(Clone, PartialEq)]
pub enum OcrDocument {
RemoteUrl {
kind: DocumentKind,
url: Url,
},
Inline {
kind: DocumentKind,
media_type: Mime,
bytes: Bytes,
},
ProviderReference {
provider: OcrDialectId,
id: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PageSelection(Vec<u32>);
impl PageSelection {
pub fn new(pages: impl IntoIterator<Item = u32>) -> Self {
Self(pages.into_iter().collect())
}
pub fn pages(&self) -> &[u32] {
&self.0
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AnnotationFormat(Value);
impl AnnotationFormat {
pub fn new(schema: Value) -> Result<Self, CompileError> {
if !schema.is_object() {
return Err(CompileError::InvalidParameter {
field: "annotation_format",
reason: "must be a JSON object",
});
}
Ok(Self(schema))
}
pub fn as_value(&self) -> &Value {
&self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TableFormat {
Html,
Markdown,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConfidenceScoresGranularity {
Page,
Word,
}
#[derive(Clone, Debug, PartialEq)]
pub struct OcrOutputOptions {
pub include_image_base64: Field<bool>,
pub image_limit: Field<u32>,
pub image_min_size: Field<u32>,
pub bbox_annotation_format: Field<AnnotationFormat>,
pub document_annotation_format: Field<AnnotationFormat>,
pub document_annotation_prompt: Field<String>,
pub extract_header: Field<bool>,
pub extract_footer: Field<bool>,
pub table_format: Field<TableFormat>,
pub confidence_scores_granularity: Field<ConfidenceScoresGranularity>,
pub include_blocks: Field<bool>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OcrRequestId(String);
impl OcrRequestId {
pub fn new(value: impl Into<String>) -> Result<Self, CompileError> {
let value = value.into();
if value.trim().is_empty() {
return Err(CompileError::InvalidParameter {
field: "id",
reason: "must not be blank",
});
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExplicitProviderExtras {
dialect: OcrDialectId,
fields: BTreeMap<String, Value>,
}
impl ExplicitProviderExtras {
pub fn try_new(
dialect: OcrDialectId,
fields: BTreeMap<String, Value>,
) -> Result<Self, CompileError> {
if let Some(field) = fields.keys().find(|field| {
OcrCanonicalField::from_wire_name(field).is_some() || is_litellm_control(field)
}) {
return Err(if OcrCanonicalField::from_wire_name(field).is_some() {
CompileError::ExtraCollidesWithCanonical(field.clone())
} else {
CompileError::ReservedExtra(field.clone())
});
}
Ok(Self { dialect, fields })
}
pub fn dialect(&self) -> OcrDialectId {
self.dialect
}
pub fn fields(&self) -> &BTreeMap<String, Value> {
&self.fields
}
}
fn is_litellm_control(field: &str) -> bool {
field.starts_with("litellm_")
|| matches!(
field,
"api_base"
| "api_key"
| "custom_llm_provider"
| "fallbacks"
| "metadata"
| "mock_response"
| "num_retries"
| "request_timeout"
| "retry_policy"
)
}
/// ```compile_fail
/// fn assert_serialize<T: serde::Serialize>() {}
/// assert_serialize::<litellm_core::ocr::canonical::CanonicalOcrRequest>();
/// ```
#[derive(Clone, PartialEq)]
pub struct CanonicalOcrRequest {
pub model: String,
pub document: OcrDocument,
pub pages: Field<PageSelection>,
pub output: OcrOutputOptions,
pub request_id: Field<OcrRequestId>,
pub provider_extras: ExplicitProviderExtras,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn extras_reject_canonical_field_collisions() {
let error = ExplicitProviderExtras::try_new(
OcrDialectId::Mistral,
BTreeMap::from([("pages".to_string(), json!([0]))]),
)
.expect_err("canonical fields cannot enter extras");
assert_eq!(
error,
CompileError::ExtraCollidesWithCanonical("pages".to_string())
);
}
#[test]
fn extras_reject_litellm_controls() {
let error = ExplicitProviderExtras::try_new(
OcrDialectId::Mistral,
BTreeMap::from([("request_timeout".to_string(), json!(30))]),
)
.expect_err("LiteLLM controls cannot enter extras");
assert_eq!(
error,
CompileError::ReservedExtra("request_timeout".to_string())
);
let prefixed_error = ExplicitProviderExtras::try_new(
OcrDialectId::Mistral,
BTreeMap::from([("litellm_future_control".to_string(), json!(true))]),
)
.expect_err("reserved LiteLLM prefixes cannot enter extras");
assert_eq!(
prefixed_error,
CompileError::ReservedExtra("litellm_future_control".to_string())
);
}
#[test]
fn extras_remain_bound_to_one_dialect() {
let extras = ExplicitProviderExtras::try_new(
OcrDialectId::ReductoV3,
BTreeMap::from([("chunking".to_string(), json!({"size": 1}))]),
)
.expect("provider field is accepted");
assert_eq!(extras.dialect(), OcrDialectId::ReductoV3);
assert_eq!(extras.fields()["chunking"], json!({"size": 1}));
}
#[test]
fn canonical_fields_preserve_absent_null_and_value() {
assert_ne!(Field::<PageSelection>::Absent, Field::Null);
assert_ne!(
Field::Null,
Field::Value(PageSelection::new([0_u32, 2_u32]))
);
}
}

View file

@ -0,0 +1,201 @@
use std::collections::BTreeMap;
use serde::Serialize;
use serde_json::Value;
use thiserror::Error;
use url::Url;
use super::canonical::{CanonicalOcrRequest, DocumentKind};
use super::plan::{CompletionPlan, DocumentPlan};
use super::policy::OcrParameterPolicy;
use super::response::NormalizedOcr;
use super::types::OcrDialectId;
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum CompileError {
#[error("invalid OCR parameter {field}: {reason}")]
InvalidParameter {
field: &'static str,
reason: &'static str,
},
#[error("provider extra collides with canonical OCR field: {0}")]
ExtraCollidesWithCanonical(String),
#[error("LiteLLM control cannot enter OCR provider extras: {0}")]
ReservedExtra(String),
#[error("OCR dialect is not compiled yet: {0:?}")]
UnsupportedDialect(OcrDialectId),
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum NormalizeError {
#[error("invalid terminal OCR response: {0}")]
InvalidPayload(&'static str),
}
#[derive(Clone, Default, PartialEq, Eq)]
pub struct OcrCredentials {
api_key: Option<String>,
oauth_token: Option<String>,
}
impl OcrCredentials {
pub fn new(api_key: Option<String>, oauth_token: Option<String>) -> Self {
Self {
api_key,
oauth_token,
}
}
pub fn api_key(&self) -> Option<&str> {
self.api_key.as_deref()
}
pub fn oauth_token(&self) -> Option<&str> {
self.oauth_token.as_deref()
}
}
/// ```compile_fail
/// fn assert_serialize<T: serde::Serialize>() {}
/// assert_serialize::<litellm_core::ocr::compiler::OcrCredentials>();
/// assert_serialize::<litellm_core::ocr::compiler::ResolvedOcrTarget>();
/// ```
#[derive(Clone, PartialEq)]
pub struct ResolvedOcrTarget {
dialect: OcrDialectId,
api_base: Url,
credentials: OcrCredentials,
}
impl ResolvedOcrTarget {
pub fn new(dialect: OcrDialectId, api_base: Url, credentials: OcrCredentials) -> Self {
Self {
dialect,
api_base,
credentials,
}
}
pub fn dialect(&self) -> OcrDialectId {
self.dialect
}
pub fn api_base(&self) -> &Url {
&self.api_base
}
pub fn credentials(&self) -> &OcrCredentials {
&self.credentials
}
}
#[derive(Clone, PartialEq)]
pub enum ProviderDocument {
RemoteUrl {
kind: DocumentKind,
url: Url,
},
InlineDataUri {
kind: DocumentKind,
data_uri: String,
},
Reference {
id: String,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethod {
Get,
Post,
}
#[derive(Clone, PartialEq, Serialize)]
#[serde(transparent)]
pub struct OcrWireBody(pub(crate) Value);
impl OcrWireBody {
pub fn as_value(&self) -> &Value {
&self.0
}
}
#[derive(Clone, PartialEq, Serialize)]
pub struct CompiledHttpRequest {
pub method: HttpMethod,
pub url: Url,
pub headers: BTreeMap<String, String>,
pub body: OcrWireBody,
}
#[derive(Clone, PartialEq)]
pub struct ProviderPayload(Value);
impl ProviderPayload {
pub fn new(value: Value) -> Self {
Self(value)
}
pub fn as_value(&self) -> &Value {
&self.0
}
pub fn into_value(self) -> Value {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OcrDocumentPolicy {
Ready,
FetchRemoteUrlAndInline,
UploadUnlessProviderReference,
}
pub trait OcrDialectCompiler: Send + Sync {
fn parameter_policy(&self) -> &'static OcrParameterPolicy;
fn prepare_document(
&self,
document: &super::canonical::OcrDocument,
target: &ResolvedOcrTarget,
) -> Result<DocumentPlan, CompileError>;
fn compile_submit(
&self,
request: &CanonicalOcrRequest,
document: ProviderDocument,
target: &ResolvedOcrTarget,
) -> Result<CompiledHttpRequest, CompileError>;
fn completion_plan(&self) -> CompletionPlan;
fn normalize(
&self,
terminal_response: ProviderPayload,
) -> Result<NormalizedOcr, NormalizeError>;
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn compiled_http_request_is_the_serializable_wire_boundary() {
let request = CompiledHttpRequest {
method: HttpMethod::Post,
url: Url::parse("https://example.com/ocr").expect("valid URL"),
headers: BTreeMap::from([("content-type".to_string(), "application/json".to_string())]),
body: OcrWireBody(json!({"model": "ocr-model"})),
};
let serialized = serde_json::to_value(request).expect("wire request serializes");
assert_eq!(serialized["method"], "POST");
assert_eq!(serialized["url"], "https://example.com/ocr");
assert_eq!(serialized["body"]["model"], "ocr-model");
}
}

View file

@ -1,2 +1,7 @@
pub mod canonical;
pub mod compiler;
pub mod plan;
pub mod policy;
pub mod response;
pub mod transformation;
pub mod types;

View file

@ -0,0 +1,50 @@
use std::time::Duration;
use url::Url;
use super::canonical::{DocumentKind, OcrDocument};
use super::compiler::ProviderDocument;
use super::types::OcrDialectId;
#[derive(Clone, PartialEq)]
pub enum DocumentPlan {
Ready(ProviderDocument),
FetchAndInline(FetchPlan),
Upload(UploadPlan),
}
#[derive(Clone, PartialEq)]
pub struct FetchPlan {
pub kind: DocumentKind,
pub url: Url,
pub max_bytes: usize,
}
#[derive(Clone, PartialEq)]
pub struct UploadPlan {
pub dialect: OcrDialectId,
pub document: OcrDocument,
}
#[derive(Clone, PartialEq)]
pub enum CompletionPlan {
Immediate,
Poll(PollPlan),
}
#[derive(Clone, PartialEq, Eq)]
pub struct PollPlan {
pub operation_location_header: &'static str,
pub interval: Duration,
pub timeout: Duration,
}
/// ```compile_fail
/// fn assert_serialize<T: serde::Serialize>() {}
/// assert_serialize::<litellm_core::ocr::plan::DocumentPlan>();
/// assert_serialize::<litellm_core::ocr::plan::FetchPlan>();
/// assert_serialize::<litellm_core::ocr::plan::UploadPlan>();
/// assert_serialize::<litellm_core::ocr::plan::CompletionPlan>();
/// assert_serialize::<litellm_core::ocr::plan::PollPlan>();
/// ```
const _: () = ();

View file

@ -0,0 +1,179 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ParameterDisposition {
Forward,
Rename(&'static str),
Transform,
ConsumeAsConfiguration,
Reject,
}
macro_rules! ocr_parameter_schema {
($(($variant:ident, $field:ident, $wire_name:literal)),+ $(,)?) => {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OcrCanonicalField {
$($variant),+
}
impl OcrCanonicalField {
pub const ALL: [Self; ocr_parameter_schema!(@count $($variant),+)] = [
$(Self::$variant),+
];
pub const fn wire_name(self) -> &'static str {
match self {
$(Self::$variant => $wire_name),+
}
}
pub fn from_wire_name(value: &str) -> Option<Self> {
match value {
$($wire_name => Some(Self::$variant)),+,
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OcrParameterPolicy {
$(pub $field: ParameterDisposition),+
}
impl OcrParameterPolicy {
pub const fn disposition(self, field: OcrCanonicalField) -> ParameterDisposition {
match field {
$(OcrCanonicalField::$variant => self.$field),+
}
}
}
};
(@count $($item:ident),+) => {
<[()]>::len(&[$(ocr_parameter_schema!(@replace $item ())),+])
};
(@replace $_item:ident $sub:expr) => { $sub };
}
ocr_parameter_schema!(
(Pages, pages, "pages"),
(
IncludeImageBase64,
include_image_base64,
"include_image_base64"
),
(ImageLimit, image_limit, "image_limit"),
(ImageMinSize, image_min_size, "image_min_size"),
(
BboxAnnotationFormat,
bbox_annotation_format,
"bbox_annotation_format"
),
(
DocumentAnnotationFormat,
document_annotation_format,
"document_annotation_format"
),
(
DocumentAnnotationPrompt,
document_annotation_prompt,
"document_annotation_prompt"
),
(ExtractHeader, extract_header, "extract_header"),
(ExtractFooter, extract_footer, "extract_footer"),
(TableFormat, table_format, "table_format"),
(
ConfidenceScoresGranularity,
confidence_scores_granularity,
"confidence_scores_granularity"
),
(IncludeBlocks, include_blocks, "include_blocks"),
(RequestId, request_id, "id"),
);
pub const MISTRAL_OCR_PARAMETER_POLICY: OcrParameterPolicy = OcrParameterPolicy {
pages: ParameterDisposition::Forward,
include_image_base64: ParameterDisposition::Forward,
image_limit: ParameterDisposition::Forward,
image_min_size: ParameterDisposition::Forward,
bbox_annotation_format: ParameterDisposition::Forward,
document_annotation_format: ParameterDisposition::Forward,
document_annotation_prompt: ParameterDisposition::Forward,
extract_header: ParameterDisposition::Forward,
extract_footer: ParameterDisposition::Forward,
table_format: ParameterDisposition::Forward,
confidence_scores_granularity: ParameterDisposition::Forward,
include_blocks: ParameterDisposition::Forward,
request_id: ParameterDisposition::Forward,
};
pub const AZURE_DOCUMENT_INTELLIGENCE_PARAMETER_POLICY: OcrParameterPolicy = OcrParameterPolicy {
pages: ParameterDisposition::Transform,
include_image_base64: ParameterDisposition::Reject,
image_limit: ParameterDisposition::Reject,
image_min_size: ParameterDisposition::Reject,
bbox_annotation_format: ParameterDisposition::Reject,
document_annotation_format: ParameterDisposition::Reject,
document_annotation_prompt: ParameterDisposition::Reject,
extract_header: ParameterDisposition::Reject,
extract_footer: ParameterDisposition::Reject,
table_format: ParameterDisposition::Reject,
confidence_scores_granularity: ParameterDisposition::Reject,
include_blocks: ParameterDisposition::Reject,
request_id: ParameterDisposition::Reject,
};
pub const REJECT_CANONICAL_OCR_PARAMETER_POLICY: OcrParameterPolicy = OcrParameterPolicy {
pages: ParameterDisposition::Reject,
include_image_base64: ParameterDisposition::Reject,
image_limit: ParameterDisposition::Reject,
image_min_size: ParameterDisposition::Reject,
bbox_annotation_format: ParameterDisposition::Reject,
document_annotation_format: ParameterDisposition::Reject,
document_annotation_prompt: ParameterDisposition::Reject,
extract_header: ParameterDisposition::Reject,
extract_footer: ParameterDisposition::Reject,
table_format: ParameterDisposition::Reject,
confidence_scores_granularity: ParameterDisposition::Reject,
include_blocks: ParameterDisposition::Reject,
request_id: ParameterDisposition::Reject,
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_canonical_field_has_a_mistral_disposition() {
let classified = OcrCanonicalField::ALL.map(|field| {
(
field.wire_name(),
MISTRAL_OCR_PARAMETER_POLICY.disposition(field),
)
});
assert_eq!(classified.len(), 13);
assert!(
classified
.iter()
.all(|(_, disposition)| *disposition == ParameterDisposition::Forward)
);
}
#[test]
fn every_canonical_field_has_an_explicit_non_mistral_disposition() {
let azure = OcrCanonicalField::ALL
.map(|field| AZURE_DOCUMENT_INTELLIGENCE_PARAMETER_POLICY.disposition(field));
let provider_bound = OcrCanonicalField::ALL
.map(|field| REJECT_CANONICAL_OCR_PARAMETER_POLICY.disposition(field));
assert_eq!(azure[0], ParameterDisposition::Transform);
assert!(
azure[1..]
.iter()
.all(|disposition| *disposition == ParameterDisposition::Reject)
);
assert!(
provider_bound
.iter()
.all(|disposition| *disposition == ParameterDisposition::Reject)
);
}
}

View file

@ -0,0 +1,86 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OcrPage {
pub index: u32,
pub markdown: String,
}
#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct OcrUsage {
pub pages_processed: Option<u32>,
pub credits: Option<f64>,
pub document_size_bytes: Option<u64>,
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct NormalizedOcr {
pub pages: Vec<OcrPage>,
pub model: String,
pub document_annotation: Option<Value>,
pub content: Option<String>,
pub tables: Option<Vec<Value>>,
pub key_value_pairs: Option<Vec<Value>>,
pub usage: OcrUsage,
}
#[derive(Clone, PartialEq)]
pub struct NativeOcrPayload(Value);
impl NativeOcrPayload {
pub fn new(value: Value) -> Self {
Self(value)
}
pub fn as_value(&self) -> &Value {
&self.0
}
pub fn into_value(self) -> Value {
self.0
}
}
/// ```compile_fail
/// fn assert_serialize<T: serde::Serialize>() {}
/// assert_serialize::<litellm_core::ocr::response::NativeOcrPayload>();
/// assert_serialize::<litellm_core::ocr::response::OcrOutcome>();
/// ```
#[derive(Clone, PartialEq)]
pub struct OcrOutcome {
pub normalized: NormalizedOcr,
pub native: NativeOcrPayload,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn normalized_serialization_cannot_include_native_payload() {
let outcome = OcrOutcome {
normalized: NormalizedOcr {
pages: vec![OcrPage {
index: 0,
markdown: "portable".to_string(),
}],
model: "ocr-model".to_string(),
document_annotation: None,
content: None,
tables: None,
key_value_pairs: None,
usage: OcrUsage::default(),
},
native: NativeOcrPayload::new(json!({"provider_secret_field": "native"})),
};
let public =
serde_json::to_value(&outcome.normalized).expect("normalized output serializes");
assert_eq!(public["pages"][0]["markdown"], "portable");
assert!(public.get("provider_secret_field").is_none());
assert_eq!(outcome.native.as_value()["provider_secret_field"], "native");
}
}

View file

@ -1,6 +1,34 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Field<T> {
Absent,
Null,
Value(T),
}
impl<T> Field<T> {
pub fn as_ref(&self) -> Field<&T> {
match self {
Self::Absent => Field::Absent,
Self::Null => Field::Null,
Self::Value(value) => Field::Value(value),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OcrDialectId {
Mistral,
AzureFoundryMistral,
AzureDocumentIntelligence,
VertexMistral,
VertexDeepSeek,
ReductoV3,
ReductoLegacy,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct OcrRequestData {
pub data: Value,

View file

@ -0,0 +1,26 @@
use std::time::Duration;
use crate::ocr::compiler::OcrDocumentPolicy;
use crate::ocr::plan::{CompletionPlan, PollPlan};
use crate::ocr::policy::{AZURE_DOCUMENT_INTELLIGENCE_PARAMETER_POLICY, OcrParameterPolicy};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AzureDocumentIntelligenceOcrCompiler;
impl AzureDocumentIntelligenceOcrCompiler {
pub fn parameter_policy(&self) -> &'static OcrParameterPolicy {
&AZURE_DOCUMENT_INTELLIGENCE_PARAMETER_POLICY
}
pub fn document_policy(&self) -> OcrDocumentPolicy {
OcrDocumentPolicy::Ready
}
pub fn completion_plan(&self, interval: Duration, timeout: Duration) -> CompletionPlan {
CompletionPlan::Poll(PollPlan {
operation_location_header: "operation-location",
interval,
timeout,
})
}
}

View file

@ -0,0 +1,17 @@
use crate::ocr::compiler::OcrDocumentPolicy;
use crate::providers::mistral::ocr::compiler::MistralOcrCodec;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AzureFoundryMistralOcrCompiler {
codec: MistralOcrCodec,
}
impl AzureFoundryMistralOcrCompiler {
pub fn codec(&self) -> &MistralOcrCodec {
&self.codec
}
pub fn document_policy(&self) -> OcrDocumentPolicy {
OcrDocumentPolicy::FetchRemoteUrlAndInline
}
}

View file

@ -1 +1,3 @@
pub mod document_intelligence;
pub mod foundry;
pub mod transformation;

View file

@ -0,0 +1,63 @@
use crate::ocr::compiler::OcrDocumentPolicy;
use crate::ocr::policy::{MISTRAL_OCR_PARAMETER_POLICY, OcrParameterPolicy};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MistralOcrCodec;
impl MistralOcrCodec {
pub fn parameter_policy(&self) -> &'static OcrParameterPolicy {
&MISTRAL_OCR_PARAMETER_POLICY
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MistralOcrCompiler {
codec: MistralOcrCodec,
}
impl MistralOcrCompiler {
pub fn codec(&self) -> &MistralOcrCodec {
&self.codec
}
pub fn document_policy(&self) -> OcrDocumentPolicy {
OcrDocumentPolicy::Ready
}
}
#[cfg(test)]
mod tests {
use crate::ocr::compiler::OcrDocumentPolicy;
use crate::ocr::policy::OcrCanonicalField;
use crate::providers::azure_ai::ocr::foundry::AzureFoundryMistralOcrCompiler;
use crate::providers::vertex_ai::ocr::mistral::VertexMistralOcrCompiler;
use super::*;
#[test]
fn mistral_compatible_dialects_share_the_codec_policy() {
let mistral = MistralOcrCompiler::default();
let foundry = AzureFoundryMistralOcrCompiler::default();
let vertex = VertexMistralOcrCompiler::default();
for field in OcrCanonicalField::ALL {
assert_eq!(
mistral.codec().parameter_policy().disposition(field),
foundry.codec().parameter_policy().disposition(field)
);
assert_eq!(
mistral.codec().parameter_policy().disposition(field),
vertex.codec().parameter_policy().disposition(field)
);
}
assert_eq!(mistral.document_policy(), OcrDocumentPolicy::Ready);
assert_eq!(
foundry.document_policy(),
OcrDocumentPolicy::FetchRemoteUrlAndInline
);
assert_eq!(
vertex.document_policy(),
OcrDocumentPolicy::FetchRemoteUrlAndInline
);
}
}

View file

@ -1 +1,2 @@
pub mod compiler;
pub mod transformation;

View file

@ -4,4 +4,5 @@ pub mod azure_ai;
pub mod bedrock;
pub mod mistral;
pub mod openai;
pub mod reducto;
pub mod vertex_ai;

View file

@ -0,0 +1 @@
pub mod ocr;

View file

@ -0,0 +1,24 @@
use crate::ocr::policy::{OcrParameterPolicy, REJECT_CANONICAL_OCR_PARAMETER_POLICY};
use super::response::ReductoOcrResponseNormalizer;
use super::upload::ReductoDocumentAdapter;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ReductoLegacyOcrCompiler {
document_adapter: ReductoDocumentAdapter,
response_normalizer: ReductoOcrResponseNormalizer,
}
impl ReductoLegacyOcrCompiler {
pub fn parameter_policy(&self) -> &'static OcrParameterPolicy {
&REJECT_CANONICAL_OCR_PARAMETER_POLICY
}
pub fn document_adapter(&self) -> &ReductoDocumentAdapter {
&self.document_adapter
}
pub fn response_normalizer(&self) -> &ReductoOcrResponseNormalizer {
&self.response_normalizer
}
}

View file

@ -0,0 +1,4 @@
pub mod legacy;
pub mod response;
pub mod upload;
pub mod v3;

View file

@ -0,0 +1,2 @@
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ReductoOcrResponseNormalizer;

View file

@ -0,0 +1,10 @@
use crate::ocr::compiler::OcrDocumentPolicy;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ReductoDocumentAdapter;
impl ReductoDocumentAdapter {
pub fn document_policy(&self) -> OcrDocumentPolicy {
OcrDocumentPolicy::UploadUnlessProviderReference
}
}

View file

@ -0,0 +1,24 @@
use crate::ocr::policy::{OcrParameterPolicy, REJECT_CANONICAL_OCR_PARAMETER_POLICY};
use super::response::ReductoOcrResponseNormalizer;
use super::upload::ReductoDocumentAdapter;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ReductoV3OcrCompiler {
document_adapter: ReductoDocumentAdapter,
response_normalizer: ReductoOcrResponseNormalizer,
}
impl ReductoV3OcrCompiler {
pub fn parameter_policy(&self) -> &'static OcrParameterPolicy {
&REJECT_CANONICAL_OCR_PARAMETER_POLICY
}
pub fn document_adapter(&self) -> &ReductoDocumentAdapter {
&self.document_adapter
}
pub fn response_normalizer(&self) -> &ReductoOcrResponseNormalizer {
&self.response_normalizer
}
}

View file

@ -0,0 +1,15 @@
use crate::ocr::compiler::OcrDocumentPolicy;
use crate::ocr::policy::{OcrParameterPolicy, REJECT_CANONICAL_OCR_PARAMETER_POLICY};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct VertexDeepSeekOcrCompiler;
impl VertexDeepSeekOcrCompiler {
pub fn parameter_policy(&self) -> &'static OcrParameterPolicy {
&REJECT_CANONICAL_OCR_PARAMETER_POLICY
}
pub fn document_policy(&self) -> OcrDocumentPolicy {
OcrDocumentPolicy::Ready
}
}

View file

@ -0,0 +1,17 @@
use crate::ocr::compiler::OcrDocumentPolicy;
use crate::providers::mistral::ocr::compiler::MistralOcrCodec;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct VertexMistralOcrCompiler {
codec: MistralOcrCodec,
}
impl VertexMistralOcrCompiler {
pub fn codec(&self) -> &MistralOcrCodec {
&self.codec
}
pub fn document_policy(&self) -> OcrDocumentPolicy {
OcrDocumentPolicy::FetchRemoteUrlAndInline
}
}

View file

@ -1 +1,3 @@
pub mod deepseek;
pub mod mistral;
pub mod transformation;