think
16px
820px

AHU GPU/LLM Gateway (P0+P1) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build the Go gateway of ahu-gpu-manager: an OpenAI-compatible reverse proxy with priority-pool admission, queue-awareness (holds, keepalives, structured queue events, 429/Retry-After), idempotency, on-prem enforcement, and fire-and-forget audit-event emission to Redis Streams with disk-spool fallback.

Architecture: Single stateless Go binary (cmd/gateway). Per-upstream slot pools (local first, Redis-coordinated for the HA pair) gate admission by traffic class. Requests route by OpenAI model field through a static YAML registry. Every call emits an audit event (schema v1) to Redis Stream ahu.ai.audit; the observatory (separate plan) consumes it.

Tech Stack: Go ≥1.22 (std net/http, no web framework) · github.com/redis/go-redis/v9 · gopkg.in/yaml.v3 · github.com/oklog/ulid/v2 · github.com/klauspost/compress/zstd · github.com/prometheus/client_golang · test-only: github.com/alicebob/miniredis/v2.

Global Constraints

  • Repo: /home/efran/remote-development/poc-ahu-ai/ahu-gpu-manager (already a git repo). Go module path: datahive.id/ahu-gpu-manager.
  • Contract values come verbatim from docs/CONVENTIONS.md v1.0 — on conflict, CONVENTIONS.md wins: gateway port 8200; audit stream key ahu.ai.audit; headers X-Tenant-Id, X-Surface, X-User-Id, X-Request-Id, X-Priority (interactive|batch|system; legacy planning/synthesisinteractive), Idempotency-Key, X-Queue-Events; saturation → 429 + Retry-After + X-Queue-Depth; upstream down → 503; audit body cap 10485760 bytes; audit events are fire-and-forget (spool to disk when Redis is down, never block a request).
  • Hold budgets (defaults): interactive 45000 ms, batch 120000 ms, system 10000 ms.
  • Missing X-Priority defaults to interactive (unpatched engines must not get worse latency).
  • No new dependencies beyond the Tech Stack list. DRY, YAGNI, TDD; commit after every task; commit messages end with Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT.
  • Every created/modified .md file must be uploaded: curl -F "file=@<file>" https://x056.think.val.id/upload (workspace CLAUDE.md rule).
  • Engine repos (ai-ahu-chatbot, ahu-ocr-akta-notaris, ahu-doc-classifier) are never modified by this plan.
  • All tests: go test ./... -count=1. Race checks on pool tasks: go test -race.

File Structure

cmd/gateway/main.go            wiring: config  registry  pools  emitter  server
internal/config/config.go      YAML config + validation (registry entries, budgets, tenants)
internal/registry/registry.go  modelupstream resolution, endpoint health, prober
internal/pool/pool.go          Pool/Ticket interfaces + LocalPool (priority heap)
internal/pool/redispool.go     Redis-coordinated pool (lease ZSET + Lua), fail-open
internal/audit/event.go        audit event schema v1
internal/audit/emitter.go      buffered emit  XADD, disk spool + replay
internal/idem/cache.go         idempotency result cache (Redis, non-streaming)
internal/proxy/handler.go      /v1 + /synthesis/v1 handler: parse, admit, forward
internal/proxy/stream.go       SSE piping, tee, queue events, keepalives
internal/proxy/stats.go        EWMA upstream latency  ETA
internal/server/server.go      mux, middleware, healthz, queue status, metrics, drain
deploy/gateway.example.yaml    x056-matching example config
deploy/compose.yaml            gateway + redis compose stack
Dockerfile

Task 1: Module scaffold + config loader

Files:
- Create: go.mod, internal/config/config.go
- Test: internal/config/config_test.go

Interfaces:
- Produces: config.Load(path string) (*Config, error); types Config, Upstream, Slots, Tenant, AuditConfig, Class (ClassInteractive|ClassBatch|ClassSystem constants) — all later tasks import these.

  • [ ] Step 1: Toolchain + module init
go version || (curl -sL https://go.dev/dl/go1.23.4.linux-amd64.tar.gz | sudo tar -C /usr/local -xz && export PATH=$PATH:/usr/local/go/bin)
cd /home/efran/remote-development/poc-ahu-ai/ahu-gpu-manager
go mod init datahive.id/ahu-gpu-manager
  • [ ] Step 2: Write the failing test

internal/config/config_test.go:

package config

import (
    "os"
    "path/filepath"
    "testing"
)

const sample = `
listen: ":8200"
allow_external_upstreams: true
coordination: local
redis_url: "redis://127.0.0.1:6379/0"
audit: {stream_key: "ahu.ai.audit", spool_dir: "/tmp/spool", body_cap_bytes: 10485760}
hold_budgets_ms: {interactive: 45000, batch: 120000, system: 10000}
route_prefixes: {synthesis: ext-397b}
tenants:
  - {id: ahu-chatbot, token: "tok-chatbot"}
upstreams:
  - id: qwen-35b
    type: llm-openai
    class: on_prem
    endpoints: ["http://192.168.83.20:8001/v1"]
    models: ["Qwen/Qwen3.6-35B-A3B-FP8", "qwen-35b"]
    slots: {total: 16, batch_max: 8}
  - id: ext-397b
    type: llm-openai
    class: external_dev
    endpoints: ["https://example.com/v1"]
    api_key_env: DASHSCOPE_API_KEY
    models: ["qwen3.5-397b-a17b"]
    slots: {total: 8, batch_max: 4}
`

func write(t *testing.T, s string) string {
    t.Helper()
    p := filepath.Join(t.TempDir(), "g.yaml")
    if err := os.WriteFile(p, []byte(s), 0o644); err != nil {
        t.Fatal(err)
    }
    return p
}

func TestLoadValid(t *testing.T) {
    c, err := Load(write(t, sample))
    if err != nil {
        t.Fatal(err)
    }
    if c.Listen != ":8200" || len(c.Upstreams) != 2 {
        t.Fatalf("bad parse: %+v", c)
    }
    if c.HoldBudgetsMs[ClassInteractive] != 45000 {
        t.Fatalf("budget: %v", c.HoldBudgetsMs)
    }
    if c.Upstreams[0].ProbePath != "/models" {
        t.Fatalf("default probe path missing: %q", c.Upstreams[0].ProbePath)
    }
}

func TestLoadRejectsBadClassAndDupModel(t *testing.T) {
    if _, err := Load(write(t, `listen: ":1"
upstreams: [{id: a, type: llm-openai, class: cloud, endpoints: ["http://x"], models: ["m"], slots: {total: 1, batch_max: 1}}]`)); err == nil {
        t.Fatal("bad class accepted")
    }
    if _, err := Load(write(t, `listen: ":1"
upstreams:
  - {id: a, type: llm-openai, class: on_prem, endpoints: ["http://x"], models: ["m"], slots: {total: 1, batch_max: 1}}
  - {id: b, type: llm-openai, class: on_prem, endpoints: ["http://y"], models: ["m"], slots: {total: 1, batch_max: 1}}`)); err == nil {
        t.Fatal("duplicate model accepted")
    }
}
  • [ ] Step 3: Run test to verify it fails

Run: go test ./internal/config/ -run TestLoad -v — Expected: FAIL (package does not compile / Load undefined).

  • [ ] Step 4: Implement

internal/config/config.go:

package config

import (
    "fmt"
    "os"

    "gopkg.in/yaml.v3"
)

type Class string

const (
    ClassInteractive Class = "interactive"
    ClassBatch       Class = "batch"
    ClassSystem      Class = "system"
)

type AuditConfig struct {
    StreamKey    string `yaml:"stream_key"`
    SpoolDir     string `yaml:"spool_dir"`
    BodyCapBytes int    `yaml:"body_cap_bytes"`
}

type Tenant struct {
    ID    string `yaml:"id"`
    Token string `yaml:"token"`
}

type Slots struct {
    Total    int `yaml:"total"`
    BatchMax int `yaml:"batch_max"`
}

type Upstream struct {
    ID        string   `yaml:"id"`
    Type      string   `yaml:"type"`  // llm-openai | embedding | vlm | ocr-http
    Class     string   `yaml:"class"` // on_prem | external_dev
    Endpoints []string `yaml:"endpoints"`
    Models    []string `yaml:"models"`
    APIKeyEnv string   `yaml:"api_key_env"`
    ProbePath string   `yaml:"probe_path"`
    Slots     Slots    `yaml:"slots"`
}

type Config struct {
    Listen                 string            `yaml:"listen"`
    AllowExternalUpstreams bool              `yaml:"allow_external_upstreams"`
    Coordination           string            `yaml:"coordination"` // local | redis
    RedisURL               string            `yaml:"redis_url"`
    MaxBodyBytes           int64             `yaml:"max_body_bytes"`
    Audit                  AuditConfig       `yaml:"audit"`
    HoldBudgetsMs          map[Class]int     `yaml:"hold_budgets_ms"`
    RoutePrefixes          map[string]string `yaml:"route_prefixes"`
    Tenants                []Tenant          `yaml:"tenants"`
    Upstreams              []Upstream        `yaml:"upstreams"`
}

func Load(path string) (*Config, error) {
    b, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }
    var c Config
    if err := yaml.Unmarshal(b, &c); err != nil {
        return nil, err
    }
    if c.MaxBodyBytes == 0 {
        c.MaxBodyBytes = 32 << 20
    }
    if c.Audit.BodyCapBytes == 0 {
        c.Audit.BodyCapBytes = 10485760
    }
    if c.HoldBudgetsMs == nil {
        c.HoldBudgetsMs = map[Class]int{}
    }
    for cl, def := range map[Class]int{ClassInteractive: 45000, ClassBatch: 120000, ClassSystem: 10000} {
        if _, ok := c.HoldBudgetsMs[cl]; !ok {
            c.HoldBudgetsMs[cl] = def
        }
    }
    if c.Coordination == "" {
        c.Coordination = "local"
    }
    seen := map[string]string{}
    ids := map[string]bool{}
    for i := range c.Upstreams {
        u := &c.Upstreams[i]
        if u.Class != "on_prem" && u.Class != "external_dev" {
            return nil, fmt.Errorf("upstream %s: invalid class %q", u.ID, u.Class)
        }
        if ids[u.ID] {
            return nil, fmt.Errorf("duplicate upstream id %s", u.ID)
        }
        ids[u.ID] = true
        if len(u.Endpoints) == 0 || len(u.Models) == 0 {
            return nil, fmt.Errorf("upstream %s: endpoints and models required", u.ID)
        }
        if u.ProbePath == "" {
            u.ProbePath = "/models"
        }
        if u.Slots.Total <= 0 {
            return nil, fmt.Errorf("upstream %s: slots.total must be > 0", u.ID)
        }
        if u.Slots.BatchMax <= 0 || u.Slots.BatchMax > u.Slots.Total {
            u.Slots.BatchMax = u.Slots.Total
        }
        for _, m := range u.Models {
            if prev, dup := seen[m]; dup {
                return nil, fmt.Errorf("model %q claimed by %s and %s", m, prev, u.ID)
            }
            seen[m] = u.ID
        }
    }
    for pfx, id := range c.RoutePrefixes {
        if !ids[id] {
            return nil, fmt.Errorf("route_prefixes.%s: unknown upstream %s", pfx, id)
        }
    }
    return &c, nil
}
  • [ ] Step 5: Run tests to verify they pass

Run: go get gopkg.in/yaml.v3 && go test ./internal/config/ -v — Expected: PASS (both tests).

  • [ ] Step 6: Commit
git add go.mod go.sum internal/config/
git commit -m "feat(config): static registry config loader with validation

Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT"

Task 2: Registry — model resolution + endpoint health + prober

Files:
- Create: internal/registry/registry.go
- Test: internal/registry/registry_test.go

Interfaces:
- Consumes: config.Config, config.Upstream.
- Produces: registry.New(cfg *config.Config) *Registry; methods Resolve(model string) (*config.Upstream, error), ResolvePrefix(prefix string) (*config.Upstream, error), PickEndpoint(upstreamID string) (string, error), MarkDown(upstreamID, endpoint string), MarkUp(upstreamID, endpoint string), Health() map[string]map[string]bool, StartProber(ctx context.Context, interval time.Duration). Errors: ErrUnknownModel, ErrNoHealthyEndpoint.

  • [ ] Step 1: Write the failing test

internal/registry/registry_test.go:

package registry

import (
    "context"
    "net/http"
    "net/http/httptest"
    "testing"
    "time"

    "datahive.id/ahu-gpu-manager/internal/config"
)

func cfg(eps ...string) *config.Config {
    return &config.Config{Upstreams: []config.Upstream{{
        ID: "u1", Type: "llm-openai", Class: "on_prem",
        Endpoints: eps, Models: []string{"m1"}, ProbePath: "/models",
        Slots: config.Slots{Total: 2, BatchMax: 2},
    }}, RoutePrefixes: map[string]string{"synthesis": "u1"}}
}

