Cut Claude API Costs 80% by Splitting Vision and Reasoning Tasks A developer building a personal knowledge system reduced Claude API costs by up to 84% by routing vision OCR tasks to the cheaper Haiku model and concept extraction to Sonnet. The technique separates image processing from reasoning, cutting a $1.10 Opus-only cost to $0.18 for a 26-page PDF. I was building an ingestion pipeline for a personal knowledge system—drop in a PDF, get structured OKF-format concept articles out the other side. Anthropic's vision models are genuinely good at reading image-only PDFs scanned docs, Medium exports printed to PDF , so I wired them into my OCR layer. Then I looked at the cost estimate for my sample: a 26-page image-only PDF. Running everything through claude-opus-4-8 would cost $1.00–$1.30 for a single document. Scale that to a few dozen docs and suddenly my personal knowledge base has a real operational cost. The fix was obvious once I thought about it—but I hadn't thought about it. Vision OCR and concept extraction are fundamentally different tasks: Once I separated these mentally, the routing became obvious. Current Claude API pricing per 1M tokens : | Model | Input | Output | |---|---|---| claude-opus-4-8 | $5.00 | $25.00 | claude-sonnet-4-6 | $3.00 | $15.00 | claude-haiku-4-5 | $1.00 | $5.00 | Image tokens are roughly width × height / 750 . My PDF rendered at 200 DPI produces Letter-size pages at ~1700×2200px—about 3.7MP—which hits near Opus's 4,800-token ceiling per page. 26 pages × ~4,800 image tokens per page = ~125,000 image tokens just for OCR. | Route | Est. cost | Notes | |---|---|---| | Opus for everything | ~$1.10 | wasteful | | Haiku OCR + Opus extraction | ~$0.20 | OCR is cheap on Haiku | | Haiku OCR + Sonnet extraction | ~$0.18 | sweet spot | | Haiku for both | ~$0.12 | lower extraction quality | That's an 84% reduction by using Haiku for the vision calls and Sonnet for the one extraction call. In .env : OCR ENGINE=claude CLAUDE VISION MODEL=claude-haiku-4-5 CLAUDE MODEL=claude-sonnet-4-6 The Claude OCR engine sends each page image to the vision model: python claude engine.py class ClaudeOCREngine: name = "claude" def ocr page self, png bytes: bytes - str: settings = get settings client = anthropic.Anthropic api key=settings.anthropic api key response = client.messages.create model=settings.claude vision model, haiku-4-5 max tokens=4096, messages= { "role": "user", "content": { "type": "image", "source": { "type": "base64", "media type": "image/png", "data": base64.b64encode png bytes .decode , }, }, { "type": "text", "text": "Extract all text from this page as clean markdown. " "Ignore navigation bars, footers, share buttons, " "and other page chrome." } , } return response.content 0 .text The extraction call goes to Sonnet with a much larger max tokens budget: php llm.py extraction async def extract async self, doc: ParsedDocument - ExtractionResult: result = await self.provider.complete Message role="system", content=SYSTEM , Message role="user", content=user prompt , temperature=0.1, max tokens=24000, extraction needs headroom; Sonnet handles it The LLMProvider abstraction routes through settings.claude model Sonnet for text completions and settings.claude vision model Haiku for image passes. Two env vars, completely separate call paths. I hit a debugging session that illustrates why you need to actually measure this instead of guessing. My first extraction run produced this error: json.decoder.JSONDecodeError: Unterminated string at char 373 Same position on every run, even after bumping max tokens from 8192 to 16000. Suspicious. I captured the raw output: $py -c " result = provider.complete ... open 'extract raw.txt','w' .write result " The saved file was clean, well-formed JSON. So coerce json was mangling it. Traced through: php THE BUG def coerce json text: str - dict: if text.startswith " " : text = text.split "The extraction document contained code samples . Sonnet wrapped its JSON in a json fence—standard behavior—but the article body also had code blocks with fences inside the JSON string values. My naive split " ", 2 shredded the output at the first interior fence, producing an empty string. Fix: strip the outer fence line-by-line instead of splitting: php def coerce json text: str - dict: lines = text.strip .splitlines Strip leading json fence if lines and lines 0 .startswith " " : lines = lines 1: Strip trailing fence if lines and lines -1 .strip == " ": lines = lines :-1 raw = "\n".join lines start = raw.find "{" end = raw.rfind "}" return json.loads raw start:end + 1 Also switched the Claude provider to streaming to avoid token truncation on large extractions—the original non-streaming call was silently hitting the max tokens ceiling without error. After the fix, one Haiku OCR pass + one Sonnet extraction call over the 26-page PDF produced 8 atomic concept drafts : concurrency/concurrency-definition diff 2 prereqs= Qs=4 concurrency/parallelism-definition diff 2 prereqs= 'concurrency/concurrency-definition' Qs=4 concurrency/async-io-event-loop diff 3 prereqs= 'concurrency/concurrency-definition' Qs=4 concurrency/race-conditions-shared-state diff 3 prereqs= 'concurrency/parallelism-definition' concurrency/amdahls-law diff 4 concurrency/ruby-gvl diff 4 prereqs= 4 ancestors concurrency/ruby-ractors diff 5 concurrency/concurrency-decision-framework diff 3 Each with a definition, examples, interview questions, prerequisite links, and provenance tied back to the source URL. Total cost: $0.18 . This isn't just about OCR. The same logic applies anywhere you're mixing vision and reasoning: The mistake is reaching for the strongest model by default and never questioning it. OCR especially—it's an unusually clear case where you're paying Opus prices for a task that's mostly pixel-to-character mapping. If you're building on the Claude API and haven't audited which model you're using for which step, that's probably the first place to look. The gap between Haiku and Opus is 5× on input tokens. Over any real volume, that compounds fast. Drafted by Claude Sonnet from my own Claude Code session transcript, then reviewed and edited before publishing.