* docs(governance): ADR 0005 (no multi-provider) + cache upgrade spec
Governance prelude for the cache upgrade work:
- docs/adr/0005-no-multi-provider.md — locks in the decision that
OCP stays single-provider (Anthropic via cli.js spawn). Cache
improvements are explicitly in scope (decision §3); multi-provider
refactor is explicitly out of scope, with three documented trigger
conditions for revisiting.
- docs/adr/README.md — index updated to reference 0005.
- docs/superpowers/specs/2026-05-07-cache-upgrade-design.md — design
for the cache upgrade work split into PR-A (per-key isolation,
cache_control bypass, chunked stream replay) and PR-B (singleflight
stampede protection). Each design decision has a written rationale.
server.mjs is not modified; this commit is doc-only and therefore
exempt from the cli.js citation requirement (ALIGNMENT.md Rule 5
applies only to commits that touch server.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(cache): per-key isolation, cache_control bypass, chunked stream replay
cli.js does not perform response caching at the proxy layer. OCP's response
cache is a value-add operation internal to OCP, between the wire and the
cli.js spawn. It does not introduce, rename, or alter any endpoint, header,
request field, or response field that cli.js emits or expects — this change
qualifies under ALIGNMENT.md Rule 2's value-add carve-out for non-wire-
affecting proxy operations. no client-observable wire shape change.
Spec: docs/superpowers/specs/2026-05-07-cache-upgrade-design.md
D1 — Per-key cache isolation (cacheHash v2 format)
keys.mjs: prepend `v2|k:<keyId or "anon">|` before existing hash fields.
Backward-compatible: absent/null/empty keyId folds to "anon".
v1-format rows in response_cache are abandoned naturally; TTL cleanup at
server.mjs:185 reaps them within one window. No migration needed.
server.mjs: pass keyId: req._authKeyId at the single cacheHash call site
(line ~1221).
D2 — cache_control bypass
keys.mjs: export hasCacheControl(messages) — walks messages and nested
content arrays for presence of cache_control field.
server.mjs: if hasCacheControl(messages) is true, set req._cacheHash = null
and log cache_skipped{reason: cache_control_present}; existing
`if (CACHE_TTL > 0 && req._cacheHash)` guards on write-back handle the skip.
D3 — Chunked stream replay (80 codepoints/chunk, no artificial delay)
server.mjs: replace single-chunk cached.response emission with an
Array.from(cached.response) loop in steps of CACHE_REPLAY_CHUNK_SIZE=80.
Array.from ensures multibyte UTF-8 codepoints (e.g. CJK) are never split.
Tests: 12 new cases in test-features.mjs (36 total, 0 failed).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Add two new features for LAN sharing governance:
Quota (budget control):
- Per-key daily/weekly/monthly request limits (NULL = unlimited)
- Idempotent schema migration for quota columns
- Single-query check (SUM/CASE) for all 3 periods — no N+1
- PATCH /api/keys/:id/quota (partial update, input validation)
- GET /api/keys/:id/quota (current limits + usage)
- 429 with structured error when exceeded
- Only applies to identified per-key users, not admin/anonymous
Response cache:
- SHA-256 hash of model + messages + temperature/max_tokens/top_p
- Opt-in via CLAUDE_CACHE_TTL env var (0 = disabled, default)
- Cache hit serves both streaming (simulated SSE) and non-streaming
- Streaming responses accumulated and cached on success
- Skips multi-turn sessions (conversationId present)
- GET /cache/stats, DELETE /cache admin endpoints
- Runtime-tunable cacheTTL via PATCH /settings
- 10-minute periodic cleanup of expired entries
Bug fix discovered during testing:
- SQLite datetime('now') stores 'YYYY-MM-DD HH:MM:SS' but JS
.toISOString() produces 'YYYY-MM-DDTHH:MM:SS.sssZ'. String
comparison breaks for same-day ranges. Added sqliteDatetime()
helper for correct format matching.
Code review fixes:
- DELETE /api/keys/:id no longer shadows /quota sub-routes
- updateKeyQuota uses partial UPDATE (only SET provided fields)
- cacheHash includes temperature/max_tokens/top_p in hash
- Replaced raw getDb() in server.mjs with findKey() encapsulation
- Unified UTC midnight calculation across checkQuota/getKeyQuota
Includes 24-test integration suite (test-features.mjs).
Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>