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,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiteLLM Client Module
|
|
3
|
+
*
|
|
4
|
+
* Client for interacting with LiteLLM proxy
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a LiteLLM client
|
|
9
|
+
* @param {Object} options - Client options
|
|
10
|
+
* @param {string} options.baseUrl - LiteLLM proxy URL
|
|
11
|
+
* @param {string} [options.apiKey] - API key for authentication
|
|
12
|
+
* @returns {Object} Client instance
|
|
13
|
+
*/
|
|
14
|
+
function createClient(options) {
|
|
15
|
+
const { baseUrl, apiKey } = options;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
baseUrl,
|
|
19
|
+
apiKey,
|
|
20
|
+
_fetch: globalThis.fetch,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Make a completion request
|
|
26
|
+
* @param {Object} client - Client instance
|
|
27
|
+
* @param {Object} options - Completion options
|
|
28
|
+
* @param {string} options.model - Model to use
|
|
29
|
+
* @param {string} options.prompt - Input prompt
|
|
30
|
+
* @param {number} [options.maxTokens] - Maximum tokens
|
|
31
|
+
* @param {number} [options.temperature] - Temperature
|
|
32
|
+
* @returns {Promise<Object>} Completion response
|
|
33
|
+
*/
|
|
34
|
+
async function completion(client, options) {
|
|
35
|
+
const { model, prompt, maxTokens, temperature } = options;
|
|
36
|
+
|
|
37
|
+
const body = {
|
|
38
|
+
model,
|
|
39
|
+
prompt,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (maxTokens) body.max_tokens = maxTokens;
|
|
43
|
+
if (temperature !== undefined) body.temperature = temperature;
|
|
44
|
+
|
|
45
|
+
const response = await client._fetch(`${client.baseUrl}/v1/completions`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
...(client.apiKey && { 'Authorization': `Bearer ${client.apiKey}` }),
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify(body),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const error = await response.text();
|
|
56
|
+
throw new Error(`Completion failed: ${error}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return response.json();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Make a chat completion request
|
|
64
|
+
* @param {Object} client - Client instance
|
|
65
|
+
* @param {Object} options - Chat options
|
|
66
|
+
* @param {string} options.model - Model to use
|
|
67
|
+
* @param {Array} options.messages - Chat messages
|
|
68
|
+
* @param {boolean} [options.stream] - Enable streaming
|
|
69
|
+
* @param {number} [options.maxTokens] - Maximum tokens
|
|
70
|
+
* @param {number} [options.temperature] - Temperature
|
|
71
|
+
* @returns {Promise<Object>} Chat response
|
|
72
|
+
*/
|
|
73
|
+
async function chat(client, options) {
|
|
74
|
+
const { model, messages, stream, maxTokens, temperature } = options;
|
|
75
|
+
|
|
76
|
+
const body = {
|
|
77
|
+
model,
|
|
78
|
+
messages,
|
|
79
|
+
stream: stream || false,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (maxTokens) body.max_tokens = maxTokens;
|
|
83
|
+
if (temperature !== undefined) body.temperature = temperature;
|
|
84
|
+
|
|
85
|
+
const response = await client._fetch(`${client.baseUrl}/v1/chat/completions`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
...(client.apiKey && { 'Authorization': `Bearer ${client.apiKey}` }),
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify(body),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const error = await response.text();
|
|
96
|
+
throw new Error(`Chat failed: ${error}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (stream && response.body) {
|
|
100
|
+
return { stream: response.body };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return response.json();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get usage statistics
|
|
108
|
+
* @param {Object} client - Client instance
|
|
109
|
+
* @param {Object} [options] - Query options
|
|
110
|
+
* @param {string} [options.startDate] - Start date
|
|
111
|
+
* @param {string} [options.endDate] - End date
|
|
112
|
+
* @returns {Promise<Object>} Usage statistics
|
|
113
|
+
*/
|
|
114
|
+
async function getUsage(client, options = {}) {
|
|
115
|
+
const { startDate, endDate } = options;
|
|
116
|
+
|
|
117
|
+
let url = `${client.baseUrl}/spend/logs`;
|
|
118
|
+
const params = new URLSearchParams();
|
|
119
|
+
|
|
120
|
+
if (startDate) params.set('start_date', startDate);
|
|
121
|
+
if (endDate) params.set('end_date', endDate);
|
|
122
|
+
|
|
123
|
+
if (params.toString()) {
|
|
124
|
+
url += `?${params.toString()}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const response = await client._fetch(url, {
|
|
128
|
+
headers: {
|
|
129
|
+
...(client.apiKey && { 'Authorization': `Bearer ${client.apiKey}` }),
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error('Failed to fetch usage');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return response.json();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get available models
|
|
142
|
+
* @param {Object} client - Client instance
|
|
143
|
+
* @returns {Promise<Array>} List of models
|
|
144
|
+
*/
|
|
145
|
+
async function getModels(client) {
|
|
146
|
+
const response = await client._fetch(`${client.baseUrl}/v1/models`, {
|
|
147
|
+
headers: {
|
|
148
|
+
...(client.apiKey && { 'Authorization': `Bearer ${client.apiKey}` }),
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error('Failed to fetch models');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result = await response.json();
|
|
157
|
+
return result.data || [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check proxy health
|
|
162
|
+
* @param {Object} client - Client instance
|
|
163
|
+
* @returns {Promise<Object>} Health status
|
|
164
|
+
*/
|
|
165
|
+
async function healthCheck(client) {
|
|
166
|
+
try {
|
|
167
|
+
const response = await client._fetch(`${client.baseUrl}/health`, {
|
|
168
|
+
headers: {
|
|
169
|
+
...(client.apiKey && { 'Authorization': `Bearer ${client.apiKey}` }),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
return { status: 'unhealthy', error: 'Bad response' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const result = await response.json();
|
|
178
|
+
return { status: result.status || 'healthy', ...result };
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return { status: 'unhealthy', error: error.message };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Set API key
|
|
186
|
+
* @param {Object} client - Client instance
|
|
187
|
+
* @param {string} apiKey - New API key
|
|
188
|
+
*/
|
|
189
|
+
function setApiKey(client, apiKey) {
|
|
190
|
+
client.apiKey = apiKey;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Execute request with fallback
|
|
195
|
+
* @param {Object} client - Client instance
|
|
196
|
+
* @param {Object} options - Fallback options
|
|
197
|
+
* @param {string} options.primary - Primary model
|
|
198
|
+
* @param {string[]} options.fallbacks - Fallback models
|
|
199
|
+
* @param {Object} options.request - Request options
|
|
200
|
+
* @returns {Promise<Object>} Response
|
|
201
|
+
*/
|
|
202
|
+
async function withFallback(client, options) {
|
|
203
|
+
const { primary, fallbacks, request } = options;
|
|
204
|
+
|
|
205
|
+
const models = [primary, ...fallbacks];
|
|
206
|
+
const errors = [];
|
|
207
|
+
|
|
208
|
+
for (const model of models) {
|
|
209
|
+
try {
|
|
210
|
+
const result = await completion(client, {
|
|
211
|
+
...request,
|
|
212
|
+
model,
|
|
213
|
+
});
|
|
214
|
+
return result;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
errors.push({ model, error: error.message });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
throw new Error(`All models failed: ${errors.map(e => `${e.model}: ${e.error}`).join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
createClient,
|
|
225
|
+
completion,
|
|
226
|
+
chat,
|
|
227
|
+
getUsage,
|
|
228
|
+
getModels,
|
|
229
|
+
healthCheck,
|
|
230
|
+
setApiKey,
|
|
231
|
+
withFallback,
|
|
232
|
+
};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiteLLM Client Tests
|
|
3
|
+
*
|
|
4
|
+
* Client for interacting with LiteLLM proxy
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { describe, it, beforeEach } = require('node:test');
|
|
8
|
+
const assert = require('node:assert');
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
createClient,
|
|
12
|
+
completion,
|
|
13
|
+
chat,
|
|
14
|
+
getUsage,
|
|
15
|
+
getModels,
|
|
16
|
+
healthCheck,
|
|
17
|
+
setApiKey,
|
|
18
|
+
withFallback,
|
|
19
|
+
} = require('./litellm-client.js');
|
|
20
|
+
|
|
21
|
+
describe('LiteLLM Client', () => {
|
|
22
|
+
let client;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
client = createClient({
|
|
26
|
+
baseUrl: 'http://localhost:4000',
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('createClient', () => {
|
|
31
|
+
it('creates client with base URL', () => {
|
|
32
|
+
assert.ok(client);
|
|
33
|
+
assert.strictEqual(client.baseUrl, 'http://localhost:4000');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('accepts API key', () => {
|
|
37
|
+
const clientWithKey = createClient({
|
|
38
|
+
baseUrl: 'http://localhost:4000',
|
|
39
|
+
apiKey: 'test-key',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
assert.ok(clientWithKey.apiKey);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('completion', () => {
|
|
47
|
+
it('sends completion request', async () => {
|
|
48
|
+
// Mock the fetch for testing
|
|
49
|
+
client._fetch = async (url, options) => ({
|
|
50
|
+
ok: true,
|
|
51
|
+
json: async () => ({
|
|
52
|
+
choices: [{ text: 'Hello world' }],
|
|
53
|
+
usage: { prompt_tokens: 10, completion_tokens: 5 },
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const result = await completion(client, {
|
|
58
|
+
model: 'claude-3-sonnet',
|
|
59
|
+
prompt: 'Say hello',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
assert.ok(result.choices);
|
|
63
|
+
assert.strictEqual(result.choices[0].text, 'Hello world');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('includes usage stats', async () => {
|
|
67
|
+
client._fetch = async () => ({
|
|
68
|
+
ok: true,
|
|
69
|
+
json: async () => ({
|
|
70
|
+
choices: [{ text: 'Response' }],
|
|
71
|
+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await completion(client, {
|
|
76
|
+
model: 'claude-3-sonnet',
|
|
77
|
+
prompt: 'Test',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
assert.ok(result.usage);
|
|
81
|
+
assert.strictEqual(result.usage.total_tokens, 15);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('chat', () => {
|
|
86
|
+
it('sends chat request', async () => {
|
|
87
|
+
client._fetch = async () => ({
|
|
88
|
+
ok: true,
|
|
89
|
+
json: async () => ({
|
|
90
|
+
choices: [{ message: { role: 'assistant', content: 'Hello!' } }],
|
|
91
|
+
usage: { prompt_tokens: 10, completion_tokens: 5 },
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const result = await chat(client, {
|
|
96
|
+
model: 'claude-3-sonnet',
|
|
97
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert.ok(result.choices);
|
|
101
|
+
assert.strictEqual(result.choices[0].message.content, 'Hello!');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('handles streaming', async () => {
|
|
105
|
+
let chunks = [];
|
|
106
|
+
client._fetch = async () => ({
|
|
107
|
+
ok: true,
|
|
108
|
+
body: {
|
|
109
|
+
getReader: () => ({
|
|
110
|
+
read: async () => {
|
|
111
|
+
if (chunks.length < 3) {
|
|
112
|
+
chunks.push({ content: 'chunk' });
|
|
113
|
+
return { done: false, value: new TextEncoder().encode('data: {"choices":[{"delta":{"content":"chunk"}}]}\n\n') };
|
|
114
|
+
}
|
|
115
|
+
return { done: true };
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const result = await chat(client, {
|
|
122
|
+
model: 'claude-3-sonnet',
|
|
123
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
124
|
+
stream: true,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
assert.ok(result.stream || result.choices);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('getUsage', () => {
|
|
132
|
+
it('returns usage statistics', async () => {
|
|
133
|
+
client._fetch = async () => ({
|
|
134
|
+
ok: true,
|
|
135
|
+
json: async () => ({
|
|
136
|
+
total_spend: 15.50,
|
|
137
|
+
total_tokens: 50000,
|
|
138
|
+
by_model: {
|
|
139
|
+
'claude-3-sonnet': { spend: 10.00, tokens: 30000 },
|
|
140
|
+
'gpt-4': { spend: 5.50, tokens: 20000 },
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const usage = await getUsage(client);
|
|
146
|
+
|
|
147
|
+
assert.ok(usage.total_spend >= 0);
|
|
148
|
+
assert.ok(usage.by_model);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('filters by date range', async () => {
|
|
152
|
+
let capturedOptions;
|
|
153
|
+
client._fetch = async (url, options) => {
|
|
154
|
+
capturedOptions = { url };
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
json: async () => ({ total_spend: 5.00, by_model: {} }),
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
await getUsage(client, {
|
|
162
|
+
startDate: '2025-01-01',
|
|
163
|
+
endDate: '2025-01-31',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
assert.ok(capturedOptions.url.includes('start') || capturedOptions.url.includes('2025'));
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('getModels', () => {
|
|
171
|
+
it('lists available models', async () => {
|
|
172
|
+
client._fetch = async () => ({
|
|
173
|
+
ok: true,
|
|
174
|
+
json: async () => ({
|
|
175
|
+
data: [
|
|
176
|
+
{ id: 'claude-3-opus', provider: 'anthropic' },
|
|
177
|
+
{ id: 'gpt-4', provider: 'openai' },
|
|
178
|
+
],
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const models = await getModels(client);
|
|
183
|
+
|
|
184
|
+
assert.ok(Array.isArray(models));
|
|
185
|
+
assert.ok(models.length > 0);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('healthCheck', () => {
|
|
190
|
+
it('returns healthy status', async () => {
|
|
191
|
+
client._fetch = async () => ({
|
|
192
|
+
ok: true,
|
|
193
|
+
json: async () => ({ status: 'healthy' }),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const health = await healthCheck(client);
|
|
197
|
+
|
|
198
|
+
assert.strictEqual(health.status, 'healthy');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('returns unhealthy on error', async () => {
|
|
202
|
+
client._fetch = async () => {
|
|
203
|
+
throw new Error('Connection refused');
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const health = await healthCheck(client);
|
|
207
|
+
|
|
208
|
+
assert.strictEqual(health.status, 'unhealthy');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('setApiKey', () => {
|
|
213
|
+
it('updates API key', () => {
|
|
214
|
+
setApiKey(client, 'new-key');
|
|
215
|
+
|
|
216
|
+
assert.strictEqual(client.apiKey, 'new-key');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('withFallback', () => {
|
|
221
|
+
it('tries fallback on primary failure', async () => {
|
|
222
|
+
let attempts = [];
|
|
223
|
+
client._fetch = async (url, options) => {
|
|
224
|
+
const body = JSON.parse(options.body);
|
|
225
|
+
attempts.push(body.model);
|
|
226
|
+
|
|
227
|
+
if (body.model === 'claude-3-opus') {
|
|
228
|
+
throw new Error('Rate limited');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
ok: true,
|
|
233
|
+
json: async () => ({
|
|
234
|
+
choices: [{ text: 'Response from fallback' }],
|
|
235
|
+
}),
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const result = await withFallback(client, {
|
|
240
|
+
primary: 'claude-3-opus',
|
|
241
|
+
fallbacks: ['claude-3-sonnet', 'gpt-4'],
|
|
242
|
+
request: { prompt: 'Test' },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
assert.ok(attempts.includes('claude-3-opus'));
|
|
246
|
+
assert.ok(attempts.includes('claude-3-sonnet'));
|
|
247
|
+
assert.ok(result.choices);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('returns error if all models fail', async () => {
|
|
251
|
+
client._fetch = async () => {
|
|
252
|
+
throw new Error('All models unavailable');
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
await withFallback(client, {
|
|
257
|
+
primary: 'claude-3-opus',
|
|
258
|
+
fallbacks: ['claude-3-sonnet'],
|
|
259
|
+
request: { prompt: 'Test' },
|
|
260
|
+
});
|
|
261
|
+
assert.fail('Should have thrown');
|
|
262
|
+
} catch (err) {
|
|
263
|
+
assert.ok(err.message.includes('unavailable') || err.message.includes('failed'));
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|