func TestResolveAndRoundRobin(t *testing.T) {
    r := New(cfg("http://a/v1", "http://b/v1"))
    u, err := r.Resolve("m1")
    if err != nil || u.ID != "u1" {
        t.Fatalf("resolve: %v %v", u, err)
    }
    if _, err := r.Resolve("nope"); err != ErrUnknownModel {
        t.Fatalf("want ErrUnknownModel, got %v", err)
    }
    if u, err := r.ResolvePrefix("synthesis"); err != nil || u.ID != "u1" {
        t.Fatalf("prefix: %v %v", u, err)
    }
    e1, _ := r.PickEndpoint("u1")
    e2, _ := r.PickEndpoint("u1")
    if e1 == e2 {
        t.Fatalf("round robin broken: %s %s", e1, e2)
    }
    r.MarkDown("u1", "http://a/v1")
    for i := 0; i < 4; i++ {
        e, err := r.PickEndpoint("u1")
        if err != nil || e != "http://b/v1" {
            t.Fatalf("down endpoint served: %s %v", e, err)
        }
    }
    r.MarkDown("u1", "http://b/v1")
    if _, err := r.PickEndpoint("u1"); err != ErrNoHealthyEndpoint {
        t.Fatalf("want ErrNoHealthyEndpoint, got %v", err)
    }
}

func TestProberRecoversEndpoint(t *testing.T) {
    up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(200) }))
    defer up.Close()
    r := New(cfg(up.URL))
    r.MarkDown("u1", up.URL)
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    r.StartProber(ctx, 20*time.Millisecond)
    deadline := time.Now().Add(2 * time.Second)
    for time.Now().Before(deadline) {
        if _, err := r.PickEndpoint("u1"); err == nil {
            return
        }
        time.Sleep(20 * time.Millisecond)
    }
    t.Fatal("prober never recovered endpoint")
}
  • [ ] Step 2: Run test to verify it fails

Run: go test ./internal/registry/ -v — Expected: FAIL (undefined: New).

  • [ ] Step 3: Implement

internal/registry/registry.go:

package registry

import (
    "context"
    "errors"
    "net/http"
    "strings"
    "sync"
    "time"

    "datahive.id/ahu-gpu-manager/internal/config"
)

var (
    ErrUnknownModel      = errors.New("unknown model")
    ErrNoHealthyEndpoint = errors.New("no healthy endpoint")
    ErrUnknownUpstream   = errors.New("unknown upstream")
)

type epState struct {
    url     string
    healthy bool
}

type upState struct {
    cfg  *config.Upstream
    eps  []*epState
    next int
}

type Registry struct {
    mu       sync.Mutex
    byModel  map[string]*upState
    byID     map[string]*upState
    byPrefix map[string]*upState
    client   *http.Client
}

func New(c *config.Config) *Registry {
    r := &Registry{
        byModel: map[string]*upState{}, byID: map[string]*upState{}, byPrefix: map[string]*upState{},
        client: &http.Client{Timeout: 5 * time.Second},
    }
    for i := range c.Upstreams {
        u := &c.Upstreams[i]
        st := &upState{cfg: u}
        for _, e := range u.Endpoints {
            st.eps = append(st.eps, &epState{url: e, healthy: true})
        }
        r.byID[u.ID] = st
        for _, m := range u.Models {
            r.byModel[m] = st
        }
    }
    for pfx, id := range c.RoutePrefixes {
        r.byPrefix[pfx] = r.byID[id]
    }
    return r
}

func (r *Registry) Resolve(model string) (*config.Upstream, error) {
    r.mu.Lock()
    defer r.mu.Unlock()
    if st, ok := r.byModel[model]; ok {
        return st.cfg, nil
    }
    return nil, ErrUnknownModel
}

func (r *Registry) ResolvePrefix(prefix string) (*config.Upstream, error) {
    r.mu.Lock()
    defer r.mu.Unlock()
    if st, ok := r.byPrefix[prefix]; ok {
        return st.cfg, nil
    }
    return nil, ErrUnknownUpstream
}

func (r *Registry) PickEndpoint(id string) (string, error) {
    r.mu.Lock()
    defer r.mu.Unlock()
    st, ok := r.byID[id]
    if !ok {
        return "", ErrUnknownUpstream
    }
    n := len(st.eps)
    for i := 0; i < n; i++ {
        ep := st.eps[(st.next+i)%n]
        if ep.healthy {
            st.next = (st.next + i + 1) % n
            return ep.url, nil
        }
    }
    return "", ErrNoHealthyEndpoint
}

func (r *Registry) setHealth(id, url string, h bool) {
    r.mu.Lock()
    defer r.mu.Unlock()
    if st, ok := r.byID[id]; ok {
        for _, ep := range st.eps {
            if ep.url == url {
                ep.healthy = h
            }
        }
    }
}

func (r *Registry) MarkDown(id, url string) { r.setHealth(id, url, false) }
func (r *Registry) MarkUp(id, url string)   { r.setHealth(id, url, true) }

func (r *Registry) Health() map[string]map[string]bool {
    r.mu.Lock()
    defer r.mu.Unlock()
    out := map[string]map[string]bool{}
    for id, st := range r.byID {
        m := map[string]bool{}
        for _, ep := range st.eps {
            m[ep.url] = ep.healthy
        }
        out[id] = m
    }
    return out
}

// StartProber re-checks every endpoint (healthy or not) so downed endpoints recover
// and silently-dead ones are caught before a request trips on them.
func (r *Registry) StartProber(ctx context.Context, interval time.Duration) {
    go func() {
        t := time.NewTicker(interval)
        defer t.Stop()
        for {
            select {
            case <-ctx.Done():
                return
            case <-t.C:
                r.mu.Lock()
                type probe struct{ id, url, path string }
                var ps []probe
                for id, st := range r.byID {
                    for _, ep := range st.eps {
                        ps = append(ps, probe{id, ep.url, st.cfg.ProbePath})
                    }
                }
                r.mu.Unlock()
                for _, p := range ps {
                    u := strings.TrimSuffix(p.url, "/") + p.path
                    resp, err := r.client.Get(u)
                    ok := err == nil && resp.StatusCode < 500
                    if resp != nil {
                        resp.Body.Close()
                    }
                    r.setHealth(p.id, p.url, ok)
                }
            }
        }
    }()
}
  • [ ] Step 4: Run tests

Run: go test ./internal/registry/ -v -race — Expected: PASS.

  • [ ] Step 5: Commit
git add internal/registry/
git commit -m "feat(registry): model routing, endpoint health, active prober

Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT"

Task 3: LocalPool — priority admission with queue position

Files:
- Create: internal/pool/pool.go
- Test: internal/pool/pool_test.go

Interfaces:
- Consumes: config.Class, config.Slots.
- Produces:

type Ticket interface {
    Ready() <-chan struct{} // closed when a slot is held
    Pos() int               // 1-based queue position; 0 once admitted
    Release()               // free the slot (only after Ready)
    Cancel()                // abandon while waiting
}
type Pool interface {
    Enqueue(class config.Class) Ticket
    Depth() map[config.Class]int // waiting per class
    Active() int                 // slots in use
}
func NewLocal(s config.Slots) Pool

Class priority order: system < batch < interactive is WRONG — the order is: interactive admits first, then system, then batch; FIFO within a class. batch_max bounds concurrently-active batch slots.

  • [ ] Step 1: Write the failing test

internal/pool/pool_test.go:

package pool

import (
    "testing"
    "time"

    "datahive.id/ahu-gpu-manager/internal/config"
)

func admitted(t Ticket) bool {
    select {
    case <-t.Ready():
        return true
    case <-time.After(50 * time.Millisecond):
        return false
    }
}

func TestCapacityAndFIFO(t *testing.T) {
    p := NewLocal(config.Slots{Total: 1, BatchMax: 1})
    a := p.Enqueue(config.ClassInteractive)
    if !admitted(a) {
        t.Fatal("first not admitted")
    }
    b := p.Enqueue(config.ClassInteractive)
    if admitted(b) {
        t.Fatal("second admitted over capacity")
    }
    if b.Pos() != 1 {
        t.Fatalf("pos = %d, want 1", b.Pos())
    }
    a.Release()
    if !admitted(b) {
        t.Fatal("b not admitted after release")
    }
    b.Release()
}

func TestInteractiveJumpsBatch(t *testing.T) {
    p := NewLocal(config.Slots{Total: 1, BatchMax: 1})
    hold := p.Enqueue(config.ClassBatch)
    admitted(hold)
    b := p.Enqueue(config.ClassBatch)
    i := p.Enqueue(config.ClassInteractive)
    hold.Release()
    if !admitted(i) {
        t.Fatal("interactive did not jump batch")
    }
    if admitted(b) {
        t.Fatal("batch admitted while interactive held the slot")
    }
    i.Release()
    if !admitted(b) {
        t.Fatal("batch never admitted")
    }
    b.Release()
}

func TestBatchMaxLeavesHeadroom(t *testing.T) {
    p := NewLocal(config.Slots{Total: 2, BatchMax: 1})
    b1 := p.Enqueue(config.ClassBatch)
    admitted(b1)
    b2 := p.Enqueue(config.ClassBatch)
    if admitted(b2) {
        t.Fatal("batch exceeded batch_max")
    }
    i := p.Enqueue(config.ClassInteractive)
    if !admitted(i) {
        t.Fatal("interactive blocked despite free slot")
    }
    if p.Active() != 2 || p.Depth()[config.ClassBatch] != 1 {
        t.Fatalf("active=%d depth=%v", p.Active(), p.Depth())
    }
    b2.Cancel()
    if p.Depth()[config.ClassBatch] != 0 {
        t.Fatal("cancel did not dequeue")
    }
}
  • [ ] Step 2: Run test to verify it fails

Run: go test ./internal/pool/ -v — Expected: FAIL (undefined: NewLocal).

  • [ ] Step 3: Implement

internal/pool/pool.go:

package pool

import (
    "sync"

    "datahive.id/ahu-gpu-manager/internal/config"
)

type Ticket interface {
    Ready() <-chan struct{}
    Pos() int
    Release()
    Cancel()
}

type Pool interface {
    Enqueue(class config.Class) Ticket
    Depth() map[config.Class]int
    Active() int
}

var rank = map[config.Class]int{config.ClassInteractive: 0, config.ClassSystem: 1, config.ClassBatch: 2}

type ticket struct {
    p        *localPool
    class    config.Class
    seq      uint64
    ready    chan struct{}
    admitted bool
    done     bool
}

func (t *ticket) Ready() <-chan struct{} { return t.ready }

func (t *ticket) Pos() int {
    t.p.mu.Lock()
    defer t.p.mu.Unlock()
    if t.admitted {
        return 0
    }
    pos := 0
    for _, w := range t.p.waiting {
        if before(w, t) || w == t {
            pos++
        }
        if w == t {
            return pos
        }
    }
    return pos
}

func (t *ticket) Release() {
    t.p.mu.Lock()
    defer t.p.mu.Unlock()
    if !t.admitted || t.done {
        return
    }
    t.done = true
    t.p.active--
    if t.class == config.ClassBatch {
        t.p.activeBatch--
    }
    t.p.pump()
}

func (t *ticket) Cancel() {
    t.p.mu.Lock()
    defer t.p.mu.Unlock()
    if t.admitted {
        return
    }
    for i, w := range t.p.waiting {
        if w == t {
            t.p.waiting = append(t.p.waiting[:i], t.p.waiting[i+1:]...)
            break
        }
    }
    t.done = true
    t.p.pump()
}

func before(a, b *ticket) bool {
    if rank[a.class] != rank[b.class] {
        return rank[a.class] < rank[b.class]
    }
    return a.seq < b.seq
}

type localPool struct {
    mu          sync.Mutex
    slots       config.Slots
    seq         uint64
    active      int
    activeBatch int
    waiting     []*ticket // kept sorted by (rank, seq)
}

func NewLocal(s config.Slots) Pool { return &localPool{slots: s} }

func (p *localPool) Enqueue(class config.Class) Ticket {
    p.mu.Lock()
    defer p.mu.Unlock()
    p.seq++
    t := &ticket{p: p, class: class, seq: p.seq, ready: make(chan struct{})}
    i := 0
    for ; i < len(p.waiting); i++ {
        if before(t, p.waiting[i]) {
            break
        }
    }
    p.waiting = append(p.waiting[:i], append([]*ticket{t}, p.waiting[i:]...)...)
    p.pump()
    return t
}

// pump admits from the head of the queue while capacity allows. Callers hold p.mu.
func (p *localPool) pump() {
    for i := 0; i < len(p.waiting); {
        t := p.waiting[i]
        if p.active >= p.slots.Total {
            return
        }
        if t.class == config.ClassBatch && p.activeBatch >= p.slots.BatchMax {
            i++ // batch head blocked; a later interactive/system waiter may still fit
            continue
        }
        p.waiting = append(p.waiting[:i], p.waiting[i+1:]...)
        t.admitted = true
        p.active++
        if t.class == config.ClassBatch {
            p.activeBatch++
        }
        close(t.ready)
    }
}

func (p *localPool) Depth() map[config.Class]int {
    p.mu.Lock()
    defer p.mu.Unlock()
    d := map[config.Class]int{}
    for _, w := range p.waiting {
        d[w.class]++
    }
    return d
}

func (p *localPool) Active() int {
    p.mu.Lock()
    defer p.mu.Unlock()
    return p.active
}
  • [ ] Step 4: Run tests

Run: go test ./internal/pool/ -v -race — Expected: PASS (3 tests).

  • [ ] Step 5: Commit
git add internal/pool/
git commit -m "feat(pool): local priority slot pool with batch_max and queue position

Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT"

Task 4: Audit event schema + emitter (XADD, spool fallback, replay)

Files:
- Create: internal/audit/event.go, internal/audit/emitter.go
- Test: internal/audit/emitter_test.go

Interfaces:
- Produces:

