1use std::sync::Arc;
10
11use serde_json::{Value, json};
12
13use crate::attachment::{Attachment, AttachmentType};
14use crate::chat::{failure_tokens, resolve_model};
15use crate::config::Config;
16use crate::cost::{Cost, Tier};
17use crate::error::{Error, Result};
18use crate::message::{Operation, UsageEntry, UsageStatus};
19use crate::providers::Provider;
20use crate::tokens::Tokens;
21use crate::transport::Connection;
22
23#[derive(Debug, Clone, PartialEq)]
25pub struct Page {
26 pub index: i64,
28 pub markdown: Option<String>,
29 pub images: Option<Value>,
31 pub tables: Option<Value>,
33 pub raw: Value,
35}
36
37#[derive(Debug, Clone)]
39pub struct Ocr {
40 pub pages: Vec<Page>,
41 pub model: String,
43 pub usage: Option<Value>,
45 pub raw: Value,
47 pub usage_entries: Vec<UsageEntry>,
49}
50
51impl Ocr {
52 pub fn new(pages: &[Value], model: impl Into<String>, usage: Option<Value>, raw: Value) -> Ocr {
55 let pages = pages
56 .iter()
57 .enumerate()
58 .map(|(position, page)| Page {
59 index: page
60 .get("index")
61 .and_then(Value::as_i64)
62 .unwrap_or(position as i64),
63 markdown: page
64 .get("markdown")
65 .and_then(Value::as_str)
66 .map(str::to_string),
67 images: page.get("images").filter(|v| !v.is_null()).cloned(),
68 tables: page.get("tables").filter(|v| !v.is_null()).cloned(),
69 raw: page.clone(),
70 })
71 .collect();
72 Ocr {
73 pages,
74 model: model.into(),
75 usage,
76 raw,
77 usage_entries: Vec::new(),
78 }
79 }
80
81 pub fn markdown(&self) -> String {
83 self.pages
84 .iter()
85 .filter_map(|p| p.markdown.as_deref())
86 .collect::<Vec<_>>()
87 .join("\n\n")
88 }
89
90 pub fn tokens(&self) -> Tokens {
92 Tokens::aggregate(self.usage_entries.iter().map(|e| &e.tokens))
93 }
94
95 pub fn cost(&self) -> Cost {
97 let complete = self.usage_entries.iter().all(UsageEntry::cost_available);
98 Cost::aggregate(self.usage_entries.iter().map(|e| &e.cost), complete)
99 }
100}
101
102#[derive(Default)]
104pub struct OcrOptions<'a> {
105 pub model: Option<&'a str>,
107 pub provider: Option<&'a str>,
108 pub assume_model_exists: bool,
109 pub pages: Option<Vec<i64>>,
111 pub provider_options: Value,
113 pub config: Option<Arc<Config>>,
115 pub metadata: Option<Value>,
117 pub owner: Option<crate::accounting::UsageOwner>,
120}
121
122pub async fn ocr(file: impl Into<Attachment>, options: OcrOptions<'_>) -> Result<Ocr> {
125 let config = options.config.clone().unwrap_or_else(crate::config);
126 let model_id = options
127 .model
128 .unwrap_or(&config.default_ocr_model)
129 .to_string();
130 let (model, provider) =
131 resolve_model(&model_id, options.provider, options.assume_model_exists)?;
132 let owner = options.owner.clone();
133 let mut event = crate::instrumentation::Event::start(&config, "ocr.rust_llm", || {
134 crate::instrumentation::payload([
135 ("provider", provider.slug().into()),
136 ("provider_class", provider.display().into()),
137 ("model", model.id.clone().into()),
138 ("pages", json!(options.pages)),
139 ("provider_options", options.provider_options.clone()),
140 (
141 "metadata",
142 crate::instrumentation::metadata(&options.metadata),
143 ),
144 ])
145 });
146 let result = event
147 .instrument(crate::accounting::owned_by(
148 owner,
149 ocr_inner(file.into(), options, config.clone(), model, provider),
150 ))
151 .await;
152 if let Ok(r) = &result {
153 crate::accounting::report(&config, &r.usage_entries).await;
154 event.set(
155 "result",
156 || json!({ "model": r.model, "pages": r.pages.len() }),
157 );
158 event.set("response_model", || r.model.clone().into());
159 }
160 event.finish(result.as_ref().err());
161 result
162}
163
164async fn ocr_inner(
165 file: Attachment,
166 options: OcrOptions<'_>,
167 config: Arc<Config>,
168 model: crate::model::Model,
169 provider: Provider,
170) -> Result<Ocr> {
171 let OcrOptions {
172 pages,
173 provider_options,
174 ..
175 } = options;
176 provider.ensure_configured(&config)?;
177 if provider != Provider::Mistral {
178 return Err(Error::Api(
179 format!("{} doesn't support OCR", provider.display()),
180 None,
181 ));
182 }
183 let connection = Connection::new(provider, config.clone())?;
184 let mut attachment = file;
185 if !attachment.is_url() {
186 attachment.load(connection.client()).await?;
187 }
188 let payload = render_payload(&attachment, &model.id, pages.as_deref(), &provider_options)?;
189
190 let mut retried: Vec<Tokens> = Vec::new();
192 let mut on_attempt = |previous: Option<&Error>| {
193 if let Some(e) = previous {
194 retried.push(failure_tokens(e, None));
195 }
196 };
197 let raw = connection
198 .post("ocr", &payload, &[], &mut on_attempt)
199 .await?;
200 let data = raw.body;
201 let pages: Vec<Value> = data
202 .get("pages")
203 .and_then(Value::as_array)
204 .cloned()
205 .unwrap_or_default();
206 let response_model = data
207 .get("model")
208 .and_then(Value::as_str)
209 .unwrap_or(&model.id)
210 .to_string();
211 let usage = data.get("usage_info").filter(|v| !v.is_null()).cloned();
212 let mut result = Ocr::new(&pages, response_model, usage, data);
213 let entry = |status, tokens: Tokens| UsageEntry {
214 id: UsageEntry::next_id(),
215 owner: crate::accounting::usage_owner(),
216 operation: Operation::Ocr,
217 provider: provider.slug().into(),
218 model: model.id.clone(),
219 status,
220 cost: Cost::new(&tokens, Some(&model), Tier::Standard),
221 tokens,
222 };
223 let mut entries: Vec<UsageEntry> = retried
224 .into_iter()
225 .map(|t| entry(UsageStatus::Failed, t))
226 .collect();
227 entries.push(entry(UsageStatus::Succeeded, Tokens::default()));
229 result.usage_entries = entries;
230 Ok(result)
231}
232
233fn render_payload(
236 attachment: &Attachment,
237 model: &str,
238 pages: Option<&[i64]>,
239 provider_options: &Value,
240) -> Result<Value> {
241 let reference = attachment.url_or_data_uri()?;
242 let document = if attachment.kind() == AttachmentType::Image {
243 json!({ "type": "image_url", "image_url": reference })
244 } else {
245 json!({ "type": "document_url", "document_url": reference })
246 };
247 let mut payload = json!({ "model": model, "document": document });
248 if let Some(pages) = pages {
249 payload["pages"] = json!(pages);
250 }
251 if let (Some(p), Some(options)) = (payload.as_object_mut(), provider_options.as_object()) {
252 p.extend(options.clone());
253 }
254 Ok(payload)
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
263 fn pages_without_an_index_take_their_position_and_keep_raw() {
264 let raw = json!({ "markdown": "# Ruby", "images": [], "tables": [] });
265 let ocr = Ocr::new(
266 std::slice::from_ref(&raw),
267 "mistral-ocr-latest",
268 None,
269 Value::Null,
270 );
271 assert_eq!(ocr.pages[0].index, 0);
272 assert_eq!(ocr.pages[0].markdown.as_deref(), Some("# Ruby"));
273 assert_eq!(ocr.pages[0].images, Some(json!([])));
274 assert_eq!(ocr.pages[0].raw, raw);
275 assert_eq!(ocr.markdown(), "# Ruby");
276 }
277}