paygate-mcp 9.7.0 → 9.8.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 +6 -3
- package/dist/cost-tags.d.ts +138 -0
- package/dist/cost-tags.d.ts.map +1 -0
- package/dist/cost-tags.js +307 -0
- package/dist/cost-tags.js.map +1 -0
- package/dist/dedup.d.ts +97 -0
- package/dist/dedup.d.ts.map +1 -0
- package/dist/dedup.js +235 -0
- package/dist/dedup.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/priority-queue.d.ts +113 -0
- package/dist/priority-queue.d.ts.map +1 -0
- package/dist/priority-queue.js +276 -0
- package/dist/priority-queue.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +172 -0
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Monetize any MCP server with one command. Add API key auth, per-tool pricing, ra
|
|
|
11
11
|
- [Quick Start](#quick-start)
|
|
12
12
|
- [What It Does](#what-it-does)
|
|
13
13
|
- [Usage](#usage) — Local stdio, remote HTTP, multi-server, client SDK
|
|
14
|
-
- [API Reference](#api-reference) — All
|
|
14
|
+
- [API Reference](#api-reference) — All 172+ endpoints
|
|
15
15
|
- [CLI Options](#cli-options)
|
|
16
16
|
- [Deployment](#deployment) — Docker, docker-compose, systemd, PM2
|
|
17
17
|
- [Load Testing](#load-testing) — k6 benchmarking for production
|
|
@@ -66,7 +66,7 @@ Agent → PayGate (auth + billing) → Your MCP Server (stdio or HTTP)
|
|
|
66
66
|
- **SSE Streaming** — Full MCP Streamable HTTP transport (POST SSE, GET notifications, DELETE sessions)
|
|
67
67
|
- **Audit Log** — Structured audit trail with retention policies, query API, CSV/JSON export
|
|
68
68
|
- **Registry/Discovery** — Agent-discoverable pricing via `/.well-known/mcp-payment`, `/pricing`, and `/.well-known/mcp.json` identity card
|
|
69
|
-
- **OpenAPI 3.1 + Interactive Docs** — Auto-generated spec at `/openapi.json`, Swagger UI at `/docs` — all
|
|
69
|
+
- **OpenAPI 3.1 + Interactive Docs** — Auto-generated spec at `/openapi.json`, Swagger UI at `/docs` — all 172+ endpoints documented
|
|
70
70
|
- **Public Endpoint Rate Limiting** — Configurable per-IP rate limit (default 300/min) on `/health`, `/info`, `/pricing`, `/docs`, `/openapi.json`, `/.well-known/*`, `/robots.txt`, `/` — 429 with Retry-After header
|
|
71
71
|
- **Robots.txt + HEAD Support** — Standard `/robots.txt` (allow public, disallow admin/keys), HEAD method on all public endpoints for uptime monitoring
|
|
72
72
|
- **Prometheus Metrics** — `/metrics` endpoint with counters, gauges, and uptime in standard text format
|
|
@@ -168,6 +168,9 @@ Agent → PayGate (auth + billing) → Your MCP Server (stdio or HTTP)
|
|
|
168
168
|
- **Request/Response Transforms** — Declarative rewriting of tool call arguments and responses — inject defaults, strip fields, rename keys, and template `{{variables}}` from context, wildcard tool matching, priority ordering, deep clone on apply, import/export for backup, manage via `GET/POST/PUT/DELETE /admin/transforms`
|
|
169
169
|
- **Backend Retry Policy** — Automatic retry with exponential backoff for transient failures — configurable max retries, base/max backoff, full jitter, retry budget (max % of traffic as retries with cold-start grace), per-tool stats, retryable error pattern matching, manage via `GET/POST /admin/retry-policy`
|
|
170
170
|
- **Adaptive Rate Limiting** — Dynamic rate adjustment based on key behavior — auto-tighten for high error rates, auto-boost for good actors, cooldown periods, configurable thresholds, per-key behavior tracking, LRU eviction, batch evaluation, manage via `GET/POST /admin/adaptive-rates`
|
|
171
|
+
- **Request Deduplication** — Idempotency layer preventing duplicate billing from agent retries — `X-Idempotency-Key` header with auto-generation fallback (SHA-256), in-flight request coalescing, configurable TTL window, LRU eviction, credits-saved tracking, manage via `GET/POST/DELETE /admin/dedup`
|
|
172
|
+
- **Priority Queue** — Tiered request prioritization (critical/high/normal/low/background) with fair scheduling — per-key priority assignment, configurable max wait times per tier, starvation prevention via automatic promotion, max queue depth limiting, manage via `GET/POST /admin/priority-queue`
|
|
173
|
+
- **Cost Allocation Tags** — Per-request cost attribution via `X-Cost-Tags` header (JSON) for enterprise chargeback — aggregated reports by any tag dimension, cross-tabulation, CSV export, required tag enforcement per key, cardinality limits, manage via `GET/POST/DELETE /admin/cost-tags`
|
|
171
174
|
- **Anomaly Detection** — `GET /admin/anomalies` identifies unusual patterns: keys with high denial rates, rapid credit depletion, low remaining credits, with severity ratings and detailed descriptions
|
|
172
175
|
- **Usage Forecasting** — `GET /admin/forecast` predicts future credit consumption with per-key depletion estimates, calls remaining, at-risk key identification, system-wide consumption aggregates, and per-tool cost breakdown
|
|
173
176
|
- **Compliance Report** — `GET /admin/compliance` generates compliance-ready report with key governance (expiry coverage), access control (ACL/IP/spending limit coverage), audit trail completeness, weighted overall score, and actionable recommendations
|
|
@@ -481,7 +484,7 @@ A real-time admin UI for managing keys, viewing usage, and monitoring tool calls
|
|
|
481
484
|
| `/.well-known/mcp-payment` | GET | None | Server payment metadata (SEP-2007) |
|
|
482
485
|
| `/.well-known/mcp.json` | GET | None | MCP Server Identity card (discovery) |
|
|
483
486
|
| `/pricing` | GET | None | Full per-tool pricing breakdown |
|
|
484
|
-
| `/openapi.json` | GET | None | OpenAPI 3.1 spec (all
|
|
487
|
+
| `/openapi.json` | GET | None | OpenAPI 3.1 spec (all 172+ endpoints) |
|
|
485
488
|
| `/docs` | GET | None | Interactive API docs (Swagger UI) |
|
|
486
489
|
| `/robots.txt` | GET | None | Crawler directives (allow public, disallow admin/keys) |
|
|
487
490
|
| `/portal` | GET | None | Self-service API key portal (browser UI, auth via X-API-Key prompt) |
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CostAllocator — Per-request cost allocation tags with chargeback reporting.
|
|
3
|
+
*
|
|
4
|
+
* Allows clients to attach key-value tags to individual tool calls via
|
|
5
|
+
* an `X-Cost-Tags` header. Tags flow through the metering pipeline and
|
|
6
|
+
* enable aggregated chargeback reports by any dimension.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - X-Cost-Tags header parsing with validation
|
|
10
|
+
* - Tag cardinality limits to prevent memory exhaustion
|
|
11
|
+
* - Aggregated chargeback reports by any tag dimension
|
|
12
|
+
* - Cross-tabulation (group by two dimensions)
|
|
13
|
+
* - CSV export support
|
|
14
|
+
* - Per-key required tag enforcement
|
|
15
|
+
*
|
|
16
|
+
* Zero external dependencies.
|
|
17
|
+
*/
|
|
18
|
+
export interface CostTagConfig {
|
|
19
|
+
/** Enable cost tags. Default true. */
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
/** Max tags per request. Default 10. */
|
|
22
|
+
maxTagsPerRequest: number;
|
|
23
|
+
/** Max key length. Default 64. */
|
|
24
|
+
maxKeyLength: number;
|
|
25
|
+
/** Max value length. Default 64. */
|
|
26
|
+
maxValueLength: number;
|
|
27
|
+
/** Max unique tag keys. Default 1_000. */
|
|
28
|
+
maxUniqueKeys: number;
|
|
29
|
+
/** Max unique values per key. Default 10_000. */
|
|
30
|
+
maxValuesPerKey: number;
|
|
31
|
+
}
|
|
32
|
+
export interface CostTagEntry {
|
|
33
|
+
tags: Record<string, string>;
|
|
34
|
+
apiKey: string;
|
|
35
|
+
toolName: string;
|
|
36
|
+
credits: number;
|
|
37
|
+
timestamp: number;
|
|
38
|
+
}
|
|
39
|
+
export interface ChargebackRow {
|
|
40
|
+
dimension: string;
|
|
41
|
+
value: string;
|
|
42
|
+
totalCredits: number;
|
|
43
|
+
totalCalls: number;
|
|
44
|
+
avgCreditsPerCall: number;
|
|
45
|
+
}
|
|
46
|
+
export interface CrossTabRow {
|
|
47
|
+
dim1Value: string;
|
|
48
|
+
dim2Value: string;
|
|
49
|
+
totalCredits: number;
|
|
50
|
+
totalCalls: number;
|
|
51
|
+
}
|
|
52
|
+
export interface ChargebackReport {
|
|
53
|
+
dimension: string;
|
|
54
|
+
rows: ChargebackRow[];
|
|
55
|
+
totalCredits: number;
|
|
56
|
+
totalCalls: number;
|
|
57
|
+
generatedAt: string;
|
|
58
|
+
}
|
|
59
|
+
export interface CrossTabReport {
|
|
60
|
+
dim1: string;
|
|
61
|
+
dim2: string;
|
|
62
|
+
rows: CrossTabRow[];
|
|
63
|
+
totalCredits: number;
|
|
64
|
+
totalCalls: number;
|
|
65
|
+
generatedAt: string;
|
|
66
|
+
}
|
|
67
|
+
export interface CostTagStats {
|
|
68
|
+
enabled: boolean;
|
|
69
|
+
config: CostTagConfig;
|
|
70
|
+
totalEntries: number;
|
|
71
|
+
uniqueKeys: number;
|
|
72
|
+
valuesPerKey: Record<string, number>;
|
|
73
|
+
totalCreditsTracked: number;
|
|
74
|
+
totalCallsTracked: number;
|
|
75
|
+
requiredTagKeys: number;
|
|
76
|
+
}
|
|
77
|
+
export declare class CostAllocator {
|
|
78
|
+
private config;
|
|
79
|
+
private entries;
|
|
80
|
+
private readonly maxEntries;
|
|
81
|
+
private knownKeys;
|
|
82
|
+
private knownValuesPerKey;
|
|
83
|
+
private requiredTags;
|
|
84
|
+
constructor(config?: Partial<CostTagConfig>);
|
|
85
|
+
/**
|
|
86
|
+
* Parse and validate tags from X-Cost-Tags header value.
|
|
87
|
+
* Header format: JSON-encoded object, e.g. {"project":"acme","dept":"eng"}
|
|
88
|
+
*/
|
|
89
|
+
parseTags(headerValue: string): Record<string, string>;
|
|
90
|
+
/**
|
|
91
|
+
* Record a tagged usage event.
|
|
92
|
+
*/
|
|
93
|
+
record(tags: Record<string, string>, apiKey: string, toolName: string, credits: number): void;
|
|
94
|
+
/**
|
|
95
|
+
* Check if a request has all required tags for the given API key.
|
|
96
|
+
*/
|
|
97
|
+
validateRequired(apiKey: string, tags: Record<string, string>): string[];
|
|
98
|
+
/**
|
|
99
|
+
* Set required tags for an API key.
|
|
100
|
+
*/
|
|
101
|
+
setRequiredTags(apiKey: string, tagKeys: string[]): void;
|
|
102
|
+
/**
|
|
103
|
+
* Get required tags for an API key.
|
|
104
|
+
*/
|
|
105
|
+
getRequiredTags(apiKey: string): string[];
|
|
106
|
+
/**
|
|
107
|
+
* Generate a chargeback report grouped by a single dimension.
|
|
108
|
+
*/
|
|
109
|
+
report(dimension: string, startMs?: number, endMs?: number): ChargebackReport;
|
|
110
|
+
/**
|
|
111
|
+
* Generate a cross-tabulation report (group by two dimensions).
|
|
112
|
+
*/
|
|
113
|
+
crossTab(dim1: string, dim2: string, startMs?: number, endMs?: number): CrossTabReport;
|
|
114
|
+
/**
|
|
115
|
+
* Export report as CSV string.
|
|
116
|
+
*/
|
|
117
|
+
reportToCsv(report: ChargebackReport): string;
|
|
118
|
+
/**
|
|
119
|
+
* Update configuration at runtime.
|
|
120
|
+
*/
|
|
121
|
+
configure(updates: Partial<CostTagConfig>): CostTagConfig;
|
|
122
|
+
/**
|
|
123
|
+
* Clear all recorded entries.
|
|
124
|
+
*/
|
|
125
|
+
clear(): void;
|
|
126
|
+
/**
|
|
127
|
+
* Get statistics.
|
|
128
|
+
*/
|
|
129
|
+
stats(): CostTagStats;
|
|
130
|
+
/** Is cost allocation enabled? */
|
|
131
|
+
get enabled(): boolean;
|
|
132
|
+
/** Current config (copy). */
|
|
133
|
+
get currentConfig(): CostTagConfig;
|
|
134
|
+
/** Number of recorded entries. */
|
|
135
|
+
get size(): number;
|
|
136
|
+
private filterEntries;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=cost-tags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-tags.d.ts","sourceRoot":"","sources":["../src/cost-tags.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CostAllocator — Per-request cost allocation tags with chargeback reporting.
|
|
4
|
+
*
|
|
5
|
+
* Allows clients to attach key-value tags to individual tool calls via
|
|
6
|
+
* an `X-Cost-Tags` header. Tags flow through the metering pipeline and
|
|
7
|
+
* enable aggregated chargeback reports by any dimension.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - X-Cost-Tags header parsing with validation
|
|
11
|
+
* - Tag cardinality limits to prevent memory exhaustion
|
|
12
|
+
* - Aggregated chargeback reports by any tag dimension
|
|
13
|
+
* - Cross-tabulation (group by two dimensions)
|
|
14
|
+
* - CSV export support
|
|
15
|
+
* - Per-key required tag enforcement
|
|
16
|
+
*
|
|
17
|
+
* Zero external dependencies.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.CostAllocator = void 0;
|
|
21
|
+
// ─── Default config ─────────────────────────────────────────────────────────
|
|
22
|
+
const DEFAULT_COST_TAG_CONFIG = {
|
|
23
|
+
enabled: true,
|
|
24
|
+
maxTagsPerRequest: 10,
|
|
25
|
+
maxKeyLength: 64,
|
|
26
|
+
maxValueLength: 64,
|
|
27
|
+
maxUniqueKeys: 1_000,
|
|
28
|
+
maxValuesPerKey: 10_000,
|
|
29
|
+
};
|
|
30
|
+
// Valid tag key/value pattern: alphanumeric, dash, underscore, colon, dot
|
|
31
|
+
const TAG_PATTERN = /^[a-zA-Z0-9\-_:.]+$/;
|
|
32
|
+
// ─── CostAllocator Class ────────────────────────────────────────────────────
|
|
33
|
+
class CostAllocator {
|
|
34
|
+
config;
|
|
35
|
+
entries = [];
|
|
36
|
+
maxEntries = 100_000;
|
|
37
|
+
// Cardinality tracking
|
|
38
|
+
knownKeys = new Set();
|
|
39
|
+
knownValuesPerKey = new Map();
|
|
40
|
+
// Required tags per key
|
|
41
|
+
requiredTags = new Map();
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.config = { ...DEFAULT_COST_TAG_CONFIG, ...config };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse and validate tags from X-Cost-Tags header value.
|
|
47
|
+
* Header format: JSON-encoded object, e.g. {"project":"acme","dept":"eng"}
|
|
48
|
+
*/
|
|
49
|
+
parseTags(headerValue) {
|
|
50
|
+
if (!headerValue || !headerValue.trim())
|
|
51
|
+
return {};
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(headerValue);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new Error('Invalid X-Cost-Tags header: must be valid JSON object');
|
|
58
|
+
}
|
|
59
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
60
|
+
throw new Error('Invalid X-Cost-Tags header: must be a JSON object');
|
|
61
|
+
}
|
|
62
|
+
const tags = parsed;
|
|
63
|
+
const keys = Object.keys(tags);
|
|
64
|
+
if (keys.length > this.config.maxTagsPerRequest) {
|
|
65
|
+
throw new Error(`Too many cost tags: ${keys.length} (max ${this.config.maxTagsPerRequest})`);
|
|
66
|
+
}
|
|
67
|
+
const validated = {};
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
const value = String(tags[key]);
|
|
70
|
+
if (key.length > this.config.maxKeyLength) {
|
|
71
|
+
throw new Error(`Tag key "${key}" exceeds max length ${this.config.maxKeyLength}`);
|
|
72
|
+
}
|
|
73
|
+
if (value.length > this.config.maxValueLength) {
|
|
74
|
+
throw new Error(`Tag value for "${key}" exceeds max length ${this.config.maxValueLength}`);
|
|
75
|
+
}
|
|
76
|
+
if (!TAG_PATTERN.test(key)) {
|
|
77
|
+
throw new Error(`Invalid tag key "${key}": must match [a-zA-Z0-9\\-_:.]`);
|
|
78
|
+
}
|
|
79
|
+
if (!TAG_PATTERN.test(value)) {
|
|
80
|
+
throw new Error(`Invalid tag value for "${key}": must match [a-zA-Z0-9\\-_:.]`);
|
|
81
|
+
}
|
|
82
|
+
validated[key] = value;
|
|
83
|
+
}
|
|
84
|
+
return validated;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Record a tagged usage event.
|
|
88
|
+
*/
|
|
89
|
+
record(tags, apiKey, toolName, credits) {
|
|
90
|
+
if (!this.config.enabled)
|
|
91
|
+
return;
|
|
92
|
+
if (Object.keys(tags).length === 0)
|
|
93
|
+
return;
|
|
94
|
+
// Check cardinality limits
|
|
95
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
96
|
+
if (!this.knownKeys.has(key)) {
|
|
97
|
+
if (this.knownKeys.size >= this.config.maxUniqueKeys)
|
|
98
|
+
continue; // Skip new keys over limit
|
|
99
|
+
this.knownKeys.add(key);
|
|
100
|
+
}
|
|
101
|
+
let valSet = this.knownValuesPerKey.get(key);
|
|
102
|
+
if (!valSet) {
|
|
103
|
+
valSet = new Set();
|
|
104
|
+
this.knownValuesPerKey.set(key, valSet);
|
|
105
|
+
}
|
|
106
|
+
if (!valSet.has(value)) {
|
|
107
|
+
if (valSet.size >= this.config.maxValuesPerKey)
|
|
108
|
+
continue; // Skip new values over limit
|
|
109
|
+
valSet.add(value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Evict oldest entries if at capacity
|
|
113
|
+
if (this.entries.length >= this.maxEntries) {
|
|
114
|
+
this.entries.splice(0, Math.floor(this.maxEntries * 0.1));
|
|
115
|
+
}
|
|
116
|
+
this.entries.push({
|
|
117
|
+
tags,
|
|
118
|
+
apiKey,
|
|
119
|
+
toolName,
|
|
120
|
+
credits,
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Check if a request has all required tags for the given API key.
|
|
126
|
+
*/
|
|
127
|
+
validateRequired(apiKey, tags) {
|
|
128
|
+
const required = this.requiredTags.get(apiKey);
|
|
129
|
+
if (!required || required.length === 0)
|
|
130
|
+
return [];
|
|
131
|
+
const missing = [];
|
|
132
|
+
for (const key of required) {
|
|
133
|
+
if (!tags[key])
|
|
134
|
+
missing.push(key);
|
|
135
|
+
}
|
|
136
|
+
return missing;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Set required tags for an API key.
|
|
140
|
+
*/
|
|
141
|
+
setRequiredTags(apiKey, tagKeys) {
|
|
142
|
+
if (tagKeys.length === 0) {
|
|
143
|
+
this.requiredTags.delete(apiKey);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
this.requiredTags.set(apiKey, tagKeys);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get required tags for an API key.
|
|
151
|
+
*/
|
|
152
|
+
getRequiredTags(apiKey) {
|
|
153
|
+
return this.requiredTags.get(apiKey) || [];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Generate a chargeback report grouped by a single dimension.
|
|
157
|
+
*/
|
|
158
|
+
report(dimension, startMs, endMs) {
|
|
159
|
+
const filtered = this.filterEntries(startMs, endMs);
|
|
160
|
+
const groups = new Map();
|
|
161
|
+
for (const entry of filtered) {
|
|
162
|
+
const value = entry.tags[dimension];
|
|
163
|
+
if (value === undefined)
|
|
164
|
+
continue;
|
|
165
|
+
const group = groups.get(value) || { credits: 0, calls: 0 };
|
|
166
|
+
group.credits += entry.credits;
|
|
167
|
+
group.calls++;
|
|
168
|
+
groups.set(value, group);
|
|
169
|
+
}
|
|
170
|
+
const rows = Array.from(groups.entries())
|
|
171
|
+
.map(([value, g]) => ({
|
|
172
|
+
dimension,
|
|
173
|
+
value,
|
|
174
|
+
totalCredits: g.credits,
|
|
175
|
+
totalCalls: g.calls,
|
|
176
|
+
avgCreditsPerCall: g.calls > 0 ? Math.round((g.credits / g.calls) * 100) / 100 : 0,
|
|
177
|
+
}))
|
|
178
|
+
.sort((a, b) => b.totalCredits - a.totalCredits);
|
|
179
|
+
const totalCredits = rows.reduce((sum, r) => sum + r.totalCredits, 0);
|
|
180
|
+
const totalCalls = rows.reduce((sum, r) => sum + r.totalCalls, 0);
|
|
181
|
+
return {
|
|
182
|
+
dimension,
|
|
183
|
+
rows,
|
|
184
|
+
totalCredits,
|
|
185
|
+
totalCalls,
|
|
186
|
+
generatedAt: new Date().toISOString(),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Generate a cross-tabulation report (group by two dimensions).
|
|
191
|
+
*/
|
|
192
|
+
crossTab(dim1, dim2, startMs, endMs) {
|
|
193
|
+
const filtered = this.filterEntries(startMs, endMs);
|
|
194
|
+
const groups = new Map();
|
|
195
|
+
for (const entry of filtered) {
|
|
196
|
+
const v1 = entry.tags[dim1];
|
|
197
|
+
const v2 = entry.tags[dim2];
|
|
198
|
+
if (v1 === undefined || v2 === undefined)
|
|
199
|
+
continue;
|
|
200
|
+
const key = `${v1}|||${v2}`;
|
|
201
|
+
const group = groups.get(key) || { credits: 0, calls: 0 };
|
|
202
|
+
group.credits += entry.credits;
|
|
203
|
+
group.calls++;
|
|
204
|
+
groups.set(key, group);
|
|
205
|
+
}
|
|
206
|
+
const rows = Array.from(groups.entries())
|
|
207
|
+
.map(([key, g]) => {
|
|
208
|
+
const [v1, v2] = key.split('|||');
|
|
209
|
+
return {
|
|
210
|
+
dim1Value: v1,
|
|
211
|
+
dim2Value: v2,
|
|
212
|
+
totalCredits: g.credits,
|
|
213
|
+
totalCalls: g.calls,
|
|
214
|
+
};
|
|
215
|
+
})
|
|
216
|
+
.sort((a, b) => b.totalCredits - a.totalCredits);
|
|
217
|
+
const totalCredits = rows.reduce((sum, r) => sum + r.totalCredits, 0);
|
|
218
|
+
const totalCalls = rows.reduce((sum, r) => sum + r.totalCalls, 0);
|
|
219
|
+
return {
|
|
220
|
+
dim1,
|
|
221
|
+
dim2,
|
|
222
|
+
rows,
|
|
223
|
+
totalCredits,
|
|
224
|
+
totalCalls,
|
|
225
|
+
generatedAt: new Date().toISOString(),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Export report as CSV string.
|
|
230
|
+
*/
|
|
231
|
+
reportToCsv(report) {
|
|
232
|
+
const lines = ['dimension,value,totalCredits,totalCalls,avgCreditsPerCall'];
|
|
233
|
+
for (const row of report.rows) {
|
|
234
|
+
lines.push(`${row.dimension},${row.value},${row.totalCredits},${row.totalCalls},${row.avgCreditsPerCall}`);
|
|
235
|
+
}
|
|
236
|
+
return lines.join('\n');
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Update configuration at runtime.
|
|
240
|
+
*/
|
|
241
|
+
configure(updates) {
|
|
242
|
+
if (updates.enabled !== undefined)
|
|
243
|
+
this.config.enabled = updates.enabled;
|
|
244
|
+
if (updates.maxTagsPerRequest !== undefined)
|
|
245
|
+
this.config.maxTagsPerRequest = Math.max(1, Math.min(50, updates.maxTagsPerRequest));
|
|
246
|
+
if (updates.maxKeyLength !== undefined)
|
|
247
|
+
this.config.maxKeyLength = Math.max(8, Math.min(256, updates.maxKeyLength));
|
|
248
|
+
if (updates.maxValueLength !== undefined)
|
|
249
|
+
this.config.maxValueLength = Math.max(8, Math.min(256, updates.maxValueLength));
|
|
250
|
+
if (updates.maxUniqueKeys !== undefined)
|
|
251
|
+
this.config.maxUniqueKeys = Math.max(10, Math.min(10_000, updates.maxUniqueKeys));
|
|
252
|
+
if (updates.maxValuesPerKey !== undefined)
|
|
253
|
+
this.config.maxValuesPerKey = Math.max(100, Math.min(100_000, updates.maxValuesPerKey));
|
|
254
|
+
return { ...this.config };
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Clear all recorded entries.
|
|
258
|
+
*/
|
|
259
|
+
clear() {
|
|
260
|
+
this.entries = [];
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get statistics.
|
|
264
|
+
*/
|
|
265
|
+
stats() {
|
|
266
|
+
const valuesPerKey = {};
|
|
267
|
+
for (const [key, valSet] of this.knownValuesPerKey) {
|
|
268
|
+
valuesPerKey[key] = valSet.size;
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
enabled: this.config.enabled,
|
|
272
|
+
config: { ...this.config },
|
|
273
|
+
totalEntries: this.entries.length,
|
|
274
|
+
uniqueKeys: this.knownKeys.size,
|
|
275
|
+
valuesPerKey,
|
|
276
|
+
totalCreditsTracked: this.entries.reduce((sum, e) => sum + e.credits, 0),
|
|
277
|
+
totalCallsTracked: this.entries.length,
|
|
278
|
+
requiredTagKeys: this.requiredTags.size,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/** Is cost allocation enabled? */
|
|
282
|
+
get enabled() {
|
|
283
|
+
return this.config.enabled;
|
|
284
|
+
}
|
|
285
|
+
/** Current config (copy). */
|
|
286
|
+
get currentConfig() {
|
|
287
|
+
return { ...this.config };
|
|
288
|
+
}
|
|
289
|
+
/** Number of recorded entries. */
|
|
290
|
+
get size() {
|
|
291
|
+
return this.entries.length;
|
|
292
|
+
}
|
|
293
|
+
// ─── Private ───────────────────────────────────────────────────────────────
|
|
294
|
+
filterEntries(startMs, endMs) {
|
|
295
|
+
if (!startMs && !endMs)
|
|
296
|
+
return this.entries;
|
|
297
|
+
return this.entries.filter(e => {
|
|
298
|
+
if (startMs && e.timestamp < startMs)
|
|
299
|
+
return false;
|
|
300
|
+
if (endMs && e.timestamp > endMs)
|
|
301
|
+
return false;
|
|
302
|
+
return true;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
exports.CostAllocator = CostAllocator;
|
|
307
|
+
//# sourceMappingURL=cost-tags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-tags.js","sourceRoot":"","sources":["../src/cost-tags.ts"],"names":[],"mappings":"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"}
|
package/dist/dedup.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RequestDeduplicator — Idempotency layer for MCP tool calls.
|
|
3
|
+
*
|
|
4
|
+
* Prevents duplicate billing when agents retry the same request due to
|
|
5
|
+
* timeouts, network flakiness, or client-side retry logic. Clients send
|
|
6
|
+
* an `X-Idempotency-Key` header or the proxy auto-generates one via
|
|
7
|
+
* SHA-256 of apiKey + tool + sorted args.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - X-Idempotency-Key header support with auto-generation fallback
|
|
11
|
+
* - In-flight request coalescing (concurrent duplicates share the first result)
|
|
12
|
+
* - Configurable TTL window for completed request dedup
|
|
13
|
+
* - Per-key dedup tracking with LRU eviction
|
|
14
|
+
* - Stats: deduplicated requests, coalesced in-flight, credits saved
|
|
15
|
+
*
|
|
16
|
+
* Zero external dependencies.
|
|
17
|
+
*/
|
|
18
|
+
export interface DedupConfig {
|
|
19
|
+
/** Enable deduplication. Default true. */
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
/** TTL in ms for completed request cache. Default 60_000 (60s). */
|
|
22
|
+
ttlMs: number;
|
|
23
|
+
/** Max cached entries. Default 10_000. */
|
|
24
|
+
maxEntries: number;
|
|
25
|
+
/** Auto-generate idempotency key from apiKey+tool+args when header missing. Default true. */
|
|
26
|
+
autoGenerate: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface DedupEntry {
|
|
29
|
+
key: string;
|
|
30
|
+
result: unknown;
|
|
31
|
+
completedAt: number;
|
|
32
|
+
toolName: string;
|
|
33
|
+
apiKey: string;
|
|
34
|
+
}
|
|
35
|
+
export interface InFlightEntry {
|
|
36
|
+
promise: Promise<unknown>;
|
|
37
|
+
toolName: string;
|
|
38
|
+
apiKey: string;
|
|
39
|
+
startedAt: number;
|
|
40
|
+
}
|
|
41
|
+
export interface DedupStats {
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
config: DedupConfig;
|
|
44
|
+
cachedEntries: number;
|
|
45
|
+
inFlightEntries: number;
|
|
46
|
+
totalDeduped: number;
|
|
47
|
+
totalCoalesced: number;
|
|
48
|
+
creditsSaved: number;
|
|
49
|
+
hitRate: number;
|
|
50
|
+
totalChecks: number;
|
|
51
|
+
}
|
|
52
|
+
export interface DedupResult<T> {
|
|
53
|
+
result: T;
|
|
54
|
+
deduplicated: boolean;
|
|
55
|
+
idempotencyKey: string;
|
|
56
|
+
}
|
|
57
|
+
export declare class RequestDeduplicator {
|
|
58
|
+
private config;
|
|
59
|
+
private cache;
|
|
60
|
+
private inFlight;
|
|
61
|
+
private totalDeduped;
|
|
62
|
+
private totalCoalesced;
|
|
63
|
+
private creditsSaved;
|
|
64
|
+
private totalChecks;
|
|
65
|
+
private insertionOrder;
|
|
66
|
+
constructor(config?: Partial<DedupConfig>);
|
|
67
|
+
/**
|
|
68
|
+
* Generate an idempotency key from request parameters.
|
|
69
|
+
*/
|
|
70
|
+
generateKey(apiKey: string, toolName: string, args: unknown): string;
|
|
71
|
+
/**
|
|
72
|
+
* Execute a function with deduplication.
|
|
73
|
+
* If an identical request is in-flight, coalesces onto it.
|
|
74
|
+
* If a completed result is cached, returns it immediately.
|
|
75
|
+
*/
|
|
76
|
+
execute<T>(idempotencyKey: string, apiKey: string, toolName: string, creditCost: number, fn: () => Promise<T>): Promise<DedupResult<T>>;
|
|
77
|
+
/**
|
|
78
|
+
* Update dedup configuration at runtime.
|
|
79
|
+
*/
|
|
80
|
+
configure(updates: Partial<DedupConfig>): DedupConfig;
|
|
81
|
+
/**
|
|
82
|
+
* Clear the dedup cache.
|
|
83
|
+
*/
|
|
84
|
+
clear(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Get statistics.
|
|
87
|
+
*/
|
|
88
|
+
stats(): DedupStats;
|
|
89
|
+
/** Is dedup enabled? */
|
|
90
|
+
get enabled(): boolean;
|
|
91
|
+
/** Current config. */
|
|
92
|
+
get currentConfig(): DedupConfig;
|
|
93
|
+
private cacheResult;
|
|
94
|
+
private pruneExpired;
|
|
95
|
+
private sortKeys;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=dedup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedup.d.ts","sourceRoot":"","sources":["../src/dedup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,MAAM,WAAW,WAAW;IAC1B,0CAA0C;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,6FAA6F;IAC7F,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,MAAM,EAAE,CAAC,CAAC;IACV,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;CACxB;AAaD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAAc;IAG5B,OAAO,CAAC,KAAK,CAAiC;IAE9C,OAAO,CAAC,QAAQ,CAAoC;IAGpD,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,WAAW,CAAK;IAGxB,OAAO,CAAC,cAAc,CAAgB;gBAE1B,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC;IAIzC;;OAEG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM;IAUpE;;;;OAIG;IACG,OAAO,CAAC,CAAC,EACb,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IA0D1B;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW;IAQrD;;OAEG;IACH,KAAK,IAAI,IAAI;IAKb;;OAEG;IACH,KAAK,IAAI,UAAU;IAenB,wBAAwB;IACxB,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,sBAAsB;IACtB,IAAI,aAAa,IAAI,WAAW,CAE/B;IAID,OAAO,CAAC,WAAW;IAiBnB,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,QAAQ;CAYjB"}
|