GPU-Gateway Integration — Final Report (ahu-ocr-tidyup, 2026-07-06)
Per the integration prompt (x056.think.val.id/prompt-ahu-ocr-tidyup.md) §7. Canonical contract: ahu-gpu-manager/docs/CONVENTIONS.md v1.0. All changes are DORMANT: with no new env set, behavior is byte-identical to before (verified per-wave by independent reviewers reading every migrated call site, plus a live one-document e2e smoke on staging after deploy).
Status: COMPLETE — deployed to staging (ahu-ai-ocr:da74795), e2e smoke green
What was built (4 reviewed waves, each SDD implementer→reviewer→fix loop)
W1 — Foundations (51fe8b1..546b53e + ecf59d6)
- backend/src/lib/gateway-headers.ts: buildGatewayHeaders() (§2 set: X-Tenant-Id: ahu-ocr, X-Surface: internal, X-Request-Id, X-Priority, optional X-User-Id/Idempotency-Key/X-Doc-Hash/X-Doc-Pages), buildIdempotencyKey() (deterministic documentId:stage:contentHash), sha256Hex().
- Dormant config: modelGatewayUrl (default ""), gpuQueueWaitMs (default 1800000). Commented env block in root .env.example (+ live .env as comments only).
W2 — Chat client, Phase A (55cd541..e7fb4af + baad3b9)
- backend/src/lib/trace-context.ts: AsyncLocalStorage trace — X-Request-Id generated once at the outermost entry (HTTP middleware uses the request id; processDocument wraps pipeline entries) and propagated through every downstream call without threading params through ~30 signatures.
- backend/src/lib/gateway-chat-client.ts: one OpenAI-compat client; URL = MODEL_GATEWAY_URL + /v1/chat/completions when set (input normalized against trailing //stray /v1) else the caller's exact current URL; §2 headers; deterministic Idempotency-Key; retryAfterMs() exported for 429/503 Retry-After honor.
W3 — Job client, Phase B (31da028..5913c9b + 13664b2)
- backend/src/lib/gpu-job-client.ts: shared submit-and-poll for the /jobs/:id pattern. CONVENTIONS §3.3 clocks: the processing-timeout budget counts only time observed in processing; queued time is capped separately by GPU_QUEUE_WAIT_MS (default 30 min) — the one deliberate behavior change, prompt-mandated (the no-RTO fix). 404 {code:"JOB_UNKNOWN"} → exactly one resubmit with the SAME Idempotency-Key; plain 404 preserves legacy per-caller behavior. queue_position/eta_ms surfaced via onQueueUpdate (UI wiring deferred). 29 unit tests (fake clock/sleep — includes queued↔processing oscillation and resubmit-tick regression).
W4 — OCR/classifier seams + doc identity (e28e635..f0a8e59 + e2a182f)
- §2 + X-Doc-Hash/X-Doc-Pages headers on every remaining direct egress; URLs and env defaults untouched (the base-URL seam was already env-driven — at flip time ops changes the env, no code change).
Consolidated caller inventory
Chat (Phase A — 13 runtime callers, all through gateway-chat-client): llm/small-card-cleanup, ocr/paddleocr-{bukti-setor,contact-info,domisili,sp-pendirian-pp}-extract, services/akta-{koran,peleburan-sources,txn-classifier}, services/apostille-{classifier,generic-extract} (3 call sites), services/{bukti-pengumuman,laporan-keuangan,surat-likuidator}-extract. Env reads, disabled-when-empty guards, payloads, timeouts, fail-soft behavior preserved exactly (reviewer-verified per caller). No caller has retry loops → no retry-storm risk; retryAfterMs available for future retries. Model ids stay env-driven per caller (CLEANUP_LLM_MODEL, AKTA_TXN_CLASSIFIER_MODEL, …) — ops sets these to registry aliases (cleanup-3b, qwen-35b) at the same deploy that sets MODEL_GATEWAY_URL.
Jobs (Phase B — 12 callers through gpu-job-client): ocr/gpu-server (/jobs), ocr/azure-on-prem-bukti-setor, llm/{ktp-cleanup,override-scrutiny,pendirian-pp-extract,perbaikan-advisory,surat-pernyataan-extract}, services/perbaikan-ai-summary-worker, services/document-processor direct blocks, azure-on-prem-domisili's legacy /jobs path (found during the wave; the survey had missed it). Priority interactive only for override-scrutiny (verifier waits); everything else batch.
Direct OCR/classify (Phase B seams — headers only, URLs unchanged): 7 azure-on-prem-* files (submit+poll) + azure-on-prem-domisili's operation-location path (harmonized post-review: full set incl. Idempotency-Key on submit, set-minus-key on polls), paddleocr-layout, ocr-ensemble, classifier, doc-forensic-client, 5 route-level classifier bypasses, /summarize + /cek-bukti GPU calls (found in the W4 sweep). sha256 once per call; FormData boundaries untouched.
Excluded deliberately: src/scripts/** (CLI dev harnesses), routes/health.ts/routes/settings.ts (status/display reads, not AI egress).
Exactness exceptions (prompt asked for any endpoint/payload/timeout not preserved)
- §3.3 clock semantics replace the old all-polls-count timeout accounting (prompt-mandated).
- Two debug-log lines in
pendirian-pp-extract/surat-pernyataan-extractno longer include job_id on schema-validation failure (accepted, non-user-facing). document-processorBA-RUPS/Jual-beli blocks: two new error classes would produce slightly different wording if ever hit (adjudicated unreachable-in-practice).
Everything else: byte-identical (error wording audited per caller by the W3 reviewer).
Phase-B seams that couldn't be made cleanly base-URL-swappable
None outstanding. One caveat to carry into the flip: azure-on-prem-domisili's operation-location poll URL is derived from Azure's Operation-Location response header (rewritten against AZURE_ON_PREM_BASE_URL) — the gateway's Azure DI adapter must emit/forward that header for the swap to be a pure env change.
Test results
- Backend:
tsc --noEmitclean; full suite 2066 pass / 1 skip / 0 fail (started at 1979 — +87 tests across the program). Every wave's gates re-run independently by its reviewer. - Frontend: untouched; re-verified 761/112 + tsc clean.
- Live e2e smoke (post-deploy, staging
ahu-ai-ocr:da74795): KTP re-upload throughPOST /api/pp/pembubaran/:id/document→ OCR → extraction DONE, 11 fields correct (same values as the pre-program run), all 4 validations PASS. Behavior unchanged.
Flip runbook (ops, per surface — NOT done yet)
- Phase A (now possible): on the staging deploy set
MODEL_GATEWAY_URL=http://192.168.83.20:8200+ the*_MODELregistry aliases (CLEANUP_LLM_MODEL=cleanup-3b,AKTA_TXN_CLASSIFIER_MODEL=qwen-35b) → all 13 chat callers route via the gateway/v1; then process one document and confirm observatory audit events showengine=ahu-ocr, one sharedtrace_idper document,doc_hashpopulated. - Phase B jobs (now possible — P2.5/P2.6 façade verified compatible 2026-07-06): the gateway's gpu-server compat façade mirrors all 12 paths our
gpu-job-clientsubmits to (verified path-by-path) plus the gpu-server-shaped poll,JOB_UNKNOWN, and idempotency dedup — and, as of P2.6,/summarize+/cek-buktitoo (synchronous passthrough, one audit event each). Those two route viagpuServerUrlso they get audit coverage on the flip. Flip =GPU_SERVER_URL=http://192.168.83.20:8200AND, in the same deploy,GPU_SERVER_DIRECT_URL=<real gpu-server>— the ONLY remaining non-façade path is/cache/stats(cache-ops, not model egress), which rides the direct seam so it keeps hitting the real gpu-server (fail-soft 502 if forgotten — cache-stats display only, no user-facing feature). - Phase B OCR/classify (once the Azure DI / PaddleOCR / classifier adapters ship): flip
AZURE_ON_PREM_BASE_URL/PADDLE_OCR_URL/CLASSIFIER_URL/DOC_FORENSIC_URLto the gateway. At that point move the Azure DI key into the gateway registry and remove it from this repo's env. Caveat for the Azure adapter:azure-on-prem-domisiliderives its poll URL from Azure'sOperation-Locationresponse header — the adapter must emit/forward it. - Queue-state UX (§3.5 "Dalam antrean — posisi N") is plumbed to the callers via
onQueueUpdatebut not yet wired to a UI surface — schedule with the first Phase-B flip.