think
16px
820px

Integration prompt — ahu-ocr-tidyup

Paste this into a Claude Code session running inside the ahu-ocr-tidyup repo. Fill in <GATEWAY_URL> before pasting (e.g. http://192.168.83.20:8200), or leave it — only the commented env entries reference it.

Supersedes the earlier prompt-ahu-ocr-akta-notaris.md (same engine, this is the current tidied repo with more egress points). If that prompt's shared-client work (§1–§5 there) is already done, treat those sections below as "verify + extend with headers/doc-identity" rather than "build from scratch."


P2.5 update — the gpu-server compat façade now EXISTS on the gateway. The
gateway mirrors the on-prem gpu-server contract exactly: the same 12 submit
paths (POST /jobs + the 11 per-op /…/extract paths this engine already
calls), each 202 {job_id}, and the gpu-server-shaped poll GET /jobs/{id}
(status + result/error/stage/progress/queue_position, 404 {"code":"JOB_UNKNOWN"} on unknown). So the Phase-B flip is simply setting
GPU_SERVER_URL=<gateway>
— the per-op-path client (backend/src/lib/gpu-job-client.ts)
needs no code change beyond the env flip already prepared here; the paths,
the {job_id} submit shape, and the poll shape are byte-for-byte what the engine
already speaks. The façade activates only when the gateway has a gpuserver-job
upstream configured (dormant otherwise). Idempotency-Key stays required, and
resubmit-on-404-with-the-same-key works as the gateway dedupes. The other Phase-B
seams (AZURE_ON_PREM_BASE_URL / PADDLE_OCR_URL / CLASSIFIER_URL) still route
through the generic Job API / adapters as described below — only GPU_SERVER_URL
gets the drop-in façade. Also covered (P2.6): the two SYNCHRONOUS gpu-server
endpoints POST /summarize and POST /cek-bukti
(called from
routes/submissions.ts and routes/perubahan.ts) — the façade forwards them
verbatim and audits each call (operation=ocr, model=summarize/cek-bukti),
so they too flow through the gateway on the GPU_SERVER_URL swap with no engine
change. They're pool-admitted, so under sustained saturation they can return
429 + Retry-After (past the hold budget) — the direct callers should tolerate
that like the job callers do.


P2.7 update (2026-07-07) — the Azure DI and doc-classifier flips are now
pure base-URL swaps too.
The gateway gained native inbound façades that
mirror this engine's own call shapes byte-for-byte (azuredi-facade,
sync-facade — see docs/superpowers/specs/2026-07-07-gateway-p2.7-native- facades-design.md), so no engine code changes are needed for either seam:
- AZURE_ON_PREM_BASE_URL=<gateway> — the façade mounts the same four
apiPath prefixes this engine already calls (/api/custom-template,
/api/id-document, /api/layout-v4, /api/read-v4), forwards
{model}/api-version verbatim, and returns an Operation-Location
shaped the same way, so this engine's own poll-URL rewrite keeps working
unchanged.
- CLASSIFIER_URL=<gateway>/api/classifier/classify — the façade mounts
the engine's exact call path (this engine already sends the §2 gateway
headers on this call), so the flip is the URL swap alone.
- The PaddleOCR façade (sync-facade on /layout) also exists gateway-side
and is ready for PADDLE_OCR_URL=<gateway>, but its live flip is
deferred until PaddleOCR is actually enabled on this engine
(OCR_LAYOUT_PROVIDER/PADDLE_OCR_URL are both unset today, so this seam
carries no live traffic yet) — no urgency, no engine change needed either
way.
- No engine changes needed for any of the three — every flip listed
above (and the deferred paddle one) is a base-URL env value only.
- Per-op audit dimensions once flipped: Azure DI submits/polls are audited
per Azure model id (sp_pendirian_pp_v4, prebuilt-idDocument, etc., per
the AZURE_MODEL_* envs already in use); PaddleOCR calls audit as model
paddle-layout; classifier calls audit as model doc-classifier — the
latter is new audit coverage since the classifier does not self-emit
today.


We are routing this OCR engine through the central GPU/LLM gateway (ahu-gpu-manager, port 8200) so that all model + OCR traffic flows through one audited path, and every call is captured by the observatory audit system automatically (the gateway emits a schema-v1 audit event per call to a Redis stream the observatory ingests — you do not call the observatory directly; routing through the gateway is the audit integration). The canonical contract is ahu-gpu-manager/docs/CONVENTIONS.md v1.0 — read it; on any conflict it wins.

