caplyr 0.2.4 → 0.3.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 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) {
@@ -765,6 +798,7 @@ function protect(client, config) {
765
798
  ...config
766
799
  };
767
800
  let shared = instances.get(resolvedConfig.apiKey);
801
+ const isExisting = !!shared;
768
802
  if (!shared) {
769
803
  const shipper2 = new LogShipper(resolvedConfig);
770
804
  const heartbeat2 = new Heartbeat(resolvedConfig);
@@ -774,7 +808,19 @@ function protect(client, config) {
774
808
  }
775
809
  const { shipper, heartbeat } = shared;
776
810
  if (resolvedConfig.budget) {
777
- const budgetConfig = typeof resolvedConfig.budget === "number" ? { monthly: resolvedConfig.budget } : resolvedConfig.budget;
811
+ const raw = typeof resolvedConfig.budget === "number" ? { monthly: resolvedConfig.budget } : resolvedConfig.budget;
812
+ const budgetConfig = { ...raw };
813
+ if (isExisting) {
814
+ const current = heartbeat.budgetStatus;
815
+ if (budgetConfig.daily !== void 0) {
816
+ const existing = current.daily_limit;
817
+ budgetConfig.daily = existing !== null ? Math.min(existing, budgetConfig.daily) : budgetConfig.daily;
818
+ }
819
+ if (budgetConfig.monthly !== void 0) {
820
+ const existing = current.monthly_limit;
821
+ budgetConfig.monthly = existing !== null ? Math.min(existing, budgetConfig.monthly) : budgetConfig.monthly;
822
+ }
823
+ }
778
824
  heartbeat.applyLocalLimits(budgetConfig);
779
825
  }
780
826
  const provider = detectProvider(client);
@@ -816,14 +862,16 @@ async function shutdown(apiKey) {
816
862
  const shared = instances.get(apiKey);
817
863
  if (shared) {
818
864
  shared.heartbeat.destroy();
819
- shared.shipper.destroy();
865
+ await shared.shipper.shutdown();
820
866
  instances.delete(apiKey);
821
867
  }
822
868
  } else {
869
+ const shutdowns = [];
823
870
  for (const [key, shared] of instances) {
824
871
  shared.heartbeat.destroy();
825
- shared.shipper.destroy();
872
+ shutdowns.push(shared.shipper.shutdown());
826
873
  }
874
+ await Promise.all(shutdowns);
827
875
  instances.clear();
828
876
  }
829
877
  }
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) {
@@ -732,6 +765,7 @@ function protect(client, config) {
732
765
  ...config
733
766
  };
734
767
  let shared = instances.get(resolvedConfig.apiKey);
768
+ const isExisting = !!shared;
735
769
  if (!shared) {
736
770
  const shipper2 = new LogShipper(resolvedConfig);
737
771
  const heartbeat2 = new Heartbeat(resolvedConfig);
@@ -741,7 +775,19 @@ function protect(client, config) {
741
775
  }
742
776
  const { shipper, heartbeat } = shared;
743
777
  if (resolvedConfig.budget) {
744
- const budgetConfig = typeof resolvedConfig.budget === "number" ? { monthly: resolvedConfig.budget } : resolvedConfig.budget;
778
+ const raw = typeof resolvedConfig.budget === "number" ? { monthly: resolvedConfig.budget } : resolvedConfig.budget;
779
+ const budgetConfig = { ...raw };
780
+ if (isExisting) {
781
+ const current = heartbeat.budgetStatus;
782
+ if (budgetConfig.daily !== void 0) {
783
+ const existing = current.daily_limit;
784
+ budgetConfig.daily = existing !== null ? Math.min(existing, budgetConfig.daily) : budgetConfig.daily;
785
+ }
786
+ if (budgetConfig.monthly !== void 0) {
787
+ const existing = current.monthly_limit;
788
+ budgetConfig.monthly = existing !== null ? Math.min(existing, budgetConfig.monthly) : budgetConfig.monthly;
789
+ }
790
+ }
745
791
  heartbeat.applyLocalLimits(budgetConfig);
746
792
  }
747
793
  const provider = detectProvider(client);
@@ -783,14 +829,16 @@ async function shutdown(apiKey) {
783
829
  const shared = instances.get(apiKey);
784
830
  if (shared) {
785
831
  shared.heartbeat.destroy();
786
- shared.shipper.destroy();
832
+ await shared.shipper.shutdown();
787
833
  instances.delete(apiKey);
788
834
  }
789
835
  } else {
836
+ const shutdowns = [];
790
837
  for (const [key, shared] of instances) {
791
838
  shared.heartbeat.destroy();
792
- shared.shipper.destroy();
839
+ shutdowns.push(shared.shipper.shutdown());
793
840
  }
841
+ await Promise.all(shutdowns);
794
842
  instances.clear();
795
843
  }
796
844
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caplyr",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
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",