feat: D81 — dashboard-data quota_v2 shape + models-registry schema_version (Phase 5) (#53)

Authority citations (required per CLAUDE.md § Hard requirements):
  1. ADR 0012 D81 — the D-day being implemented (Phase 5 charter, audit-query
     + dashboard-data extension row in § D-day table)
  2. ADR 0013 Rule 5 — schema_version in models-registry.json mandate. D80
     used a local constant QUOTA_SCHEMA_VERSION = '2026-05-26' (reviewer nit
     #4 at PR #52). D81 folds in Rule 5 compliance: adds quota_probe.schema_version
     to models-registry.json and has anthropic.mjs read from there with the
     constant as fallback via _resolveSchemaVersion().
  3. ADR 0008 — the audit-query design being amended (Amendment 1 added at D81
     to docs/adr/0008-dashboard-and-audit-query.md documenting: quota_probe in
     registry, aggregateProviderQuota() API shape, quota_v2 key, deprecation
     timeline for legacy quota key).
  4. D80 PR #52 (commit 82d2e1c) — producer of the quotaStatus() shape that
     D81 normalizes. The 13-field anthropic-ratelimit-unified-* shape from
     _parseRateLimitHeaders() is consumed by _normalizeAnthropicQuota() here.

Changes (A–G per D81 spec):

A. models-registry.json — new top-level quota_probe key
   - quota_probe.schema_version = '2026-05-26'
   - quota_probe.anthropic.{ source, endpoint, fields_pinned[13] }
   - fields_pinned is load-bearing for drift detection (ADR 0013 Rule 5)

B. lib/providers/anthropic.mjs — _resolveSchemaVersion() helper
   - Reads quota_probe.schema_version from modelsRegistryRaw at call time
   - Falls back to QUOTA_SCHEMA_VERSION constant if registry field absent
   - quotaStatus() now uses _resolveSchemaVersion() instead of raw constant

C. lib/audit-query.mjs — aggregateProviderQuota() export
   - Normalizes per-provider quotaStatus() returns to ProviderQuotaEntry shape
   - Live → { status:'live', utilization, reset, representative_claim, … }
   - Stale → { status:'stale', … }
   - null return → { status:'unavailable', reason:'no public quota api or probe disabled' }
   - throw → { status:'unavailable', reason:<error.message> }
   - getQuotaStatus injection point for full test isolation (D81 §F)

D. server.mjs handleManagementDashboardData — quota_v2 added
   - Calls auditAggregateProviderQuota({ providers: loadedProviders })
   - Both legacy quota (backwards compat) and quota_v2 in response
   - Graceful degradation: quota_v2 failure → warn log + empty array, rest of
     payload unaffected

E. server.mjs handleManagementQuota — quota_v2 added (mirrors D)

F. test-features.mjs — Suite 37 (7 new tests)
   - 37a: live shape normalization (status=live, utilization, reset, etc.)
   - 37b: stale shape (status=stale)
   - 37c: null returns → unavailable entries
   - 37d: throw path → unavailable with reason
   - 37e: mixed providers (live + unavailable)
   - 37f: getQuotaStatus injection verified
   - 37g: models-registry.json has quota_probe.schema_version + 13 fields_pinned

G. docs/adr/0008-dashboard-and-audit-query.md — Amendment 1 added

Test delta: 720 → 727 (+7), 0 failures.

Live quota_v2 JSON sample (illustrative — probe is opt-in per ADR 0013 Rule 4;
real values require quota_probe_enabled:true + valid OAuth credentials):

  GET /v0/management/dashboard-data (owner-only)
  {
    "quota_v2": [
      {
        "provider": "anthropic",
        "status": "live",
        "schema_version": "2026-05-26",
        "last_fresh_at": 1748300000000,
        "utilization": { "5h": 0.49, "7d": 0.31 },
        "reset": { "5h": 1748290000, "7d": 1748500000, "overall": 1748300000, "overage": null },
        "representative_claim": "five_hour",
        "fallback_percentage": 0.5,
        "overage": { "status": "rejected", "disabled_reason": "org_level_disabled_until" },
        "raw_available": true
      },
      {
        "provider": "openai",
        "status": "unavailable",
        "reason": "no public quota api or probe disabled",
        "schema_version": null,
        "last_fresh_at": null,
        "utilization": null,
        "reset": null,
        "representative_claim": null,
        "fallback_percentage": null,
        "overage": null,
        "raw_available": false
      }
    ]
  }

When probe is disabled (default), anthropic entry also returns unavailable:
  { "provider": "anthropic", "status": "unavailable",
    "reason": "no public quota api or probe disabled", ... }

NOT modified: dashboard.html (D82), codex.mjs, mistral.mjs, any quotaStatus()
return shape from anthropic.mjs (D80 contract unchanged — normalization is in
the new audit-query layer only).

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dtzp555-max
2026-05-26 17:06:41 +10:00
committed by GitHub
co-authored by taodeng Claude Sonnet 4.6
parent 82d2e1cbea
commit 5288493f19
6 changed files with 475 additions and 3 deletions
+29 -1
View File
@@ -144,8 +144,34 @@ const QUOTA_API_URL = 'https://api.anthropic.com/v1/messages';
const QUOTA_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes (OCP USAGE_CACHE_TTL)
const QUOTA_BACKOFF_MIN_MS = 60 * 1000; // 60s (OCP OAUTH_REFRESH_MIN_BACKOFF)
const QUOTA_BACKOFF_MAX_MS = 60 * 60 * 1000; // 3600s (OCP OAUTH_REFRESH_MAX_BACKOFF)
// Local constant is the safety net (ADR 0013 Rule 5 + D81 reviewer nit #4 fold-in).
// The live value is read from models-registry.json at module load via _resolveSchemaVersion().
// If the registry field is absent or fails to load, this constant takes over.
const QUOTA_SCHEMA_VERSION = '2026-05-26';
/**
* Read quota_probe.schema_version from models-registry.json (D81).
* ADR 0013 Rule 5: schema_version lives in models-registry.json so downstream
* consumers can detect schema drift by bumping that field on drift events.
* Local QUOTA_SCHEMA_VERSION constant is the fallback if the registry field
* is absent or the import is unavailable.
*
* @returns {string} schema_version string (e.g. '2026-05-26')
*/
function _resolveSchemaVersion() {
try {
// models-registry.json is imported at the bottom of this file as
// modelsRegistryRaw; reference it here via a lazy try/catch so that if
// the import hasn't resolved yet (edge case: circular import during tests)
// we gracefully fall through to the constant.
const registryVersion = modelsRegistryRaw?.quota_probe?.schema_version;
if (typeof registryVersion === 'string' && registryVersion.length > 0) {
return registryVersion;
}
} catch { /* fall through to constant */ }
return QUOTA_SCHEMA_VERSION;
}
// Module-level probe state — one instance per process (not per request).
// Ported from OCP server.mjs:852 (usageCache) + 862 (oauthRefreshBackoff).
const quotaProbeState = {
@@ -788,7 +814,9 @@ export async function quotaStatus(_authContext) {
const shape = {
probedAt: now,
source: 'anthropic-ratelimit-unified-headers',
schemaVersion: QUOTA_SCHEMA_VERSION,
// D81: schema_version sourced from models-registry.json with QUOTA_SCHEMA_VERSION
// as fallback per ADR 0013 Rule 5 + D81 ADR 0008 Amendment requirement.
schemaVersion: _resolveSchemaVersion(),
stale: false,
fields: probeResult.fields,
raw: probeResult.raw,