caplyr 0.2.4 → 0.3.1

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 CHANGED
@@ -36,20 +36,32 @@ var LogShipper = class {
36
36
  constructor(config) {
37
37
  this.buffer = [];
38
38
  this.timer = null;
39
+ // Store bound handlers so we can remove them on shutdown
40
+ this.processHandlers = [];
39
41
  this.endpoint = config.endpoint ?? "https://api.caplyr.com";
40
42
  this.apiKey = config.apiKey;
41
43
  this.batchSize = config.batchSize ?? 10;
42
44
  this.flushInterval = config.flushInterval ?? 3e4;
45
+ this.maxBufferSize = 1e3;
43
46
  this.onError = config.onError;
44
47
  this.timer = setInterval(() => this.flush(), this.flushInterval);
48
+ this.timer.unref?.();
45
49
  if (typeof process !== "undefined" && process.on) {
46
- const flushAndExit = () => {
50
+ const onBeforeExit = () => {
51
+ this.flush();
52
+ };
53
+ const onSignal = () => {
47
54
  this.flush().finally(() => {
48
55
  });
49
56
  };
50
- process.on("beforeExit", () => this.flush());
51
- process.on("SIGTERM", flushAndExit);
52
- process.on("SIGINT", flushAndExit);
57
+ process.on("beforeExit", onBeforeExit);
58
+ process.on("SIGTERM", onSignal);
59
+ process.on("SIGINT", onSignal);
60
+ this.processHandlers = [
61
+ { event: "beforeExit", handler: onBeforeExit },
62
+ { event: "SIGTERM", handler: onSignal },
63
+ { event: "SIGINT", handler: onSignal }
64
+ ];
53
65
  }
54
66
  }
55
67
  /**
@@ -57,6 +69,10 @@ var LogShipper = class {
57
69
  * Auto-flushes when batch size is reached.
58
70
  */
59
71
  push(log) {
72
+ if (this.buffer.length >= this.maxBufferSize) {
73
+ const excess = this.buffer.length - this.maxBufferSize + 1;
74
+ this.buffer.splice(0, excess);
75
+ }
60
76
  this.buffer.push(log);
61
77
  if (this.buffer.length >= this.batchSize) {
62
78
  this.flush();
@@ -88,7 +104,18 @@ var LogShipper = class {
88
104
  }
89
105
  }
90
106
  /**
91
- * Stop the periodic flush timer.
107
+ * Remove process signal handlers registered in the constructor.
108
+ */
109
+ removeProcessHandlers() {
110
+ if (typeof process !== "undefined" && process.removeListener) {
111
+ for (const { event, handler } of this.processHandlers) {
112
+ process.removeListener(event, handler);
113
+ }
114
+ }
115
+ this.processHandlers = [];
116
+ }
117
+ /**
118
+ * Stop the periodic flush timer and remove signal handlers.
92
119
  * Call this when tearing down the SDK.
93
120
  */
94
121
  destroy() {
@@ -96,13 +123,19 @@ var LogShipper = class {
96
123
  clearInterval(this.timer);
97
124
  this.timer = null;
98
125
  }
126
+ this.removeProcessHandlers();
99
127
  this.flush();
100
128
  }
129
+ /**
130
+ * Await the final log flush, stop timers, and remove signal handlers.
131
+ * Preferred over destroy() for clean shutdown.
132
+ */
101
133
  async shutdown() {
102
134
  if (this.timer) {
103
135
  clearInterval(this.timer);
104
136
  this.timer = null;
105
137
  }
138
+ this.removeProcessHandlers();
106
139
  await this.flush();
107
140
  }
108
141
  };
@@ -187,6 +220,7 @@ var Heartbeat = class {
187
220
  start() {
188
221
  this.beat();
189
222
  this.timer = setInterval(() => this.beat(), this.interval);
223
+ this.timer.unref?.();
190
224
  }
191
225
  /**
192
226
  * Send a single heartbeat and update local state.
@@ -206,21 +240,20 @@ var Heartbeat = class {
206
240
  throw new Error(`Heartbeat failed: ${res.status}`);
207
241
  }
208
242
  const data = await res.json();
209
- const localDailyUsed = this.budgetStatus.daily_used;
210
- const localMonthlyUsed = this.budgetStatus.monthly_used;
211
243
  const serverDailyUsed = Number(data.daily_used) || 0;
212
244
  const serverMonthlyUsed = Number(data.monthly_used) || 0;
213
245
  const serverDailyLimit = data.daily_limit != null ? Number(data.daily_limit) : null;
214
246
  const serverMonthlyLimit = data.monthly_limit != null ? Number(data.monthly_limit) : null;
215
- this.budgetStatus = {
216
- ...data,
217
- // Use whichever spend is higher — server or local tracking
218
- daily_used: Math.max(serverDailyUsed, localDailyUsed),
219
- monthly_used: Math.max(serverMonthlyUsed, localMonthlyUsed),
220
- // Use the stricter (lower) limit — local config takes priority if lower
221
- daily_limit: this.pickStricterLimit(serverDailyLimit, this.localDailyLimit),
222
- monthly_limit: this.pickStricterLimit(serverMonthlyLimit, this.localMonthlyLimit)
223
- };
247
+ const snapshotDaily = this.budgetStatus.daily_used;
248
+ const snapshotMonthly = this.budgetStatus.monthly_used;
249
+ const mergedDaily = Math.max(serverDailyUsed, snapshotDaily);
250
+ const mergedMonthly = Math.max(serverMonthlyUsed, snapshotMonthly);
251
+ this.budgetStatus.daily_used = mergedDaily + (this.budgetStatus.daily_used - snapshotDaily);
252
+ this.budgetStatus.monthly_used = mergedMonthly + (this.budgetStatus.monthly_used - snapshotMonthly);
253
+ this.budgetStatus.daily_limit = this.pickStricterLimit(serverDailyLimit, this.localDailyLimit);
254
+ this.budgetStatus.monthly_limit = this.pickStricterLimit(serverMonthlyLimit, this.localMonthlyLimit);
255
+ this.budgetStatus.status = data.status;
256
+ this.budgetStatus.kill_switch_active = data.kill_switch_active;
224
257
  this.consecutiveFailures = 0;
225
258
  const newStatus = data.kill_switch_active ? "OFF" : data.status;
226
259
  if (newStatus !== this.status) {
@@ -325,7 +358,12 @@ var MODEL_PRICING = {
325
358
  "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
326
359
  "o1": { input: 15, output: 60 },
327
360
  "o1-mini": { input: 3, output: 12 },
328
- "o3-mini": { input: 1.1, output: 4.4 }
361
+ "o3-mini": { input: 1.1, output: 4.4 },
362
+ // ---- Groq (OpenAI-compatible) ----
363
+ "llama-3.3-70b-versatile": { input: 0.59, output: 0.79 },
364
+ "llama-3.1-8b-instant": { input: 0.05, output: 0.08 },
365
+ "mixtral-8x7b-32768": { input: 0.24, output: 0.24 },
366
+ "gemma2-9b-it": { input: 0.2, output: 0.2 }
329
367
  };
330
368
  var DEFAULT_FALLBACKS = {
331
369
  // Anthropic downgrades
@@ -340,7 +378,11 @@ var DEFAULT_FALLBACKS = {
340
378
  "gpt-4-turbo": "gpt-4o-mini",
341
379
  "gpt-4": "gpt-3.5-turbo",
342
380
  "o1": "o1-mini",
343
- "o1-mini": "o3-mini"
381
+ "o1-mini": "o3-mini",
382
+ // Groq downgrades
383
+ "llama-3.3-70b-versatile": "llama-3.1-8b-instant",
384
+ "mixtral-8x7b-32768": "llama-3.1-8b-instant",
385
+ "gemma2-9b-it": "llama-3.1-8b-instant"
344
386
  };
345
387
  function calculateCost(model, inputTokens, outputTokens) {
346
388
  const pricing = MODEL_PRICING[model];
@@ -765,6 +807,7 @@ function protect(client, config) {
765
807
  ...config
766
808
  };
767
809
  let shared = instances.get(resolvedConfig.apiKey);
810
+ const isExisting = !!shared;
768
811
  if (!shared) {
769
812
  const shipper2 = new LogShipper(resolvedConfig);
770
813
  const heartbeat2 = new Heartbeat(resolvedConfig);
@@ -774,7 +817,19 @@ function protect(client, config) {
774
817
  }
775
818
  const { shipper, heartbeat } = shared;
776
819
  if (resolvedConfig.budget) {
777
- const budgetConfig = typeof resolvedConfig.budget === "number" ? { monthly: resolvedConfig.budget } : resolvedConfig.budget;
820
+ const raw = typeof resolvedConfig.budget === "number" ? { monthly: resolvedConfig.budget } : resolvedConfig.budget;
821
+ const budgetConfig = { ...raw };
822
+ if (isExisting) {
823
+ const current = heartbeat.budgetStatus;
824
+ if (budgetConfig.daily !== void 0) {
825
+ const existing = current.daily_limit;
826
+ budgetConfig.daily = existing !== null ? Math.min(existing, budgetConfig.daily) : budgetConfig.daily;
827
+ }
828
+ if (budgetConfig.monthly !== void 0) {
829
+ const existing = current.monthly_limit;
830
+ budgetConfig.monthly = existing !== null ? Math.min(existing, budgetConfig.monthly) : budgetConfig.monthly;
831
+ }
832
+ }
778
833
  heartbeat.applyLocalLimits(budgetConfig);
779
834
  }
780
835
  const provider = detectProvider(client);
@@ -816,14 +871,16 @@ async function shutdown(apiKey) {
816
871
  const shared = instances.get(apiKey);
817
872
  if (shared) {
818
873
  shared.heartbeat.destroy();
819
- shared.shipper.destroy();
874
+ await shared.shipper.shutdown();
820
875
  instances.delete(apiKey);
821
876
  }
822
877
  } else {
878
+ const shutdowns = [];
823
879
  for (const [key, shared] of instances) {
824
880
  shared.heartbeat.destroy();
825
- shared.shipper.destroy();
881
+ shutdowns.push(shared.shipper.shutdown());
826
882
  }
883
+ await Promise.all(shutdowns);
827
884
  instances.clear();
828
885
  }
829
886
  }
package/dist/index.mjs CHANGED
@@ -3,20 +3,32 @@ var LogShipper = class {
3
3
  constructor(config) {
4
4
  this.buffer = [];
5
5
  this.timer = null;
6
+ // Store bound handlers so we can remove them on shutdown
7
+ this.processHandlers = [];
6
8
  this.endpoint = config.endpoint ?? "https://api.caplyr.com";
7
9
  this.apiKey = config.apiKey;
8
10
  this.batchSize = config.batchSize ?? 10;
9
11
  this.flushInterval = config.flushInterval ?? 3e4;
12
+ this.maxBufferSize = 1e3;
10
13
  this.onError = config.onError;
11
14
  this.timer = setInterval(() => this.flush(), this.flushInterval);
15
+ this.timer.unref?.();
12
16
  if (typeof process !== "undefined" && process.on) {
13
- const flushAndExit = () => {
17
+ const onBeforeExit = () => {
18
+ this.flush();
19
+ };
20
+ const onSignal = () => {
14
21
  this.flush().finally(() => {
15
22
  });
16
23
  };
17
- process.on("beforeExit", () => this.flush());
18
- process.on("SIGTERM", flushAndExit);
19
- process.on("SIGINT", flushAndExit);
24
+ process.on("beforeExit", onBeforeExit);
25
+ process.on("SIGTERM", onSignal);
26
+ process.on("SIGINT", onSignal);
27
+ this.processHandlers = [
28
+ { event: "beforeExit", handler: onBeforeExit },
29
+ { event: "SIGTERM", handler: onSignal },
30
+ { event: "SIGINT", handler: onSignal }
31
+ ];
20
32
  }
21
33
  }
22
34
  /**
@@ -24,6 +36,10 @@ var LogShipper = class {
24
36
  * Auto-flushes when batch size is reached.
25
37
  */
26
38
  push(log) {
39
+ if (this.buffer.length >= this.maxBufferSize) {
40
+ const excess = this.buffer.length - this.maxBufferSize + 1;
41
+ this.buffer.splice(0, excess);
42
+ }
27
43
  this.buffer.push(log);
28
44
  if (this.buffer.length >= this.batchSize) {
29
45
  this.flush();
@@ -55,7 +71,18 @@ var LogShipper = class {
55
71
  }
56
72
  }
57
73
  /**
58
- * Stop the periodic flush timer.
74
+ * Remove process signal handlers registered in the constructor.
75
+ */
76
+ removeProcessHandlers() {
77
+ if (typeof process !== "undefined" && process.removeListener) {
78
+ for (const { event, handler } of this.processHandlers) {
79
+ process.removeListener(event, handler);
80
+ }
81
+ }
82
+ this.processHandlers = [];
83
+ }
84
+ /**
85
+ * Stop the periodic flush timer and remove signal handlers.
59
86
  * Call this when tearing down the SDK.
60
87
  */
61
88
  destroy() {
@@ -63,13 +90,19 @@ var LogShipper = class {
63
90
  clearInterval(this.timer);
64
91
  this.timer = null;
65
92
  }
93
+ this.removeProcessHandlers();
66
94
  this.flush();
67
95
  }
96
+ /**
97
+ * Await the final log flush, stop timers, and remove signal handlers.
98
+ * Preferred over destroy() for clean shutdown.
99
+ */
68
100
  async shutdown() {
69
101
  if (this.timer) {
70
102
  clearInterval(this.timer);
71
103
  this.timer = null;
72
104
  }
105
+ this.removeProcessHandlers();
73
106
  await this.flush();
74
107
  }
75
108
  };
@@ -154,6 +187,7 @@ var Heartbeat = class {
154
187
  start() {
155
188
  this.beat();
156
189
  this.timer = setInterval(() => this.beat(), this.interval);
190
+ this.timer.unref?.();
157
191
  }
158
192
  /**
159
193
  * Send a single heartbeat and update local state.
@@ -173,21 +207,20 @@ var Heartbeat = class {
173
207
  throw new Error(`Heartbeat failed: ${res.status}`);
174
208
  }
175
209
  const data = await res.json();
176
- const localDailyUsed = this.budgetStatus.daily_used;
177
- const localMonthlyUsed = this.budgetStatus.monthly_used;
178
210
  const serverDailyUsed = Number(data.daily_used) || 0;
179
211
  const serverMonthlyUsed = Number(data.monthly_used) || 0;
180
212
  const serverDailyLimit = data.daily_limit != null ? Number(data.daily_limit) : null;
181
213
  const serverMonthlyLimit = data.monthly_limit != null ? Number(data.monthly_limit) : null;
182
- this.budgetStatus = {
183
- ...data,
184
- // Use whichever spend is higher — server or local tracking
185
- daily_used: Math.max(serverDailyUsed, localDailyUsed),
186
- monthly_used: Math.max(serverMonthlyUsed, localMonthlyUsed),
187
- // Use the stricter (lower) limit — local config takes priority if lower
188
- daily_limit: this.pickStricterLimit(serverDailyLimit, this.localDailyLimit),
189
- monthly_limit: this.pickStricterLimit(serverMonthlyLimit, this.localMonthlyLimit)
190
- };
214
+ const snapshotDaily = this.budgetStatus.daily_used;
215
+ const snapshotMonthly = this.budgetStatus.monthly_used;
216
+ const mergedDaily = Math.max(serverDailyUsed, snapshotDaily);
217
+ const mergedMonthly = Math.max(serverMonthlyUsed, snapshotMonthly);
218
+ this.budgetStatus.daily_used = mergedDaily + (this.budgetStatus.daily_used - snapshotDaily);
219
+ this.budgetStatus.monthly_used = mergedMonthly + (this.budgetStatus.monthly_used - snapshotMonthly);
220
+ this.budgetStatus.daily_limit = this.pickStricterLimit(serverDailyLimit, this.localDailyLimit);
221
+ this.budgetStatus.monthly_limit = this.pickStricterLimit(serverMonthlyLimit, this.localMonthlyLimit);
222
+ this.budgetStatus.status = data.status;
223
+ this.budgetStatus.kill_switch_active = data.kill_switch_active;
191
224
  this.consecutiveFailures = 0;
192
225
  const newStatus = data.kill_switch_active ? "OFF" : data.status;
193
226
  if (newStatus !== this.status) {
@@ -292,7 +325,12 @@ var MODEL_PRICING = {
292
325
  "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
293
326
  "o1": { input: 15, output: 60 },
294
327
  "o1-mini": { input: 3, output: 12 },
295
- "o3-mini": { input: 1.1, output: 4.4 }
328
+ "o3-mini": { input: 1.1, output: 4.4 },
329
+ // ---- Groq (OpenAI-compatible) ----
330
+ "llama-3.3-70b-versatile": { input: 0.59, output: 0.79 },
331
+ "llama-3.1-8b-instant": { input: 0.05, output: 0.08 },
332
+ "mixtral-8x7b-32768": { input: 0.24, output: 0.24 },
333
+ "gemma2-9b-it": { input: 0.2, output: 0.2 }
296
334
  };
297
335
  var DEFAULT_FALLBACKS = {
298
336
  // Anthropic downgrades
@@ -307,7 +345,11 @@ var DEFAULT_FALLBACKS = {
307
345
  "gpt-4-turbo": "gpt-4o-mini",
308
346
  "gpt-4": "gpt-3.5-turbo",
309
347
  "o1": "o1-mini",
310
- "o1-mini": "o3-mini"
348
+ "o1-mini": "o3-mini",
349
+ // Groq downgrades
350
+ "llama-3.3-70b-versatile": "llama-3.1-8b-instant",
351
+ "mixtral-8x7b-32768": "llama-3.1-8b-instant",
352
+ "gemma2-9b-it": "llama-3.1-8b-instant"
311
353
  };
312
354
  function calculateCost(model, inputTokens, outputTokens) {
313
355
  const pricing = MODEL_PRICING[model];
@@ -732,6 +774,7 @@ function protect(client, config) {
732
774
  ...config
733
775
  };
734
776
  let shared = instances.get(resolvedConfig.apiKey);
777
+ const isExisting = !!shared;
735
778
  if (!shared) {
736
779
  const shipper2 = new LogShipper(resolvedConfig);
737
780
  const heartbeat2 = new Heartbeat(resolvedConfig);
@@ -741,7 +784,19 @@ function protect(client, config) {
741
784
  }
742
785
  const { shipper, heartbeat } = shared;
743
786
  if (resolvedConfig.budget) {
744
- const budgetConfig = typeof resolvedConfig.budget === "number" ? { monthly: resolvedConfig.budget } : resolvedConfig.budget;
787
+ const raw = typeof resolvedConfig.budget === "number" ? { monthly: resolvedConfig.budget } : resolvedConfig.budget;
788
+ const budgetConfig = { ...raw };
789
+ if (isExisting) {
790
+ const current = heartbeat.budgetStatus;
791
+ if (budgetConfig.daily !== void 0) {
792
+ const existing = current.daily_limit;
793
+ budgetConfig.daily = existing !== null ? Math.min(existing, budgetConfig.daily) : budgetConfig.daily;
794
+ }
795
+ if (budgetConfig.monthly !== void 0) {
796
+ const existing = current.monthly_limit;
797
+ budgetConfig.monthly = existing !== null ? Math.min(existing, budgetConfig.monthly) : budgetConfig.monthly;
798
+ }
799
+ }
745
800
  heartbeat.applyLocalLimits(budgetConfig);
746
801
  }
747
802
  const provider = detectProvider(client);
@@ -783,14 +838,16 @@ async function shutdown(apiKey) {
783
838
  const shared = instances.get(apiKey);
784
839
  if (shared) {
785
840
  shared.heartbeat.destroy();
786
- shared.shipper.destroy();
841
+ await shared.shipper.shutdown();
787
842
  instances.delete(apiKey);
788
843
  }
789
844
  } else {
845
+ const shutdowns = [];
790
846
  for (const [key, shared] of instances) {
791
847
  shared.heartbeat.destroy();
792
- shared.shipper.destroy();
848
+ shutdowns.push(shared.shipper.shutdown());
793
849
  }
850
+ await Promise.all(shutdowns);
794
851
  instances.clear();
795
852
  }
796
853
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caplyr",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "description": "AI Cost Control Plane — budget guardrails, auto-downgrade, and kill switch for AI API calls",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",