Talk to models in the real world
Multimodal in → text
Call a vision-capable model on an image input
- Prompt engineering fundamentals (browse)
- Structured outputs (browse)
- Multimodal basics (browse)
- OpenAI and Anthropic APIs (browse)
- Building a ChatGPT-like product: streaming, tools, and memory (example)
- Fine-tune vs prompt vs RAG: a decision framework (example)
Learning objectives
- Call a vision-capable model on an image input
- Extract structured fields from a receipt image
- Note failure modes (blur, currency, handwriting)
Multimodal is another I/O path
Vision-capable models accept images alongside text in the same chat API you already use. The product pattern is unchanged: construct messages, call the model, validate structured output. What changes is the content block format — images arrive as base64 blobs, URLs, or file IDs depending on provider — and the failure surface expands to blur, glare, cropping, and ambiguous handwriting.
Treat multimodal features as a pipeline branch, not a separate product category:
Image upload → preprocess (resize, rotate) → vision model → JSON validation → ledger DBSame engineering habits apply: prompt packs, versioning, golden fixtures — but fixtures now include PNG/JPG files and expected JSON sidecars.
Callout — pixels are untrusted input: Users can upload adversarial images, screenshots of instructions, or unrelated photos. Scope the system prompt narrowly and validate extracted fields before financial actions.
Message format for images
OpenAI-style APIs use content arrays:
messages = [{
"role": "user",
"content": [
{"type": "text", "text": "Extract expense fields from this receipt."},
{
"type": "image_url",
"image_url": {"url": "data:image/jpeg;base64,..."}
}
]
}]Anthropic and Google use analogous structures with different keys. Abstract behind a helper:
def user_message(text: str, image_path: pathlib.Path) -> dict:
b64 = base64.standard_b64encode(image_path.read_bytes()).decode()
...Keep image bytes out of git for real user data; commit only synthetic or redacted sample receipts in fixtures/images/.
Receipt → expense JSON task
Receipt extraction is an ideal teaching task because fields are constrained and errors are measurable:
| Field | Validation |
|---|---|
merchant |
Non-empty string |
date |
ISO date or null |
total |
Decimal + currency code |
line_items |
Optional list |
tax |
Optional decimal |
Reuse your Pydantic extractor from lesson 4.2. The vision model replaces OCR + LLM text parsing in one step — but you still validate totals and currency in code.
System prompt essentials:
- List exact JSON schema.
- Instruct: "If a field is unreadable, use null; do not guess."
- Specify default currency only when locale is known.
Preprocessing that actually helps
Before sending images to the API:
Resize large photos to max dimension ~2048px — sufficient detail, lower upload latency.
Auto-rotate using EXIF orientation so text is upright.
Crop optionally when UI provides a scanner overlay (mobile apps); skip aggressive crop in batch pipelines.
Format: JPEG for photos, PNG for screenshots. Avoid sending 10 MB RAW files.
Log preprocessing parameters per run — they are part of reproducibility.
Failure modes to document
Build a failure gallery in fixtures/hard/:
| Failure | Symptom | Mitigation |
|---|---|---|
| Motion blur | Wrong digits in total | Ask retake; lower confidence flag |
| Multi-currency | Mixed $ and € |
Prompt: return currency per field |
| Handwritten tips | Line items hallucinated | Prompt: omit unreadable lines |
| Thermal fade | Missing date | null + human review queue |
| Non-receipt image | Invented merchant | Add is_receipt: bool field; reject false |
Run your extractor on each hard image and record outcomes in FAILURES.md. Interviewers and future teammates learn from documented failure more than from demo happy paths.
Callout — eval with real scans: Synthetic crisp receipts overstate accuracy. Include at least two messy phone photos in your golden set.
Cost and latency notes
Vision calls cost more tokens than text-only — image tiles convert to token charges on many providers. Batch offline extraction for expense reports; do not block UI on synchronous multi-image uploads without progress indicators.
Cache results keyed by image hash + prompt version so re-uploads do not re-spend.
Batch vs interactive extraction
Two product shapes drive different architectures:
Interactive (user uploads one receipt): Optimize for latency — single image, streaming optional, show spinner with preprocessing step visible. Cache by image hash for accidental re-submission.
Batch (nightly expense folder): Optimize for throughput — queue jobs, rate-limit API calls, write results to CSV/DB. Failures go to dead-letter queue with image path for human review.
For batch, add a confidence or needs_review boolean in schema when any field is null or totals do not reconcile (subtotal + tax ≈ total within epsilon). Downstream workflows route low-confidence rows to accountants instead of auto-posting.
Vision model choice matters: smaller vision models cost less but fail on faded thermal paper; document which model your eval used so upgrades are intentional.
Privacy and retention
Receipts contain PII and payment hints. For the course project, use fake or redacted samples. In production:
- Encrypt at rest.
- Set retention TTL.
- Avoid logging full base64 in plaintext telemetry — log hash + extracted fields only.
Engineering problem (staff framing)
Images/audio expand threat + cost surface. Tokenize media, bound size, eval modality slices.
Diagram — Multimodal input path
flowchart LR
Img[Image/Audio] --> Enc[Provider encode]
Enc --> Ctx[Joint context]
Txt[Text] --> Ctx --> Model --> Out[Text out]
Precise definitions & mental model
Vision tokens, resolution tradeoffs, safety on image inputs.
Tradeoffs — when to use what
High-res detail vs token cost/latency.
Failure modes (interview + on-call)
Huge images; prompt injection via screenshots; no modality-specific evals.
Production & OSS practices
Max bytes, virus scan, strip metadata, separate rate limits.
Micro-project: Receipt → expense JSON
In m4/receipt_extractor/:
- Implement a script that accepts an image path, calls a vision-capable chat model, returns validated expense JSON.
- Include ≥5 fixture images with gold JSON sidecars.
- Document ≥3 failure modes you observed with screenshots or paths in
FAILURES.md. - Wire prompt version logging consistent with lesson 4.3.
Optional stretch: CLI flag for --dry-run that prints raw model JSON before validation.
Checklist
- Images sent via documented API format
- Output validated with Pydantic (or equivalent)
- Hard fixtures included beyond pristine samples
- No real user receipts committed to git
ShipAI delivery model is: