tlc-claude-code 1.4.1 → 1.4.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/dashboard/dist/App.js +229 -35
- package/dashboard/dist/components/AgentRegistryPane.d.ts +35 -0
- package/dashboard/dist/components/AgentRegistryPane.js +89 -0
- package/dashboard/dist/components/AgentRegistryPane.test.d.ts +1 -0
- package/dashboard/dist/components/AgentRegistryPane.test.js +200 -0
- package/dashboard/dist/components/RouterPane.d.ts +5 -0
- package/dashboard/dist/components/RouterPane.js +65 -0
- package/dashboard/dist/components/RouterPane.test.d.ts +1 -0
- package/dashboard/dist/components/RouterPane.test.js +176 -0
- package/package.json +5 -2
- package/server/index.js +178 -0
- package/server/lib/agent-cleanup.js +177 -0
- package/server/lib/agent-cleanup.test.js +359 -0
- package/server/lib/agent-hooks.js +126 -0
- package/server/lib/agent-hooks.test.js +303 -0
- package/server/lib/agent-metadata.js +179 -0
- package/server/lib/agent-metadata.test.js +383 -0
- package/server/lib/agent-persistence.js +191 -0
- package/server/lib/agent-persistence.test.js +475 -0
- package/server/lib/agent-registry-command.js +340 -0
- package/server/lib/agent-registry-command.test.js +334 -0
- package/server/lib/agent-registry.js +155 -0
- package/server/lib/agent-registry.test.js +239 -0
- package/server/lib/agent-state.js +236 -0
- package/server/lib/agent-state.test.js +375 -0
- package/server/lib/api-provider.js +186 -0
- package/server/lib/api-provider.test.js +336 -0
- package/server/lib/cli-detector.js +166 -0
- package/server/lib/cli-detector.test.js +269 -0
- package/server/lib/cli-provider.js +212 -0
- package/server/lib/cli-provider.test.js +349 -0
- package/server/lib/debug.test.js +62 -0
- package/server/lib/devserver-router-api.js +249 -0
- package/server/lib/devserver-router-api.test.js +426 -0
- package/server/lib/model-router.js +245 -0
- package/server/lib/model-router.test.js +313 -0
- package/server/lib/output-schemas.js +269 -0
- package/server/lib/output-schemas.test.js +307 -0
- package/server/lib/provider-interface.js +153 -0
- package/server/lib/provider-interface.test.js +394 -0
- package/server/lib/provider-queue.js +158 -0
- package/server/lib/provider-queue.test.js +315 -0
- package/server/lib/router-config.js +221 -0
- package/server/lib/router-config.test.js +237 -0
- package/server/lib/router-setup-command.js +419 -0
- package/server/lib/router-setup-command.test.js +375 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
AgentState,
|
|
4
|
+
createAgentState,
|
|
5
|
+
STATES,
|
|
6
|
+
VALID_TRANSITIONS,
|
|
7
|
+
} from './agent-state.js';
|
|
8
|
+
|
|
9
|
+
describe('AgentState', () => {
|
|
10
|
+
let state;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.useFakeTimers();
|
|
14
|
+
vi.setSystemTime(new Date('2024-01-15T10:00:00Z'));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('createAgentState', () => {
|
|
18
|
+
it('starts in pending state', () => {
|
|
19
|
+
state = createAgentState();
|
|
20
|
+
|
|
21
|
+
expect(state.getState()).toBe(STATES.PENDING);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('accepts optional agent ID', () => {
|
|
25
|
+
state = createAgentState({ agentId: 'agent-123' });
|
|
26
|
+
|
|
27
|
+
expect(state.getAgentId()).toBe('agent-123');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('generates ID if not provided', () => {
|
|
31
|
+
state = createAgentState();
|
|
32
|
+
|
|
33
|
+
expect(state.getAgentId()).toBeDefined();
|
|
34
|
+
expect(state.getAgentId()).toMatch(/^state-/);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('valid transitions', () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
state = createAgentState();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('transition from pending to running succeeds', () => {
|
|
44
|
+
const result = state.transition(STATES.RUNNING);
|
|
45
|
+
|
|
46
|
+
expect(result.success).toBe(true);
|
|
47
|
+
expect(state.getState()).toBe(STATES.RUNNING);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('transition from running to completed succeeds', () => {
|
|
51
|
+
state.transition(STATES.RUNNING);
|
|
52
|
+
|
|
53
|
+
const result = state.transition(STATES.COMPLETED);
|
|
54
|
+
|
|
55
|
+
expect(result.success).toBe(true);
|
|
56
|
+
expect(state.getState()).toBe(STATES.COMPLETED);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('transition from running to failed succeeds', () => {
|
|
60
|
+
state.transition(STATES.RUNNING);
|
|
61
|
+
|
|
62
|
+
const result = state.transition(STATES.FAILED, { error: 'Task error' });
|
|
63
|
+
|
|
64
|
+
expect(result.success).toBe(true);
|
|
65
|
+
expect(state.getState()).toBe(STATES.FAILED);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('transition from running to cancelled succeeds', () => {
|
|
69
|
+
state.transition(STATES.RUNNING);
|
|
70
|
+
|
|
71
|
+
const result = state.transition(STATES.CANCELLED, { reason: 'User cancelled' });
|
|
72
|
+
|
|
73
|
+
expect(result.success).toBe(true);
|
|
74
|
+
expect(state.getState()).toBe(STATES.CANCELLED);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('transition from pending to cancelled succeeds', () => {
|
|
78
|
+
const result = state.transition(STATES.CANCELLED, { reason: 'Never started' });
|
|
79
|
+
|
|
80
|
+
expect(result.success).toBe(true);
|
|
81
|
+
expect(state.getState()).toBe(STATES.CANCELLED);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('invalid transitions', () => {
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
state = createAgentState();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('transition from pending to completed fails', () => {
|
|
91
|
+
const result = state.transition(STATES.COMPLETED);
|
|
92
|
+
|
|
93
|
+
expect(result.success).toBe(false);
|
|
94
|
+
expect(result.error).toContain('Invalid transition');
|
|
95
|
+
expect(state.getState()).toBe(STATES.PENDING);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('transition from completed to running fails', () => {
|
|
99
|
+
state.transition(STATES.RUNNING);
|
|
100
|
+
state.transition(STATES.COMPLETED);
|
|
101
|
+
|
|
102
|
+
const result = state.transition(STATES.RUNNING);
|
|
103
|
+
|
|
104
|
+
expect(result.success).toBe(false);
|
|
105
|
+
expect(result.error).toContain('Invalid transition');
|
|
106
|
+
expect(state.getState()).toBe(STATES.COMPLETED);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('transition from failed to running fails', () => {
|
|
110
|
+
state.transition(STATES.RUNNING);
|
|
111
|
+
state.transition(STATES.FAILED);
|
|
112
|
+
|
|
113
|
+
const result = state.transition(STATES.RUNNING);
|
|
114
|
+
|
|
115
|
+
expect(result.success).toBe(false);
|
|
116
|
+
expect(state.getState()).toBe(STATES.FAILED);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('transition from cancelled to running fails', () => {
|
|
120
|
+
state.transition(STATES.CANCELLED);
|
|
121
|
+
|
|
122
|
+
const result = state.transition(STATES.RUNNING);
|
|
123
|
+
|
|
124
|
+
expect(result.success).toBe(false);
|
|
125
|
+
expect(state.getState()).toBe(STATES.CANCELLED);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('transition to unknown state fails', () => {
|
|
129
|
+
const result = state.transition('unknown-state');
|
|
130
|
+
|
|
131
|
+
expect(result.success).toBe(false);
|
|
132
|
+
expect(result.error).toContain('Unknown state');
|
|
133
|
+
expect(state.getState()).toBe(STATES.PENDING);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('onTransition callback', () => {
|
|
138
|
+
beforeEach(() => {
|
|
139
|
+
state = createAgentState();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('fires on state change', () => {
|
|
143
|
+
const callback = vi.fn();
|
|
144
|
+
state.onTransition(callback);
|
|
145
|
+
|
|
146
|
+
state.transition(STATES.RUNNING);
|
|
147
|
+
|
|
148
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
149
|
+
expect(callback).toHaveBeenCalledWith(
|
|
150
|
+
expect.objectContaining({
|
|
151
|
+
from: STATES.PENDING,
|
|
152
|
+
to: STATES.RUNNING,
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('does not fire on failed transition', () => {
|
|
158
|
+
const callback = vi.fn();
|
|
159
|
+
state.onTransition(callback);
|
|
160
|
+
|
|
161
|
+
state.transition(STATES.COMPLETED); // Invalid from pending
|
|
162
|
+
|
|
163
|
+
expect(callback).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('supports multiple callbacks', () => {
|
|
167
|
+
const callback1 = vi.fn();
|
|
168
|
+
const callback2 = vi.fn();
|
|
169
|
+
state.onTransition(callback1);
|
|
170
|
+
state.onTransition(callback2);
|
|
171
|
+
|
|
172
|
+
state.transition(STATES.RUNNING);
|
|
173
|
+
|
|
174
|
+
expect(callback1).toHaveBeenCalledTimes(1);
|
|
175
|
+
expect(callback2).toHaveBeenCalledTimes(1);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('returns unsubscribe function', () => {
|
|
179
|
+
const callback = vi.fn();
|
|
180
|
+
const unsubscribe = state.onTransition(callback);
|
|
181
|
+
|
|
182
|
+
unsubscribe();
|
|
183
|
+
state.transition(STATES.RUNNING);
|
|
184
|
+
|
|
185
|
+
expect(callback).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('passes metadata in callback', () => {
|
|
189
|
+
const callback = vi.fn();
|
|
190
|
+
state.onTransition(callback);
|
|
191
|
+
|
|
192
|
+
state.transition(STATES.RUNNING, { taskId: 'task-456' });
|
|
193
|
+
|
|
194
|
+
expect(callback).toHaveBeenCalledWith(
|
|
195
|
+
expect.objectContaining({
|
|
196
|
+
metadata: { taskId: 'task-456' },
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('getHistory', () => {
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
state = createAgentState();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('returns all transitions', () => {
|
|
208
|
+
state.transition(STATES.RUNNING);
|
|
209
|
+
vi.advanceTimersByTime(1000);
|
|
210
|
+
state.transition(STATES.COMPLETED);
|
|
211
|
+
|
|
212
|
+
const history = state.getHistory();
|
|
213
|
+
|
|
214
|
+
expect(history).toHaveLength(2);
|
|
215
|
+
expect(history[0].from).toBe(STATES.PENDING);
|
|
216
|
+
expect(history[0].to).toBe(STATES.RUNNING);
|
|
217
|
+
expect(history[1].from).toBe(STATES.RUNNING);
|
|
218
|
+
expect(history[1].to).toBe(STATES.COMPLETED);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('includes timestamps for each transition', () => {
|
|
222
|
+
const startTime = Date.now();
|
|
223
|
+
state.transition(STATES.RUNNING);
|
|
224
|
+
vi.advanceTimersByTime(5000);
|
|
225
|
+
state.transition(STATES.COMPLETED);
|
|
226
|
+
|
|
227
|
+
const history = state.getHistory();
|
|
228
|
+
|
|
229
|
+
expect(history[0].timestamp).toBe(startTime);
|
|
230
|
+
expect(history[1].timestamp).toBe(startTime + 5000);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('includes metadata in history', () => {
|
|
234
|
+
state.transition(STATES.RUNNING, { taskId: 'task-789' });
|
|
235
|
+
state.transition(STATES.FAILED, { error: 'Something went wrong' });
|
|
236
|
+
|
|
237
|
+
const history = state.getHistory();
|
|
238
|
+
|
|
239
|
+
expect(history[0].metadata).toEqual({ taskId: 'task-789' });
|
|
240
|
+
expect(history[1].metadata).toEqual({ error: 'Something went wrong' });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('returns empty array when no transitions', () => {
|
|
244
|
+
const history = state.getHistory();
|
|
245
|
+
|
|
246
|
+
expect(history).toEqual([]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('does not include failed transition attempts', () => {
|
|
250
|
+
state.transition(STATES.COMPLETED); // Invalid - should fail
|
|
251
|
+
state.transition(STATES.RUNNING); // Valid
|
|
252
|
+
|
|
253
|
+
const history = state.getHistory();
|
|
254
|
+
|
|
255
|
+
expect(history).toHaveLength(1);
|
|
256
|
+
expect(history[0].to).toBe(STATES.RUNNING);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('getElapsedTime', () => {
|
|
261
|
+
beforeEach(() => {
|
|
262
|
+
state = createAgentState();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('calculates duration from creation', () => {
|
|
266
|
+
vi.advanceTimersByTime(3000);
|
|
267
|
+
|
|
268
|
+
const elapsed = state.getElapsedTime();
|
|
269
|
+
|
|
270
|
+
expect(elapsed).toBe(3000);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('calculates duration in specific state', () => {
|
|
274
|
+
state.transition(STATES.RUNNING);
|
|
275
|
+
vi.advanceTimersByTime(5000);
|
|
276
|
+
|
|
277
|
+
const runningTime = state.getElapsedTime(STATES.RUNNING);
|
|
278
|
+
|
|
279
|
+
expect(runningTime).toBe(5000);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('returns 0 for state never entered', () => {
|
|
283
|
+
const failedTime = state.getElapsedTime(STATES.FAILED);
|
|
284
|
+
|
|
285
|
+
expect(failedTime).toBe(0);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('calculates completed state duration', () => {
|
|
289
|
+
state.transition(STATES.RUNNING);
|
|
290
|
+
vi.advanceTimersByTime(2000);
|
|
291
|
+
state.transition(STATES.COMPLETED);
|
|
292
|
+
vi.advanceTimersByTime(1000);
|
|
293
|
+
|
|
294
|
+
const runningTime = state.getElapsedTime(STATES.RUNNING);
|
|
295
|
+
|
|
296
|
+
expect(runningTime).toBe(2000);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('returns total elapsed when no state specified', () => {
|
|
300
|
+
state.transition(STATES.RUNNING);
|
|
301
|
+
vi.advanceTimersByTime(2000);
|
|
302
|
+
state.transition(STATES.COMPLETED);
|
|
303
|
+
vi.advanceTimersByTime(3000);
|
|
304
|
+
|
|
305
|
+
const totalTime = state.getElapsedTime();
|
|
306
|
+
|
|
307
|
+
expect(totalTime).toBe(5000);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('isTerminal', () => {
|
|
312
|
+
it('returns false for pending', () => {
|
|
313
|
+
state = createAgentState();
|
|
314
|
+
|
|
315
|
+
expect(state.isTerminal()).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('returns false for running', () => {
|
|
319
|
+
state = createAgentState();
|
|
320
|
+
state.transition(STATES.RUNNING);
|
|
321
|
+
|
|
322
|
+
expect(state.isTerminal()).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('returns true for completed', () => {
|
|
326
|
+
state = createAgentState();
|
|
327
|
+
state.transition(STATES.RUNNING);
|
|
328
|
+
state.transition(STATES.COMPLETED);
|
|
329
|
+
|
|
330
|
+
expect(state.isTerminal()).toBe(true);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('returns true for failed', () => {
|
|
334
|
+
state = createAgentState();
|
|
335
|
+
state.transition(STATES.RUNNING);
|
|
336
|
+
state.transition(STATES.FAILED);
|
|
337
|
+
|
|
338
|
+
expect(state.isTerminal()).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('returns true for cancelled', () => {
|
|
342
|
+
state = createAgentState();
|
|
343
|
+
state.transition(STATES.CANCELLED);
|
|
344
|
+
|
|
345
|
+
expect(state.isTerminal()).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('STATES constants', () => {
|
|
350
|
+
it('exports all state values', () => {
|
|
351
|
+
expect(STATES.PENDING).toBe('pending');
|
|
352
|
+
expect(STATES.RUNNING).toBe('running');
|
|
353
|
+
expect(STATES.COMPLETED).toBe('completed');
|
|
354
|
+
expect(STATES.FAILED).toBe('failed');
|
|
355
|
+
expect(STATES.CANCELLED).toBe('cancelled');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('VALID_TRANSITIONS', () => {
|
|
360
|
+
it('exports transition map', () => {
|
|
361
|
+
expect(VALID_TRANSITIONS).toBeDefined();
|
|
362
|
+
expect(VALID_TRANSITIONS[STATES.PENDING]).toContain(STATES.RUNNING);
|
|
363
|
+
expect(VALID_TRANSITIONS[STATES.PENDING]).toContain(STATES.CANCELLED);
|
|
364
|
+
expect(VALID_TRANSITIONS[STATES.RUNNING]).toContain(STATES.COMPLETED);
|
|
365
|
+
expect(VALID_TRANSITIONS[STATES.RUNNING]).toContain(STATES.FAILED);
|
|
366
|
+
expect(VALID_TRANSITIONS[STATES.RUNNING]).toContain(STATES.CANCELLED);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('terminal states have no valid transitions', () => {
|
|
370
|
+
expect(VALID_TRANSITIONS[STATES.COMPLETED]).toEqual([]);
|
|
371
|
+
expect(VALID_TRANSITIONS[STATES.FAILED]).toEqual([]);
|
|
372
|
+
expect(VALID_TRANSITIONS[STATES.CANCELLED]).toEqual([]);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Provider - Provider implementation for REST API endpoints
|
|
3
|
+
*
|
|
4
|
+
* Supports OpenAI-compatible endpoints:
|
|
5
|
+
* - DeepSeek
|
|
6
|
+
* - Mistral
|
|
7
|
+
* - Any OpenAI-compatible API
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createProvider, PROVIDER_TYPES } from './provider-interface.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* API pricing per 1K tokens (USD)
|
|
14
|
+
*/
|
|
15
|
+
export const API_PRICING = {
|
|
16
|
+
deepseek: { input: 0.0001, output: 0.0002 },
|
|
17
|
+
'deepseek-coder': { input: 0.0001, output: 0.0002 },
|
|
18
|
+
mistral: { input: 0.0002, output: 0.0006 },
|
|
19
|
+
'mistral-large': { input: 0.002, output: 0.006 },
|
|
20
|
+
groq: { input: 0.0001, output: 0.0001 },
|
|
21
|
+
default: { input: 0.001, output: 0.002 },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Calculate cost from token usage
|
|
26
|
+
* @param {Object} tokenUsage - { input, output }
|
|
27
|
+
* @param {Object} pricing - { input, output } per 1K tokens
|
|
28
|
+
* @returns {number|null} Cost in USD
|
|
29
|
+
*/
|
|
30
|
+
export function calculateCost(tokenUsage, pricing) {
|
|
31
|
+
if (!tokenUsage || !pricing) return null;
|
|
32
|
+
|
|
33
|
+
const inputCost = (tokenUsage.input * pricing.input) / 1000;
|
|
34
|
+
const outputCost = (tokenUsage.output * pricing.output) / 1000;
|
|
35
|
+
|
|
36
|
+
return inputCost + outputCost;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse API response
|
|
41
|
+
* @param {Object} response - API response
|
|
42
|
+
* @returns {Object} Parsed result
|
|
43
|
+
*/
|
|
44
|
+
export function parseResponse(response) {
|
|
45
|
+
const content = response.choices?.[0]?.message?.content || '';
|
|
46
|
+
const usage = response.usage || {};
|
|
47
|
+
|
|
48
|
+
let parsed = null;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(content);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// Not JSON, that's ok
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
raw: content,
|
|
57
|
+
parsed,
|
|
58
|
+
tokenUsage: {
|
|
59
|
+
input: usage.prompt_tokens || 0,
|
|
60
|
+
output: usage.completion_tokens || 0,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Call an OpenAI-compatible API
|
|
67
|
+
* @param {Object} params - Parameters
|
|
68
|
+
* @param {string} params.baseUrl - API base URL
|
|
69
|
+
* @param {string} params.model - Model name
|
|
70
|
+
* @param {string} params.prompt - The prompt
|
|
71
|
+
* @param {string} params.apiKey - API key
|
|
72
|
+
* @param {Object} [params.outputSchema] - JSON schema for output
|
|
73
|
+
* @param {number} [params.maxRetries=3] - Max retry attempts
|
|
74
|
+
* @param {number} [params.retryDelay=1000] - Retry delay in ms
|
|
75
|
+
* @returns {Promise<Object>} ProviderResult
|
|
76
|
+
*/
|
|
77
|
+
export async function callAPI({
|
|
78
|
+
baseUrl,
|
|
79
|
+
model,
|
|
80
|
+
prompt,
|
|
81
|
+
apiKey,
|
|
82
|
+
outputSchema,
|
|
83
|
+
maxRetries = 3,
|
|
84
|
+
retryDelay = 1000,
|
|
85
|
+
}) {
|
|
86
|
+
const url = `${baseUrl}/v1/chat/completions`;
|
|
87
|
+
|
|
88
|
+
const body = {
|
|
89
|
+
model,
|
|
90
|
+
messages: [{ role: 'user', content: prompt }],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (outputSchema) {
|
|
94
|
+
body.response_format = {
|
|
95
|
+
type: 'json_schema',
|
|
96
|
+
json_schema: {
|
|
97
|
+
name: 'response',
|
|
98
|
+
schema: outputSchema,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let lastError = null;
|
|
104
|
+
|
|
105
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
106
|
+
try {
|
|
107
|
+
const response = await fetch(url, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: {
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify(body),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Handle rate limiting
|
|
117
|
+
if (response.status === 429) {
|
|
118
|
+
const retryAfter = parseInt(response.headers.get('Retry-After') || '1', 10);
|
|
119
|
+
await new Promise(r => setTimeout(r, retryAfter * 1000 || retryDelay));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
125
|
+
lastError = new Error(errorBody.error?.message || response.statusText);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
const parsed = parseResponse(data);
|
|
131
|
+
|
|
132
|
+
// Get pricing for cost calculation
|
|
133
|
+
const pricing = API_PRICING[model] || API_PRICING.default;
|
|
134
|
+
const cost = calculateCost(parsed.tokenUsage, pricing);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
...parsed,
|
|
138
|
+
exitCode: 0,
|
|
139
|
+
cost,
|
|
140
|
+
};
|
|
141
|
+
} catch (err) {
|
|
142
|
+
lastError = err;
|
|
143
|
+
|
|
144
|
+
if (attempt < maxRetries - 1) {
|
|
145
|
+
await new Promise(r => setTimeout(r, retryDelay));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
raw: '',
|
|
152
|
+
parsed: null,
|
|
153
|
+
exitCode: 1,
|
|
154
|
+
error: lastError?.message || 'API call failed',
|
|
155
|
+
tokenUsage: null,
|
|
156
|
+
cost: null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create an API provider instance
|
|
162
|
+
* @param {Object} config - Provider configuration
|
|
163
|
+
* @returns {Object} Provider instance
|
|
164
|
+
*/
|
|
165
|
+
export function createAPIProvider(config) {
|
|
166
|
+
const runner = async (prompt, opts) => {
|
|
167
|
+
if (!config.apiKey && !process.env[`${config.name.toUpperCase()}_API_KEY`]) {
|
|
168
|
+
throw new Error(`API key not configured for ${config.name}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return callAPI({
|
|
172
|
+
baseUrl: config.baseUrl,
|
|
173
|
+
model: config.model || config.name,
|
|
174
|
+
prompt,
|
|
175
|
+
apiKey: config.apiKey || process.env[`${config.name.toUpperCase()}_API_KEY`],
|
|
176
|
+
outputSchema: opts.outputSchema,
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return createProvider({
|
|
181
|
+
...config,
|
|
182
|
+
type: PROVIDER_TYPES.API,
|
|
183
|
+
devserverOnly: config.devserverOnly ?? true,
|
|
184
|
+
runner,
|
|
185
|
+
});
|
|
186
|
+
}
|