AHU AI Platform Conventions — v1.0
This file is the canonical contract between the platform (ahu-gpu-manager, ahu-ai-observatory) and every engine (chatbot, Document Intelligence/OCR, doc classifier, and all future engines). Engines and platform services MUST conform. Changes happen by PR to this file with a version bump; the audit ingester supports schema version N and N−1 so upgrades never require lockstep deploys.
Copies/summaries elsewhere (workspace CLAUDE.md, integration prompts) are pointers — on conflict, this file wins.
1. Network & port allocations
| Service | Port | Notes |
|---|---|---|
| Gateway (data plane) | 8200 | OpenAI-compatible /v1/*, Job API /jobs, /queue/status, /healthz |
| Controller admin API | 8210 | registry, caps, replica state; consumed by ops console |
| Node agent | 8220 | one per GPU host, LAN-bound |
| Observatory query API | 8300 | dashboard + future PIE read interface |
| Observatory dashboard | 8310 | |
| DCGM exporter | 9400 | pre-existing on GPU hosts; controller scrapes it |
Audit transport: Redis Stream, key ahu.ai.audit.
2. HTTP header contract (engine → gateway)
| Header | Values / rule |
|---|---|
X-Tenant-Id |
engine slug: ahu-chatbot, ahu-ocr, doc-classifier, … (kebab-case, stable forever — it keys audit attribution) |
X-Surface |
internal | public (omit if N/A) |
X-User-Id |
end-user/session identifier when known |
X-Request-Id |
propagated end-to-end; becomes audit trace_id. Generate at the outermost entry point; reuse downstream |
X-Priority |
interactive | batch | system. Legacy values planning/synthesis are mapped to interactive by the gateway during transition |
Idempotency-Key |
REQUIRED on job submits, recommended on chat. Deterministic: derive from stable inputs (document id + stage + content hash; or session id + turn), never uuid4() at call time |
X-Queue-Events |
on to receive event: queue SSE frames while held (streaming requests only) |
3. Queue & timeout semantics
- Interactive requests are never held past the client's timeout. Hold budget per class = known client timeout − 15 s (minimum 5 s). Engines declare their client timeout when registering a tenant; changing an engine timeout means updating the tenant config.
- Past hold budget →
429(pool saturated) or503(upstream down) +Retry-After(seconds) +X-Queue-Depth. Engines MUST treat these as graceful degradation, not errors to retry-storm. - Batch: a queued job is not a slow job. Poll statuses are
queued(withqueue_position,eta_ms) →processing(withstage,progress) →completed|failed. Engine poll clients run their processing-timeout clock only duringprocessing; total queue wait is bounded separately byGPU_QUEUE_WAIT_MS(default 1 800 000 = 30 min). - A poll for an unknown job returns
404 {code: "JOB_UNKNOWN"}→ the engine resubmits with the sameIdempotency-Key(the gateway dedupes; a completed job returns its existing result). - Queue state must reach the human. UIs render queued as "Dalam antrean — posisi N (±X menit)", never a generic spinner, whenever position/ETA is available.
- A 4xx is a permanent verdict on the input; never retry it. Only
408/429/503 + Retry-Afterand 5xx are retryable. An engine that retries a 4xx burns capacity on an input that can never succeed — 8 corrupt documents retried ~5 800 times over five weeks once produced 98 % of all platform error volume, hiding every real incident underneath. Record the failure with the upstream's own reason and surface it to the user ("PDF rusak / tidak dapat dibaca"); do not translate a 4xx into a 5xx, which reads as transient and invites the retry. - The gateway remembers permanent refusals (refusal memory). When an upstream refuses a document with a status that is a verdict on the bytes (default
400,413,415,422), the gateway records that refusal keyed on the document's content fingerprint — not on the caller — and replays the upstream's original status and body, byte-identical, to every later caller. Engines see no new status code and need no change: a refusal looks exactly like the upstream answering. Three consequences engines should know:
- A replayed refusal carriesX-Negative-Cache: HIT(plusX-Negative-Cache-HitsandX-Negative-Cache-First-At) and noRetry-After— retrying is precisely what cannot help.
- It is audited aserror_code = REFUSED_HTTP_nnnwithcache_hit=trueandupstream_ms=0, deliberately distinct from the liveHTTP_nnn, so a poison pill can be separated from a real incident in every rate-based signal.
-401/403/404/408/429are never remembered (they report a caller, credential, route, or backpressure problem, not a bad document), and neither is any 5xx. The gateway refuses such a config at load rather than accepting it.
This does not relieve an engine of rule 6 — it makes the property hold fleet-wide, including for instances still on older code, which per-process engine guards cannot do.
4. Egress rules
- Once an engine's gateway env is flipped, no engine calls a model/OCR server directly — all LLM, VLM, embedding, and OCR traffic goes through the gateway. New engines integrate via the gateway from day one.
- Model-server URLs appear only in env vars, never hardcoded. Gateway base env is
MODEL_GATEWAY_URL(no/v1suffix; clients append it). - Upstream API keys/secrets live only in the gateway registry, never in engine repos or env files.
- Upstreams are classed
on_prem|external_dev. Production runsallow_external_upstreams=false; cloud endpoints (DashScope, OpenAI) are development-phase conveniences that the gateway refuses in prod. - In-process GPU tenants (anything loading models into local CUDA, e.g. the classifier) can't be proxied — they MUST use the SDK pattern instead: heartbeat to
MANAGER_URL, audit events toahu.ai.audit, fire-and-forget, dormant when env unset.
5. Audit event schema (v1)
One event per AI call, emitted by the gateway (proxied traffic) or the engine SDK (in-process tenants). Fire-and-forget; on stream failure, spool to local disk and replay — never block or fail the user request because auditing is down.
event_id (ULID/uuid4) · schema_ver=1 · ts (ISO-8601 UTC) · engine · surface · user_id · trace_id
upstream · model · upstream_class (on_prem|external_dev) · traffic_class
operation (chat|embed|ocr|classify|search|job|job_child) · status · error_code
queue_ms · upstream_ms · total_ms · tokens_in · tokens_out · doc_hash · pages
job_id · parent_job_id · config_version · request_body · response_body (capped 10 MB, zstd; truncation recorded with full-content hash)
Additive fields = no version bump. Renames/removals/semantic changes = schema_ver+1.
6. Naming
- Registry model ids: kebab-case —
qwen-397b,qwen-35b,cleanup-3b,tei-embeddings,paddleocr,azure-di-layout-v4, … - Compose projects:
ahu-platform(services),ahu-platform-agent(GPU hosts). - Env vars: gateway-related config in engines uses existing names where they exist (
MODEL_GATEWAY_URL,VLLM_BASE_URL, …); new platform-owned vars are prefixedGATEWAY_,AUDIT_, orMANAGER_.
7. Stack conventions
- Platform services: Go (static binaries). Dashboard: React + Vite + Tailwind/shadcn. Engines keep their own stacks; only the wire contract is mandatory.
- All platform services expose Prometheus-format
/metrics(costs nothing, matches the DCGM exporter and the dormant Prometheus/Grafana configs already on x056); the custom dashboard remains the product deliverable. - Every platform service:
/healthzliveness endpoint; graceful drain on SIGTERM (finish in-flight,503 + Retry-Afterfor new work, never silent connection drops).
8. Engine compliance checklist
Every engine, before its gateway flip is approved:
- [ ] Sends the §2 header set on all model/OCR calls
- [ ] Handles
429/503 + Retry-Afteras graceful degradation - [ ] Treats a 4xx as permanent — records the upstream's reason, surfaces it, does not retry (§3.6)
- [ ] Job polling distinguishes
queuedvsprocessing(clock rule §3.3) and handlesJOB_UNKNOWN - [ ] Idempotency keys are deterministic (§2)
- [ ] Queue state is user-visible (§3.5)
- [ ] No hardcoded model-server URLs; no upstream secrets in repo
- [ ] All changes dormant until env flip (unset env = unchanged behavior)