tlc-claude-code 1.4.9 → 1.5.2
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/CLAUDE.md +23 -0
- package/CODING-STANDARDS.md +408 -0
- package/bin/install.js +2 -0
- package/dashboard/dist/components/QualityGatePane.d.ts +38 -0
- package/dashboard/dist/components/QualityGatePane.js +31 -0
- package/dashboard/dist/components/QualityGatePane.test.d.ts +1 -0
- package/dashboard/dist/components/QualityGatePane.test.js +147 -0
- package/dashboard/dist/components/orchestration/AgentCard.d.ts +26 -0
- package/dashboard/dist/components/orchestration/AgentCard.js +60 -0
- package/dashboard/dist/components/orchestration/AgentCard.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentCard.test.js +63 -0
- package/dashboard/dist/components/orchestration/AgentControls.d.ts +11 -0
- package/dashboard/dist/components/orchestration/AgentControls.js +20 -0
- package/dashboard/dist/components/orchestration/AgentControls.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentControls.test.js +52 -0
- package/dashboard/dist/components/orchestration/AgentDetail.d.ts +35 -0
- package/dashboard/dist/components/orchestration/AgentDetail.js +37 -0
- package/dashboard/dist/components/orchestration/AgentDetail.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentDetail.test.js +79 -0
- package/dashboard/dist/components/orchestration/AgentList.d.ts +31 -0
- package/dashboard/dist/components/orchestration/AgentList.js +47 -0
- package/dashboard/dist/components/orchestration/AgentList.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentList.test.js +64 -0
- package/dashboard/dist/components/orchestration/CostMeter.d.ts +11 -0
- package/dashboard/dist/components/orchestration/CostMeter.js +28 -0
- package/dashboard/dist/components/orchestration/CostMeter.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/CostMeter.test.js +50 -0
- package/dashboard/dist/components/orchestration/ModelSelector.d.ts +20 -0
- package/dashboard/dist/components/orchestration/ModelSelector.js +12 -0
- package/dashboard/dist/components/orchestration/ModelSelector.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/ModelSelector.test.js +56 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.d.ts +28 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.js +28 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.js +56 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.d.ts +11 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.js +37 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.test.js +52 -0
- package/dashboard/dist/components/orchestration/index.d.ts +8 -0
- package/dashboard/dist/components/orchestration/index.js +8 -0
- package/package.json +1 -1
- package/server/lib/access-control.js +352 -0
- package/server/lib/access-control.test.js +322 -0
- package/server/lib/agents-cancel-command.js +139 -0
- package/server/lib/agents-cancel-command.test.js +180 -0
- package/server/lib/agents-get-command.js +159 -0
- package/server/lib/agents-get-command.test.js +167 -0
- package/server/lib/agents-list-command.js +150 -0
- package/server/lib/agents-list-command.test.js +149 -0
- package/server/lib/agents-logs-command.js +126 -0
- package/server/lib/agents-logs-command.test.js +198 -0
- package/server/lib/agents-retry-command.js +117 -0
- package/server/lib/agents-retry-command.test.js +192 -0
- package/server/lib/budget-limits.js +222 -0
- package/server/lib/budget-limits.test.js +214 -0
- package/server/lib/code-generator.js +291 -0
- package/server/lib/code-generator.test.js +307 -0
- package/server/lib/cost-command.js +290 -0
- package/server/lib/cost-command.test.js +202 -0
- package/server/lib/cost-optimizer.js +404 -0
- package/server/lib/cost-optimizer.test.js +232 -0
- package/server/lib/cost-projections.js +302 -0
- package/server/lib/cost-projections.test.js +217 -0
- package/server/lib/cost-reports.js +277 -0
- package/server/lib/cost-reports.test.js +254 -0
- package/server/lib/cost-tracker.js +216 -0
- package/server/lib/cost-tracker.test.js +302 -0
- package/server/lib/crypto-patterns.js +433 -0
- package/server/lib/crypto-patterns.test.js +346 -0
- package/server/lib/design-command.js +385 -0
- package/server/lib/design-command.test.js +249 -0
- package/server/lib/design-parser.js +237 -0
- package/server/lib/design-parser.test.js +290 -0
- package/server/lib/gemini-vision.js +377 -0
- package/server/lib/gemini-vision.test.js +282 -0
- package/server/lib/input-validator.js +360 -0
- package/server/lib/input-validator.test.js +295 -0
- package/server/lib/litellm-client.js +232 -0
- package/server/lib/litellm-client.test.js +267 -0
- package/server/lib/litellm-command.js +291 -0
- package/server/lib/litellm-command.test.js +260 -0
- package/server/lib/litellm-config.js +273 -0
- package/server/lib/litellm-config.test.js +212 -0
- package/server/lib/model-pricing.js +189 -0
- package/server/lib/model-pricing.test.js +178 -0
- package/server/lib/models-command.js +223 -0
- package/server/lib/models-command.test.js +193 -0
- package/server/lib/optimize-command.js +197 -0
- package/server/lib/optimize-command.test.js +193 -0
- package/server/lib/orchestration-integration.js +206 -0
- package/server/lib/orchestration-integration.test.js +235 -0
- package/server/lib/output-encoder.js +308 -0
- package/server/lib/output-encoder.test.js +312 -0
- package/server/lib/quality-evaluator.js +396 -0
- package/server/lib/quality-evaluator.test.js +337 -0
- package/server/lib/quality-gate-command.js +340 -0
- package/server/lib/quality-gate-command.test.js +321 -0
- package/server/lib/quality-gate-scorer.js +378 -0
- package/server/lib/quality-gate-scorer.test.js +376 -0
- package/server/lib/quality-history.js +265 -0
- package/server/lib/quality-history.test.js +359 -0
- package/server/lib/quality-presets.js +288 -0
- package/server/lib/quality-presets.test.js +269 -0
- package/server/lib/quality-retry.js +323 -0
- package/server/lib/quality-retry.test.js +325 -0
- package/server/lib/quality-thresholds.js +255 -0
- package/server/lib/quality-thresholds.test.js +237 -0
- package/server/lib/secure-auth.js +333 -0
- package/server/lib/secure-auth.test.js +288 -0
- package/server/lib/secure-code-command.js +540 -0
- package/server/lib/secure-code-command.test.js +309 -0
- package/server/lib/secure-errors.js +521 -0
- package/server/lib/secure-errors.test.js +298 -0
- package/server/lib/vision-command.js +372 -0
- package/server/lib/vision-command.test.js +255 -0
- package/server/lib/visual-command.js +350 -0
- package/server/lib/visual-command.test.js +256 -0
- package/server/lib/visual-testing.js +315 -0
- package/server/lib/visual-testing.test.js +357 -0
- package/server/package-lock.json +2 -2
- package/server/package.json +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents Retry Command
|
|
3
|
+
* Retry failed agents
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if agent can be retried
|
|
8
|
+
* @param {object} agent - Agent to check
|
|
9
|
+
* @returns {object} Result with canRetry and reason
|
|
10
|
+
*/
|
|
11
|
+
function canRetry(agent) {
|
|
12
|
+
if (!agent) {
|
|
13
|
+
return { canRetry: false, reason: 'Agent not found' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (agent.status === 'failed' || agent.status === 'cancelled') {
|
|
17
|
+
return { canRetry: true };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
canRetry: false,
|
|
22
|
+
reason: `Cannot retry agent with status '${agent.status}'. Only failed or cancelled agents can be retried.`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get retry context from failed agent
|
|
28
|
+
* @param {object} agent - Failed agent
|
|
29
|
+
* @returns {string} Context string
|
|
30
|
+
*/
|
|
31
|
+
function getRetryContext(agent) {
|
|
32
|
+
const parts = [];
|
|
33
|
+
|
|
34
|
+
if (agent.error?.message) {
|
|
35
|
+
parts.push(`Previous error: ${agent.error.message}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (agent.retryCount) {
|
|
39
|
+
parts.push(`Previous retries: ${agent.retryCount}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return parts.join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create retry agent from failed agent
|
|
47
|
+
* @param {object} parent - Parent agent that failed
|
|
48
|
+
* @param {object} options - Retry options
|
|
49
|
+
* @returns {object} New agent data
|
|
50
|
+
*/
|
|
51
|
+
function createRetryAgent(parent, options = {}) {
|
|
52
|
+
return {
|
|
53
|
+
parentId: parent.id,
|
|
54
|
+
model: options.model || parent.model,
|
|
55
|
+
prompt: parent.prompt,
|
|
56
|
+
status: 'queued',
|
|
57
|
+
startTime: new Date(),
|
|
58
|
+
tokens: { input: 0, output: 0 },
|
|
59
|
+
cost: 0,
|
|
60
|
+
retryCount: (parent.retryCount || 0) + 1,
|
|
61
|
+
retryContext: getRetryContext(parent),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Execute agents retry command
|
|
67
|
+
* @param {object} context - Execution context
|
|
68
|
+
* @returns {Promise<object>} Command result
|
|
69
|
+
*/
|
|
70
|
+
async function execute(context) {
|
|
71
|
+
const { registry, agentId, options = {} } = context;
|
|
72
|
+
|
|
73
|
+
const agent = registry.getAgent(agentId);
|
|
74
|
+
|
|
75
|
+
if (!agent) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: `Agent not found: ${agentId}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { canRetry: allowed, reason } = canRetry(agent);
|
|
83
|
+
|
|
84
|
+
if (!allowed) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
error: reason,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check budget
|
|
92
|
+
if (registry.budget && registry.budget.remaining <= 0) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
error: 'Budget exhausted. Cannot retry.',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Create new agent
|
|
100
|
+
const retryData = createRetryAgent(agent, options);
|
|
101
|
+
const newAgent = registry.createAgent(retryData);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
newAgentId: newAgent.id,
|
|
106
|
+
parentId: agent.id,
|
|
107
|
+
model: newAgent.model,
|
|
108
|
+
message: `Created retry agent ${newAgent.id} (from ${agent.id})`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
execute,
|
|
114
|
+
createRetryAgent,
|
|
115
|
+
canRetry,
|
|
116
|
+
getRetryContext,
|
|
117
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const { describe, it, beforeEach, mock } = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const {
|
|
4
|
+
execute,
|
|
5
|
+
createRetryAgent,
|
|
6
|
+
canRetry,
|
|
7
|
+
getRetryContext,
|
|
8
|
+
} = require('./agents-retry-command.js');
|
|
9
|
+
|
|
10
|
+
describe('agents-retry-command', () => {
|
|
11
|
+
const failedAgent = {
|
|
12
|
+
id: 'agent-failed',
|
|
13
|
+
name: 'Failed Builder',
|
|
14
|
+
model: 'gpt-4',
|
|
15
|
+
status: 'failed',
|
|
16
|
+
prompt: 'Build the feature',
|
|
17
|
+
error: { message: 'Rate limit exceeded' },
|
|
18
|
+
cost: 0.10,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('execute', () => {
|
|
22
|
+
const createMockRegistry = (agents = [], budget = { remaining: 100 }) => ({
|
|
23
|
+
getAgent: (id) => agents.find(a => a.id === id),
|
|
24
|
+
createAgent: (data) => {
|
|
25
|
+
const newAgent = { id: `agent-${Date.now()}`, ...data };
|
|
26
|
+
agents.push(newAgent);
|
|
27
|
+
return newAgent;
|
|
28
|
+
},
|
|
29
|
+
budget,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('retries failed agent', async () => {
|
|
33
|
+
const agents = [{ ...failedAgent }];
|
|
34
|
+
const result = await execute({
|
|
35
|
+
registry: createMockRegistry(agents),
|
|
36
|
+
agentId: 'agent-failed',
|
|
37
|
+
});
|
|
38
|
+
assert.ok(result.success);
|
|
39
|
+
assert.ok(result.newAgentId);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('creates new agent', async () => {
|
|
43
|
+
const agents = [{ ...failedAgent }];
|
|
44
|
+
const registry = createMockRegistry(agents);
|
|
45
|
+
const result = await execute({
|
|
46
|
+
registry,
|
|
47
|
+
agentId: 'agent-failed',
|
|
48
|
+
});
|
|
49
|
+
assert.strictEqual(agents.length, 2);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('includes failure context', async () => {
|
|
53
|
+
const agents = [{ ...failedAgent }];
|
|
54
|
+
const registry = createMockRegistry(agents);
|
|
55
|
+
const result = await execute({
|
|
56
|
+
registry,
|
|
57
|
+
agentId: 'agent-failed',
|
|
58
|
+
});
|
|
59
|
+
const newAgent = agents.find(a => a.id === result.newAgentId);
|
|
60
|
+
assert.ok(newAgent.retryContext || newAgent.parentId);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('with --model overrides', async () => {
|
|
64
|
+
const agents = [{ ...failedAgent }];
|
|
65
|
+
const registry = createMockRegistry(agents);
|
|
66
|
+
const result = await execute({
|
|
67
|
+
registry,
|
|
68
|
+
agentId: 'agent-failed',
|
|
69
|
+
options: { model: 'claude-3-opus' },
|
|
70
|
+
});
|
|
71
|
+
const newAgent = agents.find(a => a.id === result.newAgentId);
|
|
72
|
+
assert.strictEqual(newAgent.model, 'claude-3-opus');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('handles not failed agent', async () => {
|
|
76
|
+
const agents = [{ id: 'agent-1', status: 'running' }];
|
|
77
|
+
const result = await execute({
|
|
78
|
+
registry: createMockRegistry(agents),
|
|
79
|
+
agentId: 'agent-1',
|
|
80
|
+
});
|
|
81
|
+
assert.strictEqual(result.success, false);
|
|
82
|
+
assert.ok(result.error.includes('not failed') || result.error.includes('Cannot retry'));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('handles unknown agent ID', async () => {
|
|
86
|
+
const result = await execute({
|
|
87
|
+
registry: createMockRegistry([]),
|
|
88
|
+
agentId: 'unknown',
|
|
89
|
+
});
|
|
90
|
+
assert.strictEqual(result.success, false);
|
|
91
|
+
assert.ok(result.error.includes('not found'));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('shows new agent ID', async () => {
|
|
95
|
+
const agents = [{ ...failedAgent }];
|
|
96
|
+
const result = await execute({
|
|
97
|
+
registry: createMockRegistry(agents),
|
|
98
|
+
agentId: 'agent-failed',
|
|
99
|
+
});
|
|
100
|
+
assert.ok(result.newAgentId);
|
|
101
|
+
assert.ok(result.message.includes(result.newAgentId));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('tracks retry parent', async () => {
|
|
105
|
+
const agents = [{ ...failedAgent }];
|
|
106
|
+
const registry = createMockRegistry(agents);
|
|
107
|
+
const result = await execute({
|
|
108
|
+
registry,
|
|
109
|
+
agentId: 'agent-failed',
|
|
110
|
+
});
|
|
111
|
+
const newAgent = agents.find(a => a.id === result.newAgentId);
|
|
112
|
+
assert.strictEqual(newAgent.parentId, 'agent-failed');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('inherits original prompt', async () => {
|
|
116
|
+
const agents = [{ ...failedAgent }];
|
|
117
|
+
const registry = createMockRegistry(agents);
|
|
118
|
+
const result = await execute({
|
|
119
|
+
registry,
|
|
120
|
+
agentId: 'agent-failed',
|
|
121
|
+
});
|
|
122
|
+
const newAgent = agents.find(a => a.id === result.newAgentId);
|
|
123
|
+
assert.strictEqual(newAgent.prompt, 'Build the feature');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('respects budget limits', async () => {
|
|
127
|
+
const agents = [{ ...failedAgent }];
|
|
128
|
+
const result = await execute({
|
|
129
|
+
registry: createMockRegistry(agents, { remaining: 0 }),
|
|
130
|
+
agentId: 'agent-failed',
|
|
131
|
+
});
|
|
132
|
+
assert.strictEqual(result.success, false);
|
|
133
|
+
assert.ok(result.error.includes('budget') || result.error.includes('Budget'));
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('createRetryAgent', () => {
|
|
138
|
+
it('creates agent with parent reference', () => {
|
|
139
|
+
const parent = { id: 'parent-1', model: 'gpt-4', prompt: 'test' };
|
|
140
|
+
const agent = createRetryAgent(parent, {});
|
|
141
|
+
assert.strictEqual(agent.parentId, 'parent-1');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('uses parent model by default', () => {
|
|
145
|
+
const parent = { id: 'parent-1', model: 'gpt-4', prompt: 'test' };
|
|
146
|
+
const agent = createRetryAgent(parent, {});
|
|
147
|
+
assert.strictEqual(agent.model, 'gpt-4');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('allows model override', () => {
|
|
151
|
+
const parent = { id: 'parent-1', model: 'gpt-4', prompt: 'test' };
|
|
152
|
+
const agent = createRetryAgent(parent, { model: 'claude' });
|
|
153
|
+
assert.strictEqual(agent.model, 'claude');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('canRetry', () => {
|
|
158
|
+
it('returns true for failed agent', () => {
|
|
159
|
+
const result = canRetry({ status: 'failed' });
|
|
160
|
+
assert.ok(result.canRetry);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('returns true for cancelled agent', () => {
|
|
164
|
+
const result = canRetry({ status: 'cancelled' });
|
|
165
|
+
assert.ok(result.canRetry);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('returns false for running agent', () => {
|
|
169
|
+
const result = canRetry({ status: 'running' });
|
|
170
|
+
assert.strictEqual(result.canRetry, false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('returns false for completed agent', () => {
|
|
174
|
+
const result = canRetry({ status: 'completed' });
|
|
175
|
+
assert.strictEqual(result.canRetry, false);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('getRetryContext', () => {
|
|
180
|
+
it('includes error message', () => {
|
|
181
|
+
const agent = { error: { message: 'Rate limit' } };
|
|
182
|
+
const context = getRetryContext(agent);
|
|
183
|
+
assert.ok(context.includes('Rate limit'));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('includes retry count', () => {
|
|
187
|
+
const agent = { retryCount: 2 };
|
|
188
|
+
const context = getRetryContext(agent);
|
|
189
|
+
assert.ok(context.includes('2'));
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget Limits Module
|
|
3
|
+
*
|
|
4
|
+
* Configurable budget limits with enforcement
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a budget manager instance
|
|
9
|
+
* @returns {Object} Budget manager
|
|
10
|
+
*/
|
|
11
|
+
function createBudgetManager() {
|
|
12
|
+
return {
|
|
13
|
+
daily: null,
|
|
14
|
+
monthly: null,
|
|
15
|
+
byModel: {},
|
|
16
|
+
warnThresholds: [0.5, 0.8],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Set a budget limit
|
|
22
|
+
* @param {Object} manager - Budget manager instance
|
|
23
|
+
* @param {Object} options - Budget options
|
|
24
|
+
* @param {string} options.type - Budget type: 'daily', 'monthly', or 'model'
|
|
25
|
+
* @param {number} options.limit - Budget limit in dollars
|
|
26
|
+
* @param {string} [options.model] - Model name (for model budgets)
|
|
27
|
+
* @param {number[]} [options.warnAt] - Warning thresholds (e.g., [0.5, 0.8])
|
|
28
|
+
*/
|
|
29
|
+
function setBudget(manager, options) {
|
|
30
|
+
const { type, limit, model, warnAt } = options;
|
|
31
|
+
|
|
32
|
+
if (type === 'daily') {
|
|
33
|
+
manager.daily = limit;
|
|
34
|
+
} else if (type === 'monthly') {
|
|
35
|
+
manager.monthly = limit;
|
|
36
|
+
} else if (type === 'model' && model) {
|
|
37
|
+
manager.byModel[model] = limit;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (warnAt) {
|
|
41
|
+
manager.warnThresholds = warnAt;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check budget status
|
|
47
|
+
* @param {Object} manager - Budget manager instance
|
|
48
|
+
* @param {Object} options - Check options
|
|
49
|
+
* @param {number} options.currentSpend - Current spend amount
|
|
50
|
+
* @param {string} [options.type='daily'] - Budget type to check
|
|
51
|
+
* @param {string} [options.model] - Model to check (for model budgets)
|
|
52
|
+
* @returns {Object} Status result
|
|
53
|
+
*/
|
|
54
|
+
function checkBudget(manager, options) {
|
|
55
|
+
const { currentSpend, type = 'daily', model } = options;
|
|
56
|
+
|
|
57
|
+
let limit;
|
|
58
|
+
if (type === 'model' && model) {
|
|
59
|
+
limit = manager.byModel[model];
|
|
60
|
+
} else if (type === 'monthly') {
|
|
61
|
+
limit = manager.monthly;
|
|
62
|
+
} else {
|
|
63
|
+
limit = manager.daily;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (limit === null || limit === undefined) {
|
|
67
|
+
return { status: 'ok', message: 'No budget set' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const percentage = currentSpend / limit;
|
|
71
|
+
|
|
72
|
+
if (percentage >= 1) {
|
|
73
|
+
return {
|
|
74
|
+
status: 'exceeded',
|
|
75
|
+
message: `Budget exceeded: $${currentSpend.toFixed(2)} / $${limit.toFixed(2)}`,
|
|
76
|
+
percentage: percentage * 100,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check warning thresholds (in reverse order to get highest matching)
|
|
81
|
+
for (let i = manager.warnThresholds.length - 1; i >= 0; i--) {
|
|
82
|
+
const threshold = manager.warnThresholds[i];
|
|
83
|
+
if (percentage >= threshold) {
|
|
84
|
+
return {
|
|
85
|
+
status: 'warning',
|
|
86
|
+
message: `${Math.round(threshold * 100)}% of budget used: $${currentSpend.toFixed(2)} / $${limit.toFixed(2)}`,
|
|
87
|
+
percentage: percentage * 100,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
status: 'ok',
|
|
94
|
+
message: `Under budget: $${currentSpend.toFixed(2)} / $${limit.toFixed(2)}`,
|
|
95
|
+
percentage: percentage * 100,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Enforce budget (determine if operation should proceed)
|
|
101
|
+
* @param {Object} manager - Budget manager instance
|
|
102
|
+
* @param {Object} options - Enforcement options
|
|
103
|
+
* @param {number} options.currentSpend - Current spend
|
|
104
|
+
* @param {number} options.projectedCost - Cost of planned operation
|
|
105
|
+
* @param {boolean} [options.override] - Admin override flag
|
|
106
|
+
* @param {string} [options.type='daily'] - Budget type
|
|
107
|
+
* @returns {Object} Enforcement result
|
|
108
|
+
*/
|
|
109
|
+
function enforceBudget(manager, options) {
|
|
110
|
+
const { currentSpend, projectedCost, override, type = 'daily' } = options;
|
|
111
|
+
|
|
112
|
+
if (override) {
|
|
113
|
+
return { allowed: true, reason: 'Admin override' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let limit;
|
|
117
|
+
if (type === 'monthly') {
|
|
118
|
+
limit = manager.monthly;
|
|
119
|
+
} else {
|
|
120
|
+
limit = manager.daily;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (limit === null || limit === undefined) {
|
|
124
|
+
return { allowed: true, reason: 'No budget set' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const totalAfter = currentSpend + projectedCost;
|
|
128
|
+
|
|
129
|
+
if (currentSpend >= limit) {
|
|
130
|
+
return {
|
|
131
|
+
allowed: false,
|
|
132
|
+
reason: `Daily budget exceeded: $${currentSpend.toFixed(2)} >= $${limit.toFixed(2)}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (totalAfter > limit) {
|
|
137
|
+
return {
|
|
138
|
+
allowed: false,
|
|
139
|
+
reason: `Operation would exceed budget: $${totalAfter.toFixed(2)} > $${limit.toFixed(2)}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { allowed: true, reason: 'Within budget' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get daily budget limit
|
|
148
|
+
* @param {Object} manager - Budget manager instance
|
|
149
|
+
* @returns {number|null} Daily budget or null if not set
|
|
150
|
+
*/
|
|
151
|
+
function getDailyBudget(manager) {
|
|
152
|
+
return manager.daily;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get monthly budget limit
|
|
157
|
+
* @param {Object} manager - Budget manager instance
|
|
158
|
+
* @returns {number|null} Monthly budget or null if not set
|
|
159
|
+
*/
|
|
160
|
+
function getMonthlyBudget(manager) {
|
|
161
|
+
return manager.monthly;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get budget for a specific model
|
|
166
|
+
* @param {Object} manager - Budget manager instance
|
|
167
|
+
* @param {string} model - Model name
|
|
168
|
+
* @returns {number|null} Model budget or null if not set
|
|
169
|
+
*/
|
|
170
|
+
function getModelBudget(manager, model) {
|
|
171
|
+
return manager.byModel[model] || null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Reset budget spend tracking
|
|
176
|
+
* @param {Object} manager - Budget manager instance
|
|
177
|
+
* @param {Object} options - Reset options
|
|
178
|
+
* @param {string} options.type - Budget type to reset
|
|
179
|
+
*/
|
|
180
|
+
function resetBudget(manager, options) {
|
|
181
|
+
// Note: The tracker handles actual spend tracking
|
|
182
|
+
// This is mainly for admin operations
|
|
183
|
+
const { type } = options;
|
|
184
|
+
|
|
185
|
+
// Budgets themselves remain, just the tracking resets
|
|
186
|
+
// This is handled by the cost tracker, not here
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Calculate remaining budget
|
|
191
|
+
* @param {Object} manager - Budget manager instance
|
|
192
|
+
* @param {Object} options - Options with current spend
|
|
193
|
+
* @param {number} options.currentSpend - Current spend amount
|
|
194
|
+
* @returns {Object} Remaining amounts
|
|
195
|
+
*/
|
|
196
|
+
function budgetRemaining(manager, options) {
|
|
197
|
+
const { currentSpend } = options;
|
|
198
|
+
|
|
199
|
+
const result = {};
|
|
200
|
+
|
|
201
|
+
if (manager.daily !== null) {
|
|
202
|
+
result.daily = Math.max(0, manager.daily - currentSpend);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (manager.monthly !== null) {
|
|
206
|
+
result.monthly = Math.max(0, manager.monthly - currentSpend);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
createBudgetManager,
|
|
214
|
+
setBudget,
|
|
215
|
+
checkBudget,
|
|
216
|
+
enforceBudget,
|
|
217
|
+
getDailyBudget,
|
|
218
|
+
getMonthlyBudget,
|
|
219
|
+
getModelBudget,
|
|
220
|
+
resetBudget,
|
|
221
|
+
budgetRemaining,
|
|
222
|
+
};
|