// event.go
type Event struct { /* all schema-v1 fields, json tags below */ }
func NewID() string // ULID
// emitter.go
func NewEmitter(rdb redis.UniversalClient, streamKey, spoolDir string) *Emitter
func (e *Emitter) Emit(ev Event)                 // never blocks
func (e *Emitter) Run(ctx context.Context)       // background loop
func (e *Emitter) Flush(timeout time.Duration)   // used at drain
func (e *Emitter) Dropped() uint64

Stream entry format: single field json holding the marshalled Event ([]byte body fields marshal as base64; bodies are zstd-compressed by the proxy before Emit).

  • [ ] Step 1: Write the failing test

internal/audit/emitter_test.go:

package audit

import (
    "context"
    "encoding/json"
    "os"
    "path/filepath"
    "testing"
    "time"

    "github.com/alicebob/miniredis/v2"
    "github.com/redis/go-redis/v9"
)

func ev(id string) Event {
    return Event{EventID: id, SchemaVer: 1, TS: time.Now().UTC().Format(time.RFC3339Nano),
        Engine: "ahu-chatbot", Operation: "chat", Status: "ok", Model: "m1"}
}

func TestEmitReachesStream(t *testing.T) {
    mr := miniredis.RunT(t)
    rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
    e := NewEmitter(rdb, "ahu.ai.audit", t.TempDir())
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    go e.Run(ctx)
    e.Emit(ev("01A"))
    deadline := time.Now().Add(2 * time.Second)
    for time.Now().Before(deadline) {
        if msgs, _ := rdb.XRange(context.Background(), "ahu.ai.audit", "-", "+").Result(); len(msgs) == 1 {
            var got Event
            if err := json.Unmarshal([]byte(msgs[0].Values["json"].(string)), &got); err != nil || got.EventID != "01A" {
                t.Fatalf("bad payload: %v %v", got, err)
            }
            return
        }
        time.Sleep(10 * time.Millisecond)
    }
    t.Fatal("event never reached stream")
}

func TestSpoolOnRedisDownThenReplay(t *testing.T) {
    mr := miniredis.RunT(t)
    rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
    dir := t.TempDir()
    e := NewEmitter(rdb, "ahu.ai.audit", dir)
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    go e.Run(ctx)

    mr.SetError("down") // all commands fail
    e.Emit(ev("01B"))
    deadline := time.Now().Add(2 * time.Second)
    spooled := func() bool {
        fs, _ := filepath.Glob(filepath.Join(dir, "*.jsonl"))
        for _, f := range fs {
            if b, _ := os.ReadFile(f); len(b) > 0 {
                return true
            }
        }
        return false
    }
    for time.Now().Before(deadline) && !spooled() {
        time.Sleep(10 * time.Millisecond)
    }
    if !spooled() {
        t.Fatal("event not spooled while redis down")
    }

    mr.SetError("") // recover
    deadline = time.Now().Add(3 * time.Second)
    for time.Now().Before(deadline) {
        if msgs, _ := rdb.XRange(context.Background(), "ahu.ai.audit", "-", "+").Result(); len(msgs) == 1 && !spooled() {
            return
        }
        time.Sleep(20 * time.Millisecond)
    }
    t.Fatal("spooled event never replayed to stream")
}
  • [ ] Step 2: Run test to verify it fails

Run: go get github.com/redis/go-redis/v9 github.com/alicebob/miniredis/v2 github.com/oklog/ulid/v2 && go test ./internal/audit/ -v — Expected: FAIL (undefined: NewEmitter).

  • [ ] Step 3: Implement

internal/audit/event.go:

package audit

import (
    "crypto/rand"
    "time"

    "github.com/oklog/ulid/v2"
)

type Event struct {
    EventID        string `json:"event_id"`
    SchemaVer      int    `json:"schema_ver"`
    TS             string `json:"ts"`
    Engine         string `json:"engine"`
    Surface        string `json:"surface,omitempty"`
    UserID         string `json:"user_id,omitempty"`
    TraceID        string `json:"trace_id,omitempty"`
    Upstream       string `json:"upstream,omitempty"`
    Model          string `json:"model,omitempty"`
    UpstreamClass  string `json:"upstream_class,omitempty"`
    TrafficClass   string `json:"traffic_class,omitempty"`
    Operation      string `json:"operation"`
    Status         string `json:"status"`
    ErrorCode      string `json:"error_code,omitempty"`
    QueueMs        int64  `json:"queue_ms"`
    UpstreamMs     int64  `json:"upstream_ms"`
    TotalMs        int64  `json:"total_ms"`
    TokensIn       *int   `json:"tokens_in,omitempty"`
    TokensOut      *int   `json:"tokens_out,omitempty"`
    DocHash        string `json:"doc_hash,omitempty"`
    Pages          *int   `json:"pages,omitempty"`
    JobID          string `json:"job_id,omitempty"`
    ParentJobID    string `json:"parent_job_id,omitempty"`
    ConfigVersion  string `json:"config_version,omitempty"`
    RequestBodyZst []byte `json:"request_body_zst,omitempty"`
    ResponseBodyZst []byte `json:"response_body_zst,omitempty"`
    BodyTruncated  bool   `json:"body_truncated,omitempty"`
    FullBodySHA256 string `json:"full_body_sha256,omitempty"`
}

func NewID() string { return ulid.MustNew(ulid.Timestamp(time.Now()), rand.Reader).String() }

internal/audit/emitter.go:

package audit

import (
    "bufio"
    "context"
    "encoding/json"
    "fmt"
    "os"
    "path/filepath"
    "sync/atomic"
    "time"

    "github.com/redis/go-redis/v9"
)

const maxStreamLen = 1_000_000

type Emitter struct {
    rdb       redis.UniversalClient
    streamKey string
    spoolDir  string
    ch        chan Event
    dropped   atomic.Uint64
}

func NewEmitter(rdb redis.UniversalClient, streamKey, spoolDir string) *Emitter {
    return &Emitter{rdb: rdb, streamKey: streamKey, spoolDir: spoolDir, ch: make(chan Event, 10000)}
}

func (e *Emitter) Emit(ev Event) {
    select {
    case e.ch <- ev:
    default: // channel full: spool synchronously rather than block the request path
        if err := e.spool(ev); err != nil {
            e.dropped.Add(1)
        }
    }
}

func (e *Emitter) Dropped() uint64 { return e.dropped.Load() }

func (e *Emitter) xadd(ctx context.Context, ev Event) error {
    b, err := json.Marshal(ev)
    if err != nil {
        return err
    }
    return e.rdb.XAdd(ctx, &redis.XAddArgs{
        Stream: e.streamKey, MaxLen: maxStreamLen, Approx: true,
        Values: map[string]any{"json": b},
    }).Err()
}

func (e *Emitter) spool(ev Event) error {
    if err := os.MkdirAll(e.spoolDir, 0o755); err != nil {
        return err
    }
    f, err := os.OpenFile(filepath.Join(e.spoolDir, time.Now().UTC().Format("2006-01-02")+".jsonl"),
        os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
    if err != nil {
        return err
    }
    defer f.Close()
    b, err := json.Marshal(ev)
    if err != nil {
        return err
    }
    _, err = fmt.Fprintf(f, "%s\n", b)
    return err
}

func (e *Emitter) Run(ctx context.Context) {
    replay := time.NewTicker(2 * time.Second)
    defer replay.Stop()
    for {
        select {
        case <-ctx.Done():
            return
        case ev := <-e.ch:
            if err := e.xadd(ctx, ev); err != nil {
                if err := e.spool(ev); err != nil {
                    e.dropped.Add(1)
                }
            }
        case <-replay.C:
            e.replaySpool(ctx)
        }
    }
}

var maxSpoolLine = 64 << 20 // spool lines carry two base64'd 10MB-capped bodies; keep headroom

func (e *Emitter) replaySpool(ctx context.Context) {
    files, _ := filepath.Glob(filepath.Join(e.spoolDir, "*.jsonl"))
    for _, fp := range files {
        f, err := os.Open(fp)
        if err != nil {
            continue
        }
        sc := bufio.NewScanner(f)
        sc.Buffer(make([]byte, 0, 64*1024), maxSpoolLine)
        ok := true
        for sc.Scan() {
            var ev Event
            if json.Unmarshal(sc.Bytes(), &ev) != nil {
                continue // skip corrupt line
            }
            if e.xadd(ctx, ev) != nil {
                ok = false
                break
            }
        }
        // A scan error (e.g. ErrTooLong) means unread lines remain: removing the
        // file here would be silent data loss — treat it like a mid-file failure.
        if sc.Err() != nil {
            ok = false
        }
        f.Close()
        if ok {
            os.Remove(fp)
        } else {
            return // redis still down or file unreadable; retry next tick
        }
    }
}

func (e *Emitter) Flush(timeout time.Duration) {
    deadline := time.Now().Add(timeout)
    for time.Now().Before(deadline) {
        select {
        case ev := <-e.ch:
            ctx, cancel := context.WithTimeout(context.Background(), time.Second)
            if e.xadd(ctx, ev) != nil {
                _ = e.spool(ev)
            }
            cancel()
        default:
            return
        }
    }
}

Note: replay can duplicate an event if the process dies between XADD and file removal — the observatory ingester dedupes on event_id (its plan), so at-least-once is the contract here.

  • [ ] Step 4: Run tests

Run: go test ./internal/audit/ -v -race — Expected: PASS (2 tests).

  • [ ] Step 5: Commit
git add internal/audit/ go.mod go.sum
git commit -m "feat(audit): schema v1 event + fire-and-forget emitter with disk spool

Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT"

Task 5: Proxy core — non-streaming requests end-to-end

Files:
- Create: internal/proxy/handler.go, internal/proxy/stats.go
- Test: internal/proxy/handler_test.go (+ shared test stub internal/proxy/stub_test.go)

Interfaces:
- Consumes: registry.Registry, pool.Pool/pool.Ticket, audit.Emitter/audit.Event, config.*.
- Produces:

type Deps struct {
    Cfg      *config.Config
    Reg      *registry.Registry
    Pools    map[string]pool.Pool // key: upstream ID
    Emit     func(audit.Event)
    Idem     IdemCache            // Task 8 provides RedisIdem; nil disables
    Stats    *Stats
}
func NewHandler(d Deps) http.Handler // serves /v1/* and /synthesis/v1/*
type Meta struct{ Tenant, Surface, UserID, TraceID, IdemKey string; Class config.Class; QueueEvents bool }
func MetaFrom(r *http.Request, tokenTenants map[string]string) Meta
type IdemCache interface {
    Get(ctx context.Context, tenant, key string) (status int, body []byte, ok bool)
    Put(ctx context.Context, tenant, key string, status int, body []byte)
}
// stats.go
type Stats struct{ ... }
func NewStats() *Stats
func (s *Stats) Observe(upstream string, ms int64)
func (s *Stats) ETAMs(upstream string, depth, slots int) int64

Behavior contract (asserted by tests): unknown model → 404 {"error":{"code":"UNKNOWN_MODEL"}}; external_dev while disallowed → 403 {"error":{"code":"EXTERNAL_UPSTREAM_FORBIDDEN"}}; pool saturation past hold budget → 429 + Retry-After: 5 + X-Queue-Depth; upstream connect failure after both endpoints tried → 503 {"error":{"code":"UPSTREAM_DOWN"}}; success relays status/headers/body and emits one audit Event with zstd bodies, timings, and tokens from usage.

  • [ ] Step 1: Write the stub upstream test helper

internal/proxy/stub_test.go:

package proxy

import (
    "encoding/json"
    "fmt"
    "net/http"
    "net/http/httptest"
    "sync/atomic"
    "time"
)

type stubLLM struct {
    *httptest.Server
    calls atomic.Int64
    delay time.Duration
    fail  atomic.Bool
}

func newStubLLM() *stubLLM {
    s := &stubLLM{}
    s.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        s.calls.Add(1)
        if s.fail.Load() {
            w.WriteHeader(500)
            return
        }
        time.Sleep(s.delay)
        var req struct {
            Stream bool `json:"stream"`
        }
        json.NewDecoder(r.Body).Decode(&req)
        if req.Stream {
            w.Header().Set("Content-Type", "text/event-stream")
            fl := w.(http.Flusher)
            for i := 0; i < 3; i++ {
                fmt.Fprintf(w, "data: {\"choices\":[{\"delta\":{\"content\":\"tok%d\"}}]}\n\n", i)
                fl.Flush()
                time.Sleep(30 * time.Millisecond)
            }
            fmt.Fprint(w, "data: [DONE]\n\n")
            fl.Flush()
            return
        }
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprint(w, `{"id":"cmpl-1","choices":[{"message":{"content":"hello"}}],"usage":{"prompt_tokens":7,"completion_tokens":3}}`)
    }))
    return s
}
  • [ ] Step 2: Write the failing tests

internal/proxy/handler_test.go:

package proxy

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
    "time"

    "datahive.id/ahu-gpu-manager/internal/audit"
    "datahive.id/ahu-gpu-manager/internal/config"
    "datahive.id/ahu-gpu-manager/internal/pool"
    "datahive.id/ahu-gpu-manager/internal/registry"
)

