think
16px
820px

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.tsLlmHttpError (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.tsqueue ComposeEvent, ComposeUpstreamError on post-commit event: error frames
Orchestrator apps/public-web/src/lib/orchestrator/orchestrate.tsX-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 check green (typecheck, 67 public-web + 15 internal-web tests, conventions OK); pnpm lint clean.
  • 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 (diff empty for gateway.py, agents.py, knowledge.py).
  • agno pin verified: scratch venv with agno==2.4.7OpenAILike(default_headers=...) accepted and present on get_client().default_headers. (Dev .venv has 2.6.20; same API.)
  • Live check (sanctioned): real LlmClient bundle drove one non-streaming ("siap", 210 ms) + one streaming ("1, 2, 3") turn through http://192.168.83.20:8200 with the full §2 header set; /metrics shows gateway_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)

  1. 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).
  2. 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".
  3. X-Request-Id added to orchestrator base headers (required by §2/§8 though not in prompt item 3); one turn = one trace.
  4. 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.
  5. Legacy X-Priority: planning|synthesis in orchestrate.ts left as-is — §2 maps legacy values during transition; normalizing was out of scope.
  6. Pre-existing drift noted (not touched): public.env.example has SYNTHESIS_GATEWAY_URL=...compatible-mode/v1 while live env + both consumers append /v1 themselves — flip block uses the no-suffix convention.