@modelcost/sdk 0.1.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/index.d.mts +1208 -0
- package/dist/index.d.ts +1208 -0
- package/dist/index.js +2068 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2045 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +46 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2045 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
var BudgetAction = z.enum(["alert", "throttle", "block"]);
|
|
5
|
+
var BudgetScope = z.enum([
|
|
6
|
+
"organization",
|
|
7
|
+
"feature",
|
|
8
|
+
"environment",
|
|
9
|
+
"custom"
|
|
10
|
+
]);
|
|
11
|
+
var BudgetPeriod = z.enum(["daily", "weekly", "monthly", "custom"]);
|
|
12
|
+
var Provider = z.enum([
|
|
13
|
+
"openai",
|
|
14
|
+
"anthropic",
|
|
15
|
+
"google",
|
|
16
|
+
"aws_bedrock",
|
|
17
|
+
"cohere",
|
|
18
|
+
"mistral"
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
22
|
+
var ModelCostInitOptionsSchema = z.object({
|
|
23
|
+
apiKey: z.string().startsWith("mc_", { message: "API key must start with 'mc_'" }).describe("ModelCost API key"),
|
|
24
|
+
orgId: z.string().min(1, { message: "Organization ID is required" }),
|
|
25
|
+
environment: z.string().default("production"),
|
|
26
|
+
baseUrl: z.string().url().default("https://api.modelcost.ai"),
|
|
27
|
+
monthlyBudget: z.number().positive().optional(),
|
|
28
|
+
budgetAction: BudgetAction.default("alert"),
|
|
29
|
+
failOpen: z.boolean().default(true),
|
|
30
|
+
flushIntervalMs: z.number().int().positive().default(5e3),
|
|
31
|
+
flushBatchSize: z.number().int().positive().default(100),
|
|
32
|
+
syncIntervalMs: z.number().int().positive().default(1e4),
|
|
33
|
+
contentPrivacy: z.boolean().default(false)
|
|
34
|
+
});
|
|
35
|
+
var ModelCostConfig = class {
|
|
36
|
+
apiKey;
|
|
37
|
+
orgId;
|
|
38
|
+
environment;
|
|
39
|
+
baseUrl;
|
|
40
|
+
monthlyBudget;
|
|
41
|
+
budgetAction;
|
|
42
|
+
failOpen;
|
|
43
|
+
flushIntervalMs;
|
|
44
|
+
flushBatchSize;
|
|
45
|
+
syncIntervalMs;
|
|
46
|
+
contentPrivacy;
|
|
47
|
+
constructor(options) {
|
|
48
|
+
const merged = {
|
|
49
|
+
apiKey: options.apiKey ?? process.env["MODELCOST_API_KEY"],
|
|
50
|
+
orgId: options.orgId ?? process.env["MODELCOST_ORG_ID"],
|
|
51
|
+
environment: options.environment ?? process.env["MODELCOST_ENV"] ?? "production",
|
|
52
|
+
baseUrl: options.baseUrl ?? process.env["MODELCOST_BASE_URL"] ?? "https://api.modelcost.ai",
|
|
53
|
+
monthlyBudget: options.monthlyBudget,
|
|
54
|
+
budgetAction: options.budgetAction ?? "alert",
|
|
55
|
+
failOpen: options.failOpen ?? true,
|
|
56
|
+
flushIntervalMs: options.flushIntervalMs ?? 5e3,
|
|
57
|
+
flushBatchSize: options.flushBatchSize ?? 100,
|
|
58
|
+
syncIntervalMs: options.syncIntervalMs ?? 1e4,
|
|
59
|
+
contentPrivacy: options.contentPrivacy ?? (process.env["MODELCOST_CONTENT_PRIVACY"] === "true" || false)
|
|
60
|
+
};
|
|
61
|
+
const parsed = ModelCostInitOptionsSchema.parse(merged);
|
|
62
|
+
this.apiKey = parsed.apiKey;
|
|
63
|
+
this.orgId = parsed.orgId;
|
|
64
|
+
this.environment = parsed.environment;
|
|
65
|
+
this.baseUrl = parsed.baseUrl;
|
|
66
|
+
this.monthlyBudget = parsed.monthlyBudget;
|
|
67
|
+
this.budgetAction = parsed.budgetAction;
|
|
68
|
+
this.failOpen = parsed.failOpen;
|
|
69
|
+
this.flushIntervalMs = parsed.flushIntervalMs;
|
|
70
|
+
this.flushBatchSize = parsed.flushBatchSize;
|
|
71
|
+
this.syncIntervalMs = parsed.syncIntervalMs;
|
|
72
|
+
this.contentPrivacy = parsed.contentPrivacy;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/version.ts
|
|
77
|
+
var VERSION = "0.1.0";
|
|
78
|
+
|
|
79
|
+
// src/errors.ts
|
|
80
|
+
var ModelCostError = class extends Error {
|
|
81
|
+
constructor(message) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.name = "ModelCostError";
|
|
84
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var ConfigurationError = class extends ModelCostError {
|
|
88
|
+
constructor(message) {
|
|
89
|
+
super(message);
|
|
90
|
+
this.name = "ConfigurationError";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var BudgetExceededError = class extends ModelCostError {
|
|
94
|
+
remainingBudget;
|
|
95
|
+
scope;
|
|
96
|
+
overrideUrl;
|
|
97
|
+
constructor(message, remainingBudget, scope, overrideUrl) {
|
|
98
|
+
super(message);
|
|
99
|
+
this.name = "BudgetExceededError";
|
|
100
|
+
this.remainingBudget = remainingBudget;
|
|
101
|
+
this.scope = scope;
|
|
102
|
+
this.overrideUrl = overrideUrl;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var RateLimitedError = class extends ModelCostError {
|
|
106
|
+
retryAfterSeconds;
|
|
107
|
+
limitDimension;
|
|
108
|
+
constructor(message, retryAfterSeconds, limitDimension) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.name = "RateLimitedError";
|
|
111
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
112
|
+
this.limitDimension = limitDimension;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var PiiDetectedError = class extends ModelCostError {
|
|
116
|
+
detectedEntities;
|
|
117
|
+
redactedText;
|
|
118
|
+
constructor(message, detectedEntities, redactedText) {
|
|
119
|
+
super(message);
|
|
120
|
+
this.name = "PiiDetectedError";
|
|
121
|
+
this.detectedEntities = detectedEntities;
|
|
122
|
+
this.redactedText = redactedText;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var SessionBudgetExceededError = class extends ModelCostError {
|
|
126
|
+
sessionId;
|
|
127
|
+
currentSpend;
|
|
128
|
+
maxSpend;
|
|
129
|
+
constructor(message, sessionId, currentSpend, maxSpend) {
|
|
130
|
+
super(message);
|
|
131
|
+
this.name = "SessionBudgetExceededError";
|
|
132
|
+
this.sessionId = sessionId;
|
|
133
|
+
this.currentSpend = currentSpend;
|
|
134
|
+
this.maxSpend = maxSpend;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
var SessionIterationLimitExceededError = class extends ModelCostError {
|
|
138
|
+
sessionId;
|
|
139
|
+
currentIterations;
|
|
140
|
+
maxIterations;
|
|
141
|
+
constructor(message, sessionId, currentIterations, maxIterations) {
|
|
142
|
+
super(message);
|
|
143
|
+
this.name = "SessionIterationLimitExceededError";
|
|
144
|
+
this.sessionId = sessionId;
|
|
145
|
+
this.currentIterations = currentIterations;
|
|
146
|
+
this.maxIterations = maxIterations;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
var ModelCostApiError = class extends ModelCostError {
|
|
150
|
+
statusCode;
|
|
151
|
+
errorCode;
|
|
152
|
+
constructor(message, statusCode, errorCode) {
|
|
153
|
+
super(message);
|
|
154
|
+
this.name = "ModelCostApiError";
|
|
155
|
+
this.statusCode = statusCode;
|
|
156
|
+
this.errorCode = errorCode;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
z.object({
|
|
160
|
+
apiKey: z.string().min(1),
|
|
161
|
+
timestamp: z.string().datetime(),
|
|
162
|
+
provider: Provider,
|
|
163
|
+
model: z.string().min(1),
|
|
164
|
+
feature: z.string().optional(),
|
|
165
|
+
customerId: z.string().optional(),
|
|
166
|
+
inputTokens: z.number().int().min(0),
|
|
167
|
+
outputTokens: z.number().int().min(0),
|
|
168
|
+
latencyMs: z.number().int().optional(),
|
|
169
|
+
metadata: z.record(z.unknown()).optional()
|
|
170
|
+
});
|
|
171
|
+
function trackRequestToApi(request) {
|
|
172
|
+
return {
|
|
173
|
+
api_key: request.apiKey,
|
|
174
|
+
timestamp: request.timestamp,
|
|
175
|
+
provider: request.provider,
|
|
176
|
+
model: request.model,
|
|
177
|
+
feature: request.feature ?? null,
|
|
178
|
+
customer_id: request.customerId ?? null,
|
|
179
|
+
input_tokens: request.inputTokens,
|
|
180
|
+
output_tokens: request.outputTokens,
|
|
181
|
+
latency_ms: request.latencyMs ?? null,
|
|
182
|
+
metadata: request.metadata ?? null
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
var TrackResponseSchema = z.object({
|
|
186
|
+
status: z.literal("ok")
|
|
187
|
+
});
|
|
188
|
+
var CreateSessionResponseSchema = z.object({
|
|
189
|
+
id: z.string(),
|
|
190
|
+
session_id: z.string(),
|
|
191
|
+
status: z.string(),
|
|
192
|
+
max_spend_usd: z.number().nullable().optional(),
|
|
193
|
+
max_iterations: z.number().nullable().optional()
|
|
194
|
+
}).transform((raw) => ({
|
|
195
|
+
id: raw.id,
|
|
196
|
+
sessionId: raw.session_id,
|
|
197
|
+
status: raw.status,
|
|
198
|
+
maxSpendUsd: raw.max_spend_usd ?? void 0,
|
|
199
|
+
maxIterations: raw.max_iterations ?? void 0
|
|
200
|
+
}));
|
|
201
|
+
function createSessionRequestToApi(req) {
|
|
202
|
+
return {
|
|
203
|
+
api_key: req.apiKey,
|
|
204
|
+
session_id: req.sessionId,
|
|
205
|
+
feature: req.feature,
|
|
206
|
+
user_id: req.userId,
|
|
207
|
+
max_spend_usd: req.maxSpendUsd,
|
|
208
|
+
max_iterations: req.maxIterations
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function recordSessionCallRequestToApi(req) {
|
|
212
|
+
return {
|
|
213
|
+
api_key: req.apiKey,
|
|
214
|
+
call_sequence: req.callSequence,
|
|
215
|
+
call_type: req.callType,
|
|
216
|
+
tool_name: req.toolName,
|
|
217
|
+
input_tokens: req.inputTokens,
|
|
218
|
+
output_tokens: req.outputTokens,
|
|
219
|
+
cumulative_input_tokens: req.cumulativeInputTokens,
|
|
220
|
+
cost_usd: req.costUsd,
|
|
221
|
+
cumulative_cost_usd: req.cumulativeCostUsd,
|
|
222
|
+
pii_detected: req.piiDetected
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function closeSessionRequestToApi(req) {
|
|
226
|
+
return {
|
|
227
|
+
api_key: req.apiKey,
|
|
228
|
+
status: req.status,
|
|
229
|
+
termination_reason: req.terminationReason,
|
|
230
|
+
final_spend_usd: req.finalSpendUsd,
|
|
231
|
+
final_iteration_count: req.finalIterationCount
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
var BudgetCheckResponseSchema = z.object({
|
|
235
|
+
allowed: z.boolean(),
|
|
236
|
+
action: z.string().nullable(),
|
|
237
|
+
throttle_percentage: z.number().nullable(),
|
|
238
|
+
reason: z.string().nullable()
|
|
239
|
+
}).transform((data) => ({
|
|
240
|
+
allowed: data.allowed,
|
|
241
|
+
action: data.action,
|
|
242
|
+
throttlePercentage: data.throttle_percentage,
|
|
243
|
+
reason: data.reason
|
|
244
|
+
}));
|
|
245
|
+
var BudgetPolicySchema = z.object({
|
|
246
|
+
id: z.string(),
|
|
247
|
+
name: z.string(),
|
|
248
|
+
scope: BudgetScope,
|
|
249
|
+
scope_identifier: z.string().nullable(),
|
|
250
|
+
budget_amount_usd: z.number(),
|
|
251
|
+
period: BudgetPeriod,
|
|
252
|
+
custom_period_days: z.number().int().nullable(),
|
|
253
|
+
action: BudgetAction,
|
|
254
|
+
throttle_percentage: z.number().nullable(),
|
|
255
|
+
alert_thresholds: z.array(z.number().int()).nullable(),
|
|
256
|
+
current_spend_usd: z.number(),
|
|
257
|
+
spend_percentage: z.number(),
|
|
258
|
+
period_start: z.string(),
|
|
259
|
+
is_active: z.boolean(),
|
|
260
|
+
created_at: z.string(),
|
|
261
|
+
updated_at: z.string()
|
|
262
|
+
}).transform((data) => ({
|
|
263
|
+
id: data.id,
|
|
264
|
+
name: data.name,
|
|
265
|
+
scope: data.scope,
|
|
266
|
+
scopeIdentifier: data.scope_identifier,
|
|
267
|
+
budgetAmountUsd: data.budget_amount_usd,
|
|
268
|
+
period: data.period,
|
|
269
|
+
customPeriodDays: data.custom_period_days,
|
|
270
|
+
action: data.action,
|
|
271
|
+
throttlePercentage: data.throttle_percentage,
|
|
272
|
+
alertThresholds: data.alert_thresholds,
|
|
273
|
+
currentSpendUsd: data.current_spend_usd,
|
|
274
|
+
spendPercentage: data.spend_percentage,
|
|
275
|
+
periodStart: data.period_start,
|
|
276
|
+
isActive: data.is_active,
|
|
277
|
+
createdAt: data.created_at,
|
|
278
|
+
updatedAt: data.updated_at
|
|
279
|
+
}));
|
|
280
|
+
var BudgetStatusResponseSchema = z.object({
|
|
281
|
+
policies: z.array(BudgetPolicySchema),
|
|
282
|
+
total_budget_usd: z.number(),
|
|
283
|
+
total_spend_usd: z.number(),
|
|
284
|
+
policies_at_risk: z.number().int()
|
|
285
|
+
}).transform((data) => ({
|
|
286
|
+
policies: data.policies,
|
|
287
|
+
totalBudgetUsd: data.total_budget_usd,
|
|
288
|
+
totalSpendUsd: data.total_spend_usd,
|
|
289
|
+
policiesAtRisk: data.policies_at_risk
|
|
290
|
+
}));
|
|
291
|
+
z.object({
|
|
292
|
+
orgId: z.string().min(1),
|
|
293
|
+
text: z.string().min(1),
|
|
294
|
+
feature: z.string().optional(),
|
|
295
|
+
environment: z.string().optional()
|
|
296
|
+
});
|
|
297
|
+
function governanceScanRequestToApi(request) {
|
|
298
|
+
return {
|
|
299
|
+
org_id: request.orgId,
|
|
300
|
+
text: request.text,
|
|
301
|
+
feature: request.feature ?? null,
|
|
302
|
+
environment: request.environment ?? null
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
var DetectedViolationSchema = z.object({
|
|
306
|
+
type: z.string(),
|
|
307
|
+
subtype: z.string(),
|
|
308
|
+
severity: z.enum(["low", "medium", "high"]),
|
|
309
|
+
start: z.number().int(),
|
|
310
|
+
end: z.number().int()
|
|
311
|
+
});
|
|
312
|
+
var GovernanceScanResponseSchema = z.object({
|
|
313
|
+
is_allowed: z.boolean(),
|
|
314
|
+
action: z.string().nullable(),
|
|
315
|
+
violations: z.array(DetectedViolationSchema),
|
|
316
|
+
redacted_text: z.string().nullable()
|
|
317
|
+
}).transform((data) => ({
|
|
318
|
+
isAllowed: data.is_allowed,
|
|
319
|
+
action: data.action,
|
|
320
|
+
violations: data.violations,
|
|
321
|
+
redactedText: data.redacted_text
|
|
322
|
+
}));
|
|
323
|
+
function governanceSignalRequestToApi(request) {
|
|
324
|
+
return {
|
|
325
|
+
organization_id: request.organizationId,
|
|
326
|
+
violation_type: request.violationType,
|
|
327
|
+
violation_subtype: request.violationSubtype ?? null,
|
|
328
|
+
severity: request.severity,
|
|
329
|
+
user_id: request.userId ?? null,
|
|
330
|
+
feature: request.feature ?? null,
|
|
331
|
+
environment: request.environment ?? null,
|
|
332
|
+
action_taken: request.actionTaken,
|
|
333
|
+
was_allowed: request.wasAllowed,
|
|
334
|
+
detected_at: request.detectedAt ?? null,
|
|
335
|
+
source: request.source,
|
|
336
|
+
violation_count: request.violationCount ?? 1
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/client.ts
|
|
341
|
+
var CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
342
|
+
var CIRCUIT_BREAKER_COOLDOWN_MS = 6e4;
|
|
343
|
+
var ModelCostClient = class {
|
|
344
|
+
_baseUrl;
|
|
345
|
+
_apiKey;
|
|
346
|
+
_headers;
|
|
347
|
+
_failOpen;
|
|
348
|
+
/** Circuit breaker state */
|
|
349
|
+
_consecutiveFailures = 0;
|
|
350
|
+
_circuitOpenUntil = 0;
|
|
351
|
+
constructor(config) {
|
|
352
|
+
this._baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
353
|
+
this._apiKey = config.apiKey;
|
|
354
|
+
this._failOpen = config.failOpen;
|
|
355
|
+
this._headers = {
|
|
356
|
+
"Content-Type": "application/json",
|
|
357
|
+
"X-API-Key": this._apiKey,
|
|
358
|
+
"User-Agent": `modelcost-node/${VERSION}`
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Record a tracked AI API call.
|
|
363
|
+
*/
|
|
364
|
+
async track(request) {
|
|
365
|
+
const body = trackRequestToApi(request);
|
|
366
|
+
const data = await this._post("/api/v1/track", body);
|
|
367
|
+
return TrackResponseSchema.parse(data);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Pre-flight budget check before making an AI call.
|
|
371
|
+
*/
|
|
372
|
+
async checkBudget(orgId, feature, estimatedCost) {
|
|
373
|
+
const params = new URLSearchParams({
|
|
374
|
+
org_id: orgId,
|
|
375
|
+
feature,
|
|
376
|
+
estimated_cost: estimatedCost.toString()
|
|
377
|
+
});
|
|
378
|
+
const data = await this._post(`/api/v1/budgets/check?${params.toString()}`, {});
|
|
379
|
+
return BudgetCheckResponseSchema.parse(data);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Scan text for PII and governance violations.
|
|
383
|
+
*/
|
|
384
|
+
async scanText(request) {
|
|
385
|
+
const body = governanceScanRequestToApi(request);
|
|
386
|
+
const data = await this._post("/api/v1/governance/scan", body);
|
|
387
|
+
return GovernanceScanResponseSchema.parse(data);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Report a governance signal (metadata-only mode).
|
|
391
|
+
* Sends classification signals without raw content.
|
|
392
|
+
*/
|
|
393
|
+
async reportSignal(request) {
|
|
394
|
+
const body = governanceSignalRequestToApi(request);
|
|
395
|
+
await this._post("/api/v1/governance/signals", body);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Get current budget status for an organization.
|
|
399
|
+
*/
|
|
400
|
+
async getBudgetStatus(orgId) {
|
|
401
|
+
const params = new URLSearchParams({ org_id: orgId });
|
|
402
|
+
const data = await this._get(`/api/v1/budgets/status?${params.toString()}`);
|
|
403
|
+
return BudgetStatusResponseSchema.parse(data);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Create a new agent session on the server.
|
|
407
|
+
*/
|
|
408
|
+
async createSession(request) {
|
|
409
|
+
const body = createSessionRequestToApi(request);
|
|
410
|
+
const data = await this._post("/api/v1/sessions", body);
|
|
411
|
+
return CreateSessionResponseSchema.parse(data);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Record a call within an existing session.
|
|
415
|
+
*/
|
|
416
|
+
async recordSessionCall(sessionId, request) {
|
|
417
|
+
const body = recordSessionCallRequestToApi(request);
|
|
418
|
+
await this._post(`/api/v1/sessions/${sessionId}/calls`, body);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Close an existing session on the server.
|
|
422
|
+
*/
|
|
423
|
+
async closeSession(sessionId, request) {
|
|
424
|
+
const body = closeSessionRequestToApi(request);
|
|
425
|
+
await this._post(`/api/v1/sessions/${sessionId}/close`, body);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Close the client (cleanup resources).
|
|
429
|
+
*/
|
|
430
|
+
close() {
|
|
431
|
+
this._consecutiveFailures = 0;
|
|
432
|
+
this._circuitOpenUntil = 0;
|
|
433
|
+
}
|
|
434
|
+
// ─── Private helpers ───────────────────────────────────────────────
|
|
435
|
+
_isCircuitOpen() {
|
|
436
|
+
if (this._consecutiveFailures < CIRCUIT_BREAKER_THRESHOLD) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
if (Date.now() >= this._circuitOpenUntil) {
|
|
440
|
+
this._consecutiveFailures = 0;
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
_recordSuccess() {
|
|
446
|
+
this._consecutiveFailures = 0;
|
|
447
|
+
}
|
|
448
|
+
_recordFailure() {
|
|
449
|
+
this._consecutiveFailures++;
|
|
450
|
+
if (this._consecutiveFailures >= CIRCUIT_BREAKER_THRESHOLD) {
|
|
451
|
+
this._circuitOpenUntil = Date.now() + CIRCUIT_BREAKER_COOLDOWN_MS;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async _post(path, body) {
|
|
455
|
+
return this._request("POST", path, body);
|
|
456
|
+
}
|
|
457
|
+
async _get(path) {
|
|
458
|
+
return this._request("GET", path);
|
|
459
|
+
}
|
|
460
|
+
async _request(method, path, body) {
|
|
461
|
+
if (this._isCircuitOpen()) {
|
|
462
|
+
if (this._failOpen) {
|
|
463
|
+
console.warn(
|
|
464
|
+
`[ModelCost] Circuit breaker open \u2014 skipping ${method} ${path}`
|
|
465
|
+
);
|
|
466
|
+
return this._failOpenDefault(path);
|
|
467
|
+
}
|
|
468
|
+
throw new ModelCostError(
|
|
469
|
+
`Circuit breaker is open after ${CIRCUIT_BREAKER_THRESHOLD} consecutive failures`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
const url = `${this._baseUrl}${path}`;
|
|
474
|
+
const init = {
|
|
475
|
+
method,
|
|
476
|
+
headers: this._headers
|
|
477
|
+
};
|
|
478
|
+
if (body !== void 0 && method !== "GET") {
|
|
479
|
+
init.body = JSON.stringify(body);
|
|
480
|
+
}
|
|
481
|
+
const response = await fetch(url, init);
|
|
482
|
+
if (!response.ok) {
|
|
483
|
+
const errorBody = await response.json().catch(() => ({
|
|
484
|
+
error: "unknown",
|
|
485
|
+
message: response.statusText
|
|
486
|
+
}));
|
|
487
|
+
this._recordFailure();
|
|
488
|
+
const err = new ModelCostApiError(
|
|
489
|
+
errorBody.message ?? `HTTP ${response.status}`,
|
|
490
|
+
response.status,
|
|
491
|
+
errorBody.error ?? "unknown"
|
|
492
|
+
);
|
|
493
|
+
if (this._failOpen) {
|
|
494
|
+
console.warn(
|
|
495
|
+
`[ModelCost] API error (fail-open): ${err.message}`
|
|
496
|
+
);
|
|
497
|
+
return this._failOpenDefault(path);
|
|
498
|
+
}
|
|
499
|
+
throw err;
|
|
500
|
+
}
|
|
501
|
+
const data = await response.json();
|
|
502
|
+
this._recordSuccess();
|
|
503
|
+
return data;
|
|
504
|
+
} catch (error) {
|
|
505
|
+
if (error instanceof ModelCostApiError) {
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
this._recordFailure();
|
|
509
|
+
if (this._failOpen) {
|
|
510
|
+
console.warn(
|
|
511
|
+
`[ModelCost] Request failed (fail-open): ${error instanceof Error ? error.message : String(error)}`
|
|
512
|
+
);
|
|
513
|
+
return this._failOpenDefault(path);
|
|
514
|
+
}
|
|
515
|
+
throw new ModelCostError(
|
|
516
|
+
`Request to ${path} failed: ${error instanceof Error ? error.message : String(error)}`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Returns a safe default response when operating in fail-open mode.
|
|
522
|
+
*/
|
|
523
|
+
_failOpenDefault(path) {
|
|
524
|
+
if (path.includes("/budgets/check")) {
|
|
525
|
+
return {
|
|
526
|
+
allowed: true,
|
|
527
|
+
action: null,
|
|
528
|
+
throttle_percentage: null,
|
|
529
|
+
reason: "fail-open: API unavailable"
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
if (path.includes("/budgets/status")) {
|
|
533
|
+
return {
|
|
534
|
+
policies: [],
|
|
535
|
+
total_budget_usd: 0,
|
|
536
|
+
total_spend_usd: 0,
|
|
537
|
+
policies_at_risk: 0
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
if (path.includes("/governance/scan")) {
|
|
541
|
+
return {
|
|
542
|
+
is_allowed: true,
|
|
543
|
+
action: null,
|
|
544
|
+
violations: [],
|
|
545
|
+
redacted_text: null
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
if (path.includes("/sessions")) {
|
|
549
|
+
return { id: "fail-open", session_id: "fail-open", status: "active" };
|
|
550
|
+
}
|
|
551
|
+
return { status: "ok" };
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// src/budget.ts
|
|
556
|
+
var BudgetManager = class {
|
|
557
|
+
_cache = /* @__PURE__ */ new Map();
|
|
558
|
+
_lastSync = 0;
|
|
559
|
+
_syncIntervalMs;
|
|
560
|
+
constructor(syncIntervalMs) {
|
|
561
|
+
this._syncIntervalMs = syncIntervalMs;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Check whether a request with the given estimated cost is allowed.
|
|
565
|
+
* Uses the local cache if fresh, otherwise syncs from the API first.
|
|
566
|
+
*/
|
|
567
|
+
async check(client, orgId, feature, estimatedCost) {
|
|
568
|
+
if (this._isStale()) {
|
|
569
|
+
await this.sync(client, orgId);
|
|
570
|
+
}
|
|
571
|
+
return client.checkBudget(orgId, feature, estimatedCost);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Sync the budget status from the server and populate the local cache.
|
|
575
|
+
*/
|
|
576
|
+
async sync(client, orgId) {
|
|
577
|
+
const status = await client.getBudgetStatus(orgId);
|
|
578
|
+
this._cache.set(orgId, status);
|
|
579
|
+
this._lastSync = Date.now();
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Optimistically update local spend after a tracked call,
|
|
583
|
+
* so subsequent budget checks reflect the estimated spend
|
|
584
|
+
* without waiting for the next server sync.
|
|
585
|
+
*/
|
|
586
|
+
updateLocalSpend(orgId, _feature, cost) {
|
|
587
|
+
const cached = this._cache.get(orgId);
|
|
588
|
+
if (!cached) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const updated = {
|
|
592
|
+
...cached,
|
|
593
|
+
totalSpendUsd: cached.totalSpendUsd + cost,
|
|
594
|
+
policies: cached.policies.map((policy) => ({
|
|
595
|
+
...policy,
|
|
596
|
+
currentSpendUsd: policy.currentSpendUsd + cost,
|
|
597
|
+
spendPercentage: policy.budgetAmountUsd > 0 ? (policy.currentSpendUsd + cost) / policy.budgetAmountUsd * 100 : policy.spendPercentage
|
|
598
|
+
}))
|
|
599
|
+
};
|
|
600
|
+
this._cache.set(orgId, updated);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Get the cached budget status for an org, if available.
|
|
604
|
+
*/
|
|
605
|
+
getCached(orgId) {
|
|
606
|
+
return this._cache.get(orgId);
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Clear all cached state.
|
|
610
|
+
*/
|
|
611
|
+
clear() {
|
|
612
|
+
this._cache.clear();
|
|
613
|
+
this._lastSync = 0;
|
|
614
|
+
}
|
|
615
|
+
_isStale() {
|
|
616
|
+
return Date.now() - this._lastSync > this._syncIntervalMs;
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
// src/tracking.ts
|
|
621
|
+
function _loadBundledPricing() {
|
|
622
|
+
return /* @__PURE__ */ new Map([
|
|
623
|
+
["gpt-4", { provider: "openai", model: "gpt-4", inputCostPer1k: 0.03, outputCostPer1k: 0.06 }],
|
|
624
|
+
["gpt-4-turbo", { provider: "openai", model: "gpt-4-turbo", inputCostPer1k: 0.01, outputCostPer1k: 0.03 }],
|
|
625
|
+
["gpt-4o", { provider: "openai", model: "gpt-4o", inputCostPer1k: 5e-3, outputCostPer1k: 0.015 }],
|
|
626
|
+
["gpt-4o-mini", { provider: "openai", model: "gpt-4o-mini", inputCostPer1k: 15e-5, outputCostPer1k: 6e-4 }],
|
|
627
|
+
["gpt-3.5-turbo", { provider: "openai", model: "gpt-3.5-turbo", inputCostPer1k: 15e-4, outputCostPer1k: 2e-3 }],
|
|
628
|
+
["claude-opus-4", { provider: "anthropic", model: "claude-opus-4", inputCostPer1k: 0.015, outputCostPer1k: 0.075 }],
|
|
629
|
+
["claude-sonnet-4", { provider: "anthropic", model: "claude-sonnet-4", inputCostPer1k: 3e-3, outputCostPer1k: 0.015 }],
|
|
630
|
+
["claude-haiku-4", { provider: "anthropic", model: "claude-haiku-4", inputCostPer1k: 25e-5, outputCostPer1k: 125e-5 }],
|
|
631
|
+
["gemini-1.5-pro", { provider: "google", model: "gemini-1.5-pro", inputCostPer1k: 125e-5, outputCostPer1k: 5e-3 }],
|
|
632
|
+
["gemini-1.5-flash", { provider: "google", model: "gemini-1.5-flash", inputCostPer1k: 75e-6, outputCostPer1k: 3e-4 }],
|
|
633
|
+
["gemini-2.0-flash", { provider: "google", model: "gemini-2.0-flash", inputCostPer1k: 1e-4, outputCostPer1k: 4e-4 }]
|
|
634
|
+
]);
|
|
635
|
+
}
|
|
636
|
+
var _modelPricing = _loadBundledPricing();
|
|
637
|
+
var MODEL_PRICING = _modelPricing;
|
|
638
|
+
async function syncPricingFromApi(baseUrl, apiKey) {
|
|
639
|
+
try {
|
|
640
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/api/v1/pricing/models`;
|
|
641
|
+
const response = await fetch(url, {
|
|
642
|
+
headers: { "X-API-Key": apiKey }
|
|
643
|
+
});
|
|
644
|
+
if (!response.ok) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const data = await response.json();
|
|
648
|
+
const models = data.models ?? [];
|
|
649
|
+
if (models.length === 0) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
_modelPricing.clear();
|
|
653
|
+
for (const entry of models) {
|
|
654
|
+
_modelPricing.set(entry.model, {
|
|
655
|
+
provider: entry.provider,
|
|
656
|
+
model: entry.model,
|
|
657
|
+
inputCostPer1k: entry.input_cost_per_1k,
|
|
658
|
+
outputCostPer1k: entry.output_cost_per_1k
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
} catch {
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function calculateCost(model, inputTokens, outputTokens) {
|
|
665
|
+
const pricing = MODEL_PRICING.get(model);
|
|
666
|
+
if (!pricing) {
|
|
667
|
+
return 0;
|
|
668
|
+
}
|
|
669
|
+
const inputCost = inputTokens / 1e3 * pricing.inputCostPer1k;
|
|
670
|
+
const outputCost = outputTokens / 1e3 * pricing.outputCostPer1k;
|
|
671
|
+
return inputCost + outputCost;
|
|
672
|
+
}
|
|
673
|
+
var CostTracker = class _CostTracker {
|
|
674
|
+
_buffer = [];
|
|
675
|
+
_flushTimer = null;
|
|
676
|
+
_pricingSyncTimer = null;
|
|
677
|
+
_batchSize;
|
|
678
|
+
constructor(batchSize) {
|
|
679
|
+
this._batchSize = batchSize;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Add a track request to the buffer.
|
|
683
|
+
* Automatically flushes when buffer reaches batch size.
|
|
684
|
+
*/
|
|
685
|
+
record(request, client) {
|
|
686
|
+
this._buffer.push(request);
|
|
687
|
+
if (this._buffer.length >= this._batchSize && client) {
|
|
688
|
+
void this.flush(client);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Flush all buffered track requests to the API.
|
|
693
|
+
*/
|
|
694
|
+
async flush(client) {
|
|
695
|
+
if (this._buffer.length === 0) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const batch = this._buffer.splice(0, this._buffer.length);
|
|
699
|
+
const promises = batch.map(
|
|
700
|
+
(request) => client.track(request).catch((error) => {
|
|
701
|
+
console.warn(
|
|
702
|
+
`[ModelCost] Failed to track request: ${error instanceof Error ? error.message : String(error)}`
|
|
703
|
+
);
|
|
704
|
+
})
|
|
705
|
+
);
|
|
706
|
+
await Promise.allSettled(promises);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Start automatic periodic flushing.
|
|
710
|
+
*/
|
|
711
|
+
startAutoFlush(client, intervalMs) {
|
|
712
|
+
this.stopAutoFlush();
|
|
713
|
+
this._flushTimer = setInterval(() => {
|
|
714
|
+
void this.flush(client);
|
|
715
|
+
}, intervalMs);
|
|
716
|
+
if (this._flushTimer && typeof this._flushTimer === "object" && "unref" in this._flushTimer) {
|
|
717
|
+
this._flushTimer.unref();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Stop automatic periodic flushing.
|
|
722
|
+
*/
|
|
723
|
+
stopAutoFlush() {
|
|
724
|
+
if (this._flushTimer !== null) {
|
|
725
|
+
clearInterval(this._flushTimer);
|
|
726
|
+
this._flushTimer = null;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
static _PRICING_SYNC_INTERVAL_MS = 3e5;
|
|
730
|
+
// 5 minutes
|
|
731
|
+
/**
|
|
732
|
+
* Start periodic pricing sync from the server.
|
|
733
|
+
*/
|
|
734
|
+
startPricingSync(baseUrl, apiKey) {
|
|
735
|
+
this.stopPricingSync();
|
|
736
|
+
this._pricingSyncTimer = setInterval(() => {
|
|
737
|
+
void syncPricingFromApi(baseUrl, apiKey);
|
|
738
|
+
}, _CostTracker._PRICING_SYNC_INTERVAL_MS);
|
|
739
|
+
if (this._pricingSyncTimer && typeof this._pricingSyncTimer === "object" && "unref" in this._pricingSyncTimer) {
|
|
740
|
+
this._pricingSyncTimer.unref();
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Stop periodic pricing sync.
|
|
745
|
+
*/
|
|
746
|
+
stopPricingSync() {
|
|
747
|
+
if (this._pricingSyncTimer !== null) {
|
|
748
|
+
clearInterval(this._pricingSyncTimer);
|
|
749
|
+
this._pricingSyncTimer = null;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Get the current number of buffered requests.
|
|
754
|
+
*/
|
|
755
|
+
get bufferSize() {
|
|
756
|
+
return this._buffer.length;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
// src/pii.ts
|
|
761
|
+
var PII_PATTERNS = [
|
|
762
|
+
{
|
|
763
|
+
type: "ssn",
|
|
764
|
+
pattern: /\b\d{3}-\d{2}-\d{4}\b/g
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
type: "credit_card_visa",
|
|
768
|
+
pattern: /\b4\d{3}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
type: "credit_card_mastercard",
|
|
772
|
+
pattern: /\b5[1-5]\d{2}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
type: "credit_card_amex",
|
|
776
|
+
pattern: /\b3[47]\d{2}[- ]?\d{6}[- ]?\d{5}\b/g
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
type: "credit_card_discover",
|
|
780
|
+
pattern: /\b6(?:011|5\d{2})[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
type: "email",
|
|
784
|
+
pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
type: "phone_us",
|
|
788
|
+
pattern: /\b(?:\+?1[-.\s]?)?(?:\(?\d{3}\)?[-.\s]?)\d{3}[-.\s]?\d{4}\b/g
|
|
789
|
+
}
|
|
790
|
+
];
|
|
791
|
+
var CREDIT_CARD_GENERIC_PATTERN = /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g;
|
|
792
|
+
var SECRETS_PATTERNS = [
|
|
793
|
+
{
|
|
794
|
+
type: "api_key_openai",
|
|
795
|
+
pattern: /sk-[a-zA-Z0-9]{20,}/g,
|
|
796
|
+
severity: "critical"
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
type: "api_key_aws",
|
|
800
|
+
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
801
|
+
severity: "critical"
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
type: "private_key",
|
|
805
|
+
pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
|
|
806
|
+
severity: "critical"
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
type: "jwt_token",
|
|
810
|
+
pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
811
|
+
severity: "high"
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
type: "generic_secret",
|
|
815
|
+
pattern: /(?:password|api_key|apikey|secret|token|bearer)\s*[:=]\s*["']?([A-Za-z0-9_\-/.]{8,})["']?/gi,
|
|
816
|
+
severity: "critical"
|
|
817
|
+
}
|
|
818
|
+
];
|
|
819
|
+
var FINANCIAL_PATTERNS = [
|
|
820
|
+
{
|
|
821
|
+
type: "iban",
|
|
822
|
+
pattern: /\b[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}([A-Z0-9]?){0,16}\b/g,
|
|
823
|
+
severity: "high"
|
|
824
|
+
}
|
|
825
|
+
];
|
|
826
|
+
var MEDICAL_TERMS = /* @__PURE__ */ new Set([
|
|
827
|
+
"diabetes",
|
|
828
|
+
"hiv",
|
|
829
|
+
"aids",
|
|
830
|
+
"cancer",
|
|
831
|
+
"tumor",
|
|
832
|
+
"disease",
|
|
833
|
+
"medication",
|
|
834
|
+
"diagnosis",
|
|
835
|
+
"treatment",
|
|
836
|
+
"surgery",
|
|
837
|
+
"prescription",
|
|
838
|
+
"patient",
|
|
839
|
+
"doctor",
|
|
840
|
+
"hospital",
|
|
841
|
+
"clinic",
|
|
842
|
+
"medical record",
|
|
843
|
+
"insulin",
|
|
844
|
+
"prozac",
|
|
845
|
+
"chemotherapy",
|
|
846
|
+
"depression",
|
|
847
|
+
"anxiety",
|
|
848
|
+
"bipolar",
|
|
849
|
+
"schizophrenia",
|
|
850
|
+
"hepatitis",
|
|
851
|
+
"tuberculosis",
|
|
852
|
+
"epilepsy",
|
|
853
|
+
"asthma",
|
|
854
|
+
"arthritis",
|
|
855
|
+
"alzheimer"
|
|
856
|
+
]);
|
|
857
|
+
var PiiScanner = class _PiiScanner {
|
|
858
|
+
/**
|
|
859
|
+
* Scan text and return all detected PII entities, plus a redacted version.
|
|
860
|
+
*/
|
|
861
|
+
scan(text) {
|
|
862
|
+
const entities = [];
|
|
863
|
+
for (const { type, pattern } of PII_PATTERNS) {
|
|
864
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
865
|
+
let match;
|
|
866
|
+
while ((match = regex.exec(text)) !== null) {
|
|
867
|
+
if (type.startsWith("credit_card_")) {
|
|
868
|
+
const digits = match[0].replace(/[\s-]/g, "");
|
|
869
|
+
if (!_PiiScanner._isValidLuhn(digits)) continue;
|
|
870
|
+
}
|
|
871
|
+
entities.push({
|
|
872
|
+
type,
|
|
873
|
+
value: match[0],
|
|
874
|
+
start: match.index,
|
|
875
|
+
end: match.index + match[0].length
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
entities.sort((a, b) => a.start - b.start);
|
|
880
|
+
const deduped = this._removeOverlaps(entities);
|
|
881
|
+
const redactedText = this._redactText(text, deduped);
|
|
882
|
+
return {
|
|
883
|
+
detected: deduped.length > 0,
|
|
884
|
+
entities: deduped,
|
|
885
|
+
redactedText
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Full governance scan across multiple categories.
|
|
890
|
+
* Used in metadata-only mode where content never leaves the customer's environment.
|
|
891
|
+
*/
|
|
892
|
+
fullScan(text, categories = ["pii", "phi", "secrets", "financial"]) {
|
|
893
|
+
const violations = [];
|
|
894
|
+
const detectedCategories = /* @__PURE__ */ new Set();
|
|
895
|
+
if (categories.includes("pii")) {
|
|
896
|
+
const piiViolations = this._scanPiiViolations(text);
|
|
897
|
+
for (const v of piiViolations) {
|
|
898
|
+
violations.push(v);
|
|
899
|
+
detectedCategories.add("pii");
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (categories.includes("phi")) {
|
|
903
|
+
const phiViolations = this._scanPhi(text);
|
|
904
|
+
for (const v of phiViolations) {
|
|
905
|
+
violations.push(v);
|
|
906
|
+
detectedCategories.add("phi");
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (categories.includes("secrets")) {
|
|
910
|
+
const secretViolations = this._scanSecrets(text);
|
|
911
|
+
for (const v of secretViolations) {
|
|
912
|
+
violations.push(v);
|
|
913
|
+
detectedCategories.add("secrets");
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (categories.includes("financial")) {
|
|
917
|
+
const financialViolations = this._scanFinancial(text);
|
|
918
|
+
for (const v of financialViolations) {
|
|
919
|
+
violations.push(v);
|
|
920
|
+
detectedCategories.add("financial");
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
detected: violations.length > 0,
|
|
925
|
+
violations,
|
|
926
|
+
categories: [...detectedCategories]
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Convenience method: return only the redacted text.
|
|
931
|
+
*/
|
|
932
|
+
redact(text) {
|
|
933
|
+
return this.scan(text).redactedText;
|
|
934
|
+
}
|
|
935
|
+
// ─── Category Scanners ──────────────────────────────────────────────
|
|
936
|
+
_scanPiiViolations(text) {
|
|
937
|
+
const violations = [];
|
|
938
|
+
for (const match of text.matchAll(/\b\d{3}-\d{2}-\d{4}\b/g)) {
|
|
939
|
+
violations.push({
|
|
940
|
+
category: "pii",
|
|
941
|
+
type: "ssn",
|
|
942
|
+
severity: "critical",
|
|
943
|
+
start: match.index,
|
|
944
|
+
end: match.index + match[0].length
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
for (const match of text.matchAll(
|
|
948
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g
|
|
949
|
+
)) {
|
|
950
|
+
violations.push({
|
|
951
|
+
category: "pii",
|
|
952
|
+
type: "email",
|
|
953
|
+
severity: "high",
|
|
954
|
+
start: match.index,
|
|
955
|
+
end: match.index + match[0].length
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
for (const match of text.matchAll(CREDIT_CARD_GENERIC_PATTERN)) {
|
|
959
|
+
const digits = match[0].replace(/[\s-]/g, "");
|
|
960
|
+
if (_PiiScanner._isValidLuhn(digits)) {
|
|
961
|
+
violations.push({
|
|
962
|
+
category: "pii",
|
|
963
|
+
type: "credit_card",
|
|
964
|
+
severity: "critical",
|
|
965
|
+
start: match.index,
|
|
966
|
+
end: match.index + match[0].length
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
for (const match of text.matchAll(
|
|
971
|
+
/\b(?:\+?1[-.\s]?)?(?:\(?\d{3}\)?[-.\s]?)\d{3}[-.\s]?\d{4}\b/g
|
|
972
|
+
)) {
|
|
973
|
+
violations.push({
|
|
974
|
+
category: "pii",
|
|
975
|
+
type: "phone",
|
|
976
|
+
severity: "medium",
|
|
977
|
+
start: match.index,
|
|
978
|
+
end: match.index + match[0].length
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
return violations;
|
|
982
|
+
}
|
|
983
|
+
_scanPhi(text) {
|
|
984
|
+
const textLower = text.toLowerCase();
|
|
985
|
+
let hasMedicalContext = false;
|
|
986
|
+
for (const term of MEDICAL_TERMS) {
|
|
987
|
+
if (textLower.includes(term)) {
|
|
988
|
+
hasMedicalContext = true;
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (!hasMedicalContext) return [];
|
|
993
|
+
const piiViolations = this._scanPiiViolations(text);
|
|
994
|
+
return piiViolations.map((v) => ({
|
|
995
|
+
...v,
|
|
996
|
+
category: "phi",
|
|
997
|
+
type: `phi_${v.type}`,
|
|
998
|
+
severity: "critical"
|
|
999
|
+
}));
|
|
1000
|
+
}
|
|
1001
|
+
_scanSecrets(text) {
|
|
1002
|
+
const violations = [];
|
|
1003
|
+
for (const { type, pattern, severity } of SECRETS_PATTERNS) {
|
|
1004
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
1005
|
+
let match;
|
|
1006
|
+
while ((match = regex.exec(text)) !== null) {
|
|
1007
|
+
violations.push({
|
|
1008
|
+
category: "secrets",
|
|
1009
|
+
type,
|
|
1010
|
+
severity,
|
|
1011
|
+
start: match.index,
|
|
1012
|
+
end: match.index + match[0].length
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return violations;
|
|
1017
|
+
}
|
|
1018
|
+
_scanFinancial(text) {
|
|
1019
|
+
const violations = [];
|
|
1020
|
+
for (const match of text.matchAll(CREDIT_CARD_GENERIC_PATTERN)) {
|
|
1021
|
+
const digits = match[0].replace(/[\s-]/g, "");
|
|
1022
|
+
if (_PiiScanner._isValidLuhn(digits)) {
|
|
1023
|
+
violations.push({
|
|
1024
|
+
category: "financial",
|
|
1025
|
+
type: "credit_card",
|
|
1026
|
+
severity: "critical",
|
|
1027
|
+
start: match.index,
|
|
1028
|
+
end: match.index + match[0].length
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
for (const { type, pattern, severity } of FINANCIAL_PATTERNS) {
|
|
1033
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
1034
|
+
let match;
|
|
1035
|
+
while ((match = regex.exec(text)) !== null) {
|
|
1036
|
+
violations.push({
|
|
1037
|
+
category: "financial",
|
|
1038
|
+
type,
|
|
1039
|
+
severity,
|
|
1040
|
+
start: match.index,
|
|
1041
|
+
end: match.index + match[0].length
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return violations;
|
|
1046
|
+
}
|
|
1047
|
+
// ─── Utilities ──────────────────────────────────────────────────────
|
|
1048
|
+
/**
|
|
1049
|
+
* Remove overlapping entities, preferring the one that starts first
|
|
1050
|
+
* (and is longest in case of a tie).
|
|
1051
|
+
*/
|
|
1052
|
+
_removeOverlaps(entities) {
|
|
1053
|
+
if (entities.length === 0) return entities;
|
|
1054
|
+
const result = [entities[0]];
|
|
1055
|
+
for (let i = 1; i < entities.length; i++) {
|
|
1056
|
+
const current = entities[i];
|
|
1057
|
+
const last = result[result.length - 1];
|
|
1058
|
+
if (current.start >= last.end) {
|
|
1059
|
+
result.push(current);
|
|
1060
|
+
} else if (current.start === last.start && current.end > last.end) {
|
|
1061
|
+
result[result.length - 1] = current;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return result;
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Replace detected entities with [REDACTED] markers.
|
|
1068
|
+
*/
|
|
1069
|
+
_redactText(text, entities) {
|
|
1070
|
+
if (entities.length === 0) return text;
|
|
1071
|
+
let result = "";
|
|
1072
|
+
let cursor = 0;
|
|
1073
|
+
for (const entity of entities) {
|
|
1074
|
+
result += text.slice(cursor, entity.start);
|
|
1075
|
+
result += `[${entity.type.toUpperCase()}]`;
|
|
1076
|
+
cursor = entity.end;
|
|
1077
|
+
}
|
|
1078
|
+
result += text.slice(cursor);
|
|
1079
|
+
return result;
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Luhn algorithm for credit card validation.
|
|
1083
|
+
*/
|
|
1084
|
+
static _isValidLuhn(number) {
|
|
1085
|
+
if (number.length < 13 || number.length > 19) return false;
|
|
1086
|
+
let sum = 0;
|
|
1087
|
+
let alternate = false;
|
|
1088
|
+
for (let i = number.length - 1; i >= 0; i--) {
|
|
1089
|
+
const char = number[i];
|
|
1090
|
+
if (char < "0" || char > "9") return false;
|
|
1091
|
+
let n = parseInt(char, 10);
|
|
1092
|
+
if (alternate) {
|
|
1093
|
+
n *= 2;
|
|
1094
|
+
if (n > 9) n -= 9;
|
|
1095
|
+
}
|
|
1096
|
+
sum += n;
|
|
1097
|
+
alternate = !alternate;
|
|
1098
|
+
}
|
|
1099
|
+
return sum % 10 === 0;
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
// src/rate-limiter.ts
|
|
1104
|
+
var TokenBucketRateLimiter = class {
|
|
1105
|
+
_tokens;
|
|
1106
|
+
_lastRefill;
|
|
1107
|
+
_rate;
|
|
1108
|
+
_burst;
|
|
1109
|
+
_strict;
|
|
1110
|
+
/**
|
|
1111
|
+
* @param rate - Tokens refilled per second
|
|
1112
|
+
* @param burst - Maximum tokens (bucket capacity)
|
|
1113
|
+
* @param strict - If true, `allow()` throws RateLimitedError instead of returning false
|
|
1114
|
+
*/
|
|
1115
|
+
constructor(rate, burst, strict = false) {
|
|
1116
|
+
this._rate = rate;
|
|
1117
|
+
this._burst = burst;
|
|
1118
|
+
this._tokens = burst;
|
|
1119
|
+
this._lastRefill = Date.now();
|
|
1120
|
+
this._strict = strict;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Attempt to consume one token.
|
|
1124
|
+
* Returns true if allowed, false if rate-limited.
|
|
1125
|
+
* In strict mode, throws RateLimitedError instead of returning false.
|
|
1126
|
+
*/
|
|
1127
|
+
allow() {
|
|
1128
|
+
this._refill();
|
|
1129
|
+
if (this._tokens >= 1) {
|
|
1130
|
+
this._tokens -= 1;
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
if (this._strict) {
|
|
1134
|
+
const retryAfter = Math.ceil((1 - this._tokens) / this._rate);
|
|
1135
|
+
throw new RateLimitedError(
|
|
1136
|
+
`Rate limit exceeded. Retry after ${retryAfter}s.`,
|
|
1137
|
+
retryAfter,
|
|
1138
|
+
"token_bucket"
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Wait until a token is available, then consume it.
|
|
1145
|
+
* Resolves immediately if a token is available now.
|
|
1146
|
+
*/
|
|
1147
|
+
async wait() {
|
|
1148
|
+
this._refill();
|
|
1149
|
+
if (this._tokens >= 1) {
|
|
1150
|
+
this._tokens -= 1;
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const deficit = 1 - this._tokens;
|
|
1154
|
+
const waitMs = Math.ceil(deficit / this._rate * 1e3);
|
|
1155
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
1156
|
+
this._refill();
|
|
1157
|
+
this._tokens -= 1;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Get the current number of available tokens (for inspection/testing).
|
|
1161
|
+
*/
|
|
1162
|
+
get availableTokens() {
|
|
1163
|
+
this._refill();
|
|
1164
|
+
return this._tokens;
|
|
1165
|
+
}
|
|
1166
|
+
_refill() {
|
|
1167
|
+
const now = Date.now();
|
|
1168
|
+
const elapsed = (now - this._lastRefill) / 1e3;
|
|
1169
|
+
this._tokens = Math.min(this._burst, this._tokens + elapsed * this._rate);
|
|
1170
|
+
this._lastRefill = now;
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
// src/providers/openai.ts
|
|
1175
|
+
var OpenAIProvider = class {
|
|
1176
|
+
_client;
|
|
1177
|
+
_config;
|
|
1178
|
+
_budgetManager;
|
|
1179
|
+
_costTracker;
|
|
1180
|
+
_piiScanner;
|
|
1181
|
+
_rateLimiter;
|
|
1182
|
+
_session;
|
|
1183
|
+
constructor(apiClient, config, budgetManager, costTracker, piiScanner, rateLimiter, session) {
|
|
1184
|
+
this._client = apiClient;
|
|
1185
|
+
this._config = config;
|
|
1186
|
+
this._budgetManager = budgetManager;
|
|
1187
|
+
this._costTracker = costTracker;
|
|
1188
|
+
this._piiScanner = piiScanner;
|
|
1189
|
+
this._rateLimiter = rateLimiter;
|
|
1190
|
+
this._session = session;
|
|
1191
|
+
}
|
|
1192
|
+
getProviderName() {
|
|
1193
|
+
return "openai";
|
|
1194
|
+
}
|
|
1195
|
+
extractUsage(response) {
|
|
1196
|
+
const res = response;
|
|
1197
|
+
return {
|
|
1198
|
+
inputTokens: res.usage?.prompt_tokens ?? 0,
|
|
1199
|
+
outputTokens: res.usage?.completion_tokens ?? 0
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
wrap(client) {
|
|
1203
|
+
const openaiClient = client;
|
|
1204
|
+
const self = this;
|
|
1205
|
+
const completionsProxy = new Proxy(openaiClient.chat.completions, {
|
|
1206
|
+
get(target, prop, receiver) {
|
|
1207
|
+
if (prop === "create") {
|
|
1208
|
+
return async (...args) => {
|
|
1209
|
+
const params = args[0] ?? {};
|
|
1210
|
+
const model = params["model"] ?? "unknown";
|
|
1211
|
+
await self._rateLimiter.wait();
|
|
1212
|
+
const messages = params["messages"];
|
|
1213
|
+
if (messages) {
|
|
1214
|
+
for (const msg of messages) {
|
|
1215
|
+
if (typeof msg.content === "string") {
|
|
1216
|
+
const scanResult = self._piiScanner.scan(msg.content);
|
|
1217
|
+
if (scanResult.detected) {
|
|
1218
|
+
if (self._config.contentPrivacy) {
|
|
1219
|
+
const fullResult = self._piiScanner.fullScan(msg.content);
|
|
1220
|
+
if (fullResult.detected) {
|
|
1221
|
+
for (const violation of fullResult.violations) {
|
|
1222
|
+
self._client.reportSignal({
|
|
1223
|
+
organizationId: self._config.orgId,
|
|
1224
|
+
violationType: violation.category,
|
|
1225
|
+
violationSubtype: violation.type,
|
|
1226
|
+
severity: violation.severity,
|
|
1227
|
+
environment: self._config.environment,
|
|
1228
|
+
actionTaken: "block",
|
|
1229
|
+
wasAllowed: false,
|
|
1230
|
+
detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1231
|
+
source: "metadata_only",
|
|
1232
|
+
violationCount: 1
|
|
1233
|
+
}).catch(() => {
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
throw new PiiDetectedError(
|
|
1237
|
+
"Sensitive content detected and blocked locally (metadata-only mode)",
|
|
1238
|
+
fullResult.violations.map((v) => ({
|
|
1239
|
+
type: v.category,
|
|
1240
|
+
subtype: v.type,
|
|
1241
|
+
severity: v.severity,
|
|
1242
|
+
start: v.start,
|
|
1243
|
+
end: v.end
|
|
1244
|
+
})),
|
|
1245
|
+
self._piiScanner.redact(msg.content)
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
} else {
|
|
1249
|
+
const govResult = await self._client.scanText({
|
|
1250
|
+
orgId: self._config.orgId,
|
|
1251
|
+
text: msg.content,
|
|
1252
|
+
environment: self._config.environment
|
|
1253
|
+
});
|
|
1254
|
+
if (!govResult.isAllowed) {
|
|
1255
|
+
throw new PiiDetectedError(
|
|
1256
|
+
"PII detected in request and blocked by policy",
|
|
1257
|
+
govResult.violations,
|
|
1258
|
+
govResult.redactedText ?? scanResult.redactedText
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
const estimatedCost = calculateCost(model, 500, 500);
|
|
1267
|
+
const budgetCheck = await self._budgetManager.check(
|
|
1268
|
+
self._client,
|
|
1269
|
+
self._config.orgId,
|
|
1270
|
+
params["feature"] ?? "default",
|
|
1271
|
+
estimatedCost
|
|
1272
|
+
);
|
|
1273
|
+
if (!budgetCheck.allowed && budgetCheck.action === "block") {
|
|
1274
|
+
throw new BudgetExceededError(
|
|
1275
|
+
budgetCheck.reason ?? "Budget exceeded",
|
|
1276
|
+
0,
|
|
1277
|
+
"organization"
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
if (self._session) {
|
|
1281
|
+
self._session.preCallCheck(estimatedCost);
|
|
1282
|
+
}
|
|
1283
|
+
const startTime = Date.now();
|
|
1284
|
+
const response = await target.create.apply(target, args);
|
|
1285
|
+
const latencyMs = Date.now() - startTime;
|
|
1286
|
+
const usage = self.extractUsage(response);
|
|
1287
|
+
const cost = calculateCost(
|
|
1288
|
+
model,
|
|
1289
|
+
usage.inputTokens,
|
|
1290
|
+
usage.outputTokens
|
|
1291
|
+
);
|
|
1292
|
+
self._costTracker.record(
|
|
1293
|
+
{
|
|
1294
|
+
apiKey: self._config.apiKey,
|
|
1295
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1296
|
+
provider: "openai",
|
|
1297
|
+
model,
|
|
1298
|
+
inputTokens: usage.inputTokens,
|
|
1299
|
+
outputTokens: usage.outputTokens,
|
|
1300
|
+
latencyMs,
|
|
1301
|
+
metadata: {}
|
|
1302
|
+
},
|
|
1303
|
+
self._client
|
|
1304
|
+
);
|
|
1305
|
+
self._budgetManager.updateLocalSpend(
|
|
1306
|
+
self._config.orgId,
|
|
1307
|
+
params["feature"] ?? "default",
|
|
1308
|
+
cost
|
|
1309
|
+
);
|
|
1310
|
+
if (self._session) {
|
|
1311
|
+
self._session.recordCall({
|
|
1312
|
+
callType: "llm",
|
|
1313
|
+
inputTokens: usage.inputTokens,
|
|
1314
|
+
outputTokens: usage.outputTokens,
|
|
1315
|
+
costUsd: cost
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
return response;
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
return Reflect.get(target, prop, receiver);
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
const chatProxy = new Proxy(openaiClient.chat, {
|
|
1325
|
+
get(target, prop, receiver) {
|
|
1326
|
+
if (prop === "completions") {
|
|
1327
|
+
return completionsProxy;
|
|
1328
|
+
}
|
|
1329
|
+
return Reflect.get(target, prop, receiver);
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
return new Proxy(openaiClient, {
|
|
1333
|
+
get(target, prop, receiver) {
|
|
1334
|
+
if (prop === "chat") {
|
|
1335
|
+
return chatProxy;
|
|
1336
|
+
}
|
|
1337
|
+
return Reflect.get(target, prop, receiver);
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
// src/providers/anthropic.ts
|
|
1344
|
+
var AnthropicProvider = class {
|
|
1345
|
+
_client;
|
|
1346
|
+
_config;
|
|
1347
|
+
_budgetManager;
|
|
1348
|
+
_costTracker;
|
|
1349
|
+
_piiScanner;
|
|
1350
|
+
_rateLimiter;
|
|
1351
|
+
_session;
|
|
1352
|
+
constructor(apiClient, config, budgetManager, costTracker, piiScanner, rateLimiter, session) {
|
|
1353
|
+
this._client = apiClient;
|
|
1354
|
+
this._config = config;
|
|
1355
|
+
this._budgetManager = budgetManager;
|
|
1356
|
+
this._costTracker = costTracker;
|
|
1357
|
+
this._piiScanner = piiScanner;
|
|
1358
|
+
this._rateLimiter = rateLimiter;
|
|
1359
|
+
this._session = session;
|
|
1360
|
+
}
|
|
1361
|
+
getProviderName() {
|
|
1362
|
+
return "anthropic";
|
|
1363
|
+
}
|
|
1364
|
+
extractUsage(response) {
|
|
1365
|
+
const res = response;
|
|
1366
|
+
return {
|
|
1367
|
+
inputTokens: res.usage?.input_tokens ?? 0,
|
|
1368
|
+
outputTokens: res.usage?.output_tokens ?? 0
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
wrap(client) {
|
|
1372
|
+
const anthropicClient = client;
|
|
1373
|
+
const self = this;
|
|
1374
|
+
const messagesProxy = new Proxy(anthropicClient.messages, {
|
|
1375
|
+
get(target, prop, receiver) {
|
|
1376
|
+
if (prop === "create") {
|
|
1377
|
+
return async (...args) => {
|
|
1378
|
+
const params = args[0] ?? {};
|
|
1379
|
+
const model = params["model"] ?? "unknown";
|
|
1380
|
+
await self._rateLimiter.wait();
|
|
1381
|
+
const messages = params["messages"];
|
|
1382
|
+
if (messages) {
|
|
1383
|
+
for (const msg of messages) {
|
|
1384
|
+
const textContent = typeof msg.content === "string" ? msg.content : Array.isArray(msg.content) ? msg.content.map((block) => block.text ?? "").join(" ") : "";
|
|
1385
|
+
if (textContent) {
|
|
1386
|
+
const scanResult = self._piiScanner.scan(textContent);
|
|
1387
|
+
if (scanResult.detected) {
|
|
1388
|
+
if (self._config.contentPrivacy) {
|
|
1389
|
+
const fullResult = self._piiScanner.fullScan(textContent);
|
|
1390
|
+
if (fullResult.detected) {
|
|
1391
|
+
for (const violation of fullResult.violations) {
|
|
1392
|
+
self._client.reportSignal({
|
|
1393
|
+
organizationId: self._config.orgId,
|
|
1394
|
+
violationType: violation.category,
|
|
1395
|
+
violationSubtype: violation.type,
|
|
1396
|
+
severity: violation.severity,
|
|
1397
|
+
environment: self._config.environment,
|
|
1398
|
+
actionTaken: "block",
|
|
1399
|
+
wasAllowed: false,
|
|
1400
|
+
detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1401
|
+
source: "metadata_only",
|
|
1402
|
+
violationCount: 1
|
|
1403
|
+
}).catch(() => {
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
throw new PiiDetectedError(
|
|
1407
|
+
"Sensitive content detected and blocked locally (metadata-only mode)",
|
|
1408
|
+
fullResult.violations.map((v) => ({
|
|
1409
|
+
type: v.category,
|
|
1410
|
+
subtype: v.type,
|
|
1411
|
+
severity: v.severity,
|
|
1412
|
+
start: v.start,
|
|
1413
|
+
end: v.end
|
|
1414
|
+
})),
|
|
1415
|
+
self._piiScanner.redact(textContent)
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
} else {
|
|
1419
|
+
const govResult = await self._client.scanText({
|
|
1420
|
+
orgId: self._config.orgId,
|
|
1421
|
+
text: textContent,
|
|
1422
|
+
environment: self._config.environment
|
|
1423
|
+
});
|
|
1424
|
+
if (!govResult.isAllowed) {
|
|
1425
|
+
throw new PiiDetectedError(
|
|
1426
|
+
"PII detected in request and blocked by policy",
|
|
1427
|
+
govResult.violations,
|
|
1428
|
+
govResult.redactedText ?? scanResult.redactedText
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
const estimatedCost = calculateCost(model, 500, 500);
|
|
1437
|
+
const budgetCheck = await self._budgetManager.check(
|
|
1438
|
+
self._client,
|
|
1439
|
+
self._config.orgId,
|
|
1440
|
+
params["feature"] ?? "default",
|
|
1441
|
+
estimatedCost
|
|
1442
|
+
);
|
|
1443
|
+
if (!budgetCheck.allowed && budgetCheck.action === "block") {
|
|
1444
|
+
throw new BudgetExceededError(
|
|
1445
|
+
budgetCheck.reason ?? "Budget exceeded",
|
|
1446
|
+
0,
|
|
1447
|
+
"organization"
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
if (self._session) {
|
|
1451
|
+
self._session.preCallCheck(estimatedCost);
|
|
1452
|
+
}
|
|
1453
|
+
const startTime = Date.now();
|
|
1454
|
+
const response = await target.create.apply(target, args);
|
|
1455
|
+
const latencyMs = Date.now() - startTime;
|
|
1456
|
+
const usage = self.extractUsage(response);
|
|
1457
|
+
const cost = calculateCost(
|
|
1458
|
+
model,
|
|
1459
|
+
usage.inputTokens,
|
|
1460
|
+
usage.outputTokens
|
|
1461
|
+
);
|
|
1462
|
+
self._costTracker.record(
|
|
1463
|
+
{
|
|
1464
|
+
apiKey: self._config.apiKey,
|
|
1465
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1466
|
+
provider: "anthropic",
|
|
1467
|
+
model,
|
|
1468
|
+
inputTokens: usage.inputTokens,
|
|
1469
|
+
outputTokens: usage.outputTokens,
|
|
1470
|
+
latencyMs,
|
|
1471
|
+
metadata: {}
|
|
1472
|
+
},
|
|
1473
|
+
self._client
|
|
1474
|
+
);
|
|
1475
|
+
self._budgetManager.updateLocalSpend(
|
|
1476
|
+
self._config.orgId,
|
|
1477
|
+
params["feature"] ?? "default",
|
|
1478
|
+
cost
|
|
1479
|
+
);
|
|
1480
|
+
if (self._session) {
|
|
1481
|
+
self._session.recordCall({
|
|
1482
|
+
callType: "llm",
|
|
1483
|
+
inputTokens: usage.inputTokens,
|
|
1484
|
+
outputTokens: usage.outputTokens,
|
|
1485
|
+
costUsd: cost
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
return response;
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
return Reflect.get(target, prop, receiver);
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1494
|
+
return new Proxy(anthropicClient, {
|
|
1495
|
+
get(target, prop, receiver) {
|
|
1496
|
+
if (prop === "messages") {
|
|
1497
|
+
return messagesProxy;
|
|
1498
|
+
}
|
|
1499
|
+
return Reflect.get(target, prop, receiver);
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
// src/providers/google.ts
|
|
1506
|
+
var GoogleProvider = class {
|
|
1507
|
+
_client;
|
|
1508
|
+
_config;
|
|
1509
|
+
_budgetManager;
|
|
1510
|
+
_costTracker;
|
|
1511
|
+
_piiScanner;
|
|
1512
|
+
_rateLimiter;
|
|
1513
|
+
_session;
|
|
1514
|
+
constructor(apiClient, config, budgetManager, costTracker, piiScanner, rateLimiter, session) {
|
|
1515
|
+
this._client = apiClient;
|
|
1516
|
+
this._config = config;
|
|
1517
|
+
this._budgetManager = budgetManager;
|
|
1518
|
+
this._costTracker = costTracker;
|
|
1519
|
+
this._piiScanner = piiScanner;
|
|
1520
|
+
this._rateLimiter = rateLimiter;
|
|
1521
|
+
this._session = session;
|
|
1522
|
+
}
|
|
1523
|
+
getProviderName() {
|
|
1524
|
+
return "google";
|
|
1525
|
+
}
|
|
1526
|
+
extractUsage(response) {
|
|
1527
|
+
const res = response;
|
|
1528
|
+
const metadata = res.response?.usageMetadata;
|
|
1529
|
+
return {
|
|
1530
|
+
inputTokens: metadata?.promptTokenCount ?? 0,
|
|
1531
|
+
outputTokens: metadata?.candidatesTokenCount ?? 0
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
wrap(client) {
|
|
1535
|
+
const googleClient = client;
|
|
1536
|
+
const self = this;
|
|
1537
|
+
return new Proxy(googleClient, {
|
|
1538
|
+
get(target, prop, receiver) {
|
|
1539
|
+
if (prop === "getGenerativeModel") {
|
|
1540
|
+
return (params) => {
|
|
1541
|
+
const model = target.getGenerativeModel(params);
|
|
1542
|
+
return self._wrapModel(model, params.model);
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
return Reflect.get(target, prop, receiver);
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
_wrapModel(model, modelName) {
|
|
1550
|
+
const self = this;
|
|
1551
|
+
return new Proxy(model, {
|
|
1552
|
+
get(target, prop, receiver) {
|
|
1553
|
+
if (prop === "generateContent") {
|
|
1554
|
+
return async (...args) => {
|
|
1555
|
+
await self._rateLimiter.wait();
|
|
1556
|
+
if (typeof args[0] === "string") {
|
|
1557
|
+
const scanResult = self._piiScanner.scan(args[0]);
|
|
1558
|
+
if (scanResult.detected) {
|
|
1559
|
+
if (self._config.contentPrivacy) {
|
|
1560
|
+
const fullResult = self._piiScanner.fullScan(args[0]);
|
|
1561
|
+
if (fullResult.detected) {
|
|
1562
|
+
for (const violation of fullResult.violations) {
|
|
1563
|
+
self._client.reportSignal({
|
|
1564
|
+
organizationId: self._config.orgId,
|
|
1565
|
+
violationType: violation.category,
|
|
1566
|
+
violationSubtype: violation.type,
|
|
1567
|
+
severity: violation.severity,
|
|
1568
|
+
environment: self._config.environment,
|
|
1569
|
+
actionTaken: "block",
|
|
1570
|
+
wasAllowed: false,
|
|
1571
|
+
detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1572
|
+
source: "metadata_only",
|
|
1573
|
+
violationCount: 1
|
|
1574
|
+
}).catch(() => {
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
throw new PiiDetectedError(
|
|
1578
|
+
"Sensitive content detected and blocked locally (metadata-only mode)",
|
|
1579
|
+
fullResult.violations.map((v) => ({
|
|
1580
|
+
type: v.category,
|
|
1581
|
+
subtype: v.type,
|
|
1582
|
+
severity: v.severity,
|
|
1583
|
+
start: v.start,
|
|
1584
|
+
end: v.end
|
|
1585
|
+
})),
|
|
1586
|
+
self._piiScanner.redact(args[0])
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
} else {
|
|
1590
|
+
const govResult = await self._client.scanText({
|
|
1591
|
+
orgId: self._config.orgId,
|
|
1592
|
+
text: args[0],
|
|
1593
|
+
environment: self._config.environment
|
|
1594
|
+
});
|
|
1595
|
+
if (!govResult.isAllowed) {
|
|
1596
|
+
throw new PiiDetectedError(
|
|
1597
|
+
"PII detected in request and blocked by policy",
|
|
1598
|
+
govResult.violations,
|
|
1599
|
+
govResult.redactedText ?? scanResult.redactedText
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
const estimatedCost = calculateCost(modelName, 500, 500);
|
|
1606
|
+
const budgetCheck = await self._budgetManager.check(
|
|
1607
|
+
self._client,
|
|
1608
|
+
self._config.orgId,
|
|
1609
|
+
"default",
|
|
1610
|
+
estimatedCost
|
|
1611
|
+
);
|
|
1612
|
+
if (!budgetCheck.allowed && budgetCheck.action === "block") {
|
|
1613
|
+
throw new BudgetExceededError(
|
|
1614
|
+
budgetCheck.reason ?? "Budget exceeded",
|
|
1615
|
+
0,
|
|
1616
|
+
"organization"
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
if (self._session) {
|
|
1620
|
+
self._session.preCallCheck(estimatedCost);
|
|
1621
|
+
}
|
|
1622
|
+
const startTime = Date.now();
|
|
1623
|
+
const response = await target.generateContent.apply(
|
|
1624
|
+
target,
|
|
1625
|
+
args
|
|
1626
|
+
);
|
|
1627
|
+
const latencyMs = Date.now() - startTime;
|
|
1628
|
+
const usage = self.extractUsage(response);
|
|
1629
|
+
const cost = calculateCost(
|
|
1630
|
+
modelName,
|
|
1631
|
+
usage.inputTokens,
|
|
1632
|
+
usage.outputTokens
|
|
1633
|
+
);
|
|
1634
|
+
self._costTracker.record(
|
|
1635
|
+
{
|
|
1636
|
+
apiKey: self._config.apiKey,
|
|
1637
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1638
|
+
provider: "google",
|
|
1639
|
+
model: modelName,
|
|
1640
|
+
inputTokens: usage.inputTokens,
|
|
1641
|
+
outputTokens: usage.outputTokens,
|
|
1642
|
+
latencyMs,
|
|
1643
|
+
metadata: {}
|
|
1644
|
+
},
|
|
1645
|
+
self._client
|
|
1646
|
+
);
|
|
1647
|
+
self._budgetManager.updateLocalSpend(
|
|
1648
|
+
self._config.orgId,
|
|
1649
|
+
"default",
|
|
1650
|
+
cost
|
|
1651
|
+
);
|
|
1652
|
+
if (self._session) {
|
|
1653
|
+
self._session.recordCall({
|
|
1654
|
+
callType: "llm",
|
|
1655
|
+
inputTokens: usage.inputTokens,
|
|
1656
|
+
outputTokens: usage.outputTokens,
|
|
1657
|
+
costUsd: cost
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
return response;
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
return Reflect.get(target, prop, receiver);
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1669
|
+
// src/providers/index.ts
|
|
1670
|
+
function detectProvider(client) {
|
|
1671
|
+
const obj = client;
|
|
1672
|
+
if (obj["chat"] && typeof obj["chat"] === "object") {
|
|
1673
|
+
const chat = obj["chat"];
|
|
1674
|
+
if (chat["completions"]) return "openai";
|
|
1675
|
+
}
|
|
1676
|
+
if (obj["messages"] && typeof obj["messages"] === "object") {
|
|
1677
|
+
return "anthropic";
|
|
1678
|
+
}
|
|
1679
|
+
if (typeof obj["getGenerativeModel"] === "function") {
|
|
1680
|
+
return "google";
|
|
1681
|
+
}
|
|
1682
|
+
return null;
|
|
1683
|
+
}
|
|
1684
|
+
function createProviderForClient(client, apiClient, config, budgetManager, costTracker, piiScanner, rateLimiter, session) {
|
|
1685
|
+
const providerName = detectProvider(client);
|
|
1686
|
+
switch (providerName) {
|
|
1687
|
+
case "openai":
|
|
1688
|
+
return new OpenAIProvider(
|
|
1689
|
+
apiClient,
|
|
1690
|
+
config,
|
|
1691
|
+
budgetManager,
|
|
1692
|
+
costTracker,
|
|
1693
|
+
piiScanner,
|
|
1694
|
+
rateLimiter,
|
|
1695
|
+
session
|
|
1696
|
+
);
|
|
1697
|
+
case "anthropic":
|
|
1698
|
+
return new AnthropicProvider(
|
|
1699
|
+
apiClient,
|
|
1700
|
+
config,
|
|
1701
|
+
budgetManager,
|
|
1702
|
+
costTracker,
|
|
1703
|
+
piiScanner,
|
|
1704
|
+
rateLimiter,
|
|
1705
|
+
session
|
|
1706
|
+
);
|
|
1707
|
+
case "google":
|
|
1708
|
+
return new GoogleProvider(
|
|
1709
|
+
apiClient,
|
|
1710
|
+
config,
|
|
1711
|
+
budgetManager,
|
|
1712
|
+
costTracker,
|
|
1713
|
+
piiScanner,
|
|
1714
|
+
rateLimiter,
|
|
1715
|
+
session
|
|
1716
|
+
);
|
|
1717
|
+
default:
|
|
1718
|
+
throw new Error(
|
|
1719
|
+
`Unsupported AI client. Could not detect provider from client object. Supported providers: openai, anthropic, google.`
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/session.ts
|
|
1725
|
+
var SessionContext = class {
|
|
1726
|
+
sessionId;
|
|
1727
|
+
serverSessionId;
|
|
1728
|
+
feature;
|
|
1729
|
+
userId;
|
|
1730
|
+
maxSpendUsd;
|
|
1731
|
+
maxIterations;
|
|
1732
|
+
_currentSpendUsd = 0;
|
|
1733
|
+
_iterationCount = 0;
|
|
1734
|
+
_cumulativeInputTokens = 0;
|
|
1735
|
+
_status = "active";
|
|
1736
|
+
_terminationReason;
|
|
1737
|
+
_calls = [];
|
|
1738
|
+
constructor(options) {
|
|
1739
|
+
this.sessionId = options.sessionId;
|
|
1740
|
+
this.serverSessionId = options.serverSessionId;
|
|
1741
|
+
this.feature = options.feature;
|
|
1742
|
+
this.userId = options.userId;
|
|
1743
|
+
this.maxSpendUsd = options.maxSpendUsd;
|
|
1744
|
+
this.maxIterations = options.maxIterations;
|
|
1745
|
+
}
|
|
1746
|
+
// ─── Read-only accessors ──────────────────────────────────────────
|
|
1747
|
+
get currentSpendUsd() {
|
|
1748
|
+
return this._currentSpendUsd;
|
|
1749
|
+
}
|
|
1750
|
+
get iterationCount() {
|
|
1751
|
+
return this._iterationCount;
|
|
1752
|
+
}
|
|
1753
|
+
get status() {
|
|
1754
|
+
return this._status;
|
|
1755
|
+
}
|
|
1756
|
+
get terminationReason() {
|
|
1757
|
+
return this._terminationReason;
|
|
1758
|
+
}
|
|
1759
|
+
get calls() {
|
|
1760
|
+
return this._calls;
|
|
1761
|
+
}
|
|
1762
|
+
get remainingBudget() {
|
|
1763
|
+
if (this.maxSpendUsd === void 0) return void 0;
|
|
1764
|
+
return Math.max(0, this.maxSpendUsd - this._currentSpendUsd);
|
|
1765
|
+
}
|
|
1766
|
+
get remainingIterations() {
|
|
1767
|
+
if (this.maxIterations === void 0) return void 0;
|
|
1768
|
+
return Math.max(0, this.maxIterations - this._iterationCount);
|
|
1769
|
+
}
|
|
1770
|
+
// ─── Lifecycle methods ────────────────────────────────────────────
|
|
1771
|
+
/**
|
|
1772
|
+
* Pre-flight check before making an AI call.
|
|
1773
|
+
* Throws if budget or iteration limits would be exceeded.
|
|
1774
|
+
*/
|
|
1775
|
+
preCallCheck(estimatedCost) {
|
|
1776
|
+
if (this.maxSpendUsd !== void 0) {
|
|
1777
|
+
if (this._currentSpendUsd + estimatedCost > this.maxSpendUsd) {
|
|
1778
|
+
this._status = "terminated";
|
|
1779
|
+
this._terminationReason = "budget_exceeded";
|
|
1780
|
+
throw new SessionBudgetExceededError(
|
|
1781
|
+
`Session budget exceeded: current spend $${this._currentSpendUsd.toFixed(4)} + estimated $${estimatedCost.toFixed(4)} > limit $${this.maxSpendUsd.toFixed(4)}`,
|
|
1782
|
+
this.sessionId,
|
|
1783
|
+
this._currentSpendUsd,
|
|
1784
|
+
this.maxSpendUsd
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
if (this.maxIterations !== void 0) {
|
|
1789
|
+
if (this._iterationCount + 1 > this.maxIterations) {
|
|
1790
|
+
this._status = "terminated";
|
|
1791
|
+
this._terminationReason = "iteration_limit_exceeded";
|
|
1792
|
+
throw new SessionIterationLimitExceededError(
|
|
1793
|
+
`Session iteration limit exceeded: ${this._iterationCount + 1} > ${this.maxIterations}`,
|
|
1794
|
+
this.sessionId,
|
|
1795
|
+
this._iterationCount,
|
|
1796
|
+
this.maxIterations
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Record a completed call, updating cumulative counters.
|
|
1803
|
+
*/
|
|
1804
|
+
recordCall(options) {
|
|
1805
|
+
this._iterationCount++;
|
|
1806
|
+
this._currentSpendUsd += options.costUsd;
|
|
1807
|
+
this._cumulativeInputTokens += options.inputTokens;
|
|
1808
|
+
const record = {
|
|
1809
|
+
callSequence: this._iterationCount,
|
|
1810
|
+
callType: options.callType,
|
|
1811
|
+
toolName: options.toolName,
|
|
1812
|
+
inputTokens: options.inputTokens,
|
|
1813
|
+
outputTokens: options.outputTokens,
|
|
1814
|
+
cumulativeInputTokens: this._cumulativeInputTokens,
|
|
1815
|
+
costUsd: options.costUsd,
|
|
1816
|
+
cumulativeCostUsd: this._currentSpendUsd,
|
|
1817
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1818
|
+
};
|
|
1819
|
+
this._calls.push(record);
|
|
1820
|
+
return record;
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Close the session with a terminal status.
|
|
1824
|
+
*/
|
|
1825
|
+
close(reason = "completed") {
|
|
1826
|
+
this._status = reason === "completed" ? "completed" : "terminated";
|
|
1827
|
+
this._terminationReason = reason;
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
// src/index.ts
|
|
1832
|
+
var ModelCost = class _ModelCost {
|
|
1833
|
+
static _instance = null;
|
|
1834
|
+
/** Prevent direct instantiation. */
|
|
1835
|
+
constructor() {
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Initialize the ModelCost SDK. Must be called before any other method.
|
|
1839
|
+
* Subsequent calls will re-initialize (shutting down the previous instance).
|
|
1840
|
+
*/
|
|
1841
|
+
static init(options) {
|
|
1842
|
+
if (_ModelCost._instance) {
|
|
1843
|
+
_ModelCost._instance.costTracker.stopAutoFlush();
|
|
1844
|
+
_ModelCost._instance.costTracker.stopPricingSync();
|
|
1845
|
+
_ModelCost._instance.client.close();
|
|
1846
|
+
}
|
|
1847
|
+
const config = new ModelCostConfig(options);
|
|
1848
|
+
const client = new ModelCostClient(config);
|
|
1849
|
+
const budgetManager = new BudgetManager(config.syncIntervalMs);
|
|
1850
|
+
const costTracker = new CostTracker(config.flushBatchSize);
|
|
1851
|
+
const piiScanner = new PiiScanner();
|
|
1852
|
+
const rateLimiter = new TokenBucketRateLimiter(10, 50);
|
|
1853
|
+
costTracker.startAutoFlush(client, config.flushIntervalMs);
|
|
1854
|
+
costTracker.startPricingSync(config.baseUrl, config.apiKey);
|
|
1855
|
+
_ModelCost._instance = {
|
|
1856
|
+
config,
|
|
1857
|
+
client,
|
|
1858
|
+
budgetManager,
|
|
1859
|
+
costTracker,
|
|
1860
|
+
piiScanner,
|
|
1861
|
+
rateLimiter
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Wrap an AI provider client with cost tracking, budget enforcement,
|
|
1866
|
+
* and PII scanning. Returns a proxied version of the same client.
|
|
1867
|
+
*
|
|
1868
|
+
* Supports: OpenAI, Anthropic, Google Generative AI.
|
|
1869
|
+
*/
|
|
1870
|
+
static wrap(client, session) {
|
|
1871
|
+
const inst = _ModelCost._requireInstance();
|
|
1872
|
+
const provider = createProviderForClient(
|
|
1873
|
+
client,
|
|
1874
|
+
inst.client,
|
|
1875
|
+
inst.config,
|
|
1876
|
+
inst.budgetManager,
|
|
1877
|
+
inst.costTracker,
|
|
1878
|
+
inst.piiScanner,
|
|
1879
|
+
inst.rateLimiter,
|
|
1880
|
+
session
|
|
1881
|
+
);
|
|
1882
|
+
return provider.wrap(client);
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Decorator factory for tracking costs on individual functions.
|
|
1886
|
+
*
|
|
1887
|
+
* @example
|
|
1888
|
+
* ```ts
|
|
1889
|
+
* const trackedFn = ModelCost.trackCost({ model: "gpt-4o" })(myFunction);
|
|
1890
|
+
* ```
|
|
1891
|
+
*/
|
|
1892
|
+
static trackCost(options) {
|
|
1893
|
+
const inst = _ModelCost._requireInstance();
|
|
1894
|
+
return (fn) => {
|
|
1895
|
+
const wrapped = async (...args) => {
|
|
1896
|
+
if (options.session) {
|
|
1897
|
+
const estimatedCost = calculateCost(options.model, 500, 500);
|
|
1898
|
+
options.session.preCallCheck(estimatedCost);
|
|
1899
|
+
}
|
|
1900
|
+
const startTime = Date.now();
|
|
1901
|
+
const result = await fn(...args);
|
|
1902
|
+
const latencyMs = Date.now() - startTime;
|
|
1903
|
+
inst.costTracker.record(
|
|
1904
|
+
{
|
|
1905
|
+
apiKey: inst.config.apiKey,
|
|
1906
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1907
|
+
provider: options.provider ?? "openai",
|
|
1908
|
+
model: options.model,
|
|
1909
|
+
feature: options.feature,
|
|
1910
|
+
inputTokens: 0,
|
|
1911
|
+
outputTokens: 0,
|
|
1912
|
+
latencyMs,
|
|
1913
|
+
metadata: {}
|
|
1914
|
+
},
|
|
1915
|
+
inst.client
|
|
1916
|
+
);
|
|
1917
|
+
if (options.session) {
|
|
1918
|
+
options.session.recordCall({
|
|
1919
|
+
callType: "llm",
|
|
1920
|
+
inputTokens: 0,
|
|
1921
|
+
outputTokens: 0,
|
|
1922
|
+
costUsd: 0
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
return result;
|
|
1926
|
+
};
|
|
1927
|
+
return wrapped;
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Check whether a request is allowed under current budget policies.
|
|
1932
|
+
*/
|
|
1933
|
+
static async checkBudget(scope, id) {
|
|
1934
|
+
const inst = _ModelCost._requireInstance();
|
|
1935
|
+
return inst.budgetManager.check(inst.client, inst.config.orgId, `${scope}:${id}`, 0);
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Get usage/spend information for a scope and period.
|
|
1939
|
+
*/
|
|
1940
|
+
static async getUsage(scope, _period) {
|
|
1941
|
+
const inst = _ModelCost._requireInstance();
|
|
1942
|
+
const status = await inst.client.getBudgetStatus(
|
|
1943
|
+
scope === "organization" ? inst.config.orgId : scope
|
|
1944
|
+
);
|
|
1945
|
+
return {
|
|
1946
|
+
totalSpendUsd: status.totalSpendUsd,
|
|
1947
|
+
totalBudgetUsd: status.totalBudgetUsd,
|
|
1948
|
+
policiesAtRisk: status.policiesAtRisk,
|
|
1949
|
+
policies: status.policies
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Start a new agent session with optional budget and iteration limits.
|
|
1954
|
+
* Registers the session with the server (fail-open if unavailable)
|
|
1955
|
+
* and returns a local SessionContext for tracking.
|
|
1956
|
+
*/
|
|
1957
|
+
static async startSession(options = {}) {
|
|
1958
|
+
const inst = _ModelCost._requireInstance();
|
|
1959
|
+
const sessionId = options.sessionId ?? crypto.randomUUID();
|
|
1960
|
+
let serverSessionId;
|
|
1961
|
+
try {
|
|
1962
|
+
const response = await inst.client.createSession({
|
|
1963
|
+
apiKey: inst.config.apiKey,
|
|
1964
|
+
sessionId,
|
|
1965
|
+
feature: options.feature,
|
|
1966
|
+
userId: options.userId,
|
|
1967
|
+
maxSpendUsd: options.maxSpendUsd,
|
|
1968
|
+
maxIterations: options.maxIterations
|
|
1969
|
+
});
|
|
1970
|
+
serverSessionId = response.id;
|
|
1971
|
+
} catch {
|
|
1972
|
+
console.warn("[ModelCost] Failed to create server session (fail-open)");
|
|
1973
|
+
}
|
|
1974
|
+
return new SessionContext({
|
|
1975
|
+
sessionId,
|
|
1976
|
+
serverSessionId,
|
|
1977
|
+
feature: options.feature,
|
|
1978
|
+
userId: options.userId,
|
|
1979
|
+
maxSpendUsd: options.maxSpendUsd,
|
|
1980
|
+
maxIterations: options.maxIterations
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Close an active session, recording final state on the server.
|
|
1985
|
+
*/
|
|
1986
|
+
static async closeSession(session, reason = "completed") {
|
|
1987
|
+
const inst = _ModelCost._requireInstance();
|
|
1988
|
+
session.close(reason);
|
|
1989
|
+
if (session.serverSessionId) {
|
|
1990
|
+
try {
|
|
1991
|
+
await inst.client.closeSession(session.serverSessionId, {
|
|
1992
|
+
apiKey: inst.config.apiKey,
|
|
1993
|
+
status: session.status,
|
|
1994
|
+
terminationReason: session.terminationReason,
|
|
1995
|
+
finalSpendUsd: session.currentSpendUsd,
|
|
1996
|
+
finalIterationCount: session.iterationCount
|
|
1997
|
+
});
|
|
1998
|
+
} catch {
|
|
1999
|
+
console.warn("[ModelCost] Failed to close server session (fail-open)");
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Scan text for PII using the local scanner.
|
|
2005
|
+
*/
|
|
2006
|
+
static async scanPii(text) {
|
|
2007
|
+
const inst = _ModelCost._requireInstance();
|
|
2008
|
+
return inst.piiScanner.scan(text);
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Flush all buffered tracking events to the API.
|
|
2012
|
+
*/
|
|
2013
|
+
static async flush() {
|
|
2014
|
+
const inst = _ModelCost._requireInstance();
|
|
2015
|
+
await inst.costTracker.flush(inst.client);
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Gracefully shut down the SDK: flush remaining events, stop timers, close client.
|
|
2019
|
+
*/
|
|
2020
|
+
static async shutdown() {
|
|
2021
|
+
if (!_ModelCost._instance) return;
|
|
2022
|
+
const inst = _ModelCost._instance;
|
|
2023
|
+
inst.costTracker.stopAutoFlush();
|
|
2024
|
+
inst.costTracker.stopPricingSync();
|
|
2025
|
+
await inst.costTracker.flush(inst.client);
|
|
2026
|
+
inst.budgetManager.clear();
|
|
2027
|
+
inst.client.close();
|
|
2028
|
+
_ModelCost._instance = null;
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Assert the SDK has been initialized and return the instance.
|
|
2032
|
+
*/
|
|
2033
|
+
static _requireInstance() {
|
|
2034
|
+
if (!_ModelCost._instance) {
|
|
2035
|
+
throw new ConfigurationError(
|
|
2036
|
+
"ModelCost SDK is not initialized. Call ModelCost.init() first."
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
return _ModelCost._instance;
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
|
|
2043
|
+
export { AnthropicProvider, BudgetExceededError, BudgetManager, ConfigurationError, CostTracker, GoogleProvider, MODEL_PRICING, ModelCost, ModelCostApiError, ModelCostClient, ModelCostConfig, ModelCostError, OpenAIProvider, PiiDetectedError, PiiScanner, RateLimitedError, SessionBudgetExceededError, SessionContext, SessionIterationLimitExceededError, TokenBucketRateLimiter, VERSION, calculateCost };
|
|
2044
|
+
//# sourceMappingURL=index.mjs.map
|
|
2045
|
+
//# sourceMappingURL=index.mjs.map
|