func testDeps(t *testing.T, upURL string, slots, batchMax int, allowExt bool) (*Deps, *[]audit.Event) {
    cfg := &config.Config{
        AllowExternalUpstreams: allowExt,
        MaxBodyBytes:           32 << 20,
        Audit:                  config.AuditConfig{BodyCapBytes: 10485760},
        HoldBudgetsMs:          map[config.Class]int{config.ClassInteractive: 200, config.ClassBatch: 200, config.ClassSystem: 200},
        Upstreams: []config.Upstream{
            {ID: "u1", Type: "llm-openai", Class: "on_prem", Endpoints: []string{upURL},
                Models: []string{"m1"}, ProbePath: "/models", Slots: config.Slots{Total: slots, BatchMax: batchMax}},
            {ID: "ext", Type: "llm-openai", Class: "external_dev", Endpoints: []string{upURL},
                Models: []string{"m-ext"}, ProbePath: "/models", Slots: config.Slots{Total: 2, BatchMax: 2}},
        },
    }
    var events []audit.Event
    d := &Deps{Cfg: cfg, Reg: registry.New(cfg),
        Pools: map[string]pool.Pool{"u1": pool.NewLocal(cfg.Upstreams[0].Slots), "ext": pool.NewLocal(cfg.Upstreams[1].Slots)},
        Emit:  func(e audit.Event) { events = append(events, e) },
        Stats: NewStats()}
    return d, &events
}

func chat(model string, hdr map[string]string, srv http.Handler) *httptest.ResponseRecorder {
    body, _ := json.Marshal(map[string]any{"model": model, "messages": []map[string]string{{"role": "user", "content": "hi"}}})
    req := httptest.NewRequest("POST", "/v1/chat/completions", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    for k, v := range hdr {
        req.Header.Set(k, v)
    }
    w := httptest.NewRecorder()
    srv.ServeHTTP(w, req)
    return w
}

func TestHappyPathRelaysAndAudits(t *testing.T) {
    up := newStubLLM()
    defer up.Close()
    d, events := testDeps(t, up.URL, 4, 2, true)
    h := NewHandler(*d)
    w := chat("m1", map[string]string{"X-Tenant-Id": "ahu-chatbot", "X-Priority": "batch", "X-Request-Id": "trace-1"}, h)
    if w.Code != 200 || !strings.Contains(w.Body.String(), "hello") {
        t.Fatalf("relay failed: %d %s", w.Code, w.Body.String())
    }
    if len(*events) != 1 {
        t.Fatalf("events = %d", len(*events))
    }
    e := (*events)[0]
    if e.Engine != "ahu-chatbot" || e.TraceID != "trace-1" || e.TrafficClass != "batch" ||
        e.Upstream != "u1" || e.Model != "m1" || e.Status != "ok" || e.Operation != "chat" ||
        e.UpstreamClass != "on_prem" || *e.TokensIn != 7 || *e.TokensOut != 3 ||
        len(e.RequestBodyZst) == 0 || len(e.ResponseBodyZst) == 0 || e.SchemaVer != 1 {
        t.Fatalf("bad event: %+v", e)
    }
}

func TestUnknownModel404(t *testing.T) {
    up := newStubLLM()
    defer up.Close()
    d, _ := testDeps(t, up.URL, 4, 2, true)
    if w := chat("nope", nil, NewHandler(*d)); w.Code != 404 || !strings.Contains(w.Body.String(), "UNKNOWN_MODEL") {
        t.Fatalf("%d %s", w.Code, w.Body.String())
    }
}

func TestExternalForbiddenInProd(t *testing.T) {
    up := newStubLLM()
    defer up.Close()
    d, events := testDeps(t, up.URL, 4, 2, false)
    if w := chat("m-ext", nil, NewHandler(*d)); w.Code != 403 || !strings.Contains(w.Body.String(), "EXTERNAL_UPSTREAM_FORBIDDEN") {
        t.Fatalf("%d %s", w.Code, w.Body.String())
    }
    if len(*events) != 1 || (*events)[0].Status != "error" || (*events)[0].ErrorCode != "EXTERNAL_UPSTREAM_FORBIDDEN" {
        t.Fatalf("refusal not audited: %+v", *events)
    }
}

func TestSaturationReturns429WithRetryAfter(t *testing.T) {
    up := newStubLLM()
    up.delay = 500 * time.Millisecond
    defer up.Close()
    d, _ := testDeps(t, up.URL, 1, 1, true) // hold budget 200ms < 500ms occupancy
    h := NewHandler(*d)
    done := make(chan *httptest.ResponseRecorder, 1)
    go func() { done <- chat("m1", nil, h) }()
    time.Sleep(50 * time.Millisecond) // first request now holds the slot
    w := chat("m1", nil, h)
    if w.Code != 429 || w.Header().Get("Retry-After") == "" || w.Header().Get("X-Queue-Depth") == "" {
        t.Fatalf("want 429+headers, got %d %v", w.Code, w.Header())
    }
    if first := <-done; first.Code != 200 {
        t.Fatalf("first request should succeed, got %d", first.Code)
    }
}

func TestUpstreamDown503(t *testing.T) {
    up := newStubLLM()
    up.Close() // refuse connections
    d, events := testDeps(t, up.URL, 2, 2, true)
    if w := chat("m1", nil, NewHandler(*d)); w.Code != 503 || !strings.Contains(w.Body.String(), "UPSTREAM_DOWN") {
        t.Fatalf("%d %s", w.Code, w.Body.String())
    }
    if (*events)[0].Status != "error" || (*events)[0].ErrorCode != "UPSTREAM_DOWN" {
        t.Fatalf("outage not audited: %+v", *events)
    }
}
  • [ ] Step 3: Run tests to verify they fail

Run: go test ./internal/proxy/ -v — Expected: FAIL (undefined: Deps, NewHandler, NewStats).

  • [ ] Step 4: Implement stats

internal/proxy/stats.go:

package proxy

import "sync"

type Stats struct {
    mu   sync.Mutex
    ewma map[string]float64
}

func NewStats() *Stats { return &Stats{ewma: map[string]float64{}} }

func (s *Stats) Observe(upstream string, ms int64) {
    s.mu.Lock()
    defer s.mu.Unlock()
    prev, ok := s.ewma[upstream]
    if !ok {
        s.ewma[upstream] = float64(ms)
        return
    }
    s.ewma[upstream] = 0.8*prev + 0.2*float64(ms)
}

func (s *Stats) ETAMs(upstream string, depth, slots int) int64 {
    s.mu.Lock()
    defer s.mu.Unlock()
    avg, ok := s.ewma[upstream]
    if !ok || slots <= 0 {
        return 0
    }
    return int64(avg * float64(depth+1) / float64(slots))
}
  • [ ] Step 5: Implement handler

internal/proxy/handler.go:

package proxy

import (
    "bytes"
    "context"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strings"
    "time"

    "github.com/klauspost/compress/zstd"

    "datahive.id/ahu-gpu-manager/internal/audit"
    "datahive.id/ahu-gpu-manager/internal/config"
    "datahive.id/ahu-gpu-manager/internal/pool"
    "datahive.id/ahu-gpu-manager/internal/registry"
)

type IdemCache interface {
    Get(ctx context.Context, tenant, key string) (status int, body []byte, ok bool)
    Put(ctx context.Context, tenant, key string, status int, body []byte)
}

type Deps struct {
    Cfg   *config.Config
    Reg   *registry.Registry
    Pools map[string]pool.Pool
    Emit  func(audit.Event)
    Idem  IdemCache
    Stats *Stats
}

type Meta struct {
    Tenant, Surface, UserID, TraceID, IdemKey string
    Class                                     config.Class
    QueueEvents                               bool
}

func MetaFrom(r *http.Request, tokenTenants map[string]string) Meta {
    m := Meta{
        Tenant:  r.Header.Get("X-Tenant-Id"),
        Surface: r.Header.Get("X-Surface"),
        UserID:  r.Header.Get("X-User-Id"),
        TraceID: r.Header.Get("X-Request-Id"),
        IdemKey: r.Header.Get("Idempotency-Key"),
    }
    if len(tokenTenants) > 0 {
        // Auth mode: identity comes ONLY from the token; a spoofed X-Tenant-Id
        // must not bypass the handler's 401 on unmatched tokens.
        tok := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
        m.Tenant = tokenTenants[tok] // "" when unmatched → handler returns 401
    }
    if m.Tenant == "" {
        m.Tenant = "unknown"
    }
    if m.TraceID == "" {
        m.TraceID = audit.NewID()
    }
    switch r.Header.Get("X-Priority") {
    case "batch":
        m.Class = config.ClassBatch
    case "system":
        m.Class = config.ClassSystem
    case "planning", "synthesis": // legacy values → interactive (CONVENTIONS §2)
        m.Class = config.ClassInteractive
    default:
        m.Class = config.ClassInteractive
    }
    m.QueueEvents = strings.EqualFold(r.Header.Get("X-Queue-Events"), "on")
    return m
}

type handler struct {
    d            Deps
    tokenTenants map[string]string
    client       *http.Client
    zenc         *zstd.Encoder
}

func NewHandler(d Deps) http.Handler {
    tt := map[string]string{}
    for _, t := range d.Cfg.Tenants {
        if t.Token != "" {
            tt[t.Token] = t.ID
        }
    }
    enc, _ := zstd.NewWriter(nil)
    return &handler{d: d, tokenTenants: tt,
        client: &http.Client{Timeout: 0, Transport: &http.Transport{MaxIdleConnsPerHost: 64, ResponseHeaderTimeout: 600 * time.Second}},
        zenc:   enc}
}

func (h *handler) compress(b []byte) (out []byte, truncated bool, fullHash string) {
    cap := h.d.Cfg.Audit.BodyCapBytes
    if len(b) > cap {
        sum := sha256.Sum256(b)
        return h.zenc.EncodeAll(b[:cap], nil), true, hex.EncodeToString(sum[:])
    }
    return h.zenc.EncodeAll(b, nil), false, ""
}

func writeErr(w http.ResponseWriter, status int, code, msg string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(map[string]any{"error": map[string]string{"code": code, "message": msg}})
}

func operationFor(path string) string {
    if strings.Contains(path, "/embeddings") {
        return "embed"
    }
    return "chat"
}

func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    m := MetaFrom(r, h.tokenTenants)
    if len(h.tokenTenants) > 0 && m.Tenant == "unknown" {
        writeErr(w, 401, "UNAUTHENTICATED", "valid bearer token required")
        return
    }

    // Route: /synthesis/v1/* pins an upstream; /v1/* routes by model.
    path := r.URL.Path
    var up *config.Upstream
    var err error
    if rest, ok := strings.CutPrefix(path, "/synthesis"); ok {
        path = rest
        up, err = h.d.Reg.ResolvePrefix("synthesis")
        if err != nil {
            writeErr(w, 404, "UNKNOWN_ROUTE", "no synthesis upstream configured")
            return
        }
    }

    body, err := io.ReadAll(io.LimitReader(r.Body, h.d.Cfg.MaxBodyBytes+1))
    if err != nil || int64(len(body)) > h.d.Cfg.MaxBodyBytes {
        writeErr(w, 413, "BODY_TOO_LARGE", "request body exceeds limit")
        return
    }
    var parsed struct {
        Model  string `json:"model"`
        Stream bool   `json:"stream"`
    }
    _ = json.Unmarshal(body, &parsed)
    if up == nil {
        if up, err = h.d.Reg.Resolve(parsed.Model); err != nil {
            writeErr(w, 404, "UNKNOWN_MODEL", fmt.Sprintf("model %q not in registry", parsed.Model))
            return
        }
    }

    ev := audit.Event{
        EventID: audit.NewID(), SchemaVer: 1, TS: start.UTC().Format(time.RFC3339Nano),
        Engine: m.Tenant, Surface: m.Surface, UserID: m.UserID, TraceID: m.TraceID,
        Upstream: up.ID, Model: parsed.Model, UpstreamClass: up.Class,
        TrafficClass: string(m.Class), Operation: operationFor(path),
    }
    reqZ, trunc, fullHash := h.compress(body)
    ev.RequestBodyZst, ev.BodyTruncated, ev.FullBodySHA256 = reqZ, trunc, fullHash
    finish := func(status, errCode string) {
        ev.Status, ev.ErrorCode = status, errCode
        ev.TotalMs = time.Since(start).Milliseconds()
        h.d.Emit(ev)
    }

    if up.Class == "external_dev" && !h.d.Cfg.AllowExternalUpstreams {
        writeErr(w, 403, "EXTERNAL_UPSTREAM_FORBIDDEN", "external upstreams disabled in this environment")
        finish("error", "EXTERNAL_UPSTREAM_FORBIDDEN")
        return
    }

    // Idempotent replay (non-streaming only).
    if h.d.Idem != nil && m.IdemKey != "" && !parsed.Stream {
        if st, cached, ok := h.d.Idem.Get(r.Context(), m.Tenant, m.IdemKey); ok {
            w.Header().Set("Content-Type", "application/json")
            w.Header().Set("X-Idempotent-Replay", "true")
            w.WriteHeader(st)
            w.Write(cached)
            finish("ok", "")
            return
        }
    }

    // Admission.
    p := h.d.Pools[up.ID]
    tk := p.Enqueue(m.Class)
    hold := time.Duration(h.d.Cfg.HoldBudgetsMs[m.Class]) * time.Millisecond
    queuedAt := time.Now()
    admitted, earlySSE := h.waitForSlot(w, r, tk, hold, parsed.Stream && m.QueueEvents, up.ID, p)
    ev.QueueMs = time.Since(queuedAt).Milliseconds()
    if !admitted {
        if !earlySSE { // headers not yet committed
            w.Header().Set("Retry-After", "5")
            w.Header().Set("X-Queue-Depth", fmt.Sprint(depthTotal(p)))
            writeErr(w, 429, "QUEUE_TIMEOUT", "admission hold budget exceeded")
        }
        finish("error", "QUEUE_TIMEOUT")
        return
    }
    defer tk.Release()

    // Forward, retrying once on a second healthy endpoint after a connect failure.
    upStart := time.Now()
    resp, endpoint, err := h.forward(r.Context(), up, path, r, body)
    ev.UpstreamMs = time.Since(upStart).Milliseconds()
    if err != nil {
        if endpoint != "" {
            h.d.Reg.MarkDown(up.ID, endpoint)
        }
        if !earlySSE {
            writeErr(w, 503, "UPSTREAM_DOWN", "no upstream endpoint reachable")
        } else {
            writeSSEError(w, 503, "UPSTREAM_DOWN")
        }
        finish("error", "UPSTREAM_DOWN")
        return
    }
    defer resp.Body.Close()
    h.d.Stats.Observe(up.ID, ev.UpstreamMs)

    if parsed.Stream {
        respBody, streamTrunc, streamErr := h.pipeStream(w, resp, earlySSE) // Task 6/7
        z, tr, fh := h.compress(respBody)
        ev.ResponseBodyZst = z
        ev.BodyTruncated = ev.BodyTruncated || tr || streamTrunc
        if fh != "" {
            ev.FullBodySHA256 = fh
        }
        ev.TokensIn, ev.TokensOut = tokensFromStream(respBody)
        if streamErr != nil || resp.StatusCode >= 400 {
            finish("error", fmt.Sprintf("UPSTREAM_%d", resp.StatusCode))
            return
        }
        finish("ok", "")
        return
    }

    respBody, _ := io.ReadAll(io.LimitReader(resp.Body, h.d.Cfg.MaxBodyBytes))
    z, tr, fh := h.compress(respBody)
    ev.ResponseBodyZst = z
    ev.BodyTruncated = ev.BodyTruncated || tr
    if fh != "" {
        ev.FullBodySHA256 = fh
    }
    ev.TokensIn, ev.TokensOut = tokensFromJSON(respBody)
    copyHeaders(w.Header(), resp.Header)
    w.WriteHeader(resp.StatusCode)
    w.Write(respBody)
    if resp.StatusCode >= 400 {
        finish("error", fmt.Sprintf("UPSTREAM_%d", resp.StatusCode))
        return
    }
    if h.d.Idem != nil && m.IdemKey != "" {
        h.d.Idem.Put(r.Context(), m.Tenant, m.IdemKey, resp.StatusCode, respBody)
    }
    finish("ok", "")
}