Hard constraint (non-negotiable): every change is backwards-compatible and DORMANT. With no new env vars set, behavior must be byte-for-byte identical to today. Do not flip any live value in .env; only add commented-out entries showing the future values. The flip is an ops action later, per surface.

Two-phase reality — build both in this pass, they differ only in when their env flips:
- Phase A — LLM/chat egress (flips FIRST; the gateway's OpenAI-compatible /v1 is live today). The direct chat-completion callers.
- Phase B — OCR job + Azure DI + PaddleOCR + classifier egress (prepared now, flips LATER). The gateway's Job API (/jobs) and OCR/classify adapters are specified in CONVENTIONS but not yet deployed. Do the client-consolidation + headers + clocks + doc-identity now so the eventual flip is a base-URL change, not a rewrite. Keep GPU_SERVER_URL / AZURE_ON_PREM_BASE_URL / PADDLE_OCR_URL / CLASSIFIER_URL pointed at today's servers until then.

1. One shared "gateway headers" helper — applied to every model/OCR/classify call

Per CONVENTIONS §2, every call must carry:
- X-Tenant-Id: ahu-ocr — the stable engine slug (keys audit attribution; never change it)
- X-Surface: internal — this engine is the internal verifier workspace (omit only if genuinely N/A)
- X-User-Id — the verifier/operator id when a request is user-triggered; omit for autonomous pipeline stages
- X-Request-Idgenerate once at the outermost entry point (upload handler / submission dispatch / verifier action) and propagate unchanged through every downstream model/OCR call for that document. Becomes the audit trace_id, so a whole document's pipeline shares one trace.
- X-Prioritybatch for pipeline document processing (OCR, extraction, cleanup, advisory); interactive for verifier-triggered on-demand actions (override scrutiny, a re-run a human is waiting on). (Legacy planning/synthesis are gateway-mapped to interactive; don't use them.)
- Idempotency-KeyREQUIRED on job submits, recommended on chat. Deterministic: documentId + pipelineStage + contentHash — never uuid4() at call time. The gateway dedupes on it so a crash/retry never re-runs a 40-page OCR.

Build ONE helper (e.g. backend/src/lib/gateway-headers.ts) that assembles this set from a small context object, and thread the X-Request-Id/trace through the existing request context (getRequestContext already exists — see backend/src/index.ts). Every client in §2–§4 uses it. When MODEL_GATEWAY_URL is unset, the helper is still safe to call — the current upstreams ignore unknown X-* headers — so wiring it in now is dormant-safe.

2. Consolidate the submit-and-poll JOB clients (Phase B) + queued-vs-processing clocks

These each independently fetch(gpuServerUrl + …) + poll (verify the full set in this repo; known ones):
backend/src/ocr/gpu-server.ts, backend/src/ocr/azure-on-prem-domisili.ts, backend/src/ocr/azure-on-prem-bukti-setor.ts, backend/src/llm/pendirian-pp-extract.ts, backend/src/llm/surat-pernyataan-extract.ts, backend/src/llm/override-scrutiny.ts, backend/src/llm/ktp-cleanup.ts, backend/src/llm/perbaikan-advisory.ts, backend/src/services/perbaikan-ai-summary-worker.ts, backend/src/services/document-processor.ts.

Extract ONE shared job client (e.g. backend/src/lib/gpu-job-client.ts) all of them use — submit (per-call endpoint/payload), poll loop, timeout, error mapping — preserving each caller's exact current endpoint, payload, and per-call timeout defaults (refactor, not behavior change; existing tests pass). Then add:
- Clock rule (CONVENTIONS §3.3 — the no-RTO fix): the processing-timeout clock runs only while status is processing. While queued, a separate cap applies: new env GPU_QUEUE_WAIT_MS (default 1800000 = 30 min). The current gpu-server already returns queued/processing, so this works today.
- 404 {code: "JOB_UNKNOWN"} on poll → resubmit with the SAME Idempotency-Key (gateway dedupes; a completed job returns its result).
- Pass through queue_position / eta_ms when present (gateway adds them; current server doesn't) to callers.
- Send the §1 header set on submit.

3. Consolidate the direct chat-completion callers (Phase A — flips FIRST)

The non-job, OpenAI-compat callers hitting vLLM directly (verify the set; known: backend/src/services/akta-txn-classifier.ts, backend/src/llm/small-card-cleanup.ts, the CLEANUP_LLM_URL 3B path, and the paddleocr--extract / akta--extract / apostille--extract services that call chat completions). Route these through a shared chat client whose base URL is MODEL_GATEWAY_URL + "/v1" when MODEL_GATEWAY_URL is set, else the current URL (unchanged behavior). It must:
- send the §1 headers;
- on 429/503, read Retry-After (+ X-Queue-Depth if present) and degrade gracefully — these callers already tolerate errors; confirm no crash / no retry-storm and honor Retry-After on any retry;
- send the deterministic Idempotency-Key.
-
Model ids:* when routed through the gateway, the model field must be the registry alias (qwen-35b, cleanup-3b, tei-embeddings, …), not the raw vLLM model string. Make the alias env-driven (new *_MODEL envs defaulting to today's raw names) so it's dormant until flip.

4. OCR + classifier egress seam (Phase B — prepared, dormant)

backend/src/ocr/provider.ts already dispatches Azure DI (azure-* files) vs PaddleOCR (paddleocr-* files); backend/src/ocr/classifier.ts calls CLASSIFIER_URL. Give each a base-URL seam so that when the gateway's OCR/classify adapters ship, flipping AZURE_ON_PREM_BASE_URL / PADDLE_OCR_URL / CLASSIFIER_URL to <GATEWAY_URL> routes them through it (gateway operations ocr / classify, model ids azure-di-layout-v4 / paddleocr / the classifier's registry id). Add the §1 headers to these calls now. Do not change the default URLs — this is a seam + headers only until the adapters exist.

5. Document identity in the audit trail (doc_hash + pages)

The audit event carries doc_hash + pages (CONVENTIONS audit schema) — this is how a model/OCR call is linked to a specific document without the document bytes ever leaving this engine (per the platform decision to keep uploaded documents on the OCR host, not duplicate them into the observatory). On every gateway-routed OCR/LLM call for a document, send:
- X-Doc-Hash — the content hash you already compute for the idempotency key (reuse it; sha256 of the source file)
- X-Doc-Pages — page count when known

These are additive headers; if the running gateway doesn't yet read them they're ignored (dormant-safe). Once live, the observatory can answer "which model calls touched document X" from the audit trail alone.

6. Env preparation (commented, not active)

In .env.example (and .env as comments), plus gpu-server/.env.example if the gpu-server itself gains a VLLM_BASE_URL:

# --- ahu-gpu-manager gateway (do NOT enable until the gateway is live) ---
# MODEL_GATEWAY_URL=<GATEWAY_URL>          # Phase A: chat/LLM egress via /v1
# GPU_QUEUE_WAIT_MS=1800000                # queued-state cap (separate from processing timeout)
# Model-id aliases (registry names)  set alongside MODEL_GATEWAY_URL:
#   CLEANUP_LLM_MODEL=cleanup-3b
#   (akta-txn-classifier / extract model  qwen-35b, etc.)
# --- Phase B (flip only once the gateway Job API + OCR/classify adapters ship) ---
# GPU_SERVER_URL=<GATEWAY_URL>             # OCR+LLM job API (wire-compatible base-URL swap)
# AZURE_ON_PREM_BASE_URL=<GATEWAY_URL>     # Azure DI OCR via gateway adapter
# PADDLE_OCR_URL=<GATEWAY_URL>             # PaddleOCR via gateway adapter
# CLASSIFIER_URL=<GATEWAY_URL>/...         # classifier via gateway

Leave all live values as-is. Note in a comment that any upstream API keys (e.g. Azure DI key) move server-side into the gateway registry at flip time and get removed from this repo.

7. Verification

  • All existing tests pass. Add unit tests for: the shared job client's clock behavior (queued time not counted; GPU_QUEUE_WAIT_MS cap; JOB_UNKNOWN→resubmit-same-key), and the header helper (correct X-Tenant-Id/surface/priority/idempotency assembled; trace propagated).
  • Dormancy proof: with no new env set, confirm (read the constructed clients or a local smoke run) that request URLs, bodies, and headers minus the additive X-* are unchanged against the current upstreams.
  • Typecheck (bunx tsc --noEmit in backend/) + lint clean.
  • Smoke: one document end-to-end against the current servers — behavior unchanged.
  • Post-flip check (when the gateway is live, Phase A): process one document, then confirm in the observatory dashboard that audit events appear with X-Tenant-Id=ahu-ocr, the right trace_id shared across the document's calls, and doc_hash populated.

Report back: files changed; the full list of submit-poll + chat callers you consolidated (and any whose exact endpoint/payload/timeout could not be preserved, and why); any Phase-B seam that couldn't be made cleanly base-URL-swappable; and test results.