Suya OCR API — vLLM-backed, OpenAI-compatible OCR service

FastAPI service wrapping the Surya-OCR-2 model (datalab-to) served through vLLM:
legacy /v1/api/ai/* endpoints, an OpenAI-compatible /v1/chat/completions endpoint,
a coalescing request batcher, a local OCR CLI, Docker packaging, multilingual
example outputs, and quantization/concurrency benchmarks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Fu Dai
2026-06-17 10:20:02 +04:00
co-authored by Claude Opus 4.8
commit 1a585693be
147 changed files with 13827 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
import json
import logging
import logging.config
import time
import uuid
import yaml
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from surya.endpoint.legacy import router as legacy_router
from surya.endpoint.openai import router as openai_router
from surya.endpoint.service import _error_response
with open("logger.yaml", "r", encoding="utf-8") as f:
config = yaml.safe_load(f.read())
logging.config.dictConfig(config)
logger = logging.getLogger(__name__)
app = FastAPI(
title="OCR SERVICE API SERVICE",
version="1.0",
docs_url="/v1/api/ai/swagger",
openapi_url="/v1/api/ai/openapi.json",
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
request_id = getattr(request.state, "request_id", "-")
logger.warning(
"request_validation_failed request_id=%s method=%s path=%s errors=%s",
request_id,
request.method,
request.url.path,
exc.errors(),
)
return JSONResponse(
status_code=200,
content={
"data": [],
"message": json.dumps({"error info": exc.errors()}, default=str),
"code": 422,
},
)
@app.middleware("http")
async def allow_openapi_unauthorized(request: Request, call_next):
if request.url.path == "/v1/api/ai/openapi.json":
response = await call_next(request)
return response
return await call_next(request)
@app.middleware("http")
async def log_request_response(request, call_next):
request_id = uuid.uuid4().hex
request.state.request_id = request_id
start = time.perf_counter()
client = request.client.host if request.client else "-"
logger.info(
"request_start request_id=%s method=%s path=%s client=%s",
request_id,
request.method,
request.url.path,
client,
)
try:
response = await call_next(request)
except Exception as e:
duration_ms = (time.perf_counter() - start) * 1000
logger.exception(
"request_unhandled_exception request_id=%s method=%s path=%s duration_ms=%.2f",
request_id,
request.method,
request.url.path,
duration_ms,
exc_info=True,
)
return JSONResponse(status_code=500, content=_error_response(e))
duration_ms = (time.perf_counter() - start) * 1000
logger.info(
"request_end request_id=%s method=%s path=%s status_code=%s duration_ms=%.2f",
request_id,
request.method,
request.url.path,
response.status_code,
duration_ms,
)
return response
app.include_router(legacy_router)
app.include_router(openai_router)