{"slug": "running-glm-ocr-deepseek-ocr-2-and-dots", "title": "Running GLM-OCR, DeepSeek-OCR-2, and Dots.", "summary": "DeepSeek's OCR-2 and Dots.mocr, along with Zhipu's GLM-OCR, can be unified behind an OpenAI-compatible /v1/chat/completions interface using a FastAPI normalizer, enabling zero refactoring for existing callers. The wrapper routes requests by model field to each vendor's endpoint and normalizes responses to OpenAI's chat completion schema, preserving structured JSON with bounding boxes and confidence scores for RAG and layout-aware summarization. Dots.mocr is recommended for local deployment on a single 24 GB VRAM GPU via vLLM.", "body_md": "# Running GLM-OCR, DeepSeek-OCR-2, and Dots.\n\n[DeepSeek](/en/tags/deepseek/)'s OCR-2, and the newer Dots.mocr — each handles different document types better than the others, and wrapping them in an OpenAI-compatible\n\n`/v1/chat/completions`\n\ninterface means zero refactoring for existing callers.## Why bother with a unified wrapper\n\nMost OCR APIs return plain text or markdown. These three return structured JSON with bounding boxes, confidence scores, and reading order — critical when you're feeding output into an [RAG](/en/tags/rag/) chunker or a layout-aware summarizer. But each vendor ships its own SDK, auth scheme, and response schema. A thin FastAPI layer normalizes all of that.\n\n## Architecture overview\n\n```\nclient → /v1/chat/completions (OpenAI schema)\n         │\n         ├── router picks model by `model` field\n         │       ├── glm-ocr → Zhipu HTTP endpoint\n         │       ├── deepseek-ocr-2 → DeepSeek HTTP endpoint\n         │       └── dots-mocr → local vLLM / TGI instance\n         │\n         └── response normalizer → OpenAI `choices[0].message.content` (JSON string)\n```\n\n## 1. Spin up the normalizer service\n\n``` python\n# main.py\nfrom fastapi import FastAPI, HTTPException\nfrom pydantic import BaseModel\nfrom typing import Literal\nimport httpx, os, json\n\napp = FastAPI()\n\nclass ChatRequest(BaseModel):\n    model: Literal[\"glm-ocr\", \"deepseek-ocr-2\", \"dots-mocr\"]\n    messages: list[dict]\n    max_tokens: int = 4096\n    temperature: float = 0.0\n\nENDPOINTS = {\n    \"glm-ocr\": os.getenv(\"GLM_OCR_URL\", \"https://open.bigmodel.cn/api/paas/v4/chat/completions\"),\n    \"deepseek-ocr-2\": os.getenv(\"DS_OCR_URL\", \"https://api.deepseek.com/v1/chat/completions\"),\n    \"dots-mocr\": os.getenv(\"DOTS_URL\", \"http://localhost:8001/v1/chat/completions\"),\n}\n\nHEADERS = {\n    \"glm-ocr\": {\"Authorization\": f\"Bearer {os.getenv('GLM_API_KEY')}\"},\n    \"deepseek-ocr-2\": {\"Authorization\": f\"Bearer {os.getenv('DEEPSEEK_API_KEY')}\"},\n    \"dots-mocr\": {\"Authorization\": f\"Bearer {os.getenv('DOTS_API_KEY', 'local')}\"},\n}\n\nasync def call_upstream(model: str, payload: dict) -> dict:\n    async with httpx.AsyncClient(timeout=120) as client:\n        r = await client.post(ENDPOINTS[model], json=payload, headers=HEADERS[model])\n        r.raise_for_status()\n        return r.json()\n\ndef normalize(model: str, upstream: dict) -> dict:\n    \"\"\"Map each vendor's response to OpenAI shape with JSON content.\"\"\"\n    if model == \"glm-ocr\":\n        raw = upstream[\"choices\"][0][\"message\"][\"content\"]\n    elif model == \"deepseek-ocr-2\":\n        raw = upstream[\"choices\"][0][\"message\"][\"content\"]\n    else:  # dots-mocr already returns JSON string in content\n        raw = upstream[\"choices\"][0][\"message\"][\"content\"]\n    # Ensure it's valid JSON string\n    json.loads(raw)  # raises if malformed\n    return {\n        \"id\": upstream.get(\"id\", \"ocr-\" + model),\n        \"object\": \"chat.completion\",\n        \"choices\": [{\n            \"index\": 0,\n            \"message\": {\"role\": \"assistant\", \"content\": raw},\n            \"finish_reason\": \"stop\"\n        }],\n        \"usage\": upstream.get(\"usage\", {})\n    }\n\n@app.post(\"/v1/chat/completions\")\nasync def chat(req: ChatRequest):\n    if req.model not in ENDPOINTS:\n        raise HTTPException(400, f\"Unknown model {req.model}\")\n    # Extract image_url from last user message\n    user_msg = next((m for m in reversed(req.messages) if m[\"role\"] == \"user\"), None)\n    if not user_msg or \"image_url\" not in user_msg.get(\"content\", [{}])[0]:\n        raise HTTPException(400, \"Expected image_url in last user message\")\n    payload = {\n        \"model\": req.model,\n        \"messages\": req.messages,\n        \"max_tokens\": req.max_tokens,\n        \"temperature\": req.temperature,\n    }\n    upstream = await call_upstream(req.model, payload)\n    return normalize(req.model, upstream)\n```\n\n## 2. Deploy Dots.mocr locally (optional but recommended)\n\nDots.mocr runs well on a single 24 GB VRAM GPU via vLLM:\n\n```\ndocker run --gpus all -p 8001:8000 \\\n  -v $PWD/models:/models \\\n  vllm/vllm-openai:latest \\\n  --model /models/dots-mocr \\\n  --served-model-name dots-mocr \\\n  --max-model-len 8192 \\\n  --limit-mm-per-prompt image=4\n```\n\nPull the model first:\n\n```\nhuggingface-cli download DOTS-OCR/DOTS-OCR-2.0 --local-dir ./models/dots-mocr\n```\n\n## 3. Client usage stays identical\n\n``` python\nfrom openai import OpenAI\n\nclient = OpenAI(base_url=\"http://localhost:8000/v1\", api_key=\"dummy\")\n\n# GLM-OCR for Chinese dense tables\nresp = client.chat.completions.create(\n    model=\"glm-ocr\",\n    messages=[{\n        \"role\": \"user\",\n        \"content\": [\n            {\"type\": \"image_url\", \"image_url\": {\"url\": \"https://example.com/invoice.jpg\"}}\n        ]\n    }],\n    max_tokens=4096\n)\nprint(resp.choices[0].message.content)  # JSON string with cells, bbox, confidence\n\n# DeepSeek-OCR-2 for handwritten forms\nresp = client.chat.completions.create(\n    model=\"deepseek-ocr-2\",\n    messages=[{\n        \"role\": \"user\",\n        \"content\": [\n            {\"type\": \"image_url\", \"image_url\": {\"url\": \"https://example.com/handwritten.png\"}}\n        ]\n    }]\n)\n\n# Dots.mocr for multi-page PDFs (local, no egress)\nresp = client.chat.completions.create(\n    model=\"dots-mocr\",\n    messages=[{\n        \"role\": \"user\",\n        \"content\": [\n            {\"type\": \"image_url\", \"image_url\": {\"url\": \"file:///data/contract.pdf\"}}\n        ]\n    }]\n)\n```\n\n## 4. Response schema you can count on\n\nAll three normalize to this JSON structure inside `content`\n\n:\n\n``` json\n\n{\n\n\"pages\": [\n\n{\n\n\"page_index\": 0,\n\n\"width\": 2480,\n\n\"height\": 3508,\n\n\"blocks\": [\n\n{\n\n\"type\": \"table\",\n\n\"bbox\": [120, 340, 2360, 1200],\n\n\"confidence\": 0.96,\n\n\"cells\": [\n\n{\"row\": 0, \"col\": 0, \"text\": \"Item\", \"bbox\": [130, 350, 400, 410]},\n\n{\"row\": 0, \"col\": 1, \"text\": \"Qty\",\n\n[Next The hype cycle promised mass adoption by 2024 — reality check →](/en/news/6968/)", "url": "https://wpnews.pro/news/running-glm-ocr-deepseek-ocr-2-and-dots", "canonical_source": "https://promptcube3.com/en/news/6972/", "published_at": "2026-08-19 21:15:42+00:00", "updated_at": "2026-08-19 21:44:23.864390+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "machine-learning"], "entities": ["DeepSeek", "Dots.mocr", "Zhipu", "GLM-OCR", "FastAPI", "vLLM", "OpenAI"], "alternates": {"html": "https://wpnews.pro/news/running-glm-ocr-deepseek-ocr-2-and-dots", "markdown": "https://wpnews.pro/news/running-glm-ocr-deepseek-ocr-2-and-dots.md", "text": "https://wpnews.pro/news/running-glm-ocr-deepseek-ocr-2-and-dots.txt", "jsonld": "https://wpnews.pro/news/running-glm-ocr-deepseek-ocr-2-and-dots.jsonld"}}