{"slug": "cut-claude-api-costs-80-by-splitting-vision-and-reasoning-tasks", "title": "Cut Claude API Costs 80% by Splitting Vision and Reasoning Tasks", "summary": "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.", "body_md": "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.\n\nThen I looked at the cost estimate for my sample: a 26-page image-only PDF. Running everything through `claude-opus-4-8`\n\nwould 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.\n\nThe fix was obvious once I thought about it—but I hadn't thought about it.\n\nVision OCR and concept extraction are fundamentally different tasks:\n\nOnce I separated these mentally, the routing became obvious.\n\nCurrent Claude API pricing (per 1M tokens):\n\n| Model | Input | Output |\n|---|---|---|\n`claude-opus-4-8` |\n$5.00 | $25.00 |\n`claude-sonnet-4-6` |\n$3.00 | $15.00 |\n`claude-haiku-4-5` |\n$1.00 | $5.00 |\n\nImage tokens are roughly `(width × height) / 750`\n\n. 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.\n\n26 pages × ~4,800 image tokens per page = **~125,000 image tokens** just for OCR.\n\n| Route | Est. cost | Notes |\n|---|---|---|\n| Opus for everything | ~$1.10 | wasteful |\n| Haiku OCR + Opus extraction | ~$0.20 | OCR is cheap on Haiku |\n| Haiku OCR + Sonnet extraction | ~$0.18 | sweet spot |\n| Haiku for both | ~$0.12 | lower extraction quality |\n\nThat's an **84% reduction** by using Haiku for the vision calls and Sonnet for the one extraction call.\n\nIn `.env`\n\n:\n\n```\nOCR_ENGINE=claude\nCLAUDE_VISION_MODEL=claude-haiku-4-5\nCLAUDE_MODEL=claude-sonnet-4-6\n```\n\nThe Claude OCR engine sends each page image to the vision model:\n\n``` python\n# claude_engine.py\nclass ClaudeOCREngine:\n    name = \"claude\"\n\n    def ocr_page(self, png_bytes: bytes) -> str:\n        settings = get_settings()\n        client = anthropic.Anthropic(api_key=settings.anthropic_api_key)\n\n        response = client.messages.create(\n            model=settings.claude_vision_model,  # haiku-4-5\n            max_tokens=4096,\n            messages=[{\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"image\",\n                        \"source\": {\n                            \"type\": \"base64\",\n                            \"media_type\": \"image/png\",\n                            \"data\": base64.b64encode(png_bytes).decode(),\n                        },\n                    },\n                    {\n                        \"type\": \"text\",\n                        \"text\": \"Extract all text from this page as clean markdown. \"\n                                \"Ignore navigation bars, footers, share buttons, \"\n                                \"and other page chrome.\"\n                    }\n                ],\n            }]\n        )\n        return response.content[0].text\n```\n\nThe extraction call goes to Sonnet with a much larger `max_tokens`\n\nbudget:\n\n``` php\n# llm.py (extraction)\nasync def _extract_async(self, doc: ParsedDocument) -> ExtractionResult:\n    result = await self.provider.complete(\n        [Message(role=\"system\", content=SYSTEM),\n         Message(role=\"user\", content=user_prompt)],\n        temperature=0.1,\n        max_tokens=24000,  # extraction needs headroom; Sonnet handles it\n    )\n```\n\nThe `LLMProvider`\n\nabstraction routes through `settings.claude_model`\n\n(Sonnet) for text completions and `settings.claude_vision_model`\n\n(Haiku) for image passes. Two env vars, completely separate call paths.\n\nI hit a debugging session that illustrates why you need to actually measure this instead of guessing.\n\nMy first extraction run produced this error:\n\n```\njson.decoder.JSONDecodeError: Unterminated string at char 373\n```\n\nSame position on every run, even after bumping `max_tokens`\n\nfrom 8192 to 16000. Suspicious. I captured the raw output:\n\n```\n$py -c \"\nresult = provider.complete(...)\nopen('extract_raw.txt','w').write(result)\n\"\n```\n\nThe saved file was clean, well-formed JSON. So `_coerce_json`\n\nwas mangling it. Traced through:\n\n``` php\n# THE BUG\ndef _coerce_json(text: str) -> dict:\n    if text.startswith(\"```\"):\n        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 ``\n\n` fences inside the JSON string values. My naive `\n\n`split(\"`\n\n``\", 2)`\n\nshredded the output at the first interior fence, producing an empty string.\n\nFix: strip the outer fence line-by-line instead of splitting:\n\n``` php\ndef _coerce_json(text: str) -> dict:\n    lines = text.strip().splitlines()\n    # Strip leading ``` json fence\n    if lines and lines[0].startswith(\"```\"):\n        lines = lines[1:]\n    # Strip trailing ``` fence\n    if lines and lines[-1].strip() == \"```\":\n        lines = lines[:-1]\n    raw = \"\\n\".join(lines)\n    start = raw.find(\"{\")\n    end = raw.rfind(\"}\")\n    return json.loads(raw[start:end + 1])\n```\n\nAlso switched the Claude provider to streaming to avoid token truncation on large extractions—the original non-streaming call was silently hitting the `max_tokens`\n\nceiling without error.\n\nAfter the fix, one Haiku OCR pass + one Sonnet extraction call over the 26-page PDF produced **8 atomic concept drafts**:\n\n`concurrency/concurrency-definition (diff 2) prereqs=[] Qs=4`\n\nconcurrency/parallelism-definition (diff 2) prereqs=['concurrency/concurrency-definition'] Qs=4\n\nconcurrency/async-io-event-loop (diff 3) prereqs=['concurrency/concurrency-definition'] Qs=4\n\nconcurrency/race-conditions-shared-state (diff 3) prereqs=['concurrency/parallelism-definition']\n\nconcurrency/amdahls-law (diff 4)\n\nconcurrency/ruby-gvl (diff 4) prereqs=[4 ancestors]\n\nconcurrency/ruby-ractors (diff 5)\n\nconcurrency/concurrency-decision-framework (diff 3)\n\nEach with a definition, examples, interview questions, prerequisite links, and provenance tied back to the source URL. Total cost: **$0.18**.\n\nThis isn't just about OCR. The same logic applies anywhere you're mixing vision and reasoning:\n\nThe 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.\n\nIf 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.\n\n*Drafted by Claude Sonnet from my own Claude Code session transcript, then reviewed and edited before publishing.*", "url": "https://wpnews.pro/news/cut-claude-api-costs-80-by-splitting-vision-and-reasoning-tasks", "canonical_source": "https://dev.to/yogeshchavan2008/cut-claude-api-costs-80-by-splitting-vision-and-reasoning-tasks-eig", "published_at": "2026-07-09 12:11:04+00:00", "updated_at": "2026-07-09 12:35:50.540339+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-tools", "developer-tools"], "entities": ["Anthropic", "Claude Opus", "Claude Sonnet", "Claude Haiku"], "alternates": {"html": "https://wpnews.pro/news/cut-claude-api-costs-80-by-splitting-vision-and-reasoning-tasks", "markdown": "https://wpnews.pro/news/cut-claude-api-costs-80-by-splitting-vision-and-reasoning-tasks.md", "text": "https://wpnews.pro/news/cut-claude-api-costs-80-by-splitting-vision-and-reasoning-tasks.txt", "jsonld": "https://wpnews.pro/news/cut-claude-api-costs-80-by-splitting-vision-and-reasoning-tasks.jsonld"}}