feat(cag): CAG integration — memory adapter, warmup semaphore, cache-aware routing (#47)
CI / CI (push) Successful in 3m20s

## Summary

Three issues in one PR. Implements Cache-Augmented Generation (CAG) support in the api-gw.

---

### #44 — Memory ServiceAdapter: standing-context resource

- Code-registers `memory` adapter with `standing-context` resource (`POST /memory/standing-context`)
- Fixes dispatcher bug: method-level `Auth.Capability` override now respected (was always using verb-derived capability)
- Auth: `memory:read` required via method-level override; `memory:write` for ingest
- `MEMORY_UPSTREAM_URL` env configures upstream

### #45 — Continuous batching: warmup deprioritisation + per-backend semaphore

- In-memory bounded queue per backend: `VLLM_MAX_QUEUE_DEPTH` slots (default 20)
- Concurrency limiter: `VLLM_MAX_NUM_SEQS` slots (default 4)
- `X-Poimen-Warmup: true` → sleep `WARMUP_DELAY_MS` (default 500ms) before queue entry, strip header, add `X-Priority: low`
- Queue full → 503 immediately, no goroutine pile-up
- Metrics: `warmup_requests_delayed_total`, `queue_full_requests_total` at `/metrics`

### #46 — Cache-aware routing: rendezvous hash + prefix cache hit logging

- `addresses: [...]` YAML field for multi-backend models (backward-compat with `address`)
- `X-Poimen-Cache-Key` header → rendezvous-hash backend selection (same key = same GPU)
- No cache key → round-robin
- Failed backend excluded 30s; next-highest-score fallback
- `vllm-num-cached-tokens` response header captured → `vllm_prefix_cache_hit_tokens_total` counter + structured log

---

## Verification

```
go build ./...    ✅
go vet ./...      ✅
go test -race ./... ✅ (all packages, 0 races)
```

## Reviewer verdicts

- reviewer-sre: SIGN-OFF
- reviewer-quality: SIGN-OFF

## Non-blocking findings (🟡🔵 only)

- `getOrCreateTransport` not goroutine-safe by design; pre-population invariant documented
- `time.Sleep(warmupDelay)` holds goroutine; context-cancel alternative left as TODO
- OTel trace_id not propagated into CAG log events (existing gap)

Closes #44 Closes #45 Closes #46

---------

Co-authored-by: Admin Bot <[email protected]>
Reviewed-on: #47
This commit was merged in pull request #47.
This commit is contained in:
2026-09-27 11:16:40 +00:00
co-authored by Admin Bot
parent aaa9a5d1cb
commit ef5e736673
16 changed files with 1597 additions and 162 deletions
+62
View File
@@ -36,6 +36,11 @@ type Metrics struct {
itlMs map[string][]int64 // samples for histogram
// Token counts
tokenCount map[string]int64
// CAG metrics
warmupRequestsDelayed int64
prefixCacheHitTokens int64
queueFullRequests int64
}
// NewMetrics creates a new Metrics instance.
@@ -280,4 +285,61 @@ func (m *Metrics) Reset() {
m.ttftMs = make(map[string][]int64)
m.itlMs = make(map[string][]int64)
m.tokenCount = make(map[string]int64)
m.warmupRequestsDelayed = 0
m.prefixCacheHitTokens = 0
m.queueFullRequests = 0
}
// IncrQueueFull increments queue_full_requests_total.
func (m *Metrics) IncrQueueFull() {
m.mu.Lock()
defer m.mu.Unlock()
m.queueFullRequests++
}
// GetQueueFull returns queue_full_requests_total.
func (m *Metrics) GetQueueFull() int64 {
m.mu.RLock()
defer m.mu.RUnlock()
return m.queueFullRequests
}
// IncrWarmupDelayed increments warmup_requests_delayed_total.
func (m *Metrics) IncrWarmupDelayed() {
m.mu.Lock()
defer m.mu.Unlock()
m.warmupRequestsDelayed++
}
// GetWarmupDelayed returns warmup_requests_delayed_total.
func (m *Metrics) GetWarmupDelayed() int64 {
m.mu.RLock()
defer m.mu.RUnlock()
return m.warmupRequestsDelayed
}
// IncrPrefixCacheHitTokens increments vllm_prefix_cache_hit_tokens_total by n.
func (m *Metrics) IncrPrefixCacheHitTokens(n int64) {
m.mu.Lock()
defer m.mu.Unlock()
m.prefixCacheHitTokens += n
}
// GetPrefixCacheHitTokens returns vllm_prefix_cache_hit_tokens_total.
func (m *Metrics) GetPrefixCacheHitTokens() int64 {
m.mu.RLock()
defer m.mu.RUnlock()
return m.prefixCacheHitTokens
}
// globalMetrics is the singleton Metrics instance for gateway-wide telemetry.
var (
globalMetrics *Metrics
globalMetricsOnce sync.Once
)
// Global returns the singleton Metrics instance, initialising on first call.
func Global() *Metrics {
globalMetricsOnce.Do(func() { globalMetrics = NewMetrics() })
return globalMetrics
}