// waitForSlot blocks until admission, hold expiry, or client disconnect.
// Full queue-event/keepalive behavior lands in Task 7; this version waits silently.
func (h *handler) waitForSlot(w http.ResponseWriter, r *http.Request, tk pool.Ticket, hold time.Duration, wantEvents bool, upstreamID string, p pool.Pool) (admitted, earlySSE bool) {
    select {
    case <-tk.Ready():
        return true, false
    case <-time.After(hold):
        cancelOrRelease(tk)
        return false, false
    case <-r.Context().Done():
        cancelOrRelease(tk)
        return false, false
    }
}

// cancelOrRelease abandons a waiting ticket, but if admission raced our
// timeout (Cancel is a no-op on an admitted ticket), the slot MUST be
// released here — the caller is about to return without deferring Release,
// and a leaked slot permanently erodes pool capacity.
func cancelOrRelease(tk pool.Ticket) {
    tk.Cancel()
    select {
    case <-tk.Ready():
        tk.Release()
    default:
    }
}

func writeSSEError(w http.ResponseWriter, status int, code string) {
    fmt.Fprintf(w, "event: error\ndata: {\"status\":%d,\"code\":%q}\n\n", status, code)
    if f, ok := w.(http.Flusher); ok {
        f.Flush()
    }
}

func depthTotal(p pool.Pool) int {
    n := 0
    for _, v := range p.Depth() {
        n += v
    }
    return n
}

func (h *handler) forward(ctx context.Context, up *config.Upstream, path string, orig *http.Request, body []byte) (*http.Response, string, error) {
    var lastEP string
    for attempt := 0; attempt < 2; attempt++ {
        ep, err := h.d.Reg.PickEndpoint(up.ID)
        if err != nil {
            return nil, lastEP, err
        }
        lastEP = ep
        // path arrives as /v1/...; endpoint already ends in /v1 → join after stripping.
        target := strings.TrimSuffix(ep, "/") + strings.TrimPrefix(path, "/v1")
        req, err := http.NewRequestWithContext(ctx, orig.Method, target, bytes.NewReader(body))
        if err != nil {
            return nil, lastEP, err
        }
        req.Header.Set("Content-Type", "application/json")
        if up.APIKeyEnv != "" {
            if k := os.Getenv(up.APIKeyEnv); k != "" {
                req.Header.Set("Authorization", "Bearer "+k)
            }
        }
        resp, err := h.client.Do(req)
        if err == nil {
            return resp, lastEP, nil
        }
        h.d.Reg.MarkDown(up.ID, ep)
    }
    return nil, lastEP, fmt.Errorf("all endpoints failed")
}

func copyHeaders(dst, src http.Header) {
    for k, vv := range src {
        if k == "Connection" || k == "Transfer-Encoding" {
            continue
        }
        for _, v := range vv {
            dst.Add(k, v)
        }
    }
}

func tokensFromJSON(b []byte) (in, out *int) {
    var v struct {
        Usage struct {
            Prompt     *int `json:"prompt_tokens"`
            Completion *int `json:"completion_tokens"`
        } `json:"usage"`
    }
    if json.Unmarshal(b, &v) == nil {
        return v.Usage.Prompt, v.Usage.Completion
    }
    return nil, nil
}

// tokensFromStream scans SSE data lines for a chunk carrying usage (present when the
// client asked for stream_options.include_usage; nil otherwise — tokens stay unknown).
func tokensFromStream(raw []byte) (in, out *int) {
    for _, line := range bytes.Split(raw, []byte("\n")) {
        data, ok := bytes.CutPrefix(line, []byte("data: "))
        if !ok || bytes.Equal(data, []byte("[DONE]")) {
            continue
        }
        if i, o := tokensFromJSON(data); i != nil || o != nil {
            return i, o
        }
    }
    return nil, nil
}

// pipeStream is completed in Task 6; this stub keeps Task 5 compiling for
// non-streaming tests (streaming tests arrive with Task 6).
func (h *handler) pipeStream(w http.ResponseWriter, resp *http.Response, earlySSE bool) ([]byte, bool, error) {
    b, err := io.ReadAll(io.LimitReader(resp.Body, h.d.Cfg.MaxBodyBytes))
    copyHeaders(w.Header(), resp.Header)
    w.WriteHeader(resp.StatusCode)
    w.Write(b)
    return b, false, err
}
  • [ ] Step 6: Run tests

Run: go get github.com/klauspost/compress/zstd && go test ./internal/proxy/ -v -race — Expected: PASS (5 tests).

  • [ ] Step 7: Commit
git add internal/proxy/
git commit -m "feat(proxy): non-streaming OpenAI-compat proxy with admission, audit, enforcement

Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT"

Task 6: Streaming passthrough

Files:
- Modify: internal/proxy/handler.go (replace the pipeStream stub)
- Create: internal/proxy/stream.go
- Test: internal/proxy/stream_test.go

