think
16px
820px

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-extract no longer include job_id on schema-validation failure (accepted, non-user-facing).
  • document-processor BA-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 --noEmit clean; 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 through POST /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)

  1. Phase A (now possible): on the staging deploy set MODEL_GATEWAY_URL=http://192.168.83.20:8200 + the *_MODEL registry 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 show engine=ahu-ocr, one shared trace_id per document, doc_hash populated.
  2. 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-client submits to (verified path-by-path) plus the gpu-server-shaped poll, JOB_UNKNOWN, and idempotency dedup — and, as of P2.6, /summarize + /cek-bukti too (synchronous passthrough, one audit event each). Those two route via gpuServerUrl so they get audit coverage on the flip. Flip = GPU_SERVER_URL=http://192.168.83.20:8200 AND, 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).
  3. 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_URL to 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-domisili derives its poll URL from Azure's Operation-Location response header — the adapter must emit/forward it.
  4. Queue-state UX (§3.5 "Dalam antrean — posisi N") is plumbed to the callers via onQueueUpdate but not yet wired to a UI surface — schedule with the first Phase-B flip.