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.
Files changed (122) hide show
  1. package/CLAUDE.md +23 -0
  2. package/CODING-STANDARDS.md +408 -0
  3. package/bin/install.js +2 -0
  4. package/dashboard/dist/components/QualityGatePane.d.ts +38 -0
  5. package/dashboard/dist/components/QualityGatePane.js +31 -0
  6. package/dashboard/dist/components/QualityGatePane.test.d.ts +1 -0
  7. package/dashboard/dist/components/QualityGatePane.test.js +147 -0
  8. package/dashboard/dist/components/orchestration/AgentCard.d.ts +26 -0
  9. package/dashboard/dist/components/orchestration/AgentCard.js +60 -0
  10. package/dashboard/dist/components/orchestration/AgentCard.test.d.ts +1 -0
  11. package/dashboard/dist/components/orchestration/AgentCard.test.js +63 -0
  12. package/dashboard/dist/components/orchestration/AgentControls.d.ts +11 -0
  13. package/dashboard/dist/components/orchestration/AgentControls.js +20 -0
  14. package/dashboard/dist/components/orchestration/AgentControls.test.d.ts +1 -0
  15. package/dashboard/dist/components/orchestration/AgentControls.test.js +52 -0
  16. package/dashboard/dist/components/orchestration/AgentDetail.d.ts +35 -0
  17. package/dashboard/dist/components/orchestration/AgentDetail.js +37 -0
  18. package/dashboard/dist/components/orchestration/AgentDetail.test.d.ts +1 -0
  19. package/dashboard/dist/components/orchestration/AgentDetail.test.js +79 -0
  20. package/dashboard/dist/components/orchestration/AgentList.d.ts +31 -0
  21. package/dashboard/dist/components/orchestration/AgentList.js +47 -0
  22. package/dashboard/dist/components/orchestration/AgentList.test.d.ts +1 -0
  23. package/dashboard/dist/components/orchestration/AgentList.test.js +64 -0
  24. package/dashboard/dist/components/orchestration/CostMeter.d.ts +11 -0
  25. package/dashboard/dist/components/orchestration/CostMeter.js +28 -0
  26. package/dashboard/dist/components/orchestration/CostMeter.test.d.ts +1 -0
  27. package/dashboard/dist/components/orchestration/CostMeter.test.js +50 -0
  28. package/dashboard/dist/components/orchestration/ModelSelector.d.ts +20 -0
  29. package/dashboard/dist/components/orchestration/ModelSelector.js +12 -0
  30. package/dashboard/dist/components/orchestration/ModelSelector.test.d.ts +1 -0
  31. package/dashboard/dist/components/orchestration/ModelSelector.test.js +56 -0
  32. package/dashboard/dist/components/orchestration/OrchestrationDashboard.d.ts +28 -0
  33. package/dashboard/dist/components/orchestration/OrchestrationDashboard.js +28 -0
  34. package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.d.ts +1 -0
  35. package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.js +56 -0
  36. package/dashboard/dist/components/orchestration/QualityIndicator.d.ts +11 -0
  37. package/dashboard/dist/components/orchestration/QualityIndicator.js +37 -0
  38. package/dashboard/dist/components/orchestration/QualityIndicator.test.d.ts +1 -0
  39. package/dashboard/dist/components/orchestration/QualityIndicator.test.js +52 -0
  40. package/dashboard/dist/components/orchestration/index.d.ts +8 -0
  41. package/dashboard/dist/components/orchestration/index.js +8 -0
  42. package/package.json +1 -1
  43. package/server/lib/access-control.js +352 -0
  44. package/server/lib/access-control.test.js +322 -0
  45. package/server/lib/agents-cancel-command.js +139 -0
  46. package/server/lib/agents-cancel-command.test.js +180 -0
  47. package/server/lib/agents-get-command.js +159 -0
  48. package/server/lib/agents-get-command.test.js +167 -0
  49. package/server/lib/agents-list-command.js +150 -0
  50. package/server/lib/agents-list-command.test.js +149 -0
  51. package/server/lib/agents-logs-command.js +126 -0
  52. package/server/lib/agents-logs-command.test.js +198 -0
  53. package/server/lib/agents-retry-command.js +117 -0
  54. package/server/lib/agents-retry-command.test.js +192 -0
  55. package/server/lib/budget-limits.js +222 -0
  56. package/server/lib/budget-limits.test.js +214 -0
  57. package/server/lib/code-generator.js +291 -0
  58. package/server/lib/code-generator.test.js +307 -0
  59. package/server/lib/cost-command.js +290 -0
  60. package/server/lib/cost-command.test.js +202 -0
  61. package/server/lib/cost-optimizer.js +404 -0
  62. package/server/lib/cost-optimizer.test.js +232 -0
  63. package/server/lib/cost-projections.js +302 -0
  64. package/server/lib/cost-projections.test.js +217 -0
  65. package/server/lib/cost-reports.js +277 -0
  66. package/server/lib/cost-reports.test.js +254 -0
  67. package/server/lib/cost-tracker.js +216 -0
  68. package/server/lib/cost-tracker.test.js +302 -0
  69. package/server/lib/crypto-patterns.js +433 -0
  70. package/server/lib/crypto-patterns.test.js +346 -0
  71. package/server/lib/design-command.js +385 -0
  72. package/server/lib/design-command.test.js +249 -0
  73. package/server/lib/design-parser.js +237 -0
  74. package/server/lib/design-parser.test.js +290 -0
  75. package/server/lib/gemini-vision.js +377 -0
  76. package/server/lib/gemini-vision.test.js +282 -0
  77. package/server/lib/input-validator.js +360 -0
  78. package/server/lib/input-validator.test.js +295 -0
  79. package/server/lib/litellm-client.js +232 -0
  80. package/server/lib/litellm-client.test.js +267 -0
  81. package/server/lib/litellm-command.js +291 -0
  82. package/server/lib/litellm-command.test.js +260 -0
  83. package/server/lib/litellm-config.js +273 -0
  84. package/server/lib/litellm-config.test.js +212 -0
  85. package/server/lib/model-pricing.js +189 -0
  86. package/server/lib/model-pricing.test.js +178 -0
  87. package/server/lib/models-command.js +223 -0
  88. package/server/lib/models-command.test.js +193 -0
  89. package/server/lib/optimize-command.js +197 -0
  90. package/server/lib/optimize-command.test.js +193 -0
  91. package/server/lib/orchestration-integration.js +206 -0
  92. package/server/lib/orchestration-integration.test.js +235 -0
  93. package/server/lib/output-encoder.js +308 -0
  94. package/server/lib/output-encoder.test.js +312 -0
  95. package/server/lib/quality-evaluator.js +396 -0
  96. package/server/lib/quality-evaluator.test.js +337 -0
  97. package/server/lib/quality-gate-command.js +340 -0
  98. package/server/lib/quality-gate-command.test.js +321 -0
  99. package/server/lib/quality-gate-scorer.js +378 -0
  100. package/server/lib/quality-gate-scorer.test.js +376 -0
  101. package/server/lib/quality-history.js +265 -0
  102. package/server/lib/quality-history.test.js +359 -0
  103. package/server/lib/quality-presets.js +288 -0
  104. package/server/lib/quality-presets.test.js +269 -0
  105. package/server/lib/quality-retry.js +323 -0
  106. package/server/lib/quality-retry.test.js +325 -0
  107. package/server/lib/quality-thresholds.js +255 -0
  108. package/server/lib/quality-thresholds.test.js +237 -0
  109. package/server/lib/secure-auth.js +333 -0
  110. package/server/lib/secure-auth.test.js +288 -0
  111. package/server/lib/secure-code-command.js +540 -0
  112. package/server/lib/secure-code-command.test.js +309 -0
  113. package/server/lib/secure-errors.js +521 -0
  114. package/server/lib/secure-errors.test.js +298 -0
  115. package/server/lib/vision-command.js +372 -0
  116. package/server/lib/vision-command.test.js +255 -0
  117. package/server/lib/visual-command.js +350 -0
  118. package/server/lib/visual-command.test.js +256 -0
  119. package/server/lib/visual-testing.js +315 -0
  120. package/server/lib/visual-testing.test.js +357 -0
  121. package/server/package-lock.json +2 -2
  122. 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
+ };