use crate::config::ConversionConfig;
use crate::output::PageResult;
use crate::prompts::{maintain_format_context, DEFAULT_SYSTEM_PROMPT};
use edgequake_llm::{ChatMessage, CompletionOptions, ImageData, LLMProvider};
use std::sync::Arc;
use std::time::Instant;
use tokio::time::{sleep, Duration};
use tracing::{debug, warn};
pub async fn process_page(
provider: &Arc<dyn LLMProvider>,
page_num: usize,
image_data: ImageData,
prior_page: Option<&str>,
config: &ConversionConfig,
) -> PageResult {
let start = Instant::now();
let system_prompt = config
.system_prompt
.as_deref()
.unwrap_or(DEFAULT_SYSTEM_PROMPT);
let mut messages = vec![ChatMessage::system(system_prompt)];
if config.maintain_format {
if let Some(prior) = prior_page {
if !prior.is_empty() {
messages.push(ChatMessage::system(maintain_format_context(prior)));
}
}
}
messages.push(ChatMessage::user_with_images("", vec![image_data]));
let options = build_options(config);
let mut last_err: Option<String> = None;
for attempt in 0..=config.max_retries {
if attempt > 0 {
let backoff = config.retry_backoff_ms * 2u64.pow(attempt - 1);
warn!(
"Page {}: retry {}/{} after {}ms",
page_num, attempt, config.max_retries, backoff
);
sleep(Duration::from_millis(backoff)).await;
}
match provider.chat(&messages, Some(&options)).await {
Ok(response) => {
let duration = start.elapsed();
debug!(
"Page {}: {} input tokens, {} output tokens, {:?}",
page_num, response.prompt_tokens, response.completion_tokens, duration
);
return PageResult {
page_num,
markdown: response.content,
input_tokens: response.prompt_tokens,
output_tokens: response.completion_tokens,
duration_ms: duration.as_millis() as u64,
retries: attempt as u8,
error: None,
};
}
Err(e) => {
let err_msg = format!("{}", e);
warn!(
"Page {}: attempt {} failed — {}",
page_num,
attempt + 1,
err_msg
);
last_err = Some(err_msg);
}
}
}
let duration = start.elapsed();
let err_msg = last_err.unwrap_or_else(|| "Unknown error".to_string());
PageResult {
page_num,
markdown: String::new(),
input_tokens: 0,
output_tokens: 0,
duration_ms: duration.as_millis() as u64,
retries: config.max_retries as u8,
error: Some(crate::error::PageError::LlmFailed {
page: page_num,
retries: config.max_retries as u8,
detail: err_msg,
}),
}
}
fn build_options(config: &ConversionConfig) -> CompletionOptions {
CompletionOptions {
temperature: Some(config.temperature),
max_tokens: Some(config.max_tokens),
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_options_defaults() {
let config = ConversionConfig::default();
let opts = build_options(&config);
assert_eq!(opts.temperature, Some(0.1));
assert_eq!(opts.max_tokens, Some(4096));
}
}