@game_ryo/lsji 0.1.1 → 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.
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Circuit Breaker
3
+ *
4
+ * Prevents runaway costs by automatically stopping execution when thresholds are exceeded.
5
+ * Implements the circuit breaker pattern for budget protection.
6
+ */
7
+
8
+ import { globalCostTracker } from './cost-tracker.js';
9
+
10
+ /**
11
+ * Circuit breaker states
12
+ */
13
+ export const CircuitState = {
14
+ CLOSED: 'closed', // Normal operation
15
+ OPEN: 'open', // Blocking requests
16
+ HALF_OPEN: 'half_open', // Testing if service recovered
17
+ };
18
+
19
+ /**
20
+ * Circuit Breaker Configuration
21
+ * @typedef {Object} CircuitBreakerConfig
22
+ * @property {number} [costThreshold] - Cost threshold to open circuit (USD)
23
+ * @property {number} [tokenThreshold] - Token threshold to open circuit
24
+ * @property {number} [timeWindow] - Time window for threshold (ms), default 1 hour
25
+ * @property {number} [resetTimeout] - Time before trying half-open (ms), default 5 min
26
+ * @property {number} [failureThreshold] - Number of failures before opening
27
+ */
28
+
29
+ /**
30
+ * Circuit Breaker for budget protection
31
+ */
32
+ export class CircuitBreaker {
33
+ constructor(config = {}) {
34
+ this.config = {
35
+ costThreshold: config.costThreshold || 50.00, // $50 per hour
36
+ tokenThreshold: config.tokenThreshold || 500000, // 500k tokens per hour
37
+ timeWindow: config.timeWindow || 3600000, // 1 hour
38
+ resetTimeout: config.resetTimeout || 300000, // 5 minutes
39
+ failureThreshold: config.failureThreshold || 5,
40
+ };
41
+
42
+ this.state = CircuitState.CLOSED;
43
+ this.failureCount = 0;
44
+ this.lastFailureTime = null;
45
+ this.lastStateChange = Date.now();
46
+ this.costTracker = globalCostTracker;
47
+ this.listeners = new Map(); // event -> callbacks
48
+ }
49
+
50
+ /**
51
+ * Check if circuit allows execution
52
+ */
53
+ async checkBudget(budgetId, estimatedCost = 0, estimatedTokens = 0) {
54
+ // Check cost tracker budget
55
+ const budgetCheck = this.costTracker.checkBudget(budgetId, estimatedCost, estimatedTokens);
56
+
57
+ if (!budgetCheck.allowed) {
58
+ this.recordFailure('budget_exceeded', budgetCheck.errors);
59
+ return { allowed: false, reason: 'budget_exceeded', details: budgetCheck.errors };
60
+ }
61
+
62
+ // Check circuit breaker thresholds
63
+ const now = Date.now();
64
+ const windowStart = now - this.config.timeWindow;
65
+
66
+ // Get recent costs
67
+ const recentCost = this.costTracker.getCostSince(new Date(windowStart));
68
+ const recentTokens = this.getRecentTokens(windowStart);
69
+
70
+ if (recentCost >= this.config.costThreshold) {
71
+ this.recordFailure('cost_threshold_exceeded', {
72
+ current: recentCost,
73
+ threshold: this.config.costThreshold
74
+ });
75
+ return { allowed: false, reason: 'cost_threshold_exceeded', details: { current: recentCost, threshold: this.config.costThreshold } };
76
+ }
77
+
78
+ if (recentTokens >= this.config.tokenThreshold) {
79
+ this.recordFailure('token_threshold_exceeded', {
80
+ current: recentTokens,
81
+ threshold: this.config.tokenThreshold
82
+ });
83
+ return { allowed: false, reason: 'token_threshold_exceeded', details: { current: recentTokens, threshold: this.config.tokenThreshold } };
84
+ }
85
+
86
+ // Check circuit state
87
+ if (this.state === CircuitState.OPEN) {
88
+ if (now - this.lastStateChange >= this.config.resetTimeout) {
89
+ this.transitionToHalfOpen();
90
+ } else {
91
+ return { allowed: false, reason: 'circuit_open', details: { resetIn: this.config.resetTimeout - (now - this.lastStateChange) } };
92
+ }
93
+ }
94
+
95
+ if (this.state === CircuitState.HALF_OPEN) {
96
+ // Allow one request to test
97
+ return { allowed: true, reason: 'half_open_test' };
98
+ }
99
+
100
+ return { allowed: true, reason: 'ok' };
101
+ }
102
+
103
+ /**
104
+ * Record successful execution
105
+ */
106
+ recordSuccess() {
107
+ if (this.state === CircuitState.HALF_OPEN) {
108
+ this.transitionToClosed();
109
+ }
110
+ this.failureCount = 0;
111
+ }
112
+
113
+ /**
114
+ * Record failure
115
+ */
116
+ recordFailure(reason, details = {}) {
117
+ this.failureCount++;
118
+ this.lastFailureTime = Date.now();
119
+
120
+ this.emit('failure', { reason, details, failureCount: this.failureCount });
121
+
122
+ if (this.failureCount >= this.config.failureThreshold) {
123
+ this.transitionToOpen(reason, details);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Transition to OPEN state
129
+ */
130
+ transitionToOpen(reason, details) {
131
+ if (this.state !== CircuitState.OPEN) {
132
+ this.state = CircuitState.OPEN;
133
+ this.lastStateChange = Date.now();
134
+ this.emit('open', { reason, details, timestamp: this.lastStateChange });
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Transition to HALF_OPEN state
140
+ */
141
+ transitionToHalfOpen() {
142
+ this.state = CircuitState.HALF_OPEN;
143
+ this.lastStateChange = Date.now();
144
+ this.emit('half_open', { timestamp: this.lastStateChange });
145
+ }
146
+
147
+ /**
148
+ * Transition to CLOSED state
149
+ */
150
+ transitionToClosed() {
151
+ this.state = CircuitState.CLOSED;
152
+ this.lastStateChange = Date.now();
153
+ this.failureCount = 0;
154
+ this.emit('closed', { timestamp: this.lastStateChange });
155
+ }
156
+
157
+ /**
158
+ * Get recent token count
159
+ */
160
+ getRecentTokens(since) {
161
+ const history = this.costTracker.costHistory || [];
162
+ return history
163
+ .filter(r => r.timestamp >= new Date(since))
164
+ .reduce((sum, r) => sum + (r.totalTokens || 0), 0);
165
+ }
166
+
167
+ /**
168
+ * Get current state
169
+ */
170
+ getState() {
171
+ return {
172
+ state: this.state,
173
+ failureCount: this.failureCount,
174
+ lastFailureTime: this.lastFailureTime,
175
+ lastStateChange: this.lastStateChange,
176
+ config: this.config,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Reset circuit breaker
182
+ */
183
+ reset() {
184
+ this.state = CircuitState.CLOSED;
185
+ this.failureCount = 0;
186
+ this.lastFailureTime = null;
187
+ this.lastStateChange = Date.now();
188
+ this.emit('reset', { timestamp: this.lastStateChange });
189
+ }
190
+
191
+ /**
192
+ * Force open the circuit
193
+ */
194
+ forceOpen(reason = 'manual') {
195
+ this.transitionToOpen(reason, { manual: true });
196
+ }
197
+
198
+ /**
199
+ * Force close the circuit
200
+ */
201
+ forceClose() {
202
+ this.transitionToClosed();
203
+ }
204
+
205
+ /**
206
+ * Add event listener
207
+ */
208
+ on(event, callback) {
209
+ if (!this.listeners.has(event)) {
210
+ this.listeners.set(event, []);
211
+ }
212
+ this.listeners.get(event).push(callback);
213
+ }
214
+
215
+ /**
216
+ * Remove event listener
217
+ */
218
+ off(event, callback) {
219
+ if (this.listeners.has(event)) {
220
+ const callbacks = this.listeners.get(event);
221
+ const index = callbacks.indexOf(callback);
222
+ if (index >= 0) callbacks.splice(index, 1);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Emit event
228
+ */
229
+ emit(event, data) {
230
+ if (this.listeners.has(event)) {
231
+ for (const callback of this.listeners.get(event)) {
232
+ try {
233
+ callback(data);
234
+ } catch (e) {
235
+ console.error(`Circuit breaker listener error:`, e);
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Global circuit breaker instance
244
+ */
245
+ export const globalCircuitBreaker = new CircuitBreaker();
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Cost Tracker
3
+ *
4
+ * Tracks API costs with provider-specific pricing and budget limits.
5
+ */
6
+
7
+ import { globalTokenCounter } from './token-counter.js';
8
+
9
+ /**
10
+ * Cost record
11
+ * @typedef {Object} CostRecord
12
+ * @property {number} cost - Cost in USD
13
+ * @property {number} inputTokens
14
+ * @property {number} outputTokens
15
+ * @property {string} model
16
+ * @property {string} provider
17
+ * @property {Date} timestamp
18
+ * @property {string} [operation] - Operation identifier
19
+ * @property {string} [budgetId] - Budget identifier
20
+ */
21
+
22
+ /**
23
+ * Budget configuration
24
+ * @typedef {Object} BudgetConfig
25
+ * @property {number} [maxCostPerRun] - Maximum cost per run (USD)
26
+ * @property {number} [maxCostPerDay] - Maximum cost per day (USD)
27
+ * @property {number} [maxCostPerMonth] - Maximum cost per month (USD)
28
+ * @property {number} [maxTokensPerRun] - Maximum tokens per run
29
+ * @property {number} [alertThreshold] - Alert threshold (0-1), default 0.8
30
+ */
31
+
32
+ /**
33
+ * Cost Tracker - Tracks costs and enforces budgets
34
+ */
35
+ export class CostTracker {
36
+ constructor(config = {}) {
37
+ this.config = {
38
+ maxCostPerRun: config.maxCostPerRun || 10.00,
39
+ maxCostPerDay: config.maxCostPerDay || 50.00,
40
+ maxCostPerMonth: config.maxCostPerMonth || 500.00,
41
+ maxTokensPerRun: config.maxTokensPerRun || 100000,
42
+ alertThreshold: config.alertThreshold || 0.8,
43
+ };
44
+
45
+ this.costHistory = [];
46
+ this.runCosts = new Map(); // budgetId -> accumulated cost
47
+ this.runTokens = new Map(); // budgetId -> accumulated tokens
48
+ this.alerts = [];
49
+ }
50
+
51
+ /**
52
+ * Record cost from LLM usage
53
+ */
54
+ recordCost(record) {
55
+ const costRecord = {
56
+ ...record,
57
+ timestamp: record.timestamp || new Date(),
58
+ };
59
+ this.costHistory.push(costRecord);
60
+
61
+ // Track per budget
62
+ if (record.budgetId) {
63
+ const currentCost = this.runCosts.get(record.budgetId) || 0;
64
+ this.runCosts.set(record.budgetId, currentCost + costRecord.cost);
65
+
66
+ const currentTokens = this.runTokens.get(record.budgetId) || 0;
67
+ this.runTokens.set(record.budgetId, currentTokens + (record.totalTokens || 0));
68
+ }
69
+
70
+ // Check alerts
71
+ this.checkAlerts(costRecord);
72
+
73
+ return costRecord;
74
+ }
75
+
76
+ /**
77
+ * Check and trigger alerts
78
+ */
79
+ checkAlerts(record) {
80
+ if (!record.budgetId) return;
81
+
82
+ const runCost = this.runCosts.get(record.budgetId) || 0;
83
+ const runTokens = this.runTokens.get(record.budgetId) || 0;
84
+
85
+ // Check run cost limit
86
+ if (this.config.maxCostPerRun > 0) {
87
+ const ratio = runCost / this.config.maxCostPerRun;
88
+ if (ratio >= 1.0) {
89
+ this.triggerAlert('run_cost_exceeded', {
90
+ budgetId: record.budgetId,
91
+ current: runCost,
92
+ limit: this.config.maxCostPerRun,
93
+ ratio,
94
+ });
95
+ } else if (ratio >= this.config.alertThreshold) {
96
+ this.triggerAlert('run_cost_warning', {
97
+ budgetId: record.budgetId,
98
+ current: runCost,
99
+ limit: this.config.maxCostPerRun,
100
+ ratio,
101
+ });
102
+ }
103
+ }
104
+
105
+ // Check run token limit
106
+ if (this.config.maxTokensPerRun > 0) {
107
+ const ratio = runTokens / this.config.maxTokensPerRun;
108
+ if (ratio >= 1.0) {
109
+ this.triggerAlert('run_tokens_exceeded', {
110
+ budgetId: record.budgetId,
111
+ current: runTokens,
112
+ limit: this.config.maxTokensPerRun,
113
+ ratio,
114
+ });
115
+ } else if (ratio >= this.config.alertThreshold) {
116
+ this.triggerAlert('run_tokens_warning', {
117
+ budgetId: record.budgetId,
118
+ current: runTokens,
119
+ limit: this.config.maxTokensPerRun,
120
+ ratio,
121
+ });
122
+ }
123
+ }
124
+
125
+ // Check daily limit
126
+ const todayStart = new Date();
127
+ todayStart.setHours(0, 0, 0, 0);
128
+ const todayCost = this.getCostSince(todayStart);
129
+ if (this.config.maxCostPerDay > 0) {
130
+ const ratio = todayCost / this.config.maxCostPerDay;
131
+ if (ratio >= 1.0) {
132
+ this.triggerAlert('daily_cost_exceeded', { current: todayCost, limit: this.config.maxCostPerDay, ratio });
133
+ } else if (ratio >= this.config.alertThreshold) {
134
+ this.triggerAlert('daily_cost_warning', { current: todayCost, limit: this.config.maxCostPerDay, ratio });
135
+ }
136
+ }
137
+
138
+ // Check monthly limit
139
+ const monthStart = new Date();
140
+ monthStart.setDate(1);
141
+ monthStart.setHours(0, 0, 0, 0);
142
+ const monthCost = this.getCostSince(monthStart);
143
+ if (this.config.maxCostPerMonth > 0) {
144
+ const ratio = monthCost / this.config.maxCostPerMonth;
145
+ if (ratio >= 1.0) {
146
+ this.triggerAlert('monthly_cost_exceeded', { current: monthCost, limit: this.config.maxCostPerMonth, ratio });
147
+ } else if (ratio >= this.config.alertThreshold) {
148
+ this.triggerAlert('monthly_cost_warning', { current: monthCost, limit: this.config.maxCostPerMonth, ratio });
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Trigger alert
155
+ */
156
+ triggerAlert(type, data) {
157
+ const alert = { type, data, timestamp: new Date() };
158
+ this.alerts.push(alert);
159
+
160
+ // Emit event for external handlers
161
+ if (typeof window !== 'undefined' && window.dispatchEvent) {
162
+ window.dispatchEvent(new CustomEvent('lsji:budget-alert', { detail: alert }));
163
+ }
164
+
165
+ return alert;
166
+ }
167
+
168
+ /**
169
+ * Get alerts
170
+ */
171
+ getAlerts(since = null) {
172
+ let filtered = this.alerts;
173
+ if (since) {
174
+ filtered = this.alerts.filter(a => a.timestamp >= since);
175
+ }
176
+ return filtered;
177
+ }
178
+
179
+ /**
180
+ * Clear alerts
181
+ */
182
+ clearAlerts() {
183
+ this.alerts = [];
184
+ }
185
+
186
+ /**
187
+ * Get cost since a date
188
+ */
189
+ getCostSince(since) {
190
+ return this.costHistory
191
+ .filter(r => r.timestamp >= since)
192
+ .reduce((sum, r) => sum + r.cost, 0);
193
+ }
194
+
195
+ /**
196
+ * Get cost for a specific budget
197
+ */
198
+ getRunCost(budgetId) {
199
+ return this.runCosts.get(budgetId) || 0;
200
+ }
201
+
202
+ /**
203
+ * Get tokens for a specific budget
204
+ */
205
+ getRunTokens(budgetId) {
206
+ return this.runTokens.get(budgetId) || 0;
207
+ }
208
+
209
+ /**
210
+ * Check if budget allows an operation
211
+ */
212
+ checkBudget(budgetId, estimatedCost = 0, estimatedTokens = 0) {
213
+ const currentCost = this.getRunCost(budgetId);
214
+ const currentTokens = this.getRunTokens(budgetId);
215
+
216
+ const projectedCost = currentCost + estimatedCost;
217
+ const projectedTokens = currentTokens + estimatedTokens;
218
+
219
+ const result = {
220
+ allowed: true,
221
+ warnings: [],
222
+ errors: [],
223
+ };
224
+
225
+ if (this.config.maxCostPerRun > 0 && projectedCost > this.config.maxCostPerRun) {
226
+ result.allowed = false;
227
+ result.errors.push({
228
+ type: 'max_cost_per_run_exceeded',
229
+ current: currentCost,
230
+ projected: projectedCost,
231
+ limit: this.config.maxCostPerRun,
232
+ });
233
+ }
234
+
235
+ if (this.config.maxTokensPerRun > 0 && projectedTokens > this.config.maxTokensPerRun) {
236
+ result.allowed = false;
237
+ result.errors.push({
238
+ type: 'max_tokens_per_run_exceeded',
239
+ current: currentTokens,
240
+ projected: projectedTokens,
241
+ limit: this.config.maxTokensPerRun,
242
+ });
243
+ }
244
+
245
+ // Check daily limit
246
+ const todayStart = new Date();
247
+ todayStart.setHours(0, 0, 0, 0);
248
+ const todayCost = this.getCostSince(todayStart);
249
+ if (this.config.maxCostPerDay > 0 && todayCost + estimatedCost > this.config.maxCostPerDay) {
250
+ result.allowed = false;
251
+ result.errors.push({
252
+ type: 'max_daily_cost_exceeded',
253
+ current: todayCost,
254
+ projected: todayCost + estimatedCost,
255
+ limit: this.config.maxCostPerDay,
256
+ });
257
+ }
258
+
259
+ // Check monthly limit
260
+ const monthStart = new Date();
261
+ monthStart.setDate(1);
262
+ monthStart.setHours(0, 0, 0, 0);
263
+ const monthCost = this.getCostSince(monthStart);
264
+ if (this.config.maxCostPerMonth > 0 && monthCost + estimatedCost > this.config.maxCostPerMonth) {
265
+ result.allowed = false;
266
+ result.errors.push({
267
+ type: 'max_monthly_cost_exceeded',
268
+ current: monthCost,
269
+ projected: monthCost + estimatedCost,
270
+ limit: this.config.maxCostPerMonth,
271
+ });
272
+ }
273
+
274
+ // Warnings
275
+ if (this.config.maxCostPerRun > 0) {
276
+ const ratio = projectedCost / this.config.maxCostPerRun;
277
+ if (ratio >= this.config.alertThreshold) {
278
+ result.warnings.push({
279
+ type: 'cost_warning',
280
+ ratio,
281
+ current: projectedCost,
282
+ limit: this.config.maxCostPerRun,
283
+ });
284
+ }
285
+ }
286
+
287
+ return result;
288
+ }
289
+
290
+ /**
291
+ * Reset run budget (for new run)
292
+ */
293
+ resetRunBudget(budgetId) {
294
+ this.runCosts.delete(budgetId);
295
+ this.runTokens.delete(budgetId);
296
+ }
297
+
298
+ /**
299
+ * Get total cost
300
+ */
301
+ getTotalCost(since = null) {
302
+ return this.getCostSince(since);
303
+ }
304
+
305
+ /**
306
+ * Get cost breakdown by model
307
+ */
308
+ getCostByModel(since = null) {
309
+ let filtered = this.costHistory;
310
+ if (since) {
311
+ filtered = this.costHistory.filter(r => r.timestamp >= since);
312
+ }
313
+
314
+ const byModel = {};
315
+ for (const r of filtered) {
316
+ const model = r.model || 'unknown';
317
+ if (!byModel[model]) {
318
+ byModel[model] = { cost: 0, requests: 0, inputTokens: 0, outputTokens: 0 };
319
+ }
320
+ byModel[model].cost += r.cost || 0;
321
+ byModel[model].requests += 1;
322
+ byModel[model].inputTokens += r.inputTokens || 0;
323
+ byModel[model].outputTokens += r.outputTokens || 0;
324
+ }
325
+ return byModel;
326
+ }
327
+
328
+ /**
329
+ * Get cost breakdown by provider
330
+ */
331
+ getCostByProvider(since = null) {
332
+ let filtered = this.costHistory;
333
+ if (since) {
334
+ filtered = this.costHistory.filter(r => r.timestamp >= since);
335
+ }
336
+
337
+ const byProvider = {};
338
+ for (const r of filtered) {
339
+ const provider = r.provider || 'unknown';
340
+ if (!byProvider[provider]) {
341
+ byProvider[provider] = { cost: 0, requests: 0, inputTokens: 0, outputTokens: 0 };
342
+ }
343
+ byProvider[provider].cost += r.cost || 0;
344
+ byProvider[provider].requests += 1;
345
+ byProvider[provider].inputTokens += r.inputTokens || 0;
346
+ byProvider[provider].outputTokens += r.outputTokens || 0;
347
+ }
348
+ return byProvider;
349
+ }
350
+
351
+ /**
352
+ * Get status summary
353
+ */
354
+ getStatus(budgetId = null) {
355
+ const todayStart = new Date();
356
+ todayStart.setHours(0, 0, 0, 0);
357
+ const monthStart = new Date();
358
+ monthStart.setDate(1);
359
+ monthStart.setHours(0, 0, 0, 0);
360
+
361
+ return {
362
+ config: this.config,
363
+ runCost: budgetId ? this.getRunCost(budgetId) : null,
364
+ runTokens: budgetId ? this.getRunTokens(budgetId) : null,
365
+ todayCost: this.getCostSince(todayStart),
366
+ monthCost: this.getCostSince(monthStart),
367
+ totalCost: this.getTotalCost(),
368
+ recentAlerts: this.alerts.slice(-10),
369
+ budgetStatus: budgetId ? this.checkBudget(budgetId) : null,
370
+ };
371
+ }
372
+
373
+ /**
374
+ * Clear all data
375
+ */
376
+ clear() {
377
+ this.costHistory = [];
378
+ this.runCosts.clear();
379
+ this.runTokens.clear();
380
+ this.alerts = [];
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Global cost tracker instance
386
+ */
387
+ export const globalCostTracker = new CostTracker();
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Budget Control System
3
+ *
4
+ * Exports all budget-related components:
5
+ * - TokenCounter: Tracks token usage
6
+ * - CostTracker: Tracks costs with limits and alerts
7
+ * - CircuitBreaker: Prevents runaway costs
8
+ */
9
+
10
+ import { TokenCounter } from './token-counter.js';
11
+ import { CostTracker } from './cost-tracker.js';
12
+ import { CircuitBreaker } from './circuit-breaker.js';
13
+
14
+ export { TokenCounter, globalTokenCounter } from './token-counter.js';
15
+ export { CostTracker, globalCostTracker } from './cost-tracker.js';
16
+ export { CircuitBreaker, CircuitState, globalCircuitBreaker } from './circuit-breaker.js';
17
+
18
+ /**
19
+ * Create a complete budget controller with all components
20
+ */
21
+ export function createBudgetController(config = {}) {
22
+ return {
23
+ tokenCounter: new TokenCounter(),
24
+ costTracker: new CostTracker(config),
25
+ circuitBreaker: new CircuitBreaker(config),
26
+
27
+ /**
28
+ * Check budget before operation
29
+ */
30
+ async checkBudget(budgetId, estimatedCost = 0, estimatedTokens = 0) {
31
+ return this.circuitBreaker.checkBudget(budgetId, estimatedCost, estimatedTokens);
32
+ },
33
+
34
+ /**
35
+ * Record usage after operation
36
+ */
37
+ recordUsage(budgetId, usage) {
38
+ // usage: { inputTokens, outputTokens, totalTokens, model, provider, cost }
39
+ this.tokenCounter.recordUsage({ ...usage, budgetId });
40
+ if (usage.cost !== undefined) {
41
+ this.costTracker.recordCost({ ...usage, budgetId });
42
+ }
43
+ },
44
+
45
+ /**
46
+ * Get status
47
+ */
48
+ getStatus(budgetId = null) {
49
+ return {
50
+ tokens: this.tokenCounter.getSummary(),
51
+ costs: this.costTracker.getStatus(budgetId),
52
+ circuit: this.circuitBreaker.getState(),
53
+ };
54
+ },
55
+
56
+ /**
57
+ * Reset run budget
58
+ */
59
+ resetRunBudget(budgetId) {
60
+ this.costTracker.resetRunBudget(budgetId);
61
+ },
62
+ };
63
+ }