Interfaces:
- Produces: (h *handler) pipeStream(w http.ResponseWriter, resp *http.Response, earlySSE bool) (body []byte, truncated bool, err error) — forwards SSE chunk-by-chunk with per-chunk flush; accumulates only bytes confirmed written to the client (write-then-accumulate, using Write's returned count), capped at Audit.BodyCapBytes with truncated reporting whether forwarded bytes were dropped from the audit copy. When earlySSE is true the 200/SSE headers were already committed by waitForSlot (Task 7), so it must not write status/headers again.

  • [ ] Step 1: Write the failing test

internal/proxy/stream_test.go:

package proxy

import (
    "bufio"
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
    "time"
)

// streamChat performs a real HTTP request (httptest.Server, not Recorder) so
// incremental flushing is observable.
func streamChat(t *testing.T, gwURL, model string, hdr map[string]string) (*http.Response, []string, []time.Time) {
    body, _ := json.Marshal(map[string]any{"model": model, "stream": true,
        "messages": []map[string]string{{"role": "user", "content": "hi"}}})
    req, _ := http.NewRequest("POST", gwURL+"/v1/chat/completions", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    for k, v := range hdr {
        req.Header.Set(k, v)
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        t.Fatal(err)
    }
    var lines []string
    var stamps []time.Time
    sc := bufio.NewScanner(resp.Body)
    for sc.Scan() {
        if l := sc.Text(); l != "" {
            lines = append(lines, l)
            stamps = append(stamps, time.Now())
        }
    }
    resp.Body.Close()
    return resp, lines, stamps
}

func TestStreamingIsIncrementalAndAudited(t *testing.T) {
    up := newStubLLM()
    defer up.Close()
    d, events := testDeps(t, up.URL, 4, 2, true)
    gw := httptest.NewServer(NewHandler(*d))
    defer gw.Close()

    resp, lines, stamps := streamChat(t, gw.URL, "m1", nil)
    if resp.StatusCode != 200 || !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") {
        t.Fatalf("status/ct: %d %s", resp.StatusCode, resp.Header.Get("Content-Type"))
    }
    joined := strings.Join(lines, "\n")
    if !strings.Contains(joined, "tok0") || !strings.Contains(joined, "[DONE]") {
        t.Fatalf("missing chunks: %s", joined)
    }
    // stub sleeps 30ms between chunks: incremental delivery means visible spread.
    if stamps[len(stamps)-1].Sub(stamps[0]) < 40*time.Millisecond {
        t.Fatal("stream appears buffered, not incremental")
    }
    if len(*events) != 1 || (*events)[0].Status != "ok" || len((*events)[0].ResponseBodyZst) == 0 {
        t.Fatalf("stream not audited: %+v", *events)
    }
}
  • [ ] Step 2: Run test to verify it fails

Run: go test ./internal/proxy/ -run TestStreaming -v — Expected: FAIL (the Task-5 stub buffers the whole stream, so the incremental-delivery assertion fails).

  • [ ] Step 3: Implement

Delete the pipeStream stub from handler.go; create internal/proxy/stream.go:

package proxy

import (
    "io"
    "net/http"
)

func (h *handler) pipeStream(w http.ResponseWriter, resp *http.Response, earlySSE bool) ([]byte, bool, error) {
    if !earlySSE {
        copyHeaders(w.Header(), resp.Header)
        w.WriteHeader(resp.StatusCode)
    }
    fl, _ := w.(http.Flusher)
    var acc []byte
    var truncated bool
    buf := make([]byte, 32*1024)
    capBytes := h.d.Cfg.Audit.BodyCapBytes
    for {
        n, err := resp.Body.Read(buf)
        if n > 0 {
            written, werr := w.Write(buf[:n])
            if fl != nil {
                fl.Flush()
            }
            // Audit only bytes confirmed delivered to the client.
            if written > 0 {
                if len(acc) < capBytes {
                    take := written
                    if len(acc)+take > capBytes {
                        take = capBytes - len(acc)
                        truncated = true
                    }
                    acc = append(acc, buf[:take]...)
                } else {
                    truncated = true
                }
            }
            if werr != nil {
                return acc, truncated, werr
            }
        }
        if err == io.EOF {
            return acc, truncated, nil
        }
        if err != nil {
            return acc, truncated, err
        }
    }
}
  • [ ] Step 4: Run tests

Run: go test ./internal/proxy/ -v -race — Expected: PASS (all proxy tests including Task 5's).

  • [ ] Step 5: Commit
git add internal/proxy/
git commit -m "feat(proxy): incremental SSE streaming passthrough with audit tee

Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT"

Task 7: Queue events + opt-in early SSE + keepalives

Files:
- Modify: internal/proxy/handler.go (replace waitForSlot)
- Test: append to internal/proxy/stream_test.go

Interfaces:
- Consumes: pool.Ticket.Pos(), Stats.ETAMs.
- Behavior: only when the request is streaming AND sends X-Queue-Events: on AND admission does not complete within 150 ms, the gateway commits 200 + Content-Type: text/event-stream early and emits event: queue frames ({"position":N,"eta_ms":M}) every 2 s plus on first wait, and : ka comment lines between frames. On later upstream failure it emits event: error (already wired in Task 5 via writeSSEError). Non-opt-in requests hold silently (no bytes) — their client timeout contract is protected by hold budgets.

  • [ ] Step 1: Write the failing test

Append to internal/proxy/stream_test.go:

func TestQueueEventsEmittedWhileHeld(t *testing.T) {
    up := newStubLLM()
    up.delay = 400 * time.Millisecond
    defer up.Close()
    d, _ := testDeps(t, up.URL, 1, 1, true)
    d.Cfg.HoldBudgetsMs[config.ClassInteractive] = 5000
    gw := httptest.NewServer(NewHandler(*d))
    defer gw.Close()

    blocker := make(chan struct{})
    go func() { streamChat(t, gw.URL, "m1", nil); close(blocker) }()
    time.Sleep(50 * time.Millisecond) // blocker holds the only slot

    _, lines, _ := streamChat(t, gw.URL, "m1", map[string]string{"X-Queue-Events": "on"})
    joined := strings.Join(lines, "\n")
    if !strings.Contains(joined, "event: queue") || !strings.Contains(joined, `"position":1`) {
        t.Fatalf("no queue frame before tokens: %s", joined)
    }
    if !strings.Contains(joined, "tok0") {
        t.Fatalf("stream never completed after queueing: %s", joined)
    }
    if strings.Index(joined, "event: queue") > strings.Index(joined, "tok0") {
        t.Fatal("queue frame arrived after first token")
    }
    <-blocker
}

Note: bufio.Scanner drops SSE comment lines' emptiness fine — : ka lines appear as : ka strings; no assertion needed on them.

  • [ ] Step 2: Run test to verify it fails

Run: go test ./internal/proxy/ -run TestQueueEvents -v — Expected: FAIL (silent hold → no event: queue line).

  • [ ] Step 3: Implement

Replace waitForSlot in internal/proxy/handler.go:

func (h *handler) waitForSlot(w http.ResponseWriter, r *http.Request, tk pool.Ticket, hold time.Duration, wantEvents bool, upstreamID string, p pool.Pool) (admitted, earlySSE bool) {
    deadline := time.NewTimer(hold)
    defer deadline.Stop()

    // Fast path: admitted almost immediately → no early commit needed.
    select {
    case <-tk.Ready():
        return true, false
    case <-time.After(150 * time.Millisecond):
    case <-deadline.C:
        cancelOrRelease(tk)
        return false, false
    case <-r.Context().Done():
        cancelOrRelease(tk)
        return false, false
    }

    if !wantEvents {
        select {
        case <-tk.Ready():
            return true, false
        case <-deadline.C:
            cancelOrRelease(tk)
            return false, false
        case <-r.Context().Done():
            cancelOrRelease(tk)
            return false, false
        }
    }

    // Opt-in path: commit SSE early and narrate the queue.
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.WriteHeader(http.StatusOK)
    fl, _ := w.(http.Flusher)
    emit := func() {
        eta := h.d.Stats.ETAMs(upstreamID, tk.Pos()-1, poolSlots(p))
        fmt.Fprintf(w, "event: queue\ndata: {\"position\":%d,\"eta_ms\":%d}\n\n", tk.Pos(), eta)
        if fl != nil {
            fl.Flush()
        }
    }
    emit()
    frame := time.NewTicker(2 * time.Second)
    ka := time.NewTicker(700 * time.Millisecond)
    defer frame.Stop()
    defer ka.Stop()
    for {
        select {
        case <-tk.Ready():
            return true, true
        case <-frame.C:
            emit()
        case <-ka.C:
            fmt.Fprint(w, ": ka\n\n")
            if fl != nil {
                fl.Flush()
            }
        case <-deadline.C:
            cancelOrRelease(tk)
            writeSSEError(w, 429, "QUEUE_TIMEOUT")
            return false, true
        case <-r.Context().Done():
            cancelOrRelease(tk)
            return false, true
        }
    }
}

func poolSlots(p pool.Pool) int { return p.Active() + 1 } // conservative slot estimate for ETA
  • [ ] Step 4: Run tests

Run: go test ./internal/proxy/ -v -race — Expected: PASS (all, including Tasks 5–6).

  • [ ] Step 5: Commit
git add internal/proxy/
git commit -m "feat(proxy): opt-in queue events, keepalives, early-SSE hold narration

Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT"

Task 8: Idempotency cache (Redis)

Files:
- Create: internal/idem/cache.go
- Test: internal/idem/cache_test.go

Interfaces:
- Consumes: implements proxy.IdemCache.
- Produces: idem.NewRedis(rdb redis.UniversalClient, ttl time.Duration) *RedisIdem with Get/Put matching proxy.IdemCache exactly.

  • [ ] Step 1: Write the failing test

internal/idem/cache_test.go:

package idem

import (
    "context"
    "testing"
    "time"

    "github.com/alicebob/miniredis/v2"
    "github.com/redis/go-redis/v9"
)

func TestPutGetIsolatedByTenantAndTTL(t *testing.T) {
    mr := miniredis.RunT(t)
    rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
    c := NewRedis(rdb, 300*time.Second)
    ctx := context.Background()

    if _, _, ok := c.Get(ctx, "t1", "k1"); ok {
        t.Fatal("phantom hit")
    }
    c.Put(ctx, "t1", "k1", 200, []byte(`{"a":1}`))
    st, body, ok := c.Get(ctx, "t1", "k1")
    if !ok || st != 200 || string(body) != `{"a":1}` {
        t.Fatalf("miss: %v %d %s", ok, st, body)
    }
    if _, _, ok := c.Get(ctx, "t2", "k1"); ok {
        t.Fatal("tenant isolation broken")
    }
    mr.FastForward(301 * time.Second)
    if _, _, ok := c.Get(ctx, "t1", "k1"); ok {
        t.Fatal("TTL not applied")
    }
}

func TestGetFailsOpenWhenRedisDown(t *testing.T) {
    mr := miniredis.RunT(t)
    rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
    c := NewRedis(rdb, time.Minute)
    mr.SetError("down")
    if _, _, ok := c.Get(context.Background(), "t1", "k1"); ok {
        t.Fatal("must miss, not error, when redis is down")
    }
    c.Put(context.Background(), "t1", "k1", 200, []byte("x")) // must not panic
}
  • [ ] Step 2: Run test to verify it fails

Run: go test ./internal/idem/ -v — Expected: FAIL (undefined: NewRedis).

  • [ ] Step 3: Implement

internal/idem/cache.go:

package idem

import (
    "context"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "time"

    "github.com/redis/go-redis/v9"
)

type RedisIdem struct {
    rdb redis.UniversalClient
    ttl time.Duration
}

func NewRedis(rdb redis.UniversalClient, ttl time.Duration) *RedisIdem {
    return &RedisIdem{rdb: rdb, ttl: ttl}
}

type entry struct {
    Status int    `json:"status"`
    Body   []byte `json:"body"`
}

func key(tenant, k string) string {
    h := sha256.Sum256([]byte(k))
    return "idem:" + tenant + ":" + hex.EncodeToString(h[:16])
}

func (c *RedisIdem) Get(ctx context.Context, tenant, k string) (int, []byte, bool) {
    b, err := c.rdb.Get(ctx, key(tenant, k)).Bytes()
    if err != nil {
        return 0, nil, false
    }
    var e entry
    if json.Unmarshal(b, &e) != nil {
        return 0, nil, false
    }
    return e.Status, e.Body, true
}

func (c *RedisIdem) Put(ctx context.Context, tenant, k string, status int, body []byte) {
    b, err := json.Marshal(entry{Status: status, Body: body})
    if err != nil {
        return
    }
    _ = c.rdb.Set(ctx, key(tenant, k), b, c.ttl).Err() // best-effort
}
  • [ ] Step 4: Run tests + wire-through test

Run: go test ./internal/idem/ -v — Expected: PASS.

Append a wire-through test to internal/proxy/handler_test.go proving the handler uses it (stub call count stays at 1):

func TestIdempotentReplayAvoidsSecondUpstreamCall(t *testing.T) {
    up := newStubLLM()
    defer up.Close()
    mr := miniredis.RunT(t)
    rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
    d, _ := testDeps(t, up.URL, 4, 2, true)
    d.Idem = idem.NewRedis(rdb, time.Minute)
    h := NewHandler(*d)
    hdr := map[string]string{"Idempotency-Key": "same-key", "X-Tenant-Id": "ahu-ocr"}
    w1 := chat("m1", hdr, h)
    w2 := chat("m1", hdr, h)
    if w1.Code != 200 || w2.Code != 200 {
        t.Fatalf("codes: %d %d", w1.Code, w2.Code)
    }
    if w2.Header().Get("X-Idempotent-Replay") != "true" {
        t.Fatal("second call not served from cache")
    }
    if up.calls.Load() != 1 {
        t.Fatalf("upstream called %d times, want 1", up.calls.Load())
    }
}

(Add imports "github.com/alicebob/miniredis/v2", "github.com/redis/go-redis/v9", "datahive.id/ahu-gpu-manager/internal/idem" to the test file.)

Run: go test ./internal/proxy/ -run TestIdempotent -v — Expected: PASS.

  • [ ] Step 5: Commit
git add internal/idem/ internal/proxy/
git commit -m "feat(idem): redis idempotency cache with tenant isolation, wired into proxy

Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT"

Task 9: Server assembly — mux, healthz, queue status, metrics, drain

Files:
- Create: internal/server/server.go, cmd/gateway/main.go
- Test: internal/server/server_test.go

Interfaces:
- Consumes: everything above.
- Produces: server.New(cfg *config.Config, reg *registry.Registry, pools map[string]pool.Pool, emitter *audit.Emitter, ic proxy.IdemCache, stats *proxy.Stats) *Server with Handler() http.Handler and Shutdown(ctx context.Context). Routes: POST /v1/...+POST /synthesis/v1/... → proxy handler; GET /healthz; GET /queue/status; GET /metrics (Prometheus).

  • [ ] Step 1: Write the failing test

internal/server/server_test.go:

package server

import (
    "context"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
    "time"

    "github.com/alicebob/miniredis/v2"
    "github.com/redis/go-redis/v9"

    "datahive.id/ahu-gpu-manager/internal/audit"
    "datahive.id/ahu-gpu-manager/internal/config"
    "datahive.id/ahu-gpu-manager/internal/pool"
    "datahive.id/ahu-gpu-manager/internal/proxy"
    "datahive.id/ahu-gpu-manager/internal/registry"
)

func testServer(t *testing.T) (*Server, *config.Config) {
    up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(200) }))
    t.Cleanup(up.Close)
    cfg := &config.Config{
        MaxBodyBytes: 1 << 20, Audit: config.AuditConfig{StreamKey: "ahu.ai.audit", SpoolDir: t.TempDir(), BodyCapBytes: 1024},
        HoldBudgetsMs: map[config.Class]int{config.ClassInteractive: 100, config.ClassBatch: 100, config.ClassSystem: 100},
        Upstreams: []config.Upstream{{ID: "u1", Type: "llm-openai", Class: "on_prem",
            Endpoints: []string{up.URL}, Models: []string{"m1"}, ProbePath: "/", Slots: config.Slots{Total: 2, BatchMax: 1}}},
    }
    mr := miniredis.RunT(t)
    rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
    reg := registry.New(cfg)
    pools := map[string]pool.Pool{"u1": pool.NewLocal(cfg.Upstreams[0].Slots)}
    em := audit.NewEmitter(rdb, cfg.Audit.StreamKey, cfg.Audit.SpoolDir)
    return New(cfg, reg, pools, em, nil, proxy.NewStats()), cfg
}

func TestHealthzAndQueueStatusAndMetrics(t *testing.T) {
    s, _ := testServer(t)
    ts := httptest.NewServer(s.Handler())
    defer ts.Close()

    r, _ := http.Get(ts.URL + "/healthz")
    if r.StatusCode != 200 {
        t.Fatalf("healthz: %d", r.StatusCode)
    }
    r, _ = http.Get(ts.URL + "/queue/status?upstream=u1")
    var qs struct {
        Upstream string         `json:"upstream"`
        Active   int            `json:"active"`
        Depth    map[string]int `json:"depth"`
        EtaMs    int64          `json:"eta_ms"`
    }
    if err := json.NewDecoder(r.Body).Decode(&qs); err != nil || qs.Upstream != "u1" {
        t.Fatalf("queue status: %v %+v", err, qs)
    }
    r, _ = http.Get(ts.URL + "/metrics")
    if r.StatusCode != 200 {
        t.Fatalf("metrics: %d", r.StatusCode)
    }
}

func TestDrainRejectsNewFinishesInflight(t *testing.T) {
    s, _ := testServer(t)
    ts := httptest.NewServer(s.Handler())
    defer ts.Close()

    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    go s.Shutdown(ctx)
    time.Sleep(50 * time.Millisecond)

    r, err := http.Post(ts.URL+"/v1/chat/completions", "application/json",
        strings.NewReader(`{"model":"m1"}`))
    if err != nil {
        t.Fatal(err)
    }
    if r.StatusCode != 503 || r.Header.Get("Retry-After") == "" {
        t.Fatalf("draining server must 503+Retry-After new work, got %d", r.StatusCode)
    }
    rh, _ := http.Get(ts.URL + "/healthz")
    if rh.StatusCode != 503 {
        t.Fatalf("healthz must fail during drain for LB removal, got %d", rh.StatusCode)
    }
}
  • [ ] Step 2: Run test to verify it fails

Run: go test ./internal/server/ -v — Expected: FAIL (undefined: New).

  • [ ] Step 3: Implement server

internal/server/server.go:

package server

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "sync"
    "sync/atomic"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
    "github.com/prometheus/client_golang/prometheus/promhttp"

    "datahive.id/ahu-gpu-manager/internal/audit"
    "datahive.id/ahu-gpu-manager/internal/config"
    "datahive.id/ahu-gpu-manager/internal/pool"
    "datahive.id/ahu-gpu-manager/internal/proxy"
    "datahive.id/ahu-gpu-manager/internal/registry"
)

type Server struct {
    cfg      *config.Config
    reg      *registry.Registry
    pools    map[string]pool.Pool
    emitter  *audit.Emitter
    mux      *http.ServeMux
    draining atomic.Bool
    inflight sync.WaitGroup

    promReg   *prometheus.Registry
    reqTotal  *prometheus.CounterVec
    queueWait prometheus.Histogram
}

