Completion notes — Gateway integration prep (dormant)
Date: 2026-07-04 · Spec: specs/2026-07-04-gateway-integration-prep.md · Plan: plans/2026-07-04-gateway-integration-prep.md
Source prompt: https://x056.think.val.id/prompt-ai-ahu-chatbot-filled.md · Contract: ../ahu-gpu-manager/docs/CONVENTIONS.md v1.0
Files changed
| Area | Files |
|---|---|
| Orchestrator LLM client | apps/public-web/src/lib/orchestrator/llm/client.ts — LlmHttpError (status + Retry-After/X-Queue-Depth on 429/503), deterministic per-call Idempotency-Key, streamEvents() typed SSE parser (queue/error frames, unknown events ignored), X-Queue-Events opt-in; stream() unchanged wrapper |
| Compose step | apps/public-web/src/lib/orchestrator/steps/compose.ts — queue ComposeEvent, ComposeUpstreamError on post-commit event: error frames |
| Orchestrator | apps/public-web/src/lib/orchestrator/orchestrate.ts — X-Request-Id (${sessionId}-t${turn}), idempotency bases per client, LLM_QUEUE_EVENTS=on gate, queue → "Dalam antrean — posisi N (±M menit)" status, 429/503 → "Sistem sedang sibuk" degradation |
| Agents (mirrored ×2) | apps/{internal,public}-agent/dash/gateway.py (new, light module), dash/agents.py (default_headers on 3 OpenAILike sites + TODO(gateway)), app/api/knowledge.py (bare OpenAI()/gpt-4o → knowledge_llm_config()) |
| Env | infra/env/{internal,public,shared}.env + committed .example files — commented flip blocks only; no live value changed |
| Tests | llm-client.test.ts (+8), steps.test.ts (+2, 2 mocks updated), orchestrate.test.ts (+2, mocks keep real error classes), tests/test_gateway.py ×2 apps (+5 each, file-path loader pattern) |
Verification
pnpm checkgreen (typecheck, 67 public-web + 15 internal-web tests, conventions OK);pnpm lintclean.- Agent pytest (DB-free subset, both apps): 8/8 passed. Policy/middleware tests require Postgres — unchanged, run in Docker at ship time.
- Mirrors byte-identical (
diffempty forgateway.py,agents.py,knowledge.py). - agno pin verified: scratch venv with agno==2.4.7 —
OpenAILike(default_headers=...)accepted and present onget_client().default_headers. (Dev.venvhas 2.6.20; same API.) - Live check (sanctioned): real
LlmClientbundle drove one non-streaming ("siap", 210 ms) + one streaming ("1, 2, 3") turn throughhttp://192.168.83.20:8200with the full §2 header set;/metricsshowsgateway_requests_total{class="interactive",status="ok",tenant="ahu-chatbot",upstream="qwen-35b"} 2. No queue/error frames (pool idle).
Dormancy
With no new env set: request URLs and bodies byte-identical; only sanctioned additions appear — static X-Tenant-Id/X-Surface/X-User-Id/X-Request-Id/X-Priority (already partially present) and Idempotency-Key (prompt item 3, unconditional). X-Queue-Events only with LLM_QUEUE_EVENTS=on. knowledge.py stays on bare OpenAI()+gpt-4o until KNOWLEDGE_LLM_MODEL is set (Efran's decision — MODEL_GATEWAY_URL alone is NOT the flip signal because staging already sets it, pointed at plain vLLM).
Agno API limitation found (prompt asked)
agno 2.4.7 OpenAILike supports only client-level default_headers/extra_headers; Agent.run(user_id=...) never reaches the HTTP layer, so per-run X-User-Id cannot be propagated from the Agno agents. Static identity headers shipped; # TODO(gateway) marks the revisit point. (Unchanged in 2.6.20.)
Contract interpretations (prompt asked)
- Queue copy: prompt said "Antrian ke-N (±Xs)"; CONVENTIONS §3.5 mandates "Dalam antrean — posisi N (±X menit)" — conventions won (minutes, rounded up, min 1).
- Callback vs generator: queue frames arrive while the stream is held; only a yielding generator can reach the browser SSE channel before the first content chunk, so
streamEvents()is the surface and orchestrate forwards frames on the existing status channel. Same intent as the prompt's "callback". - X-Request-Id added to orchestrator base headers (required by §2/§8 though not in prompt item 3); one turn = one trace.
- Idempotency keys:
${sessionId}:t${turn}:{plan|syn}:${callSeq}— distinct logical calls in a turn get distinct keys (else the gateway would dedupe plan vs compose), while a resubmitted turn regenerates the same series. Deterministic, never random. - Legacy
X-Priority: planning|synthesisin orchestrate.ts left as-is — §2 maps legacy values during transition; normalizing was out of scope. - Pre-existing drift noted (not touched):
public.env.examplehasSYNTHESIS_GATEWAY_URL=...compatible-mode/v1while live env + both consumers append/v1themselves — flip block uses the no-suffix convention.