paygate-mcp 8.3.0 → 8.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +240 -0
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,6 +110,8 @@ Agent → PayGate (auth + billing) → Your MCP Server (stdio or HTTP)
|
|
|
110
110
|
- **Admin Notifications** — `GET /admin/notifications` scans all keys for actionable issues: expired/expiring keys, zero credits, credit depletion velocity, suspended keys, high error rates, and rate limit pressure — with severity filtering and priority sorting
|
|
111
111
|
- **System Dashboard** — `GET /admin/dashboard` system-wide overview with key counts (active/suspended/revoked/expired), credit summary (allocated/spent/remaining), usage breakdown with deny reasons, top consumers, top tools, notification counts, and uptime
|
|
112
112
|
- **Key Lifecycle Report** — `GET /admin/lifecycle` aggregated lifecycle trends with daily creation/revocation/suspension buckets, average key lifetime, and at-risk keys (expiring, expired, zero credits)
|
|
113
|
+
- **Cost Analysis** — `GET /admin/costs` cost-centric view with per-tool and per-namespace cost breakdowns, hourly spending trends, top spenders, average cost per call, and namespace filtering
|
|
114
|
+
- **Rate Limit Analysis** — `GET /admin/rate-limits` rate limit utilization analysis with per-key and per-tool breakdown, denial trends, most throttled keys, and current window utilization
|
|
113
115
|
- **Config Hot Reload** — `POST /config/reload` reloads pricing, rate limits, webhooks, quotas, and behavior flags from config file without server restart
|
|
114
116
|
- **Webhook Events** — POST batched usage events to any URL for external billing/alerting
|
|
115
117
|
- **Config File Mode** — Load all settings from a JSON file (`--config`)
|
|
@@ -2502,6 +2504,94 @@ curl "http://localhost:3402/admin/lifecycle?since=2026-02-01&until=2026-02-28" \
|
|
|
2502
2504
|
|
|
2503
2505
|
Shows aggregated lifecycle event counts, daily trend buckets (sorted chronologically), average key lifetime in hours (for revoked keys), and at-risk keys with their risk category (`expired`, `expiring_soon`, `zero_credits`). Supports `?since=` and `?until=` date filters. Excludes suspended and revoked keys from at-risk list. Read-only.
|
|
2504
2506
|
|
|
2507
|
+
### Cost Analysis
|
|
2508
|
+
|
|
2509
|
+
Get a cost-centric breakdown of credit usage across tools, namespaces, and time:
|
|
2510
|
+
|
|
2511
|
+
```bash
|
|
2512
|
+
# Full cost analysis
|
|
2513
|
+
curl http://localhost:3402/admin/costs \
|
|
2514
|
+
-H "X-Admin-Key: YOUR_ADMIN_KEY"
|
|
2515
|
+
|
|
2516
|
+
# Filter by namespace
|
|
2517
|
+
curl "http://localhost:3402/admin/costs?namespace=prod" \
|
|
2518
|
+
-H "X-Admin-Key: YOUR_ADMIN_KEY"
|
|
2519
|
+
|
|
2520
|
+
# Filter by time range
|
|
2521
|
+
curl "http://localhost:3402/admin/costs?since=2026-02-01" \
|
|
2522
|
+
-H "X-Admin-Key: YOUR_ADMIN_KEY"
|
|
2523
|
+
```
|
|
2524
|
+
|
|
2525
|
+
```json
|
|
2526
|
+
{
|
|
2527
|
+
"summary": {
|
|
2528
|
+
"totalCredits": 4250,
|
|
2529
|
+
"totalCalls": 312,
|
|
2530
|
+
"totalAllowed": 298,
|
|
2531
|
+
"totalDenied": 14,
|
|
2532
|
+
"avgCostPerCall": 13.62
|
|
2533
|
+
},
|
|
2534
|
+
"perTool": [
|
|
2535
|
+
{ "tool": "generate_report", "calls": 85, "credits": 1700, "avgCost": 20 },
|
|
2536
|
+
{ "tool": "query_data", "calls": 142, "credits": 1420, "avgCost": 10 }
|
|
2537
|
+
],
|
|
2538
|
+
"perNamespace": [
|
|
2539
|
+
{ "namespace": "prod", "calls": 210, "credits": 3150 },
|
|
2540
|
+
{ "namespace": "staging", "calls": 102, "credits": 1100 }
|
|
2541
|
+
],
|
|
2542
|
+
"hourlyTrends": [
|
|
2543
|
+
{ "hour": "2026-02-26T14:00:00.000Z", "calls": 23, "credits": 345, "denied": 1 },
|
|
2544
|
+
{ "hour": "2026-02-26T15:00:00.000Z", "calls": 31, "credits": 465, "denied": 0 }
|
|
2545
|
+
],
|
|
2546
|
+
"topSpenders": [
|
|
2547
|
+
{ "key": "pg_a1b2...c3d4", "name": "ml-pipeline", "credits": 1800, "calls": 90 },
|
|
2548
|
+
{ "key": "pg_e5f6...g7h8", "name": "batch-worker", "credits": 1200, "calls": 120 }
|
|
2549
|
+
]
|
|
2550
|
+
}
|
|
2551
|
+
```
|
|
2552
|
+
|
|
2553
|
+
Returns per-tool cost breakdown (with average cost per call), per-namespace spending, hourly trend buckets (last 24 hours), and top 10 spenders ranked by credits consumed. Supports `?since=` and `?namespace=` query filters. Keys without an explicit namespace appear under `default`. Read-only.
|
|
2554
|
+
|
|
2555
|
+
### Rate Limit Analysis
|
|
2556
|
+
|
|
2557
|
+
Analyze rate limit utilization across keys and tools:
|
|
2558
|
+
|
|
2559
|
+
```bash
|
|
2560
|
+
curl http://localhost:3402/admin/rate-limits \
|
|
2561
|
+
-H "X-Admin-Key: YOUR_ADMIN_KEY"
|
|
2562
|
+
```
|
|
2563
|
+
|
|
2564
|
+
```json
|
|
2565
|
+
{
|
|
2566
|
+
"config": {
|
|
2567
|
+
"globalLimitPerMin": 60,
|
|
2568
|
+
"windowMs": 60000
|
|
2569
|
+
},
|
|
2570
|
+
"summary": {
|
|
2571
|
+
"totalCalls": 450,
|
|
2572
|
+
"totalRateLimited": 12,
|
|
2573
|
+
"rateLimitRate": 0.0267
|
|
2574
|
+
},
|
|
2575
|
+
"perKey": [
|
|
2576
|
+
{ "name": "ml-pipeline", "calls": 200, "rateLimited": 8, "currentWindowUsed": 45, "currentWindowRemaining": 15 },
|
|
2577
|
+
{ "name": "batch-worker", "calls": 150, "rateLimited": 4, "currentWindowUsed": 12, "currentWindowRemaining": 48 }
|
|
2578
|
+
],
|
|
2579
|
+
"perTool": [
|
|
2580
|
+
{ "tool": "generate_report", "calls": 180, "rateLimited": 10 },
|
|
2581
|
+
{ "tool": "query_data", "calls": 270, "rateLimited": 2 }
|
|
2582
|
+
],
|
|
2583
|
+
"hourlyTrends": [
|
|
2584
|
+
{ "hour": "2026-02-26T14", "calls": 52, "rateLimited": 3 },
|
|
2585
|
+
{ "hour": "2026-02-26T15", "calls": 48, "rateLimited": 1 }
|
|
2586
|
+
],
|
|
2587
|
+
"mostThrottled": [
|
|
2588
|
+
{ "name": "ml-pipeline", "rateLimited": 8, "calls": 200, "throttleRate": 0.04 }
|
|
2589
|
+
]
|
|
2590
|
+
}
|
|
2591
|
+
```
|
|
2592
|
+
|
|
2593
|
+
Returns rate limit configuration, denial summary with throttle rate, per-key breakdown with current sliding window utilization, per-tool denial counts, hourly denial trends (last 24 hours), and top 10 most throttled keys ranked by denial count. Handles unlimited rate limits (globalLimitPerMin: 0). Read-only.
|
|
2594
|
+
|
|
2505
2595
|
### IP Allowlisting
|
|
2506
2596
|
|
|
2507
2597
|
Restrict API keys to specific IP addresses or CIDR ranges:
|
package/dist/server.d.ts
CHANGED
|
@@ -245,6 +245,8 @@ export declare class PayGateServer {
|
|
|
245
245
|
private handleAdminNotifications;
|
|
246
246
|
private handleSystemDashboard;
|
|
247
247
|
private handleKeyLifecycleReport;
|
|
248
|
+
private handleCostAnalysis;
|
|
249
|
+
private handleRateLimitAnalysis;
|
|
248
250
|
private handleGetNotes;
|
|
249
251
|
private handleAddNote;
|
|
250
252
|
private handleDeleteNote;
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAgB,eAAe,EAA0B,MAAM,MAAM,CAAC;AAI7E,OAAO,EAAE,aAAa,EAAkB,mBAAmB,EAAkB,MAAM,SAAS,CAAC;AAU7F,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,cAAc,EAAqD,MAAM,WAAW,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAmB,MAAM,SAAS,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAS,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,eAAe,EAA6B,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,aAAa,EAAqB,MAAM,UAAU,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAKrD,0EAA0E;AAC1E,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAErE;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAsBvF;AAyCD,yCAAyC;AACzC,KAAK,YAAY,GAAG,QAAQ,GAAG,YAAY,CAAC;AAa5C,qBAAa,aAAa;IACxB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IACpC,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC1C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,oEAAoE;IACpE,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,aAAa,CAAqC;IAC1D,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAQ;IAC5C,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,2BAA2B;IAC3B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;IACnC,mCAAmC;IACnC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,gCAAgC;IAChC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAAQ;IAC5C,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,oCAAoC;IACpC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,oDAAoD;IACpD,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAC;IACvC,sCAAsC;IACtC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,yCAAyC;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;IAChD,gEAAgE;IAChE,OAAO,CAAC,QAAQ,CAAS;IACzB,wEAAwE;IACxE,OAAO,CAAC,eAAe,CAAS;IAChC,mDAAmD;IACnD,OAAO,CAAC,kBAAkB,CAAiC;IAC3D,kDAAkD;IAClD,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,gDAAgD;IAChD,OAAO,CAAC,iBAAiB,CAAqF;IAC9G,8CAA8C;IAC9C,OAAO,CAAC,wBAAwB,CAA+C;IAC/E,8BAA8B;IAC9B,OAAO,CAAC,gBAAgB,CAOhB;IACR,2CAA2C;IAC3C,OAAO,CAAC,aAAa,CAA+C;IACpE,4CAA4C;IAC5C,OAAO,CAAC,cAAc,CAAK;IAC3B,kCAAkC;IAClC,OAAO,CAAC,kBAAkB,CAOX;IACf,+CAA+C;IAC/C,OAAO,CAAC,iBAAiB,CAAK;IAC9B,qDAAqD;IACrD,OAAO,CAAC,UAAU,CAUV;IACR,gCAAgC;IAChC,OAAO,CAAC,gBAAgB,CAAK;IAC7B,4CAA4C;IAC5C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAQ;IAC7C,wCAAwC;IACxC,OAAO,CAAC,QAAQ,CAAK;IACrB,sEAAsE;IACtE,OAAO,CAAC,UAAU,CAAuB;IAEzC,0DAA0D;IAC1D,OAAO,KAAK,OAAO,GAElB;gBAGC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,EAC1D,QAAQ,CAAC,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,mBAAmB,CAAC,EAAE,MAAM,EAC5B,OAAO,CAAC,EAAE,mBAAmB,EAAE,EAC/B,QAAQ,CAAC,EAAE,MAAM;IAsMnB;;;OAGG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIjC;;;;;;;;;;;OAWG;IACH,GAAG,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI;IAK1B,KAAK,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;YA0C5C,aAAa;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/server.js
CHANGED
|
@@ -816,6 +816,18 @@ class PayGateServer {
|
|
|
816
816
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
817
817
|
res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
|
|
818
818
|
return;
|
|
819
|
+
case '/admin/costs':
|
|
820
|
+
if (req.method === 'GET')
|
|
821
|
+
return this.handleCostAnalysis(req, res);
|
|
822
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
823
|
+
res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
|
|
824
|
+
return;
|
|
825
|
+
case '/admin/rate-limits':
|
|
826
|
+
if (req.method === 'GET')
|
|
827
|
+
return this.handleRateLimitAnalysis(req, res);
|
|
828
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
829
|
+
res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
|
|
830
|
+
return;
|
|
819
831
|
// ─── Plugin endpoints ──────────────────────────────────────────────
|
|
820
832
|
case '/plugins':
|
|
821
833
|
return this.handleListPlugins(req, res);
|
|
@@ -1359,6 +1371,8 @@ class PayGateServer {
|
|
|
1359
1371
|
adminNotifications: 'GET /admin/notifications — Actionable notifications for expiring keys, low credits, high error rates, and rate limit pressure (requires X-Admin-Key)',
|
|
1360
1372
|
systemDashboard: 'GET /admin/dashboard — System-wide overview with key stats, credit summary, usage breakdown, top consumers, and uptime (requires X-Admin-Key)',
|
|
1361
1373
|
keyLifecycle: 'GET /admin/lifecycle — Key lifecycle report with creation/revocation/expiry trends, average lifetime, and at-risk keys (requires X-Admin-Key)',
|
|
1374
|
+
costAnalysis: 'GET /admin/costs — Cost analysis with per-tool, per-namespace breakdown, hourly trends, and top spenders (requires X-Admin-Key)',
|
|
1375
|
+
rateLimitAnalysis: 'GET /admin/rate-limits — Rate limit utilization analysis with per-key and per-tool breakdown, denial trends, and most throttled keys (requires X-Admin-Key)',
|
|
1362
1376
|
...(this.oauth ? {
|
|
1363
1377
|
oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
|
|
1364
1378
|
oauthRegister: 'POST /oauth/register — Register OAuth client',
|
|
@@ -4658,6 +4672,232 @@ class PayGateServer {
|
|
|
4658
4672
|
atRisk: atRiskKeys,
|
|
4659
4673
|
}));
|
|
4660
4674
|
}
|
|
4675
|
+
// ─── /admin/costs — Cost analysis ─────────────────────────────────────────
|
|
4676
|
+
handleCostAnalysis(req, res) {
|
|
4677
|
+
if (!this.checkAdmin(req, res))
|
|
4678
|
+
return;
|
|
4679
|
+
const urlParts = req.url?.split('?') || [];
|
|
4680
|
+
const params = new URLSearchParams(urlParts[1] || '');
|
|
4681
|
+
const since = params.get('since') || undefined;
|
|
4682
|
+
const namespace = params.get('namespace') || undefined;
|
|
4683
|
+
// Get all usage events
|
|
4684
|
+
const events = this.gate.meter.getEvents(since, namespace);
|
|
4685
|
+
// ── Per-tool cost breakdown ──
|
|
4686
|
+
const toolCosts = new Map();
|
|
4687
|
+
for (const e of events) {
|
|
4688
|
+
if (!toolCosts.has(e.tool))
|
|
4689
|
+
toolCosts.set(e.tool, { calls: 0, credits: 0, denied: 0, avgCost: 0 });
|
|
4690
|
+
const t = toolCosts.get(e.tool);
|
|
4691
|
+
t.calls++;
|
|
4692
|
+
if (e.allowed) {
|
|
4693
|
+
t.credits += e.creditsCharged;
|
|
4694
|
+
}
|
|
4695
|
+
else {
|
|
4696
|
+
t.denied++;
|
|
4697
|
+
}
|
|
4698
|
+
}
|
|
4699
|
+
for (const [, t] of toolCosts) {
|
|
4700
|
+
const allowed = t.calls - t.denied;
|
|
4701
|
+
t.avgCost = allowed > 0 ? Math.round(t.credits / allowed * 100) / 100 : 0;
|
|
4702
|
+
}
|
|
4703
|
+
const perTool = Array.from(toolCosts.entries())
|
|
4704
|
+
.map(([tool, stats]) => ({ tool, ...stats }))
|
|
4705
|
+
.sort((a, b) => b.credits - a.credits);
|
|
4706
|
+
// ── Per-namespace cost breakdown ──
|
|
4707
|
+
const nsCosts = new Map();
|
|
4708
|
+
for (const e of events) {
|
|
4709
|
+
const ns = e.namespace || 'default';
|
|
4710
|
+
if (!nsCosts.has(ns))
|
|
4711
|
+
nsCosts.set(ns, { calls: 0, credits: 0, denied: 0 });
|
|
4712
|
+
const n = nsCosts.get(ns);
|
|
4713
|
+
n.calls++;
|
|
4714
|
+
if (e.allowed) {
|
|
4715
|
+
n.credits += e.creditsCharged;
|
|
4716
|
+
}
|
|
4717
|
+
else {
|
|
4718
|
+
n.denied++;
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
const perNamespace = Array.from(nsCosts.entries())
|
|
4722
|
+
.map(([ns, stats]) => ({ namespace: ns, ...stats }))
|
|
4723
|
+
.sort((a, b) => b.credits - a.credits);
|
|
4724
|
+
// ── Hourly trends (last 24 buckets) ──
|
|
4725
|
+
const hourBuckets = new Map();
|
|
4726
|
+
for (const e of events) {
|
|
4727
|
+
const hour = e.timestamp.slice(0, 13); // YYYY-MM-DDTHH
|
|
4728
|
+
if (!hourBuckets.has(hour))
|
|
4729
|
+
hourBuckets.set(hour, { calls: 0, credits: 0, denied: 0 });
|
|
4730
|
+
const h = hourBuckets.get(hour);
|
|
4731
|
+
h.calls++;
|
|
4732
|
+
if (e.allowed) {
|
|
4733
|
+
h.credits += e.creditsCharged;
|
|
4734
|
+
}
|
|
4735
|
+
else {
|
|
4736
|
+
h.denied++;
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
const hourlyTrends = Array.from(hourBuckets.entries())
|
|
4740
|
+
.map(([hour, stats]) => ({ hour, ...stats }))
|
|
4741
|
+
.sort((a, b) => a.hour.localeCompare(b.hour))
|
|
4742
|
+
.slice(-24);
|
|
4743
|
+
// ── Top spenders (by credits) ──
|
|
4744
|
+
const keyCosts = new Map();
|
|
4745
|
+
for (const e of events) {
|
|
4746
|
+
const name = e.keyName || e.apiKey.slice(0, 10);
|
|
4747
|
+
if (!keyCosts.has(name))
|
|
4748
|
+
keyCosts.set(name, { calls: 0, credits: 0, denied: 0 });
|
|
4749
|
+
const k = keyCosts.get(name);
|
|
4750
|
+
k.calls++;
|
|
4751
|
+
if (e.allowed) {
|
|
4752
|
+
k.credits += e.creditsCharged;
|
|
4753
|
+
}
|
|
4754
|
+
else {
|
|
4755
|
+
k.denied++;
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
const topSpenders = Array.from(keyCosts.entries())
|
|
4759
|
+
.map(([name, stats]) => ({ name, ...stats }))
|
|
4760
|
+
.sort((a, b) => b.credits - a.credits)
|
|
4761
|
+
.slice(0, 10);
|
|
4762
|
+
// ── Totals ──
|
|
4763
|
+
let totalCredits = 0;
|
|
4764
|
+
let totalCalls = 0;
|
|
4765
|
+
let totalDenied = 0;
|
|
4766
|
+
for (const e of events) {
|
|
4767
|
+
totalCalls++;
|
|
4768
|
+
if (e.allowed) {
|
|
4769
|
+
totalCredits += e.creditsCharged;
|
|
4770
|
+
}
|
|
4771
|
+
else {
|
|
4772
|
+
totalDenied++;
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
const avgCostPerCall = totalCalls - totalDenied > 0
|
|
4776
|
+
? Math.round(totalCredits / (totalCalls - totalDenied) * 100) / 100
|
|
4777
|
+
: 0;
|
|
4778
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4779
|
+
res.end(JSON.stringify({
|
|
4780
|
+
summary: {
|
|
4781
|
+
totalCredits,
|
|
4782
|
+
totalCalls,
|
|
4783
|
+
totalAllowed: totalCalls - totalDenied,
|
|
4784
|
+
totalDenied,
|
|
4785
|
+
avgCostPerCall,
|
|
4786
|
+
},
|
|
4787
|
+
perTool,
|
|
4788
|
+
perNamespace,
|
|
4789
|
+
hourlyTrends,
|
|
4790
|
+
topSpenders,
|
|
4791
|
+
}));
|
|
4792
|
+
}
|
|
4793
|
+
// ─── /admin/rate-limits — Rate limit utilization analysis ───────────────
|
|
4794
|
+
handleRateLimitAnalysis(req, res) {
|
|
4795
|
+
if (!this.checkAdmin(req, res))
|
|
4796
|
+
return;
|
|
4797
|
+
const rateLimiter = this.gate.rateLimiter;
|
|
4798
|
+
const globalLimit = rateLimiter.globalLimit;
|
|
4799
|
+
// Get all usage events to analyze rate limit denials
|
|
4800
|
+
const events = this.gate.meter.getEvents();
|
|
4801
|
+
// ── Summary ──
|
|
4802
|
+
let totalCalls = 0;
|
|
4803
|
+
let totalRateLimited = 0;
|
|
4804
|
+
for (const e of events) {
|
|
4805
|
+
totalCalls++;
|
|
4806
|
+
if (!e.allowed && e.denyReason?.includes('rate_limited')) {
|
|
4807
|
+
totalRateLimited++;
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4810
|
+
const rateLimitRate = totalCalls > 0
|
|
4811
|
+
? Math.round(totalRateLimited / totalCalls * 10000) / 10000
|
|
4812
|
+
: 0;
|
|
4813
|
+
// ── Per-key breakdown ──
|
|
4814
|
+
// Build a name→fullKey map from the key store for rate limiter status lookups
|
|
4815
|
+
const allRecords = this.gate.store.getAllRecords();
|
|
4816
|
+
const nameToFullKey = new Map();
|
|
4817
|
+
for (const rec of allRecords) {
|
|
4818
|
+
nameToFullKey.set(rec.name, rec.key);
|
|
4819
|
+
}
|
|
4820
|
+
const keyMap = new Map();
|
|
4821
|
+
for (const e of events) {
|
|
4822
|
+
const name = e.keyName || e.apiKey.slice(0, 10);
|
|
4823
|
+
if (!keyMap.has(name))
|
|
4824
|
+
keyMap.set(name, { name, calls: 0, rateLimited: 0 });
|
|
4825
|
+
const k = keyMap.get(name);
|
|
4826
|
+
k.calls++;
|
|
4827
|
+
if (!e.allowed && e.denyReason?.includes('rate_limited')) {
|
|
4828
|
+
k.rateLimited++;
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
// Add current window utilization using full key from store
|
|
4832
|
+
const perKey = Array.from(keyMap.values()).map(k => {
|
|
4833
|
+
const fullKey = nameToFullKey.get(k.name) || '';
|
|
4834
|
+
const status = fullKey ? rateLimiter.getStatus(fullKey) : { used: 0, remaining: globalLimit > 0 ? globalLimit : Infinity };
|
|
4835
|
+
return {
|
|
4836
|
+
name: k.name,
|
|
4837
|
+
calls: k.calls,
|
|
4838
|
+
rateLimited: k.rateLimited,
|
|
4839
|
+
currentWindowUsed: status.used,
|
|
4840
|
+
currentWindowRemaining: status.remaining,
|
|
4841
|
+
};
|
|
4842
|
+
}).sort((a, b) => b.calls - a.calls);
|
|
4843
|
+
// ── Per-tool breakdown ──
|
|
4844
|
+
const toolMap = new Map();
|
|
4845
|
+
for (const e of events) {
|
|
4846
|
+
if (!toolMap.has(e.tool))
|
|
4847
|
+
toolMap.set(e.tool, { calls: 0, rateLimited: 0 });
|
|
4848
|
+
const t = toolMap.get(e.tool);
|
|
4849
|
+
t.calls++;
|
|
4850
|
+
if (!e.allowed && e.denyReason?.includes('rate_limited')) {
|
|
4851
|
+
t.rateLimited++;
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
const perTool = Array.from(toolMap.entries())
|
|
4855
|
+
.map(([tool, stats]) => ({ tool, ...stats }))
|
|
4856
|
+
.sort((a, b) => b.rateLimited - a.rateLimited);
|
|
4857
|
+
// ── Hourly trends (rate limit denials) ──
|
|
4858
|
+
const hourBuckets = new Map();
|
|
4859
|
+
for (const e of events) {
|
|
4860
|
+
const hour = e.timestamp.slice(0, 13); // YYYY-MM-DDTHH
|
|
4861
|
+
if (!hourBuckets.has(hour))
|
|
4862
|
+
hourBuckets.set(hour, { calls: 0, rateLimited: 0 });
|
|
4863
|
+
const h = hourBuckets.get(hour);
|
|
4864
|
+
h.calls++;
|
|
4865
|
+
if (!e.allowed && e.denyReason?.includes('rate_limited')) {
|
|
4866
|
+
h.rateLimited++;
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
const hourlyTrends = Array.from(hourBuckets.entries())
|
|
4870
|
+
.map(([hour, stats]) => ({ hour, ...stats }))
|
|
4871
|
+
.sort((a, b) => a.hour.localeCompare(b.hour))
|
|
4872
|
+
.slice(-24);
|
|
4873
|
+
// ── Most throttled keys (by rate limit denials) ──
|
|
4874
|
+
const mostThrottled = Array.from(keyMap.values())
|
|
4875
|
+
.filter(k => k.rateLimited > 0)
|
|
4876
|
+
.map(k => ({
|
|
4877
|
+
name: k.name,
|
|
4878
|
+
rateLimited: k.rateLimited,
|
|
4879
|
+
calls: k.calls,
|
|
4880
|
+
throttleRate: Math.round(k.rateLimited / k.calls * 10000) / 10000,
|
|
4881
|
+
}))
|
|
4882
|
+
.sort((a, b) => b.rateLimited - a.rateLimited)
|
|
4883
|
+
.slice(0, 10);
|
|
4884
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4885
|
+
res.end(JSON.stringify({
|
|
4886
|
+
config: {
|
|
4887
|
+
globalLimitPerMin: globalLimit,
|
|
4888
|
+
windowMs: 60000,
|
|
4889
|
+
},
|
|
4890
|
+
summary: {
|
|
4891
|
+
totalCalls,
|
|
4892
|
+
totalRateLimited,
|
|
4893
|
+
rateLimitRate,
|
|
4894
|
+
},
|
|
4895
|
+
perKey,
|
|
4896
|
+
perTool,
|
|
4897
|
+
hourlyTrends,
|
|
4898
|
+
mostThrottled,
|
|
4899
|
+
}));
|
|
4900
|
+
}
|
|
4661
4901
|
// ─── /keys/notes — Timestamped notes on API keys ─────────────────────────
|
|
4662
4902
|
handleGetNotes(req, res) {
|
|
4663
4903
|
if (!this.checkAdmin(req, res))
|