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,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost Command Tests
|
|
3
|
+
*
|
|
4
|
+
* CLI for cost management
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { describe, it, beforeEach } = require('node:test');
|
|
8
|
+
const assert = require('node:assert');
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
CostCommand,
|
|
12
|
+
parseArgs,
|
|
13
|
+
formatStatus,
|
|
14
|
+
} = require('./cost-command.js');
|
|
15
|
+
|
|
16
|
+
describe('Cost Command', () => {
|
|
17
|
+
let command;
|
|
18
|
+
let mockTracker;
|
|
19
|
+
let mockPricing;
|
|
20
|
+
let mockBudget;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
mockTracker = {
|
|
24
|
+
getDailyCost: () => 5.00,
|
|
25
|
+
getMonthlyCost: () => 25.00,
|
|
26
|
+
getCostByModel: () => ({ 'claude-3-opus': 15.00, 'gpt-4': 10.00 }),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
mockPricing = {
|
|
30
|
+
getPricing: (model) => ({ inputPer1kTokens: 0.01, outputPer1kTokens: 0.03 }),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
mockBudget = {
|
|
34
|
+
getDailyBudget: () => 10.00,
|
|
35
|
+
getMonthlyBudget: () => 100.00,
|
|
36
|
+
budgetRemaining: () => ({ daily: 5.00, monthly: 75.00 }),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
command = new CostCommand({
|
|
40
|
+
tracker: mockTracker,
|
|
41
|
+
pricing: mockPricing,
|
|
42
|
+
budget: mockBudget,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('execute status', () => {
|
|
47
|
+
it('shows spend summary', async () => {
|
|
48
|
+
const result = await command.execute('status');
|
|
49
|
+
|
|
50
|
+
assert.ok(result.output);
|
|
51
|
+
assert.ok(result.output.includes('5.00') || result.output.includes('$5'));
|
|
52
|
+
assert.ok(result.output.includes('25.00') || result.output.includes('$25'));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('shows budget remaining', async () => {
|
|
56
|
+
const result = await command.execute('status');
|
|
57
|
+
|
|
58
|
+
assert.ok(result.output.includes('remaining') || result.output.includes('left'));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('execute budget', () => {
|
|
63
|
+
it('sets daily limit', async () => {
|
|
64
|
+
let setBudgetCalled = false;
|
|
65
|
+
mockBudget.setBudget = ({ type, limit }) => {
|
|
66
|
+
if (type === 'daily' && limit === 20.00) {
|
|
67
|
+
setBudgetCalled = true;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const result = await command.execute('budget --daily 20.00');
|
|
72
|
+
|
|
73
|
+
assert.ok(setBudgetCalled);
|
|
74
|
+
assert.ok(result.success);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('sets monthly limit', async () => {
|
|
78
|
+
let setBudgetCalled = false;
|
|
79
|
+
mockBudget.setBudget = ({ type, limit }) => {
|
|
80
|
+
if (type === 'monthly' && limit === 200.00) {
|
|
81
|
+
setBudgetCalled = true;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const result = await command.execute('budget --monthly 200.00');
|
|
86
|
+
|
|
87
|
+
assert.ok(setBudgetCalled);
|
|
88
|
+
assert.ok(result.success);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('execute report', () => {
|
|
93
|
+
it('generates report', async () => {
|
|
94
|
+
mockTracker.getRecords = () => [
|
|
95
|
+
{ date: '2025-01-15', model: 'claude-3-opus', cost: 1.00 },
|
|
96
|
+
{ date: '2025-01-16', model: 'gpt-4', cost: 0.50 },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const result = await command.execute('report');
|
|
100
|
+
|
|
101
|
+
assert.ok(result.output);
|
|
102
|
+
assert.ok(result.report);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('filters by period', async () => {
|
|
106
|
+
let filterStart, filterEnd;
|
|
107
|
+
mockTracker.getRecords = (options) => {
|
|
108
|
+
filterStart = options?.startDate;
|
|
109
|
+
filterEnd = options?.endDate;
|
|
110
|
+
return [];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
await command.execute('report --from 2025-01-01 --to 2025-01-31');
|
|
114
|
+
|
|
115
|
+
assert.strictEqual(filterStart, '2025-01-01');
|
|
116
|
+
assert.strictEqual(filterEnd, '2025-01-31');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('execute estimate', () => {
|
|
121
|
+
it('projects cost for task', async () => {
|
|
122
|
+
const result = await command.execute('estimate "Write a sorting function"');
|
|
123
|
+
|
|
124
|
+
assert.ok(result.output);
|
|
125
|
+
assert.ok(result.estimate);
|
|
126
|
+
assert.ok(result.estimate.estimatedCost >= 0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('compares models', async () => {
|
|
130
|
+
const result = await command.execute('estimate "Write a sorting function" --compare');
|
|
131
|
+
|
|
132
|
+
assert.ok(result.output);
|
|
133
|
+
assert.ok(result.comparison);
|
|
134
|
+
assert.ok(Array.isArray(result.comparison));
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('execute optimize', () => {
|
|
139
|
+
it('shows suggestions', async () => {
|
|
140
|
+
mockTracker.getRecords = () => [
|
|
141
|
+
{ model: 'claude-3-opus', operation: 'chat', cost: 5.00 },
|
|
142
|
+
{ model: 'claude-3-opus', operation: 'chat', cost: 3.00 },
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const result = await command.execute('optimize');
|
|
146
|
+
|
|
147
|
+
assert.ok(result.output);
|
|
148
|
+
assert.ok(result.suggestions);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('formatStatus', () => {
|
|
153
|
+
it('creates readable output', () => {
|
|
154
|
+
const status = {
|
|
155
|
+
dailySpend: 5.00,
|
|
156
|
+
monthlySpend: 25.00,
|
|
157
|
+
dailyBudget: 10.00,
|
|
158
|
+
monthlyBudget: 100.00,
|
|
159
|
+
dailyRemaining: 5.00,
|
|
160
|
+
monthlyRemaining: 75.00,
|
|
161
|
+
byModel: { 'claude-3-opus': 15.00, 'gpt-4': 10.00 },
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const formatted = formatStatus(status);
|
|
165
|
+
|
|
166
|
+
assert.ok(typeof formatted === 'string');
|
|
167
|
+
assert.ok(formatted.includes('$'));
|
|
168
|
+
assert.ok(formatted.includes('claude-3-opus') || formatted.includes('Model'));
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('parseArgs', () => {
|
|
173
|
+
it('parses status command', () => {
|
|
174
|
+
const parsed = parseArgs('status');
|
|
175
|
+
|
|
176
|
+
assert.strictEqual(parsed.command, 'status');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('parses budget with flags', () => {
|
|
180
|
+
const parsed = parseArgs('budget --daily 20.00 --monthly 200.00');
|
|
181
|
+
|
|
182
|
+
assert.strictEqual(parsed.command, 'budget');
|
|
183
|
+
assert.strictEqual(parsed.daily, 20.00);
|
|
184
|
+
assert.strictEqual(parsed.monthly, 200.00);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('parses report with date range', () => {
|
|
188
|
+
const parsed = parseArgs('report --from 2025-01-01 --to 2025-01-31');
|
|
189
|
+
|
|
190
|
+
assert.strictEqual(parsed.command, 'report');
|
|
191
|
+
assert.strictEqual(parsed.from, '2025-01-01');
|
|
192
|
+
assert.strictEqual(parsed.to, '2025-01-31');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('parses estimate with prompt', () => {
|
|
196
|
+
const parsed = parseArgs('estimate "Write a function"');
|
|
197
|
+
|
|
198
|
+
assert.strictEqual(parsed.command, 'estimate');
|
|
199
|
+
assert.strictEqual(parsed.prompt, 'Write a function');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost Optimizer Module
|
|
3
|
+
*
|
|
4
|
+
* Recommend cheaper alternatives and optimization strategies
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { getPricing } = require('./model-pricing.js');
|
|
8
|
+
|
|
9
|
+
// Model quality ratings (0-100)
|
|
10
|
+
const MODEL_QUALITY = {
|
|
11
|
+
'claude-3-opus': 95,
|
|
12
|
+
'claude-opus-4-5-20251101': 98,
|
|
13
|
+
'claude-3-sonnet': 85,
|
|
14
|
+
'claude-3.5-sonnet': 90,
|
|
15
|
+
'claude-3-haiku': 70,
|
|
16
|
+
'gpt-4': 90,
|
|
17
|
+
'gpt-4-turbo': 88,
|
|
18
|
+
'gpt-4o': 85,
|
|
19
|
+
'gpt-3.5-turbo': 65,
|
|
20
|
+
'o1': 92,
|
|
21
|
+
'o3': 94,
|
|
22
|
+
'deepseek-r1': 80,
|
|
23
|
+
'deepseek-chat': 72,
|
|
24
|
+
'deepseek-coder': 75,
|
|
25
|
+
'gemini-2.0-flash': 75,
|
|
26
|
+
'gemini-1.5-pro': 85,
|
|
27
|
+
'gemini-1.5-flash': 70,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Model cost ratings (0-100, higher = cheaper)
|
|
31
|
+
const MODEL_COST_SCORE = {
|
|
32
|
+
'claude-3-opus': 20,
|
|
33
|
+
'claude-opus-4-5-20251101': 20,
|
|
34
|
+
'claude-3-sonnet': 60,
|
|
35
|
+
'claude-3.5-sonnet': 60,
|
|
36
|
+
'claude-3-haiku': 95,
|
|
37
|
+
'gpt-4': 15,
|
|
38
|
+
'gpt-4-turbo': 40,
|
|
39
|
+
'gpt-4o': 55,
|
|
40
|
+
'gpt-3.5-turbo': 90,
|
|
41
|
+
'o1': 30,
|
|
42
|
+
'o3': 30,
|
|
43
|
+
'deepseek-r1': 85,
|
|
44
|
+
'deepseek-chat': 95,
|
|
45
|
+
'deepseek-coder': 95,
|
|
46
|
+
'gemini-2.0-flash': 100,
|
|
47
|
+
'gemini-1.5-pro': 75,
|
|
48
|
+
'gemini-1.5-flash': 95,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Task type to minimum quality mapping
|
|
52
|
+
const TASK_QUALITY_REQUIREMENTS = {
|
|
53
|
+
'simple-chat': 60,
|
|
54
|
+
'code-review': 80,
|
|
55
|
+
'code-gen': 85,
|
|
56
|
+
'refactor': 85,
|
|
57
|
+
'complex-reasoning': 90,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Model alternatives by provider
|
|
61
|
+
const MODEL_ALTERNATIVES = {
|
|
62
|
+
'claude-3-opus': ['claude-3.5-sonnet', 'claude-3-sonnet', 'claude-3-haiku'],
|
|
63
|
+
'claude-opus-4-5-20251101': ['claude-3.5-sonnet', 'claude-3-sonnet', 'claude-3-haiku'],
|
|
64
|
+
'claude-3-sonnet': ['claude-3-haiku'],
|
|
65
|
+
'claude-3.5-sonnet': ['claude-3-sonnet', 'claude-3-haiku'],
|
|
66
|
+
'claude-3-haiku': [],
|
|
67
|
+
'gpt-4': ['gpt-4-turbo', 'gpt-4o', 'gpt-3.5-turbo'],
|
|
68
|
+
'gpt-4-turbo': ['gpt-4o', 'gpt-3.5-turbo'],
|
|
69
|
+
'gpt-4o': ['gpt-3.5-turbo'],
|
|
70
|
+
'gpt-3.5-turbo': [],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create an optimizer instance
|
|
75
|
+
* @returns {Object} Optimizer instance
|
|
76
|
+
*/
|
|
77
|
+
function createOptimizer() {
|
|
78
|
+
return {
|
|
79
|
+
preferences: {
|
|
80
|
+
qualityWeight: 0.5,
|
|
81
|
+
costWeight: 0.5,
|
|
82
|
+
preferredProviders: [],
|
|
83
|
+
minQuality: 0,
|
|
84
|
+
},
|
|
85
|
+
choiceHistory: [],
|
|
86
|
+
getLearnedPreferences() {
|
|
87
|
+
return { ...this.preferences };
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Analyze usage patterns
|
|
94
|
+
* @param {Object} optimizer - Optimizer instance
|
|
95
|
+
* @param {Array} usage - Usage records
|
|
96
|
+
* @returns {Object} Analysis results
|
|
97
|
+
*/
|
|
98
|
+
function analyzeUsage(optimizer, usage) {
|
|
99
|
+
if (!usage || usage.length === 0) {
|
|
100
|
+
return {
|
|
101
|
+
expensiveOperations: [],
|
|
102
|
+
modelBreakdown: {},
|
|
103
|
+
totalCost: 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Sort by cost descending to find expensive operations
|
|
108
|
+
const expensiveOperations = [...usage]
|
|
109
|
+
.sort((a, b) => b.cost - a.cost);
|
|
110
|
+
|
|
111
|
+
// Group by model
|
|
112
|
+
const modelBreakdown = {};
|
|
113
|
+
for (const record of usage) {
|
|
114
|
+
modelBreakdown[record.model] = (modelBreakdown[record.model] || 0) + record.cost;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const totalCost = usage.reduce((sum, r) => sum + r.cost, 0);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
expensiveOperations,
|
|
121
|
+
modelBreakdown,
|
|
122
|
+
totalCost,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Suggest a cheaper model alternative
|
|
128
|
+
* @param {Object} optimizer - Optimizer instance
|
|
129
|
+
* @param {Object} options - Options
|
|
130
|
+
* @param {string} options.currentModel - Current model
|
|
131
|
+
* @param {string} options.taskType - Type of task
|
|
132
|
+
* @returns {Object|null} Suggestion or null
|
|
133
|
+
*/
|
|
134
|
+
function suggestCheaperModel(optimizer, options) {
|
|
135
|
+
const { currentModel, taskType } = options;
|
|
136
|
+
|
|
137
|
+
const minQuality = TASK_QUALITY_REQUIREMENTS[taskType] || 60;
|
|
138
|
+
const alternatives = MODEL_ALTERNATIVES[currentModel] || [];
|
|
139
|
+
|
|
140
|
+
const currentPricing = getPricing(currentModel);
|
|
141
|
+
const currentQuality = MODEL_QUALITY[currentModel] || 50;
|
|
142
|
+
|
|
143
|
+
// Find cheapest alternative that meets quality requirements
|
|
144
|
+
let bestAlternative = null;
|
|
145
|
+
let bestSavings = 0;
|
|
146
|
+
|
|
147
|
+
for (const alt of alternatives) {
|
|
148
|
+
const altQuality = MODEL_QUALITY[alt] || 50;
|
|
149
|
+
if (altQuality < minQuality) continue;
|
|
150
|
+
|
|
151
|
+
const altPricing = getPricing(alt);
|
|
152
|
+
if (!altPricing || !currentPricing) continue;
|
|
153
|
+
|
|
154
|
+
// Estimate savings based on average token usage
|
|
155
|
+
const avgTokens = 1000;
|
|
156
|
+
const currentCost = (avgTokens / 1000) * currentPricing.inputPer1kTokens +
|
|
157
|
+
(avgTokens / 1000) * currentPricing.outputPer1kTokens;
|
|
158
|
+
const altCost = (avgTokens / 1000) * altPricing.inputPer1kTokens +
|
|
159
|
+
(avgTokens / 1000) * altPricing.outputPer1kTokens;
|
|
160
|
+
|
|
161
|
+
const savings = currentCost - altCost;
|
|
162
|
+
|
|
163
|
+
if (savings > bestSavings) {
|
|
164
|
+
bestSavings = savings;
|
|
165
|
+
bestAlternative = alt;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!bestAlternative || bestSavings <= 0) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
alternativeModel: bestAlternative,
|
|
175
|
+
estimatedSavings: bestSavings,
|
|
176
|
+
qualityDelta: (MODEL_QUALITY[bestAlternative] || 50) - currentQuality,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Suggest batching for similar operations
|
|
182
|
+
* @param {Object} optimizer - Optimizer instance
|
|
183
|
+
* @param {Array} usage - Usage records
|
|
184
|
+
* @returns {Object} Batching suggestion
|
|
185
|
+
*/
|
|
186
|
+
function suggestBatching(optimizer, usage) {
|
|
187
|
+
if (!usage || usage.length < 3) {
|
|
188
|
+
return { batchable: false };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Group by operation
|
|
192
|
+
const operationCounts = {};
|
|
193
|
+
for (const record of usage) {
|
|
194
|
+
operationCounts[record.operation] = (operationCounts[record.operation] || 0) + 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Find operations that repeat at least 3 times
|
|
198
|
+
const batchableOps = Object.entries(operationCounts)
|
|
199
|
+
.filter(([, count]) => count >= 3)
|
|
200
|
+
.sort((a, b) => b[1] - a[1]);
|
|
201
|
+
|
|
202
|
+
if (batchableOps.length === 0) {
|
|
203
|
+
return { batchable: false };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const [operation, count] = batchableOps[0];
|
|
207
|
+
|
|
208
|
+
// Estimate savings from batching (roughly 20% reduction)
|
|
209
|
+
const opRecords = usage.filter(r => r.operation === operation);
|
|
210
|
+
const totalTokens = opRecords.reduce((sum, r) => sum + (r.inputTokens || 0), 0);
|
|
211
|
+
const estimatedSavings = totalTokens * 0.0001 * 0.2; // Rough estimate
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
batchable: true,
|
|
215
|
+
operation,
|
|
216
|
+
count,
|
|
217
|
+
estimatedSavings: Math.max(0.01, estimatedSavings),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Suggest caching for repeated prompts
|
|
223
|
+
* @param {Object} optimizer - Optimizer instance
|
|
224
|
+
* @param {Array} usage - Usage records with prompt hashes
|
|
225
|
+
* @returns {Object} Caching suggestion
|
|
226
|
+
*/
|
|
227
|
+
function suggestCaching(optimizer, usage) {
|
|
228
|
+
if (!usage || usage.length < 2) {
|
|
229
|
+
return { cacheable: false, repeatedPrompts: 0 };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Count prompt hash occurrences
|
|
233
|
+
const hashCounts = {};
|
|
234
|
+
for (const record of usage) {
|
|
235
|
+
if (record.hash) {
|
|
236
|
+
hashCounts[record.hash] = (hashCounts[record.hash] || 0) + 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Count prompts that appear more than once
|
|
241
|
+
const repeatedPrompts = Object.values(hashCounts)
|
|
242
|
+
.filter(count => count > 1)
|
|
243
|
+
.reduce((sum, count) => sum + count - 1, 0);
|
|
244
|
+
|
|
245
|
+
if (repeatedPrompts === 0) {
|
|
246
|
+
return { cacheable: false, repeatedPrompts: 0 };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
cacheable: true,
|
|
251
|
+
repeatedPrompts,
|
|
252
|
+
uniqueHashes: Object.keys(hashCounts).length,
|
|
253
|
+
potentialSavings: repeatedPrompts * 0.01, // Rough estimate
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get quality score for a model
|
|
259
|
+
* @param {string} model - Model name
|
|
260
|
+
* @returns {number} Quality score (0-100)
|
|
261
|
+
*/
|
|
262
|
+
function getQualityScore(model) {
|
|
263
|
+
return MODEL_QUALITY[model] || 50;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get cost efficiency score for a model
|
|
268
|
+
* @param {string} model - Model name
|
|
269
|
+
* @returns {number} Cost score (0-100, higher = cheaper)
|
|
270
|
+
*/
|
|
271
|
+
function getCostScore(model) {
|
|
272
|
+
return MODEL_COST_SCORE[model] || 50;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Rank models by value (quality/cost ratio)
|
|
277
|
+
* @param {Object} optimizer - Optimizer instance
|
|
278
|
+
* @param {string[]} models - Models to rank
|
|
279
|
+
* @returns {Array} Ranked models with scores
|
|
280
|
+
*/
|
|
281
|
+
function rankByValue(optimizer, models) {
|
|
282
|
+
const { qualityWeight, costWeight } = optimizer.preferences;
|
|
283
|
+
|
|
284
|
+
return models
|
|
285
|
+
.map(model => {
|
|
286
|
+
const qualityScore = getQualityScore(model);
|
|
287
|
+
const costScore = getCostScore(model);
|
|
288
|
+
const valueScore = (qualityScore * qualityWeight) + (costScore * costWeight);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
model,
|
|
292
|
+
qualityScore,
|
|
293
|
+
costScore,
|
|
294
|
+
valueScore,
|
|
295
|
+
};
|
|
296
|
+
})
|
|
297
|
+
.sort((a, b) => b.valueScore - a.valueScore);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Filter suggestions based on user preferences
|
|
302
|
+
* @param {Object} optimizer - Optimizer instance
|
|
303
|
+
* @param {Array} suggestions - Suggestions to filter
|
|
304
|
+
* @param {Object} preferences - User preferences
|
|
305
|
+
* @returns {Array} Filtered suggestions
|
|
306
|
+
*/
|
|
307
|
+
function applyPreferences(optimizer, suggestions, preferences) {
|
|
308
|
+
const { preferredProviders = [], minQuality = 0 } = preferences;
|
|
309
|
+
|
|
310
|
+
return suggestions.filter(suggestion => {
|
|
311
|
+
// Check provider preference
|
|
312
|
+
if (preferredProviders.length > 0) {
|
|
313
|
+
const isPreferredProvider = preferredProviders.some(provider => {
|
|
314
|
+
if (provider === 'anthropic') return suggestion.model.includes('claude');
|
|
315
|
+
if (provider === 'openai') return suggestion.model.includes('gpt') || suggestion.model.includes('o1') || suggestion.model.includes('o3');
|
|
316
|
+
if (provider === 'deepseek') return suggestion.model.includes('deepseek');
|
|
317
|
+
if (provider === 'google') return suggestion.model.includes('gemini');
|
|
318
|
+
return false;
|
|
319
|
+
});
|
|
320
|
+
if (!isPreferredProvider) return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Check minimum quality
|
|
324
|
+
const quality = getQualityScore(suggestion.model);
|
|
325
|
+
if (quality < minQuality) return false;
|
|
326
|
+
|
|
327
|
+
return true;
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Learn from user choices
|
|
333
|
+
* @param {Object} optimizer - Optimizer instance
|
|
334
|
+
* @param {Object} choice - User choice
|
|
335
|
+
* @param {string} choice.chosen - Chosen model
|
|
336
|
+
* @param {string[]} choice.alternatives - Alternative options
|
|
337
|
+
*/
|
|
338
|
+
function learnPreferences(optimizer, choice) {
|
|
339
|
+
const { chosen, alternatives } = choice;
|
|
340
|
+
|
|
341
|
+
optimizer.choiceHistory.push(choice);
|
|
342
|
+
|
|
343
|
+
// If user consistently chooses higher quality models, increase quality weight
|
|
344
|
+
const chosenQuality = getQualityScore(chosen);
|
|
345
|
+
const altQualities = alternatives.map(getQualityScore);
|
|
346
|
+
const avgAltQuality = altQualities.length > 0
|
|
347
|
+
? altQualities.reduce((a, b) => a + b, 0) / altQualities.length
|
|
348
|
+
: 0;
|
|
349
|
+
|
|
350
|
+
if (chosenQuality > avgAltQuality) {
|
|
351
|
+
// User prefers quality
|
|
352
|
+
optimizer.preferences.qualityWeight = Math.min(1, optimizer.preferences.qualityWeight + 0.1);
|
|
353
|
+
optimizer.preferences.costWeight = Math.max(0, optimizer.preferences.costWeight - 0.1);
|
|
354
|
+
} else if (chosenQuality < avgAltQuality) {
|
|
355
|
+
// User prefers cost
|
|
356
|
+
optimizer.preferences.costWeight = Math.min(1, optimizer.preferences.costWeight + 0.1);
|
|
357
|
+
optimizer.preferences.qualityWeight = Math.max(0, optimizer.preferences.qualityWeight - 0.1);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Format suggestions for display
|
|
363
|
+
* @param {Array} suggestions - Suggestions to format
|
|
364
|
+
* @returns {string} Formatted output
|
|
365
|
+
*/
|
|
366
|
+
function formatSuggestions(suggestions) {
|
|
367
|
+
if (!suggestions || suggestions.length === 0) {
|
|
368
|
+
return 'No optimization suggestions available.';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const lines = ['Cost Optimization Suggestions:', ''];
|
|
372
|
+
|
|
373
|
+
for (const suggestion of suggestions) {
|
|
374
|
+
if (suggestion.type === 'model') {
|
|
375
|
+
lines.push(`• Switch from ${suggestion.current} to ${suggestion.suggested}`);
|
|
376
|
+
lines.push(` Estimated savings: $${suggestion.savings.toFixed(2)}`);
|
|
377
|
+
} else if (suggestion.type === 'caching') {
|
|
378
|
+
lines.push(`• Enable response caching`);
|
|
379
|
+
lines.push(` Estimated savings: $${suggestion.savings.toFixed(2)}`);
|
|
380
|
+
} else if (suggestion.type === 'batching') {
|
|
381
|
+
lines.push(`• Batch similar operations`);
|
|
382
|
+
lines.push(` Estimated savings: $${suggestion.savings.toFixed(2)}`);
|
|
383
|
+
}
|
|
384
|
+
lines.push('');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return lines.join('\n');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
module.exports = {
|
|
391
|
+
createOptimizer,
|
|
392
|
+
analyzeUsage,
|
|
393
|
+
suggestCheaperModel,
|
|
394
|
+
suggestBatching,
|
|
395
|
+
suggestCaching,
|
|
396
|
+
getQualityScore,
|
|
397
|
+
getCostScore,
|
|
398
|
+
rankByValue,
|
|
399
|
+
applyPreferences,
|
|
400
|
+
learnPreferences,
|
|
401
|
+
formatSuggestions,
|
|
402
|
+
MODEL_QUALITY,
|
|
403
|
+
MODEL_COST_SCORE,
|
|
404
|
+
};
|