caplyr 0.1.1 → 0.1.3
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/dist/index.d.mts +20 -141
- package/dist/index.d.ts +20 -141
- package/dist/index.js +219 -372
- package/dist/index.mjs +219 -372
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ __export(index_exports, {
|
|
|
32
32
|
module.exports = __toCommonJS(index_exports);
|
|
33
33
|
|
|
34
34
|
// src/logger.ts
|
|
35
|
+
var DEFAULT_MAX_BUFFER = 500;
|
|
35
36
|
var LogShipper = class {
|
|
36
37
|
constructor(config) {
|
|
37
38
|
this.buffer = [];
|
|
@@ -40,6 +41,7 @@ var LogShipper = class {
|
|
|
40
41
|
this.apiKey = config.apiKey;
|
|
41
42
|
this.batchSize = config.batchSize ?? 10;
|
|
42
43
|
this.flushInterval = config.flushInterval ?? 3e4;
|
|
44
|
+
this.maxBufferSize = config.maxBufferSize ?? DEFAULT_MAX_BUFFER;
|
|
43
45
|
this.onError = config.onError;
|
|
44
46
|
this.timer = setInterval(() => this.flush(), this.flushInterval);
|
|
45
47
|
if (typeof process !== "undefined" && process.on) {
|
|
@@ -52,20 +54,12 @@ var LogShipper = class {
|
|
|
52
54
|
process.on("SIGINT", flushAndExit);
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Add a log entry to the buffer.
|
|
57
|
-
* Auto-flushes when batch size is reached.
|
|
58
|
-
*/
|
|
59
57
|
push(log) {
|
|
60
58
|
this.buffer.push(log);
|
|
61
59
|
if (this.buffer.length >= this.batchSize) {
|
|
62
60
|
this.flush();
|
|
63
61
|
}
|
|
64
62
|
}
|
|
65
|
-
/**
|
|
66
|
-
* Flush all buffered logs to the backend.
|
|
67
|
-
* Non-blocking — errors are swallowed and reported via onError.
|
|
68
|
-
*/
|
|
69
63
|
async flush() {
|
|
70
64
|
if (this.buffer.length === 0) return;
|
|
71
65
|
const batch = this.buffer.splice(0);
|
|
@@ -80,7 +74,7 @@ var LogShipper = class {
|
|
|
80
74
|
signal: AbortSignal.timeout(1e4)
|
|
81
75
|
});
|
|
82
76
|
if (!res.ok) {
|
|
83
|
-
this.
|
|
77
|
+
this.requeueFailed(batch);
|
|
84
78
|
throw new Error(`Ingest failed: ${res.status} ${res.statusText}`);
|
|
85
79
|
}
|
|
86
80
|
} catch (err) {
|
|
@@ -88,9 +82,14 @@ var LogShipper = class {
|
|
|
88
82
|
}
|
|
89
83
|
}
|
|
90
84
|
/**
|
|
91
|
-
*
|
|
92
|
-
* Call this when tearing down the SDK.
|
|
85
|
+
* Re-queue failed logs, dropping oldest if buffer would exceed max size.
|
|
93
86
|
*/
|
|
87
|
+
requeueFailed(batch) {
|
|
88
|
+
const available = this.maxBufferSize - this.buffer.length;
|
|
89
|
+
if (available <= 0) return;
|
|
90
|
+
const toKeep = batch.slice(-available);
|
|
91
|
+
this.buffer.unshift(...toKeep);
|
|
92
|
+
}
|
|
94
93
|
destroy() {
|
|
95
94
|
if (this.timer) {
|
|
96
95
|
clearInterval(this.timer);
|
|
@@ -113,7 +112,6 @@ var Heartbeat = class {
|
|
|
113
112
|
this.timer = null;
|
|
114
113
|
this.consecutiveFailures = 0;
|
|
115
114
|
this.maxFailuresBeforeDegraded = 3;
|
|
116
|
-
/** Current budget status from the backend */
|
|
117
115
|
this.budgetStatus = {
|
|
118
116
|
daily_used: 0,
|
|
119
117
|
daily_limit: null,
|
|
@@ -122,7 +120,6 @@ var Heartbeat = class {
|
|
|
122
120
|
status: "ACTIVE",
|
|
123
121
|
kill_switch_active: false
|
|
124
122
|
};
|
|
125
|
-
/** Current protection status */
|
|
126
123
|
this.status = "ACTIVE";
|
|
127
124
|
this.endpoint = config.endpoint ?? "https://api.caplyr.com";
|
|
128
125
|
this.apiKey = config.apiKey;
|
|
@@ -130,17 +127,10 @@ var Heartbeat = class {
|
|
|
130
127
|
this.onStatusChange = config.onStatusChange;
|
|
131
128
|
this.onError = config.onError;
|
|
132
129
|
}
|
|
133
|
-
/**
|
|
134
|
-
* Start the heartbeat loop.
|
|
135
|
-
* Immediately sends first heartbeat, then repeats on interval.
|
|
136
|
-
*/
|
|
137
130
|
start() {
|
|
138
131
|
this.beat();
|
|
139
132
|
this.timer = setInterval(() => this.beat(), this.interval);
|
|
140
133
|
}
|
|
141
|
-
/**
|
|
142
|
-
* Send a single heartbeat and update local state.
|
|
143
|
-
*/
|
|
144
134
|
async beat() {
|
|
145
135
|
try {
|
|
146
136
|
const res = await fetch(`${this.endpoint}/api/heartbeat`, {
|
|
@@ -152,9 +142,7 @@ var Heartbeat = class {
|
|
|
152
142
|
body: JSON.stringify({ timestamp: Date.now() }),
|
|
153
143
|
signal: AbortSignal.timeout(5e3)
|
|
154
144
|
});
|
|
155
|
-
if (!res.ok) {
|
|
156
|
-
throw new Error(`Heartbeat failed: ${res.status}`);
|
|
157
|
-
}
|
|
145
|
+
if (!res.ok) throw new Error(`Heartbeat failed: ${res.status}`);
|
|
158
146
|
const data = await res.json();
|
|
159
147
|
this.budgetStatus = data;
|
|
160
148
|
this.consecutiveFailures = 0;
|
|
@@ -172,32 +160,18 @@ var Heartbeat = class {
|
|
|
172
160
|
}
|
|
173
161
|
}
|
|
174
162
|
}
|
|
175
|
-
/**
|
|
176
|
-
* Update local budget tracking (called after each request).
|
|
177
|
-
* This provides real-time budget awareness between heartbeats.
|
|
178
|
-
*/
|
|
179
163
|
trackSpend(cost) {
|
|
180
164
|
this.budgetStatus.daily_used += cost;
|
|
181
165
|
this.budgetStatus.monthly_used += cost;
|
|
182
166
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Check if the monthly budget is exceeded.
|
|
185
|
-
*/
|
|
186
167
|
isMonthlyBudgetExceeded() {
|
|
187
168
|
if (this.budgetStatus.monthly_limit === null) return false;
|
|
188
169
|
return this.budgetStatus.monthly_used >= this.budgetStatus.monthly_limit;
|
|
189
170
|
}
|
|
190
|
-
/**
|
|
191
|
-
* Check if the daily budget is exceeded.
|
|
192
|
-
*/
|
|
193
171
|
isDailyBudgetExceeded() {
|
|
194
172
|
if (this.budgetStatus.daily_limit === null) return false;
|
|
195
173
|
return this.budgetStatus.daily_used >= this.budgetStatus.daily_limit;
|
|
196
174
|
}
|
|
197
|
-
/**
|
|
198
|
-
* Check if the downgrade threshold is reached.
|
|
199
|
-
* Returns true if usage exceeds the given threshold (0-1) of any budget.
|
|
200
|
-
*/
|
|
201
175
|
isDowngradeThresholdReached(threshold) {
|
|
202
176
|
if (this.budgetStatus.monthly_limit !== null && this.budgetStatus.monthly_used >= this.budgetStatus.monthly_limit * threshold) {
|
|
203
177
|
return true;
|
|
@@ -207,15 +181,9 @@ var Heartbeat = class {
|
|
|
207
181
|
}
|
|
208
182
|
return false;
|
|
209
183
|
}
|
|
210
|
-
/**
|
|
211
|
-
* Check if the kill switch is active.
|
|
212
|
-
*/
|
|
213
184
|
isKillSwitchActive() {
|
|
214
185
|
return this.budgetStatus.kill_switch_active;
|
|
215
186
|
}
|
|
216
|
-
/**
|
|
217
|
-
* Stop the heartbeat loop.
|
|
218
|
-
*/
|
|
219
187
|
destroy() {
|
|
220
188
|
if (this.timer) {
|
|
221
189
|
clearInterval(this.timer);
|
|
@@ -230,7 +198,6 @@ var MODEL_PRICING = {
|
|
|
230
198
|
"claude-opus-4-20250514": { input: 15, output: 75 },
|
|
231
199
|
"claude-sonnet-4-20250514": { input: 3, output: 15 },
|
|
232
200
|
"claude-haiku-4-5-20251001": { input: 0.8, output: 4 },
|
|
233
|
-
// Aliases
|
|
234
201
|
"claude-opus-4": { input: 15, output: 75 },
|
|
235
202
|
"claude-sonnet-4": { input: 3, output: 15 },
|
|
236
203
|
"claude-3-5-sonnet-20241022": { input: 3, output: 15 },
|
|
@@ -248,13 +215,11 @@ var MODEL_PRICING = {
|
|
|
248
215
|
"o3-mini": { input: 1.1, output: 4.4 }
|
|
249
216
|
};
|
|
250
217
|
var DEFAULT_FALLBACKS = {
|
|
251
|
-
// Anthropic downgrades
|
|
252
218
|
"claude-opus-4-20250514": "claude-sonnet-4-20250514",
|
|
253
219
|
"claude-opus-4": "claude-sonnet-4",
|
|
254
220
|
"claude-sonnet-4-20250514": "claude-haiku-4-5-20251001",
|
|
255
221
|
"claude-sonnet-4": "claude-haiku-4-5-20251001",
|
|
256
222
|
"claude-3-5-sonnet-20241022": "claude-haiku-4-5-20251001",
|
|
257
|
-
// OpenAI downgrades
|
|
258
223
|
"gpt-4o": "gpt-4o-mini",
|
|
259
224
|
"gpt-4o-2024-11-20": "gpt-4o-mini",
|
|
260
225
|
"gpt-4-turbo": "gpt-4o-mini",
|
|
@@ -274,9 +239,7 @@ function getDefaultFallback(model) {
|
|
|
274
239
|
}
|
|
275
240
|
function registerModel(model, pricing, fallback) {
|
|
276
241
|
MODEL_PRICING[model] = pricing;
|
|
277
|
-
if (fallback)
|
|
278
|
-
DEFAULT_FALLBACKS[model] = fallback;
|
|
279
|
-
}
|
|
242
|
+
if (fallback) DEFAULT_FALLBACKS[model] = fallback;
|
|
280
243
|
}
|
|
281
244
|
function isKnownModel(model) {
|
|
282
245
|
return model in MODEL_PRICING;
|
|
@@ -285,160 +248,184 @@ function getModelPricing(model) {
|
|
|
285
248
|
return MODEL_PRICING[model] ?? null;
|
|
286
249
|
}
|
|
287
250
|
|
|
288
|
-
// src/
|
|
251
|
+
// src/utils.ts
|
|
289
252
|
var idCounter = 0;
|
|
290
253
|
function generateId() {
|
|
291
|
-
return `caplyr_${Date.now()}_${++idCounter}`;
|
|
254
|
+
return `caplyr_${Date.now()}_${process.pid ?? 0}_${++idCounter}`;
|
|
292
255
|
}
|
|
293
|
-
function
|
|
294
|
-
const
|
|
256
|
+
function getNextResetTime(reason) {
|
|
257
|
+
const now = /* @__PURE__ */ new Date();
|
|
258
|
+
if (reason.includes("daily")) {
|
|
259
|
+
const tomorrow = new Date(now);
|
|
260
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
261
|
+
tomorrow.setHours(0, 0, 0, 0);
|
|
262
|
+
return tomorrow.toISOString();
|
|
263
|
+
}
|
|
264
|
+
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
265
|
+
return nextMonth.toISOString();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/interceptors/enforce.ts
|
|
269
|
+
function enforcePreCall(model, config, heartbeat, shipper, provider, startTime) {
|
|
295
270
|
const dashboardUrl = `${config.endpoint ?? "https://app.caplyr.com"}/dashboard`;
|
|
271
|
+
const downgradeThreshold = config.downgradeThreshold ?? 0.8;
|
|
272
|
+
if (heartbeat.isKillSwitchActive()) {
|
|
273
|
+
const reason = "kill_switch_active";
|
|
274
|
+
const blockError = {
|
|
275
|
+
code: "KILL_SWITCH_ACTIVE",
|
|
276
|
+
message: "Caplyr kill switch is active. All AI API calls are halted.",
|
|
277
|
+
budget_used: heartbeat.budgetStatus.monthly_used,
|
|
278
|
+
budget_limit: heartbeat.budgetStatus.monthly_limit ?? 0,
|
|
279
|
+
dashboard_url: dashboardUrl
|
|
280
|
+
};
|
|
281
|
+
pushBlockedLog(shipper, provider, model, startTime, reason, config.endpoint_tag);
|
|
282
|
+
if (config.mode === "alert_only") {
|
|
283
|
+
config.onEnforcement?.({
|
|
284
|
+
type: "kill_switch",
|
|
285
|
+
timestamp: Date.now(),
|
|
286
|
+
reason: blockError.message,
|
|
287
|
+
budget_used: blockError.budget_used,
|
|
288
|
+
budget_limit: blockError.budget_limit
|
|
289
|
+
});
|
|
290
|
+
return { proceed: true, model, downgraded: false, enforcementReason: reason };
|
|
291
|
+
}
|
|
292
|
+
throw Object.assign(new Error(blockError.message), { caplyr: blockError });
|
|
293
|
+
}
|
|
294
|
+
if (config.mode === "cost_protect") {
|
|
295
|
+
if (heartbeat.isMonthlyBudgetExceeded() || heartbeat.isDailyBudgetExceeded()) {
|
|
296
|
+
const reason = heartbeat.isDailyBudgetExceeded() ? "daily_budget_exceeded" : "monthly_budget_exceeded";
|
|
297
|
+
const blockError = {
|
|
298
|
+
code: "BUDGET_EXCEEDED",
|
|
299
|
+
message: `AI budget exceeded. ${reason.replace(/_/g, " ")}.`,
|
|
300
|
+
budget_used: heartbeat.budgetStatus.monthly_used,
|
|
301
|
+
budget_limit: heartbeat.budgetStatus.monthly_limit ?? 0,
|
|
302
|
+
retry_after: getNextResetTime(reason),
|
|
303
|
+
dashboard_url: dashboardUrl
|
|
304
|
+
};
|
|
305
|
+
pushBlockedLog(shipper, provider, model, startTime, reason, config.endpoint_tag);
|
|
306
|
+
throw Object.assign(new Error(blockError.message), { caplyr: blockError });
|
|
307
|
+
}
|
|
308
|
+
if (heartbeat.isDowngradeThresholdReached(downgradeThreshold)) {
|
|
309
|
+
const fallback = config.fallback ?? getDefaultFallback(model);
|
|
310
|
+
if (fallback && fallback !== model) {
|
|
311
|
+
config.onEnforcement?.({
|
|
312
|
+
type: "downgrade",
|
|
313
|
+
timestamp: Date.now(),
|
|
314
|
+
reason: `Budget at ${Math.round(downgradeThreshold * 100)}% \u2014 downgraded ${model} \u2192 ${fallback}`,
|
|
315
|
+
original_model: model,
|
|
316
|
+
fallback_model: fallback,
|
|
317
|
+
budget_used: heartbeat.budgetStatus.monthly_used,
|
|
318
|
+
budget_limit: heartbeat.budgetStatus.monthly_limit ?? 0,
|
|
319
|
+
estimated_savings: 0
|
|
320
|
+
});
|
|
321
|
+
return {
|
|
322
|
+
proceed: true,
|
|
323
|
+
model: fallback,
|
|
324
|
+
downgraded: true,
|
|
325
|
+
originalModel: model,
|
|
326
|
+
enforcementReason: "auto_downgrade_threshold"
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { proceed: true, model, downgraded: false };
|
|
332
|
+
}
|
|
333
|
+
function logSuccess(shipper, provider, model, startTime, inputTokens, outputTokens, enforcement, heartbeat, endpointTag) {
|
|
334
|
+
const cost = calculateCost(model, inputTokens, outputTokens);
|
|
335
|
+
heartbeat.trackSpend(cost);
|
|
336
|
+
shipper.push({
|
|
337
|
+
id: generateId(),
|
|
338
|
+
timestamp: startTime,
|
|
339
|
+
provider,
|
|
340
|
+
model,
|
|
341
|
+
input_tokens: inputTokens,
|
|
342
|
+
output_tokens: outputTokens,
|
|
343
|
+
cost,
|
|
344
|
+
latency_ms: Date.now() - startTime,
|
|
345
|
+
endpoint_tag: endpointTag,
|
|
346
|
+
downgraded: enforcement.downgraded,
|
|
347
|
+
original_model: enforcement.originalModel,
|
|
348
|
+
blocked: false,
|
|
349
|
+
enforcement_reason: enforcement.enforcementReason
|
|
350
|
+
});
|
|
351
|
+
return cost;
|
|
352
|
+
}
|
|
353
|
+
function logProviderError(shipper, provider, model, startTime, enforcement, endpointTag) {
|
|
354
|
+
shipper.push({
|
|
355
|
+
id: generateId(),
|
|
356
|
+
timestamp: startTime,
|
|
357
|
+
provider,
|
|
358
|
+
model,
|
|
359
|
+
input_tokens: 0,
|
|
360
|
+
output_tokens: 0,
|
|
361
|
+
cost: 0,
|
|
362
|
+
latency_ms: Date.now() - startTime,
|
|
363
|
+
endpoint_tag: endpointTag,
|
|
364
|
+
downgraded: enforcement.downgraded,
|
|
365
|
+
original_model: enforcement.originalModel,
|
|
366
|
+
blocked: false,
|
|
367
|
+
enforcement_reason: "provider_error"
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
function pushBlockedLog(shipper, provider, model, startTime, reason, endpointTag) {
|
|
371
|
+
shipper.push({
|
|
372
|
+
id: generateId(),
|
|
373
|
+
timestamp: startTime,
|
|
374
|
+
provider,
|
|
375
|
+
model,
|
|
376
|
+
input_tokens: 0,
|
|
377
|
+
output_tokens: 0,
|
|
378
|
+
cost: 0,
|
|
379
|
+
latency_ms: Date.now() - startTime,
|
|
380
|
+
endpoint_tag: endpointTag,
|
|
381
|
+
downgraded: false,
|
|
382
|
+
blocked: true,
|
|
383
|
+
enforcement_reason: reason
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/interceptors/anthropic.ts
|
|
388
|
+
function wrapAnthropic(client, config, shipper, heartbeat) {
|
|
296
389
|
const messagesProxy = new Proxy(client.messages, {
|
|
297
390
|
get(target, prop, receiver) {
|
|
298
391
|
if (prop === "create") {
|
|
299
392
|
return async function caplyrInterceptedCreate(params, options) {
|
|
300
393
|
const startTime = Date.now();
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
code: "KILL_SWITCH_ACTIVE",
|
|
311
|
-
message: "Caplyr kill switch is active. All AI API calls are halted.",
|
|
312
|
-
budget_used: heartbeat.budgetStatus.monthly_used,
|
|
313
|
-
budget_limit: heartbeat.budgetStatus.monthly_limit ?? 0,
|
|
314
|
-
dashboard_url: dashboardUrl
|
|
315
|
-
};
|
|
316
|
-
shipper.push({
|
|
317
|
-
id: generateId(),
|
|
318
|
-
timestamp: startTime,
|
|
319
|
-
provider: "anthropic",
|
|
320
|
-
model,
|
|
321
|
-
input_tokens: 0,
|
|
322
|
-
output_tokens: 0,
|
|
323
|
-
cost: 0,
|
|
324
|
-
latency_ms: Date.now() - startTime,
|
|
325
|
-
endpoint_tag: config.endpoint_tag,
|
|
326
|
-
downgraded: false,
|
|
327
|
-
blocked: true,
|
|
328
|
-
enforcement_reason: enforcementReason
|
|
329
|
-
});
|
|
330
|
-
if (config.mode === "alert_only") {
|
|
331
|
-
} else {
|
|
332
|
-
throw Object.assign(new Error(blockError.message), {
|
|
333
|
-
caplyr: blockError
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
if (config.mode === "cost_protect") {
|
|
338
|
-
if (heartbeat.isMonthlyBudgetExceeded() || heartbeat.isDailyBudgetExceeded()) {
|
|
339
|
-
blocked = true;
|
|
340
|
-
enforcementReason = heartbeat.isDailyBudgetExceeded() ? "daily_budget_exceeded" : "monthly_budget_exceeded";
|
|
341
|
-
const blockError = {
|
|
342
|
-
code: "BUDGET_EXCEEDED",
|
|
343
|
-
message: `AI budget exceeded. ${enforcementReason.replace(/_/g, " ")}.`,
|
|
344
|
-
budget_used: heartbeat.budgetStatus.monthly_used,
|
|
345
|
-
budget_limit: heartbeat.budgetStatus.monthly_limit ?? 0,
|
|
346
|
-
retry_after: getNextResetTime(enforcementReason),
|
|
347
|
-
dashboard_url: dashboardUrl
|
|
348
|
-
};
|
|
349
|
-
shipper.push({
|
|
350
|
-
id: generateId(),
|
|
351
|
-
timestamp: startTime,
|
|
352
|
-
provider: "anthropic",
|
|
353
|
-
model,
|
|
354
|
-
input_tokens: 0,
|
|
355
|
-
output_tokens: 0,
|
|
356
|
-
cost: 0,
|
|
357
|
-
latency_ms: Date.now() - startTime,
|
|
358
|
-
endpoint_tag: config.endpoint_tag,
|
|
359
|
-
downgraded: false,
|
|
360
|
-
blocked: true,
|
|
361
|
-
enforcement_reason: enforcementReason
|
|
362
|
-
});
|
|
363
|
-
throw Object.assign(new Error(blockError.message), {
|
|
364
|
-
caplyr: blockError
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
if (heartbeat.isDowngradeThresholdReached(downgradeThreshold)) {
|
|
368
|
-
const fallback = config.fallback ?? getDefaultFallback(model);
|
|
369
|
-
if (fallback && fallback !== model) {
|
|
370
|
-
originalModel = model;
|
|
371
|
-
model = fallback;
|
|
372
|
-
downgraded = true;
|
|
373
|
-
enforcementReason = "auto_downgrade_threshold";
|
|
374
|
-
config.onEnforcement?.({
|
|
375
|
-
type: "downgrade",
|
|
376
|
-
timestamp: Date.now(),
|
|
377
|
-
reason: `Budget at ${Math.round(downgradeThreshold * 100)}% \u2014 downgraded ${originalModel} \u2192 ${model}`,
|
|
378
|
-
original_model: originalModel,
|
|
379
|
-
fallback_model: model,
|
|
380
|
-
budget_used: heartbeat.budgetStatus.monthly_used,
|
|
381
|
-
budget_limit: heartbeat.budgetStatus.monthly_limit ?? 0,
|
|
382
|
-
estimated_savings: 0
|
|
383
|
-
// Calculated after response
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
const requestParams = downgraded ? { ...params, model } : params;
|
|
394
|
+
const enforcement = enforcePreCall(
|
|
395
|
+
params.model,
|
|
396
|
+
config,
|
|
397
|
+
heartbeat,
|
|
398
|
+
shipper,
|
|
399
|
+
"anthropic",
|
|
400
|
+
startTime
|
|
401
|
+
);
|
|
402
|
+
const requestParams = enforcement.downgraded ? { ...params, model: enforcement.model } : params;
|
|
389
403
|
try {
|
|
390
|
-
const response = await target.create.call(
|
|
391
|
-
target,
|
|
392
|
-
requestParams,
|
|
393
|
-
options
|
|
394
|
-
);
|
|
395
|
-
const latency = Date.now() - startTime;
|
|
404
|
+
const response = await target.create.call(target, requestParams, options);
|
|
396
405
|
const inputTokens = response?.usage?.input_tokens ?? 0;
|
|
397
406
|
const outputTokens = response?.usage?.output_tokens ?? 0;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
shipper.push({
|
|
410
|
-
id: generateId(),
|
|
411
|
-
timestamp: startTime,
|
|
412
|
-
provider: "anthropic",
|
|
413
|
-
model,
|
|
414
|
-
input_tokens: inputTokens,
|
|
415
|
-
output_tokens: outputTokens,
|
|
416
|
-
cost,
|
|
417
|
-
latency_ms: latency,
|
|
418
|
-
endpoint_tag: config.endpoint_tag,
|
|
419
|
-
downgraded,
|
|
420
|
-
original_model: originalModel,
|
|
421
|
-
blocked: false,
|
|
422
|
-
enforcement_reason: enforcementReason
|
|
423
|
-
});
|
|
407
|
+
logSuccess(
|
|
408
|
+
shipper,
|
|
409
|
+
"anthropic",
|
|
410
|
+
enforcement.model,
|
|
411
|
+
startTime,
|
|
412
|
+
inputTokens,
|
|
413
|
+
outputTokens,
|
|
414
|
+
enforcement,
|
|
415
|
+
heartbeat,
|
|
416
|
+
config.endpoint_tag
|
|
417
|
+
);
|
|
424
418
|
return response;
|
|
425
419
|
} catch (err) {
|
|
426
420
|
if (err?.caplyr) throw err;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
latency_ms: Date.now() - startTime,
|
|
436
|
-
endpoint_tag: config.endpoint_tag,
|
|
437
|
-
downgraded,
|
|
438
|
-
original_model: originalModel,
|
|
439
|
-
blocked: false,
|
|
440
|
-
enforcement_reason: "provider_error"
|
|
441
|
-
});
|
|
421
|
+
logProviderError(
|
|
422
|
+
shipper,
|
|
423
|
+
"anthropic",
|
|
424
|
+
enforcement.model,
|
|
425
|
+
startTime,
|
|
426
|
+
enforcement,
|
|
427
|
+
config.endpoint_tag
|
|
428
|
+
);
|
|
442
429
|
throw err;
|
|
443
430
|
}
|
|
444
431
|
};
|
|
@@ -448,170 +435,55 @@ function wrapAnthropic(client, config, shipper, heartbeat) {
|
|
|
448
435
|
});
|
|
449
436
|
return new Proxy(client, {
|
|
450
437
|
get(target, prop, receiver) {
|
|
451
|
-
if (prop === "messages")
|
|
452
|
-
return messagesProxy;
|
|
453
|
-
}
|
|
438
|
+
if (prop === "messages") return messagesProxy;
|
|
454
439
|
return Reflect.get(target, prop, receiver);
|
|
455
440
|
}
|
|
456
441
|
});
|
|
457
442
|
}
|
|
458
|
-
function getNextResetTime(reason) {
|
|
459
|
-
const now = /* @__PURE__ */ new Date();
|
|
460
|
-
if (reason.includes("daily")) {
|
|
461
|
-
const tomorrow = new Date(now);
|
|
462
|
-
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
463
|
-
tomorrow.setHours(0, 0, 0, 0);
|
|
464
|
-
return tomorrow.toISOString();
|
|
465
|
-
}
|
|
466
|
-
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
467
|
-
return nextMonth.toISOString();
|
|
468
|
-
}
|
|
469
443
|
|
|
470
444
|
// src/interceptors/openai.ts
|
|
471
|
-
var idCounter2 = 0;
|
|
472
|
-
function generateId2() {
|
|
473
|
-
return `caplyr_${Date.now()}_${++idCounter2}`;
|
|
474
|
-
}
|
|
475
445
|
function wrapOpenAI(client, config, shipper, heartbeat) {
|
|
476
|
-
const downgradeThreshold = config.downgradeThreshold ?? 0.8;
|
|
477
|
-
const dashboardUrl = `${config.endpoint ?? "https://app.caplyr.com"}/dashboard`;
|
|
478
446
|
const completionsProxy = new Proxy(client.chat.completions, {
|
|
479
447
|
get(target, prop, receiver) {
|
|
480
448
|
if (prop === "create") {
|
|
481
449
|
return async function caplyrInterceptedCreate(params, options) {
|
|
482
450
|
const startTime = Date.now();
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
code: "KILL_SWITCH_ACTIVE",
|
|
493
|
-
message: "Caplyr kill switch is active. All AI API calls are halted.",
|
|
494
|
-
budget_used: heartbeat.budgetStatus.monthly_used,
|
|
495
|
-
budget_limit: heartbeat.budgetStatus.monthly_limit ?? 0,
|
|
496
|
-
dashboard_url: dashboardUrl
|
|
497
|
-
};
|
|
498
|
-
shipper.push({
|
|
499
|
-
id: generateId2(),
|
|
500
|
-
timestamp: startTime,
|
|
501
|
-
provider: "openai",
|
|
502
|
-
model,
|
|
503
|
-
input_tokens: 0,
|
|
504
|
-
output_tokens: 0,
|
|
505
|
-
cost: 0,
|
|
506
|
-
latency_ms: Date.now() - startTime,
|
|
507
|
-
endpoint_tag: config.endpoint_tag,
|
|
508
|
-
downgraded: false,
|
|
509
|
-
blocked: true,
|
|
510
|
-
enforcement_reason: enforcementReason
|
|
511
|
-
});
|
|
512
|
-
if (config.mode === "alert_only") {
|
|
513
|
-
} else {
|
|
514
|
-
throw Object.assign(new Error(blockError.message), {
|
|
515
|
-
caplyr: blockError
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
if (config.mode === "cost_protect") {
|
|
520
|
-
if (heartbeat.isMonthlyBudgetExceeded() || heartbeat.isDailyBudgetExceeded()) {
|
|
521
|
-
blocked = true;
|
|
522
|
-
enforcementReason = heartbeat.isDailyBudgetExceeded() ? "daily_budget_exceeded" : "monthly_budget_exceeded";
|
|
523
|
-
const blockError = {
|
|
524
|
-
code: "BUDGET_EXCEEDED",
|
|
525
|
-
message: `AI budget exceeded. ${enforcementReason.replace(/_/g, " ")}.`,
|
|
526
|
-
budget_used: heartbeat.budgetStatus.monthly_used,
|
|
527
|
-
budget_limit: heartbeat.budgetStatus.monthly_limit ?? 0,
|
|
528
|
-
retry_after: getNextResetTime2(enforcementReason),
|
|
529
|
-
dashboard_url: dashboardUrl
|
|
530
|
-
};
|
|
531
|
-
shipper.push({
|
|
532
|
-
id: generateId2(),
|
|
533
|
-
timestamp: startTime,
|
|
534
|
-
provider: "openai",
|
|
535
|
-
model,
|
|
536
|
-
input_tokens: 0,
|
|
537
|
-
output_tokens: 0,
|
|
538
|
-
cost: 0,
|
|
539
|
-
latency_ms: Date.now() - startTime,
|
|
540
|
-
endpoint_tag: config.endpoint_tag,
|
|
541
|
-
downgraded: false,
|
|
542
|
-
blocked: true,
|
|
543
|
-
enforcement_reason: enforcementReason
|
|
544
|
-
});
|
|
545
|
-
throw Object.assign(new Error(blockError.message), {
|
|
546
|
-
caplyr: blockError
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
if (heartbeat.isDowngradeThresholdReached(downgradeThreshold)) {
|
|
550
|
-
const fallback = config.fallback ?? getDefaultFallback(model);
|
|
551
|
-
if (fallback && fallback !== model) {
|
|
552
|
-
originalModel = model;
|
|
553
|
-
model = fallback;
|
|
554
|
-
downgraded = true;
|
|
555
|
-
enforcementReason = "auto_downgrade_threshold";
|
|
556
|
-
config.onEnforcement?.({
|
|
557
|
-
type: "downgrade",
|
|
558
|
-
timestamp: Date.now(),
|
|
559
|
-
reason: `Budget at ${Math.round(downgradeThreshold * 100)}% \u2014 downgraded ${originalModel} \u2192 ${model}`,
|
|
560
|
-
original_model: originalModel,
|
|
561
|
-
fallback_model: model,
|
|
562
|
-
budget_used: heartbeat.budgetStatus.monthly_used,
|
|
563
|
-
budget_limit: heartbeat.budgetStatus.monthly_limit ?? 0,
|
|
564
|
-
estimated_savings: 0
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
const requestParams = downgraded ? { ...params, model } : params;
|
|
451
|
+
const enforcement = enforcePreCall(
|
|
452
|
+
params.model,
|
|
453
|
+
config,
|
|
454
|
+
heartbeat,
|
|
455
|
+
shipper,
|
|
456
|
+
"openai",
|
|
457
|
+
startTime
|
|
458
|
+
);
|
|
459
|
+
const requestParams = enforcement.downgraded ? { ...params, model: enforcement.model } : params;
|
|
570
460
|
try {
|
|
571
|
-
const response = await target.create.call(
|
|
572
|
-
target,
|
|
573
|
-
requestParams,
|
|
574
|
-
options
|
|
575
|
-
);
|
|
576
|
-
const latency = Date.now() - startTime;
|
|
461
|
+
const response = await target.create.call(target, requestParams, options);
|
|
577
462
|
const usage = response?.usage;
|
|
578
463
|
const inputTokens = usage?.prompt_tokens ?? 0;
|
|
579
464
|
const outputTokens = usage?.completion_tokens ?? 0;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
endpoint_tag: config.endpoint_tag,
|
|
592
|
-
downgraded,
|
|
593
|
-
original_model: originalModel,
|
|
594
|
-
blocked: false,
|
|
595
|
-
enforcement_reason: enforcementReason
|
|
596
|
-
});
|
|
465
|
+
logSuccess(
|
|
466
|
+
shipper,
|
|
467
|
+
"openai",
|
|
468
|
+
enforcement.model,
|
|
469
|
+
startTime,
|
|
470
|
+
inputTokens,
|
|
471
|
+
outputTokens,
|
|
472
|
+
enforcement,
|
|
473
|
+
heartbeat,
|
|
474
|
+
config.endpoint_tag
|
|
475
|
+
);
|
|
597
476
|
return response;
|
|
598
477
|
} catch (err) {
|
|
599
478
|
if (err?.caplyr) throw err;
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
latency_ms: Date.now() - startTime,
|
|
609
|
-
endpoint_tag: config.endpoint_tag,
|
|
610
|
-
downgraded,
|
|
611
|
-
original_model: originalModel,
|
|
612
|
-
blocked: false,
|
|
613
|
-
enforcement_reason: "provider_error"
|
|
614
|
-
});
|
|
479
|
+
logProviderError(
|
|
480
|
+
shipper,
|
|
481
|
+
"openai",
|
|
482
|
+
enforcement.model,
|
|
483
|
+
startTime,
|
|
484
|
+
enforcement,
|
|
485
|
+
config.endpoint_tag
|
|
486
|
+
);
|
|
615
487
|
throw err;
|
|
616
488
|
}
|
|
617
489
|
};
|
|
@@ -621,32 +493,17 @@ function wrapOpenAI(client, config, shipper, heartbeat) {
|
|
|
621
493
|
});
|
|
622
494
|
const chatProxy = new Proxy(client.chat, {
|
|
623
495
|
get(target, prop, receiver) {
|
|
624
|
-
if (prop === "completions")
|
|
625
|
-
return completionsProxy;
|
|
626
|
-
}
|
|
496
|
+
if (prop === "completions") return completionsProxy;
|
|
627
497
|
return Reflect.get(target, prop, receiver);
|
|
628
498
|
}
|
|
629
499
|
});
|
|
630
500
|
return new Proxy(client, {
|
|
631
501
|
get(target, prop, receiver) {
|
|
632
|
-
if (prop === "chat")
|
|
633
|
-
return chatProxy;
|
|
634
|
-
}
|
|
502
|
+
if (prop === "chat") return chatProxy;
|
|
635
503
|
return Reflect.get(target, prop, receiver);
|
|
636
504
|
}
|
|
637
505
|
});
|
|
638
506
|
}
|
|
639
|
-
function getNextResetTime2(reason) {
|
|
640
|
-
const now = /* @__PURE__ */ new Date();
|
|
641
|
-
if (reason.includes("daily")) {
|
|
642
|
-
const tomorrow = new Date(now);
|
|
643
|
-
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
644
|
-
tomorrow.setHours(0, 0, 0, 0);
|
|
645
|
-
return tomorrow.toISOString();
|
|
646
|
-
}
|
|
647
|
-
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
648
|
-
return nextMonth.toISOString();
|
|
649
|
-
}
|
|
650
507
|
|
|
651
508
|
// src/protect.ts
|
|
652
509
|
function detectProvider(client) {
|
|
@@ -661,27 +518,20 @@ function detectProvider(client) {
|
|
|
661
518
|
var instances = /* @__PURE__ */ new Map();
|
|
662
519
|
function protect(client, config) {
|
|
663
520
|
if (!config.apiKey) {
|
|
664
|
-
throw new Error(
|
|
665
|
-
"Caplyr: apiKey is required. Get yours at https://app.caplyr.com"
|
|
666
|
-
);
|
|
521
|
+
throw new Error("Caplyr: apiKey is required. Get yours at https://app.caplyr.com");
|
|
667
522
|
}
|
|
523
|
+
const mode = config.mode ?? (config.budget ? "cost_protect" : "alert_only");
|
|
668
524
|
const resolvedConfig = {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
flushInterval: 3e4,
|
|
673
|
-
heartbeatInterval: 6e4,
|
|
674
|
-
...config
|
|
525
|
+
...config,
|
|
526
|
+
mode,
|
|
527
|
+
downgradeThreshold: config.downgradeThreshold ?? 0.8
|
|
675
528
|
};
|
|
676
|
-
if (resolvedConfig.budget && !resolvedConfig.mode) {
|
|
677
|
-
resolvedConfig.mode = "cost_protect";
|
|
678
|
-
}
|
|
679
529
|
let shared = instances.get(resolvedConfig.apiKey);
|
|
680
530
|
if (!shared) {
|
|
681
531
|
const shipper2 = new LogShipper(resolvedConfig);
|
|
682
532
|
const heartbeat2 = new Heartbeat(resolvedConfig);
|
|
683
533
|
heartbeat2.start();
|
|
684
|
-
shared = { shipper: shipper2, heartbeat: heartbeat2 };
|
|
534
|
+
shared = { shipper: shipper2, heartbeat: heartbeat2, mode, requestCount: 0 };
|
|
685
535
|
instances.set(resolvedConfig.apiKey, shared);
|
|
686
536
|
}
|
|
687
537
|
const { shipper, heartbeat } = shared;
|
|
@@ -693,28 +543,25 @@ function protect(client, config) {
|
|
|
693
543
|
return wrapOpenAI(client, resolvedConfig, shipper, heartbeat);
|
|
694
544
|
default:
|
|
695
545
|
throw new Error(
|
|
696
|
-
|
|
546
|
+
"Caplyr: Unrecognized AI client. Supported providers: Anthropic, OpenAI. Make sure you're passing a client instance (e.g., new Anthropic() or new OpenAI())."
|
|
697
547
|
);
|
|
698
548
|
}
|
|
699
549
|
}
|
|
700
550
|
function getStatus(apiKey) {
|
|
701
|
-
|
|
702
|
-
return shared?.heartbeat.status ?? "OFF";
|
|
551
|
+
return instances.get(apiKey)?.heartbeat.status ?? "OFF";
|
|
703
552
|
}
|
|
704
553
|
function getState(apiKey) {
|
|
705
554
|
const shared = instances.get(apiKey);
|
|
706
555
|
if (!shared) return null;
|
|
707
|
-
const { heartbeat } = shared;
|
|
556
|
+
const { heartbeat, mode, requestCount } = shared;
|
|
708
557
|
return {
|
|
709
558
|
status: heartbeat.status,
|
|
710
|
-
mode
|
|
711
|
-
// Will be stored properly in next iteration
|
|
559
|
+
mode,
|
|
712
560
|
budget_daily_used: heartbeat.budgetStatus.daily_used,
|
|
713
561
|
budget_monthly_used: heartbeat.budgetStatus.monthly_used,
|
|
714
562
|
kill_switch_active: heartbeat.budgetStatus.kill_switch_active,
|
|
715
563
|
last_heartbeat: Date.now(),
|
|
716
|
-
request_count:
|
|
717
|
-
// Will be tracked in next iteration
|
|
564
|
+
request_count: requestCount,
|
|
718
565
|
total_cost: heartbeat.budgetStatus.monthly_used,
|
|
719
566
|
total_savings: 0
|
|
720
567
|
};
|
|
@@ -728,7 +575,7 @@ async function shutdown(apiKey) {
|
|
|
728
575
|
instances.delete(apiKey);
|
|
729
576
|
}
|
|
730
577
|
} else {
|
|
731
|
-
for (const [
|
|
578
|
+
for (const [, shared] of instances) {
|
|
732
579
|
shared.heartbeat.destroy();
|
|
733
580
|
shared.shipper.destroy();
|
|
734
581
|
}
|