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