@game_ryo/lsji 0.1.0 → 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/package.json +15 -7
- package/src/cli.js +395 -62
- package/src/execution/budget/circuit-breaker.js +245 -0
- package/src/execution/budget/cost-tracker.js +387 -0
- package/src/execution/budget/index.js +63 -0
- package/src/execution/budget/token-counter.js +159 -0
- package/src/execution/engine.js +428 -0
- package/src/execution/hitl/approval-gate.js +210 -0
- package/src/execution/hitl/index.js +12 -0
- package/src/execution/hitl/notifier.js +151 -0
- package/src/execution/hitl/store.js +311 -0
- package/src/execution/idempotency.js +312 -0
- package/src/execution/index.js +14 -0
- package/src/index.js +80 -4
- package/src/llm/index.js +21 -0
- package/src/llm/llm-agent.js +357 -0
- package/src/llm/memory/conversation.js +271 -0
- package/src/llm/memory/episodic.js +312 -0
- package/src/llm/memory/index.js +12 -0
- package/src/llm/memory/semantic.js +324 -0
- package/src/llm/plugins/index.js +202 -0
- package/src/llm/prompt-manager.js +332 -0
- package/src/llm/providers/anthropic.js +250 -0
- package/src/llm/providers/base.js +116 -0
- package/src/llm/providers/local.js +163 -0
- package/src/llm/providers/openai.js +212 -0
- package/src/llm/tools/registry.js +342 -0
- package/src/server/index.js +416 -0
- package/src/server/ui/index.html +16 -0
- package/src/server/ui/package.json +19 -0
- package/src/server/ui/src/main.jsx +10 -0
- package/src/server/ui/src/styles.css +260 -0
- package/src/server/ui/vite.config.js +27 -0
- package/docs/README.md +0 -43
- package/docs/blog/2019-05-28-first-blog-post.mdx +0 -12
- package/docs/blog/2019-05-29-long-blog-post.mdx +0 -44
- package/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -24
- package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
- package/docs/blog/2021-08-26-welcome/index.mdx +0 -29
- package/docs/blog/authors.yml +0 -25
- package/docs/blog/tags.yml +0 -19
- package/docs/docs/api/agent.md +0 -151
- package/docs/docs/api/env.md +0 -133
- package/docs/docs/api/environments.md +0 -102
- package/docs/docs/api/qlearning.md +0 -138
- package/docs/docs/api/storage.md +0 -168
- package/docs/docs/architecture.md +0 -155
- package/docs/docs/cli.md +0 -210
- package/docs/docs/contributing.md +0 -162
- package/docs/docs/core-concepts.md +0 -152
- package/docs/docs/examples/advanced-training.md +0 -244
- package/docs/docs/examples/custom-environment.md +0 -198
- package/docs/docs/examples/custom-storage.md +0 -251
- package/docs/docs/getting-started.md +0 -91
- package/docs/docusaurus.config.ts +0 -149
- package/docs/package-lock.json +0 -19522
- package/docs/package.json +0 -49
- package/docs/sidebars.ts +0 -33
- package/docs/src/components/HomepageFeatures/index.tsx +0 -71
- package/docs/src/components/HomepageFeatures/styles.module.css +0 -11
- package/docs/src/css/custom.css +0 -79
- package/docs/src/pages/index.module.css +0 -23
- package/docs/src/pages/index.tsx +0 -44
- package/docs/src/pages/markdown-page.mdx +0 -7
- package/docs/static/.nojekyll +0 -0
- package/docs/static/img/docusaurus-social-card.jpg +0 -0
- package/docs/static/img/docusaurus.png +0 -0
- package/docs/static/img/favicon.ico +0 -0
- package/docs/static/img/logo.png +0 -0
- package/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
- package/docs/static/img/undraw_docusaurus_react.svg +0 -170
- package/docs/static/img/undraw_docusaurus_tree.svg +0 -40
- package/docs/tsconfig.json +0 -12
- package/legacy/worker.js +0 -166
- package/legacy/wrangler.toml +0 -11
|
@@ -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
|
+
}
|