Files
olp/docs/adr/0012-phase-5-charter-quota-probes-dashboard.md
d872330c9e docs: Phase 5 close-prep — README § Plan Usage + supported-provider matrix + live E2E artifact (#56)
* docs: Phase 5 close-prep — README § Plan Usage + supported-provider matrix + live E2E artifact

Pre-close documentation pass per ADR 0012 § Exit gate items 3 + 9.
v0.5.0 close PR (package.json bump + CHANGELOG promotion + tag) is
maintainer-triggered per CLAUDE.md release_kit.phase_close_trigger.

What this PR ships:

(1) README § What you get — added Plan-usage probe bullet with anchor to
    the new § Plan Usage section.

(2) README § Supported Providers — table gained "Quota probe (v0.5.0+)"
    column. Anthropic  live (13 anthropic-ratelimit-unified-* headers,
    opt-in via quota_probe_enabled). Codex  no public quota API.
    Mistral  per D84 spike 2026-05-26 (no /v1/usage at docs.mistral.ai/api).
    Others TBD (Phase 8+).

(3) README new § Plan Usage (live quota probe) between § Configuration and
    § API Endpoints. Documents: how the probe works (POST /v1/messages
    with max_tokens:1 + headers-only parse), opt-in config block, OAuth
    token sources (env / .credentials.json / macOS Keychain), olp doctor
    integration, schema-drift protection (Path A `strings` over compiled
    binary + Path B live probe diff), full ADR cross-refs. Embeds the
    dashboard screenshot below.

(4) README § API Endpoints — updated /dashboard / /v0/management/dashboard-data
    / /v0/management/quota rows to reflect Phase 5 changes:
    - /dashboard: Claude.ai-style Plan Usage section + 60s auto-refresh + manual button
    - /v0/management/dashboard-data: new quota_v2 field alongside legacy quota
    - /v0/management/quota: mirrors dashboard-data quota_v2 for scripted monitoring

(5) docs/img/dashboard-v0.5.0.png — dashboard screenshot rendered from
    live MacBook server JSON (utilization_5h: 36%, utilization_7d: 34%,
    representative_claim: five_hour, overage: rejected) merged into D83
    dashboard.html. Identifying IPs / hostnames / Tailscale nodes redacted
    from rendered output per public-repo hygiene rule (cc-rules AGENTS.md).

(6) docs/exit-gates/phase-5-e2e.json — sanitized record of the Live
    MacBook E2E verification (exit-gate item 9). Confirms quota_v2 shape
    produced live, anthropic probe returned 12/13 fields (overage-reset
    absent per audit memory — only fires on active overage), opt-in
    mechanism verified end-to-end (config flag flipped → server probed
    → dashboard renders). Post-test cleanup noted: temp owner key
    revoked, config restored to baseline, test server terminated.

Remaining exit-gate items for the v0.5.0 close PR (maintainer-triggered):

- CHANGELOG.md "Unreleased" → "## v0.5.0 — <date>" promotion
- package.json 0.4.4 → 0.5.0
- CLAUDE.md release_kit.phase_rolling_mode: Phase 5 → Phase 6,
  0.5.0-phase5 → 0.6.0-phase6
- Tag v0.5.0 push (triggers .github/workflows/release.yml auto-release)

Authority cited:
- ADR 0012 § Exit gate items 3 + 9
- ADR 0012 Amendment 1 (D84 NO-GO rationale in Mistral row)
- ADR 0002 Amendment 8 + ADR 0013 (the constitutional context for the
  Plan Usage section's "how it works" framing)
- D80 commit 82d2e1c (probe producer cited in API Endpoints table)
- D81 commit 5288493 (quota_v2 shape producer)
- D82 commit a41420d (dashboard UI consumer)
- D83 commit 2b07a3b (test coverage)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: PR #56 fold-in — 3 maintainer findings (F1 doctor kind / F2 Mistral / F3 SPOT)

Maintainer review of PR #56 surfaced 3 valid accuracy issues. Folding in.

F1 [P2] — README claim "fix_oauth on 401/403, fix_provider on 429/network"
is wrong. The anthropic.quota_probe_reachable check has category: 'provider'
(anthropic.mjs:999), so deriveKind() at lib/doctor.mjs:464 maps ALL failures
to fix_provider regardless of underlying HTTP status. Furthermore, _probeOnce
collapses 401/403 → null before reaching the doctor aggregate layer
(anthropic.mjs:451), so the HTTP status isn't observable to deriveKind at
all. README would have steered AI-repair loops toward an unreachable
discriminator.

Fix: README now accurately says all probe failures discriminate to
kind: fix_provider, with the actionable text inside human_steps[] being
auth-aware (re-login recipe for OAuth failures, wait-and-retry for
rate-limit). Note that splitting the check across the provider/auth
category boundary would let fix_oauth fire for OAuth-class failures
specifically; deferred to v1.x pending consumer-reported ambiguity.

F2 [P2] — README + ADR 0012 Amendment 1 said Mistral has "no programmatic
quota API; only web console". Maintainer pointed to Mistral's Admin API
(https://docs.mistral.ai/admin/security-access/admin-api) which DOES
expose Billing and Usage queries — just gated to org-admin-scoped keys.

The D84 NO-GO conclusion is still correct: OLP's deployment posture
(maintainer's personal Le Chat Pro account, trusted-LAN per ADR 0011)
uses Vibe / Le Chat member / La Plateforme keys, NOT org-admin keys.
Provisioning + storing an org-admin token raises the credential-scope
ceiling beyond the trusted-LAN design.

Fix: tightened wording in 3 places (README Supported Providers table,
README Plan Usage provider coverage table, ADR 0012 Amendment 1) to
say "no public quota endpoint accessible to Vibe / Le Chat member /
La Plateforme API keys" + acknowledge the Admin API surface + cite it
as a forward re-entry point if OLP scope expands to org-admin context.

F3 [P3] — Supported Providers table claims it's re-generated from
models-registry.json, but the new "Quota probe (v0.5.0+)" column has
values for OpenAI/Mistral/TBD that aren't in the registry (registry only
had quota_probe.anthropic block before this PR).

Fix: closed the SPOT drift formally by adding quota_probe.openai and
quota_probe.mistral entries to the registry with {status, reason,
re_entry_point} for each, plus an admin_api_reference for Mistral. Also
added explicit "status": "live" field to quota_probe.anthropic for
symmetry. Tightened README's source-of-truth statement to name BOTH
the providers.<key> block (model metadata) and quota_probe.<key> block
(probe status/reason/source).

This makes the column fully derivable from the registry — when D84 ever
becomes GO (Mistral Admin API integration, or OpenAI publishes a quota
endpoint), the registry is the single edit point.

Test impact: 756/756 still pass. Suite 37g (registry presence + 13 fields
for anthropic) unchanged; the new openai/mistral quota_probe entries are
additive and don't break any consumer.

Authority:
- F1: lib/doctor.mjs:464 deriveKind logic + anthropic.mjs:999 category
- F2: https://docs.mistral.ai/admin/security-access/admin-api + ADR 0011
  trusted-LAN deployment context for the "out of scope" framing
- F3: CLAUDE.md release_kit overlay § "Supported Providers table sourced
  from models-registry.json" — same SPOT discipline applies to the new
  D81 column

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 20:59:24 +10:00

148 lines
16 KiB
Markdown

# ADR 0012 — Phase 5 Charter: Provider Quota Probes + Dashboard Enrichment
**Status:** Accepted (Phase 5 open as of 2026-05-26)
**Date:** 2026-05-26
**D-day:** D79 (charter + ADR 0002 Amendment 8 + ADR 0013 land together as the constitutional layer of Phase 5)
## Amendments
### Amendment 1 — 2026-05-26: D84 Mistral probe NO-GO (post-D79-close spike)
The D-day table originally listed D84 as "optional, depends on D79-close 30-min Mistral docs spike". The spike completed 2026-05-26 with verdict **NO-GO** — Mistral does not expose a programmatic quota/usage endpoint **accessible to Vibe / Le Chat member / La Plateforme API keys** (the key tier OLP uses for spawning the `vibe` CLI):
- `docs.mistral.ai/api` (the public API spec) covers Chat, FIM, Embeddings, Classifiers, Files, Models, Batch, OCR, Audio, Events, Beta (Agents/Conversations/Libraries/Workflows/Observability). No usage/quota/credits/billing/limits endpoint accessible to a member API key.
- Direct probe `https://api.mistral.ai/v1/usage` returns 404.
- Mistral's "Limits and Usage" help article documents limit viewing via the `admin.mistral.ai/plateforme/limits` web console.
- No `x-ratelimit-*` response headers documented on `/v1/chat/completions`. (Third-party summaries mentioning these headers are unsourced — appears to be OpenAI-convention extrapolation.)
- OLP `lib/providers/mistral.mjs` already records this independently — DL-7 comment: "If quota/budget API surfaces in Le Chat Pro, pin the endpoint here."
**Out-of-scope but worth pinning for future revisit.** Mistral's [Admin API](https://docs.mistral.ai/admin/security-access/admin-api) DOES expose programmatic "Billing and usage queries", and the [Usage limits docs](https://docs.mistral.ai/admin/user-management-finops/usage-limits) describe usage/cost queries via that surface. The Admin API requires an **org-admin scoped API key** (separate from the member key OLP uses). For OLP's family-tier deployment posture (a maintainer's personal Le Chat Pro / La Plateforme account, not an organization's admin console), provisioning + storing an org-admin token raises the credential-scope ceiling beyond what the trusted-LAN deployment context (ADR 0011) was designed for. The NO-GO at v0.5.0 is therefore "out of scope for OLP's current deployment posture", NOT "Mistral has no programmatic surface". If the deployment posture expands to an org-admin context (e.g., a small-business multi-user deployment), this decision should be re-evaluated.
**Disposition:**
- D84 row dropped from D-day plan (struck through below).
- Mistral dashboard row in D82 UI shows "spend tracking only" badge sourced from `audit-query.mjs` aggregates (request count, estimated cost from `estimateCost()`).
- `DL-7` in `mistral.mjs` is the documented re-entry point if Mistral ever publishes a usage endpoint.
- Phase 5 total D-day budget revised: ~5 D-days (down from ~6).
---
## Context
Phase 4 (ADR 0010) shipped OLP's operator + client UX layer — `bin/olp` operator CLI, `olp doctor` framework, `olp-connect` zero-config IDE wiring, OpenClaw `/olp` slash commands, anonymous-key deployment-context limits, SSE heartbeat. v0.4.4 is the current shipped state. Phase 4 closed every gap on the OCP-feature-parity matrix EXCEPT one: **live quota / plan-usage surfacing**.
Today `lib/providers/anthropic.mjs:445` has a stub `quotaStatus()` returning `null` (D4 placeholder). The OLP dashboard's quota panel renders "—" for all providers. OCP, in contrast, exposes a live "39% session / 30% weekly" panel — the maintainer uses this multiple times per day to decide when to throttle voluntary `claude -p` traffic away from interactive sessions. OLP cannot become an OCP successor in practice (vs. just feature-parity-on-paper) until quota surfacing works.
A pre-flight institutional-knowledge audit (2026-05-26 — see `~/.cc-rules/memory/learnings/anthropic_plan_usage_probe_schema_2026_05_26.md`) confirmed:
1. **The OCP probe still works today** — Anthropic returns the same `anthropic-ratelimit-unified-*` headers on every `POST /v1/messages` call. Tested live 2026-05-26 from PI231 OAuth credentials.
2. **Schema added 3 fields since OCP's 2026-04 capture**`5h-status`, `7d-status` (per-window status), `overage-reset` (only on active overage). No fields removed or renamed.
3. **Verification protocol has shifted** — Claude Code v2.1.x is now a **compiled binary** (Mach-O / ELF), not bundled JS. OCP's "grep cli.js" approach no longer applies; the replacement protocol is `strings` against the binary + periodic live probe diff.
4. **OAuth refresh path unchanged**`platform.claude.com/v1/oauth/token` + `9d1c250a-...` client_id + 60s-3600s exponential backoff.
The audit makes Phase 5 implementation low-risk: this is a port of a working OCP function, not a re-derivation. The work is mechanical + adapter-layer plumbing into OLP's plugin contract.
A parallel maintainer request (2026-05-26, with reference screenshot of claude.ai/settings/usage) asked for Claude.ai-style dashboard enrichment: per-row utilization bars, reset countdown, 1-minute auto-refresh, manual refresh button. P5-1 (probe) + P5-2 (dashboard) together unlock both: the data plus the surface. v1.x roadmap #8 is closed by P5-2.
---
## Decision
Phase 5 scope is **Provider quota probes + dashboard enrichment**. The phase opens 2026-05-26 with D79 (this charter + ADR 0002 Amendment 8 + ADR 0013 OAuth READ-ONLY consumption rules). Phase 5 close ships v0.5.0; per `CLAUDE.md release_kit.phase_rolling_mode`, the close PR is maintainer-triggered.
### In scope — Phase 5 D-day plan (~6 D-days)
| D-day | Deliverable | Authority | Estimate |
|---|---|---|---|
| **D79** | This charter ADR 0012 + ADR 0002 Amendment 8 (direct-API READ-ONLY) + ADR 0013 (OAuth READ-ONLY consumption rules + schema-drift mitigation) + `package.json` `current_pre_release_identifier``0.5.0-phase5` + `CLAUDE.md release_kit.phase_rolling_mode.current_phase` → Phase 5 | This charter + audit memory | 0.5d |
| **D80** | `lib/providers/anthropic.mjs:quotaStatus()` ported from OCP `server.mjs:842-1109` — full probe with macOS-keychain auth read added (existing OLP reader only handles env + `.credentials.json`) + 5min cache + 60s-3600s refresh backoff + stale-cache-on-429 + all 13 headers parsed (including new 5h-status / 7d-status / overage-reset) | Port OCP probe + ALIGNMENT.md Rule 2 exemption per ADR 0002 Amendment 8 + audit memory | 2d |
| **D81** | `lib/audit-query.mjs` + `/v0/management/dashboard-data` extended to surface the new quota shape per provider (utilization, reset, representative-claim, fallback-percentage, overage-status). Audit-query stays in-memory scan per ADR 0008 Lane 2 = A (no SQLite). Schema migration documented in ADR 0008 § Amendment | ADR 0008 + this charter | 1d |
| **D82** | `dashboard.html` Claude.ai-style restructure — per-provider rows replace the current single Quota panel; each row: provider badge, model placeholder, utilization bar (5h + 7d), reset countdown ("Your limit will reset at HH:MM AM/PM" format from the user-shared claude.ai screenshot), status badge, representative-claim hint. 1-minute auto-refresh via `setInterval` with `document.visibilityState` guard. Manual refresh button calls `/v0/management/dashboard-data` directly | v1.x roadmap #8 + maintainer reference screenshot | 1.5d |
| **D83** | Test coverage — Suite 38 quota-probe unit tests (mock HTTP server returning the 13 headers; assert parse + cache + backoff + stale-on-429); Suite 39 dashboard rendering smoke (curl `/dashboard` after pre-seeding mock quota cache; assert HTML contains expected utilization strings); update Suite 33 doctor checks for new `anthropic.quota_probe_reachable` check | Test convention from existing suites | 1d |
| ~~D84~~ **DROPPED** | ~~Mistral `quotaStatus()` port — depends on D79-close spike~~ **NO-GO per 2026-05-26 spike (see § Amendment 1).** Mistral dashboard row in D82 shows "spend tracking only" badge sourced from `audit-query.mjs` aggregates. `DL-7` hook point in `mistral.mjs` already marks the location for future upgrade if Mistral ever publishes a usage endpoint. Codex permanently skipped (no public API). | n/a (dropped) | 0d |
| **close** | v0.5.0 release PR — `package.json` `0.4.4 → 0.5.0`, CHANGELOG promotion, `release_kit.phase_rolling_mode.current_pre_release_identifier` advance to Phase 6 token | `CLAUDE.md release_kit overlay` | maintainer-triggered |
### Out of Phase 5 scope (with explicit triggers)
#### `X-OLP-Cost-USD` per-request response header
**Status:** Deferred to Phase 6. Was listed in ADR 0010 § Out-of-scope as "Phase 5 prerequisite". The prerequisite (provider-cost weights table) is non-trivial — needs per-(provider, model) `input_cost_per_1k_tokens` / `output_cost_per_1k_tokens` / `cache_read_discount` data sourced from each provider's published pricing page. Phase 5 already pulls in two new ADRs; adding a third data-onboarding ADR is scope creep.
**Re-open condition.** Phase 6 unless a maintainer reports a cost-attribution debugging need that warrants pulling forward.
#### `context_window_exceeded` fallback trigger (LiteLLM prior-art)
**Status:** Deferred. ADR 0010 listed this as opportunistic-in-Phase-5 unless the trigger fires sooner. The trigger has not fired in Phase 4 production traffic. Continue to defer.
#### per-(provider, model) live stats Map (replacing audit-query scan)
**Status:** Deferred. Current scan latency is ~20ms at 7-day depth. Acceptable until volume grows (>100k requests/day). Re-evaluate at Phase 6 if dashboard latency degrades.
#### Anthropic interactive-mode P0 (ADR 0009)
**Status:** Still trigger-gated on Anthropic's 2026-06-15 billing-split rollout. Phase 5 does NOT depend on P0 — the quota probe reads `anthropic-ratelimit-unified-*` headers regardless of which billing pool the spawn path consumes. If P0 succeeds Phase 7+ Phase 5's probe code remains unchanged; if P0 fails Phase 5's probe code remains unchanged. The probe is billing-pool-agnostic because the headers are subscription-pool metadata, not Agent-SDK-Credit metadata.
#### `/v1/messages` Anthropic-shape entry surface
**Status:** Still deferred per ADR 0010 § Out-of-scope. No change in Phase 5.
#### v1.x roadmap #3 / #5 / #6
**Status:** Still trigger-gated per `docs/v1x-roadmap.md`. None has fired. Continue to defer.
### Opportunistic Phase 5 micro-additions (not blocking)
Items small enough to land alongside a planned D-day without scope creep, if encountered:
- README § Dashboard screenshot update (post-P5-2 enrichment) — capture from MacBook test path per `~/.cc-rules/memory/feedback/mac_mini_never_for_testing.md`.
- `olp usage` CLI subcommand (bin/olp.mjs) surfaces the parsed quota shape in terminal form. Already partially exists (cmdUsage in bin/olp.mjs); confirm payload alignment after D80.
- Add `claude_code_oauth_client_id` config override in `~/.olp/config.json` so power users can override the hardcoded `9d1c250a-...` UUID without env-var fiddling. Mirrors compiled binary's `CLAUDE_CODE_OAUTH_CLIENT_ID` env support.
- `docs/provider-audits/anthropic.md` re-capture with current `claude --version` (v2.1.142 MacBook / v2.1.150 PI231) + binary distribution layout note.
### Exit gate — v0.5.0 close criteria
1. D79 — D84 all merged with fresh-context opus reviewer APPROVE per Iron Rule 10.
2. CI green on every D-day merge commit and on the v0.5.0 release commit head. `alignment.yml` blacklist re-confirmed (no new hallucinated tokens introduced).
3. README § Quota / Plan Usage section present with screenshot of the enriched dashboard. README § Supported Providers table updated to note "quota probe: anthropic ✅, mistral ⚠️/✅ (D84 outcome), codex ❌ (no public API)".
4. ADR 0012 (this charter) + ADR 0002 Amendment 8 + ADR 0013 (OAuth READ-ONLY consumption) on disk.
5. `CHANGELOG.md "Unreleased"` promoted to `"## v0.5.0 — <date>"` with D79 — D84 entries.
6. `package.json` bumped to `0.5.0`.
7. `CLAUDE.md release_kit.phase_rolling_mode.current_phase` advances `Phase 5 → Phase 6`; `current_pre_release_identifier` advances `0.5.0-phase5 → 0.6.0-phase6`.
8. Standing autopilot grant covers D-day-by-D-day execution; v0.5.0 close PR is maintainer-triggered.
9. Live MacBook E2E verification — dashboard renders enriched panel with real quota data (probe live, not mocked).
---
## Consequences
**Positive.**
- OLP finally has the load-bearing observability OCP had — maintainer can see live "39% session / 30% weekly" and decide whether voluntary `claude -p` traffic stays or moves.
- Family members on the LAN see real reset times instead of "—", which makes the "wait 2 hours" guidance concrete vs. abstract.
- The institutional-knowledge audit captured the schema in a memory file pinned with date stamps — future ports (mistral, future provider) re-use the verification protocol without re-deriving.
- v1.x roadmap #8 (Dashboard enrichment per Claude.ai-style usage page) closes inside Phase 5 rather than waiting for a separate phase.
- Compiled-binary-distribution awareness ("no more cli.js to grep") is now codified in OLP governance; the next time Anthropic ships a major CC version, the verification protocol is already written.
**Negative.**
- ADR 0002 gains another amendment (Amendment 8). The constitution surface area for `anthropic.mjs` grows. Counter-pressure: the alternative (probe lives in `server.mjs`, like OCP) violates the plugin-architecture principle that per-provider knowledge stays in `lib/providers/`. Amendment 8 is the smaller violation.
- The probe makes one `/v1/messages` call per 5min cache miss. That's ~12 calls/hour worst case across the whole proxy (probe is per-credentials, not per-key). With `max_tokens: 1` the cost is < $0.01/day at family-scale traffic. Negligible but not zero.
- Schema-drift risk over the long horizon. Anthropic could rename or remove headers in a future version. The mitigation protocol (strings + live probe diff) is in place, but it's a manual check — needs to be invoked by the maintainer or scheduled.
- Dashboard refactor introduces a breaking-change risk for the existing dashboard.html consumers (none today, but conceptually). Bumping to v0.5.0 signals this clearly.
**Neutral.**
- Phase 5 has more ADR work than Phase 4 (3 governance docs vs. 2). The constitutional layer is deliberately heavier because direct-API access is the single biggest authority decision since the plugin contract itself.
---
## Authority + cross-references
- **Iron Rule 11 (IDR)** — Phase 5 ships across 6 D-days, each a minimum reviewable unit. The governance trio (this ADR + Amendment 8 + ADR 0013) lands at D79 as a single coupled commit (reviewing them separately cannot verify consumer-producer alignment), per ADR 0002 Amendment 7's precedent.
- **Iron Rule 12 (prior-art search)** — discharged via the audit memory at `~/.cc-rules/memory/learnings/anthropic_plan_usage_probe_schema_2026_05_26.md`. Memory committed prior to D80 implementation.
- **ALIGNMENT.md Rule 1 (citation)** — D80 commit must cite compiled-binary `strings` evidence per audit memory § Path A (Claude Code v2.1.x has no traditional `§ section` structure because it is a Mach-O / ELF compiled binary) plus the audit memory file path. Live-probe transcript MUST be included in the commit body.
- **ALIGNMENT.md Rule 2 (provider-CLI-as-authority)** — direct-API access bypasses the spawn-binary contract. Amendment 8 is the explicit exemption. Without Amendment 8, the D80 commit is unalignable.
- **ALIGNMENT.md Rule 5 (CI alignment.yml)** — must continue to pass. `api.anthropic.com/v1/messages` is NOT on the blacklist (correct — that's the real endpoint). The hallucinated `/api/oauth/usage` IS on the blacklist (transitive from OCP) and must remain.
- **ADR 0002 Amendment 8** — companion ADR. Direct-API access scoping; READ-ONLY constraint; opt-in via config flag (default off).
- **ADR 0013** — companion ADR. OAuth credentials shared between spawn path + probe path; refresh backoff; schema-drift mitigation protocol.
- **CLAUDE.md release_kit** — Phase boundary triggers maintainer-led version bump. D-day commits within Phase 5 stay under "Unreleased". `0.5.0-phase5` is the pre-release identifier during the phase.