func New(cfg *config.Config, reg *registry.Registry, pools map[string]pool.Pool,
    em *audit.Emitter, ic proxy.IdemCache, stats *proxy.Stats) *Server {
    s := &Server{cfg: cfg, reg: reg, pools: pools, emitter: em, mux: http.NewServeMux()}
    // Per-server registry: the default global registry would panic on duplicate
    // registration when tests construct more than one Server per process.
    s.promReg = prometheus.NewRegistry()
    factory := promauto.With(s.promReg)
    s.reqTotal = factory.NewCounterVec(prometheus.CounterOpts{Name: "gateway_requests_total"},
        []string{"tenant", "upstream", "class", "status"})
    s.queueWait = factory.NewHistogram(prometheus.HistogramOpts{
        Name: "gateway_queue_wait_ms", Buckets: []float64{10, 50, 100, 500, 1000, 5000, 15000, 45000, 120000}})

    emit := func(ev audit.Event) {
        s.reqTotal.WithLabelValues(ev.Engine, ev.Upstream, ev.TrafficClass, ev.Status).Inc()
        s.queueWait.Observe(float64(ev.QueueMs))
        em.Emit(ev)
    }
    ph := proxy.NewHandler(proxy.Deps{Cfg: cfg, Reg: reg, Pools: pools, Emit: emit, Idem: ic, Stats: stats})

    guard := func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if s.draining.Load() {
                w.Header().Set("Retry-After", "5")
                http.Error(w, `{"error":{"code":"DRAINING"}}`, http.StatusServiceUnavailable)
                return
            }
            s.inflight.Add(1)
            defer s.inflight.Done()
            next.ServeHTTP(w, r)
        })
    }
    s.mux.Handle("/v1/", guard(ph))
    s.mux.Handle("/synthesis/v1/", guard(ph))
    s.mux.HandleFunc("GET /healthz", s.healthz)
    s.mux.HandleFunc("GET /queue/status", func(w http.ResponseWriter, r *http.Request) {
        id := r.URL.Query().Get("upstream")
        p, ok := s.pools[id]
        if !ok {
            http.Error(w, `{"error":{"code":"UNKNOWN_UPSTREAM"}}`, 404)
            return
        }
        depth := map[string]int{}
        total := 0
        for c, n := range p.Depth() {
            depth[string(c)] = n
            total += n
        }
        json.NewEncoder(w).Encode(map[string]any{
            "upstream": id, "active": p.Active(), "depth": depth,
            "eta_ms": stats.ETAMs(id, total, p.Active()+1),
        })
    })
    s.mux.Handle("GET /metrics", promhttp.HandlerFor(s.promReg, promhttp.HandlerOpts{}))
    return s
}

func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
    if s.draining.Load() {
        http.Error(w, "draining", http.StatusServiceUnavailable)
        return
    }
    fmt.Fprintf(w, `{"status":"ok","upstreams":%d,"audit_dropped":%d}`, len(s.pools), s.emitter.Dropped())
}

func (s *Server) Handler() http.Handler { return s.mux }

func (s *Server) Shutdown(ctx context.Context) {
    s.draining.Store(true)
    done := make(chan struct{})
    go func() { s.inflight.Wait(); close(done) }()
    select {
    case <-done:
    case <-ctx.Done():
    }
    s.emitter.Flush(5_000_000_000) // 5s
}
  • [ ] Step 4: Implement main

cmd/gateway/main.go:

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/redis/go-redis/v9"

    "datahive.id/ahu-gpu-manager/internal/audit"
    "datahive.id/ahu-gpu-manager/internal/config"
    "datahive.id/ahu-gpu-manager/internal/idem"
    "datahive.id/ahu-gpu-manager/internal/pool"
    "datahive.id/ahu-gpu-manager/internal/proxy"
    "datahive.id/ahu-gpu-manager/internal/registry"
    "datahive.id/ahu-gpu-manager/internal/server"
)

func main() {
    cfgPath := os.Getenv("GATEWAY_CONFIG")
    if cfgPath == "" {
        cfgPath = "/etc/ahu-gateway/gateway.yaml"
    }
    cfg, err := config.Load(cfgPath)
    if err != nil {
        log.Fatalf("config: %v", err)
    }

    opts, err := redis.ParseURL(cfg.RedisURL)
    if err != nil {
        log.Fatalf("redis url: %v", err)
    }
    rdb := redis.NewClient(opts)

    reg := registry.New(cfg)
    pools := map[string]pool.Pool{}
    for i := range cfg.Upstreams {
        u := &cfg.Upstreams[i]
        if cfg.Coordination == "redis" {
            pools[u.ID] = pool.NewRedisPool(rdb, u.ID, u.Slots) // Task 10
        } else {
            pools[u.ID] = pool.NewLocal(u.Slots)
        }
    }
    em := audit.NewEmitter(rdb, cfg.Audit.StreamKey, cfg.Audit.SpoolDir)
    srv := server.New(cfg, reg, pools, em, idem.NewRedis(rdb, 5*time.Minute), proxy.NewStats())

    ctx, cancel := context.WithCancel(context.Background())
    go em.Run(ctx)
    reg.StartProber(ctx, 15*time.Second)

    hs := &http.Server{Addr: cfg.Listen, Handler: srv.Handler()}
    go func() {
        log.Printf("gateway listening on %s (%d upstreams)", cfg.Listen, len(cfg.Upstreams))
        if err := hs.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("listen: %v", err)
        }
    }()

    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
    <-sig
    log.Print("draining...")
    dctx, dcancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer dcancel()
    srv.Shutdown(dctx)
    hs.Shutdown(dctx)
    cancel()
}

(Until Task 10 exists, temporarily guard the coordination == "redis" branch with log.Fatalf("redis coordination arrives in Task 10") so the package compiles; Task 10 replaces it.)

  • [ ] Step 5: Run tests + build

Run: go get github.com/prometheus/client_golang/prometheus && go test ./... -count=1 && go build ./cmd/gateway — Expected: all PASS; binary builds.

  • [ ] Step 6: Commit
git add internal/server/ cmd/
git commit -m "feat(server): mux assembly, healthz, queue status, prometheus metrics, graceful drain

Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT"

Amendment (post-review, commit d97c1a3): the drain design above (atomic.Bool + sync.WaitGroup) has a TOCTOU that can panic mid-drain (Add racing Wait at counter zero — empirically reproduced 6/8 runs under stress). The shipped internal/server/server.go replaces it with a mutex-guarded gate (enter()/leave()/isDraining() + drainDone channel) making check-and-admit a single critical section, plus ReadHeaderTimeout: 10 * time.Second on the http.Server. The repo code is authoritative for this task; do not regenerate from the snippet above.

Task 10: RedisPool — cluster-wide slots for the HA pair

Files:
- Create: internal/pool/redispool.go
- Modify: cmd/gateway/main.go (drop the Task-9 guard)
- Test: internal/pool/redispool_test.go

Interfaces:
- Produces: pool.NewRedisPool(rdb redis.UniversalClient, upstreamID string, s config.Slots) Pool — same Pool/Ticket interfaces as NewLocal. Semantics: cluster-wide Total/BatchMax enforced via a lease ZSET (gwslots:{upstream} / gwslots:{upstream}:batch, score = lease expiry unix-ms, 30 s leases refreshed every 10 s while held); waiters poll every 100 ms; fail-open: on Redis error, acquisition falls back to an embedded LocalPool.

  • [ ] Step 1: Write the failing test

internal/pool/redispool_test.go:

package pool

import (
    "testing"
    "time"

    "github.com/alicebob/miniredis/v2"
    "github.com/redis/go-redis/v9"

    "datahive.id/ahu-gpu-manager/internal/config"
)

func rpAdmitted(t Ticket, d time.Duration) bool {
    select {
    case <-t.Ready():
        return true
    case <-time.After(d):
        return false
    }
}

func TestTwoInstancesShareCap(t *testing.T) {
    mr := miniredis.RunT(t)
    rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
    s := config.Slots{Total: 1, BatchMax: 1}
    p1 := NewRedisPool(rdb, "u1", s)
    p2 := NewRedisPool(rdb, "u1", s)

    a := p1.Enqueue(config.ClassInteractive)
    if !rpAdmitted(a, time.Second) {
        t.Fatal("first not admitted")
    }
    b := p2.Enqueue(config.ClassInteractive)
    if rpAdmitted(b, 300*time.Millisecond) {
        t.Fatal("cap not shared across instances")
    }
    a.Release()
    if !rpAdmitted(b, time.Second) {
        t.Fatal("b not admitted after cross-instance release")
    }
    b.Release()
}

func TestLeaseExpiryFreesSlotAfterCrash(t *testing.T) {
    mr := miniredis.RunT(t)
    rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
    s := config.Slots{Total: 1, BatchMax: 1}
    p1 := NewRedisPool(rdb, "u1", s)
    a := p1.Enqueue(config.ClassInteractive)
    rpAdmitted(a, time.Second)
    // simulate crash: no Release, no refresh; jump past lease TTL
    p1.(*redisPool).stopRefresh()
    mr.FastForward(31 * time.Second)
    p2 := NewRedisPool(rdb, "u1", s)
    b := p2.Enqueue(config.ClassInteractive)
    if !rpAdmitted(b, 2*time.Second) {
        t.Fatal("expired lease never reclaimed")
    }
    b.Release()
}

func TestFailOpenWhenRedisDown(t *testing.T) {
    mr := miniredis.RunT(t)
    rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
    p := NewRedisPool(rdb, "u1", config.Slots{Total: 2, BatchMax: 1})
    mr.SetError("down")
    a := p.Enqueue(config.ClassInteractive)
    if !rpAdmitted(a, time.Second) {
        t.Fatal("fail-open broken: request blocked by dead redis")
    }
    a.Release()
}
  • [ ] Step 2: Run test to verify it fails

Run: go test ./internal/pool/ -run TestTwoInstances -v — Expected: FAIL (undefined: NewRedisPool).

  • [ ] Step 3: Implement

internal/pool/redispool.go:

package pool

import (
    "context"
    "fmt"
    "sync"
    "time"

    "github.com/redis/go-redis/v9"

    "datahive.id/ahu-gpu-manager/internal/config"
)

const (
    leaseTTL    = 30 * time.Second
    refreshTick = 10 * time.Second
    pollTick    = 100 * time.Millisecond
)

// acquire: prune expired leases, then take a slot iff total (and batch, when
// applicable) caps have headroom. KEYS[1]=all ZSET, KEYS[2]=batch ZSET.
// ARGV: now_ms, expiry_ms, lease_id, total_cap, batch_cap, is_batch
var acquireScript = redis.NewScript(`
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[1])
redis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', ARGV[1])
if redis.call('ZCARD', KEYS[1]) >= tonumber(ARGV[4]) then return 0 end
if ARGV[6] == '1' and redis.call('ZCARD', KEYS[2]) >= tonumber(ARGV[5]) then return 0 end
redis.call('ZADD', KEYS[1], ARGV[2], ARGV[3])
if ARGV[6] == '1' then redis.call('ZADD', KEYS[2], ARGV[2], ARGV[3]) end
return 1
`)

type redisPool struct {
    rdb      redis.UniversalClient
    id       string
    slots    config.Slots
    local    Pool // fail-open fallback and local FIFO/priority ordering
    mu       sync.Mutex
    refresh  map[string]context.CancelFunc
}

func NewRedisPool(rdb redis.UniversalClient, upstreamID string, s config.Slots) Pool {
    return &redisPool{rdb: rdb, id: upstreamID, slots: s,
        local: NewLocal(s), refresh: map[string]context.CancelFunc{}}
}

func (p *redisPool) keys() (string, string) {
    return "gwslots:" + p.id, "gwslots:" + p.id + ":batch"
}

type rticket struct {
    inner   Ticket // local ticket: preserves class priority + position locally
    p       *redisPool
    class   config.Class
    leaseID string
    ready   chan struct{}
    cancel  chan struct{}
    once    sync.Once
}

func (t *rticket) Ready() <-chan struct{} { return t.ready }
func (t *rticket) Pos() int               { return t.inner.Pos() }

func (t *rticket) Cancel() {
    t.once.Do(func() { close(t.cancel) })
    t.inner.Cancel()
}

func (t *rticket) Release() {
    t.inner.Release()
    t.p.mu.Lock()
    if c, ok := t.p.refresh[t.leaseID]; ok {
        c()
        delete(t.p.refresh, t.leaseID)
    }
    t.p.mu.Unlock()
    all, batch := t.p.keys()
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    t.p.rdb.ZRem(ctx, all, t.leaseID)
    if t.class == config.ClassBatch {
        t.p.rdb.ZRem(ctx, batch, t.leaseID)
    }
}

func (p *redisPool) Enqueue(class config.Class) Ticket {
    t := &rticket{inner: p.local.Enqueue(class), p: p, class: class,
        leaseID: fmt.Sprintf("%s-%d", p.id, time.Now().UnixNano()),
        ready:   make(chan struct{}), cancel: make(chan struct{})}
    go func() {
        // Stage 1: local admission (priority order among this instance's waiters).
        select {
        case <-t.inner.Ready():
        case <-t.cancel:
            return
        }
        // Stage 2: cluster-wide lease.
        tick := time.NewTicker(pollTick)
        defer tick.Stop()
        for {
            ok, err := p.tryAcquire(class, t.leaseID)
            if err != nil {
                close(t.ready) // fail-open: local pool already granted a slot
                return
            }
            if ok {
                p.startRefresh(t.leaseID, class)
                close(t.ready)
                return
            }
            select {
            case <-tick.C:
            case <-t.cancel:
                return
            }
        }
    }()
    return t
}

