cd /news/developer-tools/running-glm-ocr-deepseek-ocr-2-and-d… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-103577] src=promptcube3.com β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

Running GLM-OCR, DeepSeek-OCR-2, and Dots.

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.

read3 min views1 publishedAug 19, 2026
Running GLM-OCR, DeepSeek-OCR-2, and Dots.
Image: Promptcube3 (auto-discovered)

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

/v1/chat/completions

interface means zero refactoring for existing callers.## Why bother with a unified wrapper

Most 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 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.

Architecture overview #

client β†’ /v1/chat/completions (OpenAI schema)
         β”‚
         β”œβ”€β”€ router picks model by `model` field
         β”‚       β”œβ”€β”€ glm-ocr β†’ Zhipu HTTP endpoint
         β”‚       β”œβ”€β”€ deepseek-ocr-2 β†’ DeepSeek HTTP endpoint
         β”‚       └── dots-mocr β†’ local vLLM / TGI instance
         β”‚
         └── response normalizer β†’ OpenAI `choices[0].message.content` (JSON string)

1. Spin up the normalizer service #

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Literal
import httpx, os, json

app = FastAPI()

class ChatRequest(BaseModel):
    model: Literal["glm-ocr", "deepseek-ocr-2", "dots-mocr"]
    messages: list[dict]
    max_tokens: int = 4096
    temperature: float = 0.0

ENDPOINTS = {
    "glm-ocr": os.getenv("GLM_OCR_URL", "https://open.bigmodel.cn/api/paas/v4/chat/completions"),
    "deepseek-ocr-2": os.getenv("DS_OCR_URL", "https://api.deepseek.com/v1/chat/completions"),
    "dots-mocr": os.getenv("DOTS_URL", "http://localhost:8001/v1/chat/completions"),
}

HEADERS = {
    "glm-ocr": {"Authorization": f"Bearer {os.getenv('GLM_API_KEY')}"},
    "deepseek-ocr-2": {"Authorization": f"Bearer {os.getenv('DEEPSEEK_API_KEY')}"},
    "dots-mocr": {"Authorization": f"Bearer {os.getenv('DOTS_API_KEY', 'local')}"},
}

async def call_upstream(model: str, payload: dict) -> dict:
    async with httpx.AsyncClient(timeout=120) as client:
        r = await client.post(ENDPOINTS[model], json=payload, headers=HEADERS[model])
        r.raise_for_status()
        return r.json()

def normalize(model: str, upstream: dict) -> dict:
    """Map each vendor's response to OpenAI shape with JSON content."""
    if model == "glm-ocr":
        raw = upstream["choices"][0]["message"]["content"]
    elif model == "deepseek-ocr-2":
        raw = upstream["choices"][0]["message"]["content"]
    else:  # dots-mocr already returns JSON string in content
        raw = upstream["choices"][0]["message"]["content"]
    json.loads(raw)  # raises if malformed
    return {
        "id": upstream.get("id", "ocr-" + model),
        "object": "chat.completion",
        "choices": [{
            "index": 0,
            "message": {"role": "assistant", "content": raw},
            "finish_reason": "stop"
        }],
        "usage": upstream.get("usage", {})
    }

@app.post("/v1/chat/completions")
async def chat(req: ChatRequest):
    if req.model not in ENDPOINTS:
        raise HTTPException(400, f"Unknown model {req.model}")
    user_msg = next((m for m in reversed(req.messages) if m["role"] == "user"), None)
    if not user_msg or "image_url" not in user_msg.get("content", [{}])[0]:
        raise HTTPException(400, "Expected image_url in last user message")
    payload = {
        "model": req.model,
        "messages": req.messages,
        "max_tokens": req.max_tokens,
        "temperature": req.temperature,
    }
    upstream = await call_upstream(req.model, payload)
    return normalize(req.model, upstream)

Dots.mocr runs well on a single 24 GB VRAM GPU via vLLM:

docker run --gpus all -p 8001:8000 \
  -v $PWD/models:/models \
  vllm/vllm-openai:latest \
  --model /models/dots-mocr \
  --served-model-name dots-mocr \
  --max-model-len 8192 \
  --limit-mm-per-prompt image=4

Pull the model first:

huggingface-cli download DOTS-OCR/DOTS-OCR-2.0 --local-dir ./models/dots-mocr

3. Client usage stays identical #

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")

resp = client.chat.completions.create(
    model="glm-ocr",
    messages=[{
        "role": "user",
        "content": [
            {"type": "image_url", "image_url": {"url": "https://example.com/invoice.jpg"}}
        ]
    }],
    max_tokens=4096
)
print(resp.choices[0].message.content)  # JSON string with cells, bbox, confidence

resp = client.chat.completions.create(
    model="deepseek-ocr-2",
    messages=[{
        "role": "user",
        "content": [
            {"type": "image_url", "image_url": {"url": "https://example.com/handwritten.png"}}
        ]
    }]
)

resp = client.chat.completions.create(
    model="dots-mocr",
    messages=[{
        "role": "user",
        "content": [
            {"type": "image_url", "image_url": {"url": "file:///data/contract.pdf"}}
        ]
    }]
)

4. Response schema you can count on #

All three normalize to this JSON structure inside content

:


{

"pages": [

{

"page_index": 0,

"width": 2480,

"height": 3508,

"blocks": [

{

"type": "table",

"bbox": [120, 340, 2360, 1200],

"confidence": 0.96,

"cells": [

{"row": 0, "col": 0, "text": "Item", "bbox": [130, 350, 400, 410]},

{"row": 0, "col": 1, "text": "Qty",

[Next The hype cycle promised mass adoption by 2024 β€” reality check β†’](/en/news/6968/)
── more in #developer-tools 4 stories Β· sorted by recency
── more on @deepseek 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/running-glm-ocr-deep…] indexed:0 read:3min 2026-08-19 Β· β€”