Gateway P2.7 — Native inbound façades: Azure DI, PaddleOCR, doc-classifier
Date: 2026-07-07 · Status: approved scope (Efran: "full sweep", staged as Phase 2 after the gpu-server flip) · Depends on: P2.5/P2.6 façade machinery (live), the ahu-ocr flip of GPU_SERVER_URL (done 2026-07-07).
Goal
Route the OCR engine's remaining AI egress seams through the gateway so every model/OCR call is pool-admitted, audited, and tenant-attributed — with the engine unchanged (base-URL env swaps only, per the hands-off rule). After P2.7 the OCR engine has zero un-audited AI egress.
Ground-truth contracts (read from ahu-ocr-tidyup engine code — the façades must mirror these byte-for-byte)
1. Azure DI on-prem (AZURE_ON_PREM_BASE_URL, async, the big one)
- Submit:
POST {base}{apiPath}/{dialect}/documentModels/{model}:analyze?api-version={2023-07-31|2024-11-30}— body is the document (binary or JSON per caller), auth headerOcp-Apim-Subscription-Key. - Four apiPath prefixes are in live use (each caller hardcodes its own):
/api/custom-template(bukti-setor/domisili/contact-info/NPWP/SP customs),/api/id-document(prebuilt-idDocument),/api/layout-v4+/api/read-v4(azureOcr() mode). Two dialects:formrecognizer/documentModels(v2023-07-31 callers, ×7) anddocumentintelligence/documentModels(v4 callers, ×5). The façade must treat prefix+dialect+model as opaque and forward{prefix}{rest}verbatim — configured asfacade_prefixes: ["/api/custom-template","/api/id-document","/api/layout-v4","/api/read-v4"]. - Models used: custom (
sp_pendirian_pp_v4, bukti-setor/domisili/contact-info/NPWP customs fromAZURE_MODEL_*envs) +prebuilt-idDocument, etc. The façade must treat{model}as an opaque path segment (wildcard), never an allowlist. - Response:
202+Operation-Locationheader; the engine rewrites that URL onto its own base by taking the path from/documentintelligence/onward and prefixing${azureOnPremBaseUrl}${apiPath}(ocr/azure-on-prem-layout.ts:27-42). After the flipazureOnPremBaseUrl= the gateway, so the gateway only needs to (a) serve the same poll path shape and (b) return an Operation-Location containing/documentintelligence/...— the engine maps it home itself. - Poll:
GETthe rewritten URL (same key header) untilstatus: succeeded|failed→analyzeResult. - Façade shape: mount
POST /documentintelligence/documentModels/{model}:analyze(any api-version query, forwarded verbatim) +GET /documentintelligence/**poll passthrough on anazure-di-facadeupstream. Pool-admit the submit (batch/interactive by X-Priority); polls are免 admission (cheap). Audit: one event per analyze submit (operation=ocr, model={azure model}) and one on terminal poll (status + upstream_ms) — mirror the SyncForward/emitSync pattern with a small in-memory submit→terminal correlation (keyed by the operation id in the poll path; TTL ~1h; correlation loss degrades to submit-only events, never an error). - Auth: forward
Ocp-Apim-Subscription-Keyverbatim (the engine holds the key; the gateway does NOT need it in env for the passthrough).
2. PaddleOCR (PADDLE_OCR_URL, sync) — build flip-ready, live flip DEFERRED
- Single endpoint:
POST /layout, multipart form (file), → sync JSON{rawText, pageCount, imageWidth, imageHeight, words[], ...}(ocr/paddleocr-layout.ts). All paddle callers go through this one path. - Live state 2026-07-07: dormant.
OCR_LAYOUT_PROVIDERunset (default azure-di) andPADDLE_OCR_URLunset, so no live traffic uses paddle. Ship the façade + gateway upstream (endpoints: ["http://192.168.83.20:8108"]— the container is host-published) so enabling paddle later isPADDLE_OCR_URL={gateway}in one step; do NOT count it in live-flip verification. - Façade shape: reuse the existing SyncForward machinery (P2.6) generalized to serve per-upstream sync paths:
POST /layouton apaddleocrupstream (operation=ocr, model=paddle-layout). Timeout: engine usesPADDLE_OCR_TIMEOUT_MSdefault 300 000 — the façade's per-call timeout must be ≥ that (makesyncForwardTimeoutper-upstream-configurable; today it is a 120s const, too short for paddle).
3. Doc-classifier (CLASSIFIER_URL, sync)
- Engine calls the FULL url
.../api/classifier/classifywith multipart FormData →{classification, confidence}(ocr/classifier.ts:27). It ALREADY sends the §2 gateway headers (tenant/trace/idempotency) on this call. - Façade shape: SyncForward path
POST /api/classifier/classify(serve the full engine-side path so the flip is exactlyCLASSIFIER_URL={gateway}/api/classifier/classify) on adoc-classifierupstream (operation=classify, model=doc-classifier, interactive default per the classify priority rule inclassFromPriority). - Verified 2026-07-07: the audit stream has no self-emitted
doc-classifierevents, so this adds real coverage (not redundant with the SDK pattern — that remains unimplemented in the classifier).
NOT in P2.7 (folds into config / later)
akta-txn-classifier+apostille-classifier: LLM chat calls through the engine's existinggateway-chat-client(guided_json to a vLLM/v1). Flip = pointAKTA_TXN_CLASSIFIER_URL/CLEANUP_LLM_URLat the gateway/v1when those features are enabled (they are env-disabled today). No build.doc-forensic: unused project (Efran 2026-07-07) — dropped entirely.vote: not an AI egress (shareholder e-voting links) — never in scope.PADDLEOCR_VL_URL/MINERU_URL: ensemble engines, env-empty today; the paddle façade pattern covers them later if enabled.
Architecture
Generalize the P2.5/P2.6 façade from "the one gpu-server upstream" to per-upstream inbound façades declared in config:
upstreams:
- id: azure-di-onprem
type: ocr-http
class: on_prem
adapter: azuredi-facade # new: inbound native async façade
operation: ocr
endpoints: ["https://x056.ahu-azure.val.id"] # the same nginx base the engine uses today — no per-container knowledge needed
facade_prefixes: ["/api/custom-template", "/api/id-document", "/api/layout-v4", "/api/read-v4"]
models: ["azure-di"] # registry key only (config requires non-empty models); the façade is a wildcard, not an allowlist
slots: {total: 6, batch_max: 4}
- id: paddleocr
type: ocr-http
class: on_prem
adapter: sync-facade # new: generic sync passthrough (P2.6 SyncForward, generalized)
operation: ocr
endpoints: ["<real paddle url>"]
models: ["paddle-layout"] # registry key only (config requires non-empty models)
sync_paths: [{path: /layout, model: paddle-layout}]
sync_timeout_ms: 300000
slots: {total: 4, batch_max: 4}
- id: doc-classifier
type: classify
class: on_prem
adapter: sync-facade
operation: classify
endpoints: ["<real classifier base>"]
models: ["doc-classifier"] # registry key only (config requires non-empty models)
sync_paths: [{path: /api/classifier/classify, model: doc-classifier}]
slots: {total: 4, batch_max: 2}
Key decisions:
1. Sync façade = generalize SyncForward (pool admission → verbatim forward → header relay minus hop-by-hop → emitSync audit). New config: sync_paths (path→model label) + sync_timeout_ms. The gpu-server's hardcoded /summarize//cek-bukti become the same mechanism (no behavior change — keep their existing mount for compat).
2. Azure DI façade is async-passthrough, NOT job-store-backed: the engine drives its own poll loop against Operation-Location; wrapping it in the P2 job store would change the wire contract. The gateway forwards submit + polls, audits submit and terminal, and pool-admits only the submit.
3. Route collision safety: façade mounts must not shadow /v1, /jobs, /metrics, /health, or the gpu-server façade paths; config validation rejects a sync_paths entry that collides with a reserved or already-mounted path.
4. Dormancy invariant (contract §6): no azuredi-facade/sync-facade upstream configured ⇒ binary is byte-identical to P2.6 behavior.
5. On-prem enforcement + tenant auth: same predicates as SyncForward (class check, header-trust/token mode) on every new inbound path.
Flip plan (after deploy)
~/ahu-ai-staging/ocr/infra/env/shared.env:
- AZURE_ON_PREM_BASE_URL=http://192.168.83.20:8200 (engine's apiPath+rewrite logic maps polls onto the gateway automatically)
- PADDLE_OCR_URL=http://192.168.83.20:8200
- CLASSIFIER_URL=http://192.168.83.20:8200/api/classifier/classify
Real backends move to gateway config (endpoints:). Backups + one-seam-at-a-time flips with a real-document smoke between each; instant revert = restore env line + recreate web.
Verification
- Unit: per-façade adapter/handler tests mirroring P2.5/P2.6 suites (auth, admission/429+slot-release, verbatim forward incl. multipart bodies + query strings, header relay, audit emission incl. truncation, Operation-Location poll correlation, JOB-style error mapping).
- Live: per seam — real document through the engine UI/API, confirm result parity + audit event (
engine=ahu-ocr, correct operation/model), chatbot/v1regression check after the redeploy.
Risks
- Azure DI poll-path variety (api-version differences, absolute vs relative Operation-Location) — mitigated by wildcard passthrough + engine-side rewrite doing the heavy lifting.
- Multipart bodies through the façade must be forwarded byte-verbatim with the original
Content-Typeboundary (SyncForward already does this — the gpu-server/jobsmultipart path proved it live). - Paddle 300s calls holding slots — sized pool (4) matches the paddle service's real concurrency.