func (p *redisPool) tryAcquire(class config.Class, leaseID string) (bool, error) {
    all, batch := p.keys()
    now := time.Now().UnixMilli()
    isBatch := "0"
    if class == config.ClassBatch {
        isBatch = "1"
    }
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    n, err := acquireScript.Run(ctx, p.rdb, []string{all, batch},
        now, now+leaseTTL.Milliseconds(), leaseID, p.slots.Total, p.slots.BatchMax, isBatch).Int()
    if err != nil {
        return false, err
    }
    return n == 1, nil
}

func (p *redisPool) startRefresh(leaseID string, class config.Class) {
    ctx, cancel := context.WithCancel(context.Background())
    p.mu.Lock()
    p.refresh[leaseID] = cancel
    p.mu.Unlock()
    go func() {
        tick := time.NewTicker(refreshTick)
        defer tick.Stop()
        all, batch := p.keys()
        for {
            select {
            case <-ctx.Done():
                return
            case <-tick.C:
                exp := float64(time.Now().Add(leaseTTL).UnixMilli())
                c, cancel2 := context.WithTimeout(context.Background(), time.Second)
                p.rdb.ZAdd(c, all, redis.Z{Score: exp, Member: leaseID})
                if class == config.ClassBatch {
                    p.rdb.ZAdd(c, batch, redis.Z{Score: exp, Member: leaseID})
                }
                cancel2()
            }
        }
    }()
}

func (p *redisPool) stopRefresh() { // test hook: simulate crashed holder
    p.mu.Lock()
    defer p.mu.Unlock()
    for id, c := range p.refresh {
        c()
        delete(p.refresh, id)
    }
}

func (p *redisPool) Depth() map[config.Class]int { return p.local.Depth() }
func (p *redisPool) Active() int                 { return p.local.Active() }

Also remove the Task-9 log.Fatalf guard in cmd/gateway/main.go so coordination: redis constructs pool.NewRedisPool(rdb, u.ID, u.Slots).

  • [ ] Step 4: Run tests

Run: go test ./internal/pool/ -v -race && go build ./cmd/gateway — Expected: PASS (all pool tests).

  • [ ] Step 5: Commit
git add internal/pool/ cmd/
git commit -m "feat(pool): redis-coordinated cluster-wide slots with lease expiry and fail-open

Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT"

Amendment (post-review, commit dbc8d3a): the prescribed TestLeaseExpiryFreesSlotAfterCrash is impossible as written — miniredis.FastForward advances Redis-native TTLs only, never the Go time.Now() that lease ZSET scores compare against. Shipped test seeds an already-expired lease (score in the past) and asserts a new pool instance reclaims it, which pins the Lua ZREMRANGEBYSCORE prune path directly (verified: commenting the prune line fails the test). The stopRefresh() test hook was deleted as unused. Repo code is authoritative.

Task 11: Packaging — Dockerfile, compose, example config, README, smoke script

Files:
- Create: Dockerfile, deploy/compose.yaml, deploy/gateway.example.yaml, scripts/smoke.sh, README.md

Interfaces:
- Consumes: the built binary; env GATEWAY_CONFIG, DASHSCOPE_API_KEY.

  • [ ] Step 1: Dockerfile
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /gateway ./cmd/gateway

FROM alpine:3.20
RUN adduser -D -H gateway && mkdir -p /var/lib/ahu-gateway/spool && chown gateway /var/lib/ahu-gateway/spool
USER gateway
COPY --from=build /gateway /usr/local/bin/gateway
EXPOSE 8200
ENTRYPOINT ["/usr/local/bin/gateway"]
  • [ ] Step 2: Example config (matches the x056 survey — see docs/deploy/)

deploy/gateway.example.yaml:

listen: ":8200"
allow_external_upstreams: true   # MUST be false in production
coordination: local              # switch to "redis" when running the HA pair
redis_url: "redis://ahu-platform-redis:6379/0"
audit:
  stream_key: "ahu.ai.audit"
  spool_dir: "/var/lib/ahu-gateway/spool"
  body_cap_bytes: 10485760
hold_budgets_ms: {interactive: 45000, batch: 120000, system: 10000}
route_prefixes:
  synthesis: ext-dashscope-397b
tenants:
  - {id: ahu-chatbot, token: ""}   # set tokens to enforce auth; empty = header-trust mode
  - {id: ahu-ocr, token: ""}
  - {id: doc-classifier, token: ""}
upstreams:
  - id: qwen-35b
    type: llm-openai
    class: on_prem
    endpoints: ["http://192.168.83.20:8001/v1"]   # ahu-vllm, host-published
    models: ["Qwen/Qwen3.6-35B-A3B-FP8", "qwen-35b"]
    slots: {total: 16, batch_max: 8}
  - id: cleanup-3b
    type: llm-openai
    class: on_prem
    endpoints: ["http://192.168.83.20:8003/v1"]
    models: ["cleanup-3b"]
    slots: {total: 8, batch_max: 8}
  - id: tei-embeddings
    type: embedding
    class: on_prem
    endpoints: ["http://192.168.83.20:8100/v1"]
    models: ["Qwen/Qwen3-Embedding-4B"]
    slots: {total: 8, batch_max: 8}
  - id: ext-dashscope-397b
    type: llm-openai
    class: external_dev
    endpoints: ["https://ws-08046ecd.cn-beijing.dashscope.aliyuncs.com/compatible-mode/v1"]
    api_key_env: DASHSCOPE_API_KEY
    models: ["qwen3.5-397b-a17b"]
    slots: {total: 8, batch_max: 4}

(Verify the DashScope base URL against ai-ahu-chatbot/infra/env/*.env at deploy time — read-only check, do not modify that repo.)

  • [ ] Step 3: Compose stack

deploy/compose.yaml:

name: ahu-platform
services:
  gateway:
    build: ..
    restart: unless-stopped
    ports: ["8200:8200"]
    environment:
      GATEWAY_CONFIG: /etc/ahu-gateway/gateway.yaml
      DASHSCOPE_API_KEY: ${DASHSCOPE_API_KEY:-}
    volumes:
      - ./gateway.yaml:/etc/ahu-gateway/gateway.yaml:ro
      - gateway-spool:/var/lib/ahu-gateway/spool
    depends_on: [redis]
  redis:
    image: redis:7-alpine
    container_name: ahu-platform-redis
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    volumes: [redis-data:/data]
volumes:
  gateway-spool:
  redis-data:
  • [ ] Step 4: Smoke script

scripts/smoke.sh:

#!/usr/bin/env bash
# Usage: GW=http://localhost:8200 MODEL="Qwen/Qwen3.6-35B-A3B-FP8" ./scripts/smoke.sh
set -euo pipefail
GW="${GW:-http://localhost:8200}"
MODEL="${MODEL:-qwen-35b}"
echo "== healthz"; curl -sf "$GW/healthz"; echo
echo "== queue status"; curl -sf "$GW/queue/status?upstream=qwen-35b"; echo
echo "== chat (non-streaming)"
curl -sf "$GW/v1/chat/completions" -H 'Content-Type: application/json' \
  -H 'X-Tenant-Id: smoke' -H 'X-Priority: batch' -H "Idempotency-Key: smoke-$(date +%s)" \
  -d "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"Say OK\"}],\"max_tokens\":5}"
echo; echo "== chat (streaming, queue events on)"
curl -sfN "$GW/v1/chat/completions" -H 'Content-Type: application/json' \
  -H 'X-Tenant-Id: smoke' -H 'X-Queue-Events: on' \
  -d "{\"model\":\"$MODEL\",\"stream\":true,\"messages\":[{\"role\":\"user\",\"content\":\"Count to 3\"}],\"max_tokens\":20}" | head -20
echo; echo "== audit stream depth"
docker exec ahu-platform-redis redis-cli XLEN ahu.ai.audit
echo "smoke OK"

chmod +x scripts/smoke.sh

  • [ ] Step 5: README

README.md — quickstart (build, test, compose up with cp deploy/gateway.example.yaml deploy/gateway.yaml, run smoke), pointer table to docs/CONVENTIONS.md, the spec, the deploy survey, and the integration prompts. Keep under 80 lines. Upload after writing: curl -F "file=@README.md" https://x056.think.val.id/upload.

  • [ ] Step 6: Verify build end-to-end

Run: go vet ./... && go test ./... -count=1 && docker build -t ahu-gateway:dev . — Expected: tests PASS, image builds. (If Docker is unavailable locally, note it and verify the static build with CGO_ENABLED=0 go build ./cmd/gateway.)

  • [ ] Step 7: Commit
git add Dockerfile deploy/ scripts/ README.md
git commit -m "feat(deploy): dockerfile, compose stack, x056 example config, smoke script

Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT"

Task 12: Live-fire verification against a real vLLM-shaped upstream (P0 exit)

Files:
- Create: scripts/loadcheck.sh
- No production-code changes expected; fixes discovered here get their own micro-commits.

  • [ ] Step 1: Stand the stack up locally
cd deploy && cp gateway.example.yaml gateway.yaml
# For a machine without GPU access, point qwen-35b endpoints at the x056 vLLM
# (http://192.168.83.20:8001/v1 — reachable per the deploy survey) or run a local stub.
docker compose up -d --build
GW=http://localhost:8200 MODEL="Qwen/Qwen3.6-35B-A3B-FP8" ../scripts/smoke.sh

Expected: all four smoke sections succeed; XLEN ahu.ai.audit ≥ 2.

  • [ ] Step 2: Real-client compatibility check (openai-python, mimicking the engines)
uv run --with openai python3 - <<'EOF'
from openai import OpenAI
c = OpenAI(base_url="http://localhost:8200/v1", api_key="EMPTY",
           default_headers={"X-Tenant-Id": "compat-check", "X-Priority": "interactive"})
r = c.chat.completions.create(model="Qwen/Qwen3.6-35B-A3B-FP8",
    messages=[{"role": "user", "content": "Say OK"}], max_tokens=5)
print("non-stream:", r.choices[0].message.content)
s = c.chat.completions.create(model="Qwen/Qwen3.6-35B-A3B-FP8", stream=True,
    messages=[{"role": "user", "content": "Count to 3"}], max_tokens=20)
print("stream:", "".join(ch.choices[0].delta.content or "" for ch in s if ch.choices))
EOF

Expected: both print content; no SDK parse errors (this validates keepalive/queue-frame tolerance of the exact client the engines use).

  • [ ] Step 3: Saturation behavior under parallel load

scripts/loadcheck.sh:

#!/usr/bin/env bash
# Fires 40 concurrent batch requests at a pool of 16; verifies queueing + 429s are clean.
set -euo pipefail
GW="${GW:-http://localhost:8200}"; MODEL="${MODEL:-Qwen/Qwen3.6-35B-A3B-FP8}"
seq 40 | xargs -P40 -I{} curl -s -o /dev/null -w "%{http_code}\n" \
  "$GW/v1/chat/completions" -H 'Content-Type: application/json' \
  -H 'X-Tenant-Id: loadcheck' -H 'X-Priority: batch' \
  -d "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":8}" \
  | sort | uniq -c

Expected output: a mix of 200 and (if the 120 s batch hold is exceeded) 429 — zero 5xx, zero connection resets. Then: curl -s localhost:8200/metrics | grep gateway_queue_wait shows non-zero histogram counts.

  • [ ] Step 4: Kill-the-gateway drill (drop-detection contract)
docker compose restart gateway & sleep 0.2
curl -s -o /dev/null -w "%{http_code}\n" localhost:8200/healthz || true
sleep 3
GW=http://localhost:8200 ../scripts/smoke.sh
docker exec ahu-platform-redis redis-cli XLEN ahu.ai.audit

Expected: brief connection errors during restart (engines' retry contract handles this), full recovery after; audit stream length strictly grew; spool volume empty (docker compose exec gateway ls /var/lib/ahu-gateway/spool → no files).

  • [ ] Step 5: Commit + tag P0
git add scripts/loadcheck.sh
git commit -m "test(p0): smoke, real-client compat, saturation and restart drills

Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT"
git tag p0-gateway

Self-review notes (performed at plan time)

  • Spec coverage: §4.1 registry → Tasks 1–2; §4.2 classes/pools → Task 3 (+10 for HA); §4.3(a) OpenAI passthrough → Tasks 5–6; §4.4 queue-awareness → Tasks 5, 7; §4.5 idempotency/drop-detection → Tasks 8, 12; §5.1 event emission side → Task 4; §3 HA software obligations (stateless, Redis slots, fail-open, drain) → Tasks 9–10; §7 testing (real clients, chaos-lite, load) → Task 12; §8 P0/P1 packaging → Task 11. Deliberately out of this plan: Job API + fan-out + OCR adapters (P2), controller/node-agent/autoscaler (P3), and the entire observatory consumer side (separate plan; the Redis Stream is the boundary, and the emitter's at-least-once note obliges that plan to dedupe on event_id).
  • Type consistency check: pool.Ticket/Pool used identically in Tasks 3, 5, 7, 10; proxy.IdemCache defined Task 5, implemented Task 8; audit.Event fields match CONVENTIONS §5 (with classify in the operation enum unused by the gateway — emitted only by the classifier SDK).
  • Known simplifications (explicit, not accidental): streaming requests are not idempotency-replayed (re-run instead — documented in Task 5 behavior contract); token counts for streams are null unless the client requests stream_options.include_usage; /queue/status ETA is an EWMA heuristic, not a promise.