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