@auxiora/providers 1.0.0

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 (88) hide show
  1. package/LICENSE +191 -0
  2. package/dist/anthropic.d.ts +82 -0
  3. package/dist/anthropic.d.ts.map +1 -0
  4. package/dist/anthropic.js +618 -0
  5. package/dist/anthropic.js.map +1 -0
  6. package/dist/claude-code-tools.d.ts +29 -0
  7. package/dist/claude-code-tools.d.ts.map +1 -0
  8. package/dist/claude-code-tools.js +221 -0
  9. package/dist/claude-code-tools.js.map +1 -0
  10. package/dist/claude-oauth.d.ts +86 -0
  11. package/dist/claude-oauth.d.ts.map +1 -0
  12. package/dist/claude-oauth.js +318 -0
  13. package/dist/claude-oauth.js.map +1 -0
  14. package/dist/cohere.d.ts +18 -0
  15. package/dist/cohere.d.ts.map +1 -0
  16. package/dist/cohere.js +163 -0
  17. package/dist/cohere.js.map +1 -0
  18. package/dist/deepseek.d.ts +18 -0
  19. package/dist/deepseek.d.ts.map +1 -0
  20. package/dist/deepseek.js +164 -0
  21. package/dist/deepseek.js.map +1 -0
  22. package/dist/factory.d.ts +19 -0
  23. package/dist/factory.d.ts.map +1 -0
  24. package/dist/factory.js +108 -0
  25. package/dist/factory.js.map +1 -0
  26. package/dist/google.d.ts +18 -0
  27. package/dist/google.d.ts.map +1 -0
  28. package/dist/google.js +141 -0
  29. package/dist/google.js.map +1 -0
  30. package/dist/groq.d.ts +18 -0
  31. package/dist/groq.d.ts.map +1 -0
  32. package/dist/groq.js +186 -0
  33. package/dist/groq.js.map +1 -0
  34. package/dist/index.d.ts +15 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +14 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/ollama.d.ts +18 -0
  39. package/dist/ollama.d.ts.map +1 -0
  40. package/dist/ollama.js +141 -0
  41. package/dist/ollama.js.map +1 -0
  42. package/dist/openai-compatible.d.ts +20 -0
  43. package/dist/openai-compatible.d.ts.map +1 -0
  44. package/dist/openai-compatible.js +112 -0
  45. package/dist/openai-compatible.js.map +1 -0
  46. package/dist/openai.d.ts +20 -0
  47. package/dist/openai.d.ts.map +1 -0
  48. package/dist/openai.js +259 -0
  49. package/dist/openai.js.map +1 -0
  50. package/dist/replicate.d.ts +20 -0
  51. package/dist/replicate.d.ts.map +1 -0
  52. package/dist/replicate.js +186 -0
  53. package/dist/replicate.js.map +1 -0
  54. package/dist/thinking-levels.d.ts +16 -0
  55. package/dist/thinking-levels.d.ts.map +1 -0
  56. package/dist/thinking-levels.js +34 -0
  57. package/dist/thinking-levels.js.map +1 -0
  58. package/dist/types.d.ts +157 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/dist/xai.d.ts +18 -0
  63. package/dist/xai.d.ts.map +1 -0
  64. package/dist/xai.js +164 -0
  65. package/dist/xai.js.map +1 -0
  66. package/package.json +30 -0
  67. package/src/anthropic.ts +691 -0
  68. package/src/claude-code-tools.ts +233 -0
  69. package/src/claude-oauth.ts +410 -0
  70. package/src/cohere.ts +242 -0
  71. package/src/deepseek.ts +241 -0
  72. package/src/factory.ts +142 -0
  73. package/src/google.ts +176 -0
  74. package/src/groq.ts +263 -0
  75. package/src/index.ts +44 -0
  76. package/src/ollama.ts +194 -0
  77. package/src/openai-compatible.ts +154 -0
  78. package/src/openai.ts +307 -0
  79. package/src/replicate.ts +247 -0
  80. package/src/thinking-levels.ts +37 -0
  81. package/src/types.ts +171 -0
  82. package/src/xai.ts +241 -0
  83. package/tests/adapters.test.ts +185 -0
  84. package/tests/claude-oauth.test.ts +45 -0
  85. package/tests/new-providers.test.ts +732 -0
  86. package/tests/thinking-levels.test.ts +82 -0
  87. package/tsconfig.json +8 -0
  88. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,732 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { GroqProvider } from '../src/groq.js';
3
+ import { ReplicateProvider } from '../src/replicate.js';
4
+ import { DeepSeekProvider } from '../src/deepseek.js';
5
+ import { CohereProvider } from '../src/cohere.js';
6
+ import { XAIProvider } from '../src/xai.js';
7
+ import { ProviderFactory } from '../src/factory.js';
8
+
9
+ // ──────────────────────────────────────────
10
+ // Groq Provider
11
+ // ──────────────────────────────────────────
12
+
13
+ describe('GroqProvider', () => {
14
+ let provider: GroqProvider;
15
+
16
+ beforeEach(() => {
17
+ provider = new GroqProvider({ apiKey: 'test-groq-key' });
18
+ });
19
+
20
+ afterEach(() => {
21
+ vi.restoreAllMocks();
22
+ });
23
+
24
+ it('should have correct metadata', () => {
25
+ expect(provider.name).toBe('groq');
26
+ expect(provider.metadata.name).toBe('groq');
27
+ expect(provider.metadata.displayName).toBe('Groq');
28
+ expect(provider.metadata.models['llama-3.3-70b-versatile']).toBeDefined();
29
+ expect(provider.metadata.models['llama-3.1-8b-instant']).toBeDefined();
30
+ expect(provider.metadata.models['mixtral-8x7b-32768']).toBeDefined();
31
+ expect(provider.metadata.models['gemma2-9b-it']).toBeDefined();
32
+ });
33
+
34
+ it('should have correct model capabilities', () => {
35
+ const model = provider.metadata.models['llama-3.3-70b-versatile'];
36
+ expect(model.supportsTools).toBe(true);
37
+ expect(model.supportsStreaming).toBe(true);
38
+ expect(model.supportsImageGen).toBe(false);
39
+ expect(model.isLocal).toBe(false);
40
+ expect(model.maxContextTokens).toBe(128000);
41
+ });
42
+
43
+ it('should use custom model', () => {
44
+ const custom = new GroqProvider({ apiKey: 'key', model: 'mixtral-8x7b-32768' });
45
+ expect(custom.name).toBe('groq');
46
+ });
47
+
48
+ it('should make correct API call for complete', async () => {
49
+ const mockResponse = {
50
+ choices: [{ message: { role: 'assistant', content: 'Hello from Groq!' }, finish_reason: 'stop' }],
51
+ usage: { prompt_tokens: 10, completion_tokens: 5 },
52
+ model: 'llama-3.3-70b-versatile',
53
+ };
54
+
55
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
56
+ ok: true,
57
+ json: async () => mockResponse,
58
+ } as Response);
59
+
60
+ const result = await provider.complete([{ role: 'user', content: 'Hello' }]);
61
+ expect(result.content).toBe('Hello from Groq!');
62
+ expect(result.usage.inputTokens).toBe(10);
63
+ expect(result.usage.outputTokens).toBe(5);
64
+ expect(result.finishReason).toBe('stop');
65
+ });
66
+
67
+ it('should send correct headers', async () => {
68
+ const mockResponse = {
69
+ choices: [{ message: { role: 'assistant', content: 'Hi' }, finish_reason: 'stop' }],
70
+ usage: { prompt_tokens: 1, completion_tokens: 1 },
71
+ model: 'llama-3.3-70b-versatile',
72
+ };
73
+
74
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
75
+ ok: true,
76
+ json: async () => mockResponse,
77
+ } as Response);
78
+
79
+ await provider.complete([{ role: 'user', content: 'Hi' }]);
80
+
81
+ expect(fetchSpy).toHaveBeenCalledWith(
82
+ 'https://api.groq.com/openai/v1/chat/completions',
83
+ expect.objectContaining({
84
+ method: 'POST',
85
+ headers: expect.objectContaining({
86
+ Authorization: 'Bearer test-groq-key',
87
+ 'Content-Type': 'application/json',
88
+ }),
89
+ }),
90
+ );
91
+ });
92
+
93
+ it('should handle API errors', async () => {
94
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
95
+ ok: false,
96
+ status: 429,
97
+ statusText: 'Too Many Requests',
98
+ } as Response);
99
+
100
+ await expect(provider.complete([{ role: 'user', content: 'Hello' }]))
101
+ .rejects.toThrow('Groq API error: 429');
102
+ });
103
+
104
+ it('should include system prompt in messages', async () => {
105
+ const mockResponse = {
106
+ choices: [{ message: { role: 'assistant', content: 'Ok' }, finish_reason: 'stop' }],
107
+ usage: { prompt_tokens: 1, completion_tokens: 1 },
108
+ model: 'llama-3.3-70b-versatile',
109
+ };
110
+
111
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
112
+ ok: true,
113
+ json: async () => mockResponse,
114
+ } as Response);
115
+
116
+ await provider.complete([{ role: 'user', content: 'Hi' }], {
117
+ systemPrompt: 'You are helpful.',
118
+ });
119
+
120
+ const body = JSON.parse(fetchSpy.mock.calls[0][1]!.body as string);
121
+ expect(body.messages[0]).toEqual({ role: 'system', content: 'You are helpful.' });
122
+ expect(body.messages[1]).toEqual({ role: 'user', content: 'Hi' });
123
+ });
124
+
125
+ it('should check availability via /models endpoint', async () => {
126
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: true } as Response);
127
+ const available = await provider.metadata.isAvailable();
128
+ expect(available).toBe(true);
129
+ });
130
+
131
+ it('should return false when unreachable', async () => {
132
+ vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(new Error('Connection refused'));
133
+ const available = await provider.metadata.isAvailable();
134
+ expect(available).toBe(false);
135
+ });
136
+
137
+ it('should handle streaming errors', async () => {
138
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
139
+ ok: false,
140
+ status: 500,
141
+ statusText: 'Internal Server Error',
142
+ } as Response);
143
+
144
+ const chunks: Array<{ type: string; error?: string }> = [];
145
+ for await (const chunk of provider.stream([{ role: 'user', content: 'Hello' }])) {
146
+ chunks.push(chunk);
147
+ }
148
+
149
+ expect(chunks).toHaveLength(1);
150
+ expect(chunks[0].type).toBe('error');
151
+ expect(chunks[0].error).toContain('Groq API error: 500');
152
+ });
153
+ });
154
+
155
+ // ──────────────────────────────────────────
156
+ // Replicate Provider
157
+ // ──────────────────────────────────────────
158
+
159
+ describe('ReplicateProvider', () => {
160
+ let provider: ReplicateProvider;
161
+
162
+ beforeEach(() => {
163
+ provider = new ReplicateProvider({ apiToken: 'test-replicate-token' });
164
+ });
165
+
166
+ afterEach(() => {
167
+ vi.restoreAllMocks();
168
+ });
169
+
170
+ it('should have correct metadata', () => {
171
+ expect(provider.name).toBe('replicate');
172
+ expect(provider.metadata.name).toBe('replicate');
173
+ expect(provider.metadata.displayName).toBe('Replicate');
174
+ expect(provider.metadata.models['meta/meta-llama-3-70b-instruct']).toBeDefined();
175
+ expect(provider.metadata.models['stability-ai/sdxl']).toBeDefined();
176
+ });
177
+
178
+ it('should support image generation model', () => {
179
+ const sdxl = provider.metadata.models['stability-ai/sdxl'];
180
+ expect(sdxl.supportsImageGen).toBe(true);
181
+ expect(sdxl.strengths).toContain('image-generation');
182
+ });
183
+
184
+ it('should make prediction and poll for result', async () => {
185
+ const createResponse = { id: 'pred-123', status: 'starting' };
186
+ const pollResponse = {
187
+ id: 'pred-123',
188
+ status: 'succeeded',
189
+ output: ['Hello ', 'from ', 'Replicate!'],
190
+ };
191
+
192
+ vi.spyOn(globalThis, 'fetch')
193
+ .mockResolvedValueOnce({
194
+ ok: true,
195
+ json: async () => createResponse,
196
+ } as Response)
197
+ .mockResolvedValueOnce({
198
+ ok: true,
199
+ json: async () => pollResponse,
200
+ } as Response);
201
+
202
+ const result = await provider.complete([{ role: 'user', content: 'Hello' }]);
203
+ expect(result.content).toBe('Hello from Replicate!');
204
+ expect(result.finishReason).toBe('stop');
205
+ });
206
+
207
+ it('should handle failed predictions', async () => {
208
+ const createResponse = { id: 'pred-456', status: 'starting' };
209
+ const failResponse = {
210
+ id: 'pred-456',
211
+ status: 'failed',
212
+ error: 'Model not found',
213
+ };
214
+
215
+ vi.spyOn(globalThis, 'fetch')
216
+ .mockResolvedValueOnce({
217
+ ok: true,
218
+ json: async () => createResponse,
219
+ } as Response)
220
+ .mockResolvedValueOnce({
221
+ ok: true,
222
+ json: async () => failResponse,
223
+ } as Response);
224
+
225
+ await expect(provider.complete([{ role: 'user', content: 'Hello' }]))
226
+ .rejects.toThrow('Replicate prediction failed: Model not found');
227
+ });
228
+
229
+ it('should handle API errors on create', async () => {
230
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
231
+ ok: false,
232
+ status: 401,
233
+ statusText: 'Unauthorized',
234
+ } as Response);
235
+
236
+ await expect(provider.complete([{ role: 'user', content: 'Hello' }]))
237
+ .rejects.toThrow('Replicate API error: 401');
238
+ });
239
+
240
+ it('should handle string output', async () => {
241
+ const createResponse = { id: 'pred-789', status: 'starting' };
242
+ const pollResponse = {
243
+ id: 'pred-789',
244
+ status: 'succeeded',
245
+ output: 'Single string output',
246
+ };
247
+
248
+ vi.spyOn(globalThis, 'fetch')
249
+ .mockResolvedValueOnce({
250
+ ok: true,
251
+ json: async () => createResponse,
252
+ } as Response)
253
+ .mockResolvedValueOnce({
254
+ ok: true,
255
+ json: async () => pollResponse,
256
+ } as Response);
257
+
258
+ const result = await provider.complete([{ role: 'user', content: 'Hello' }]);
259
+ expect(result.content).toBe('Single string output');
260
+ });
261
+
262
+ it('should check availability', async () => {
263
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: true } as Response);
264
+ const available = await provider.metadata.isAvailable();
265
+ expect(available).toBe(true);
266
+ });
267
+
268
+ it('should return false when unreachable', async () => {
269
+ vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(new Error('Connection refused'));
270
+ const available = await provider.metadata.isAvailable();
271
+ expect(available).toBe(false);
272
+ });
273
+
274
+ it('should use custom poll interval', () => {
275
+ const custom = new ReplicateProvider({ apiToken: 'token', pollInterval: 2000 });
276
+ expect(custom.name).toBe('replicate');
277
+ });
278
+ });
279
+
280
+ // ──────────────────────────────────────────
281
+ // DeepSeek Provider
282
+ // ──────────────────────────────────────────
283
+
284
+ describe('DeepSeekProvider', () => {
285
+ let provider: DeepSeekProvider;
286
+
287
+ beforeEach(() => {
288
+ provider = new DeepSeekProvider({ apiKey: 'test-deepseek-key' });
289
+ });
290
+
291
+ afterEach(() => {
292
+ vi.restoreAllMocks();
293
+ });
294
+
295
+ it('should have correct metadata', () => {
296
+ expect(provider.name).toBe('deepseek');
297
+ expect(provider.metadata.name).toBe('deepseek');
298
+ expect(provider.metadata.displayName).toBe('DeepSeek');
299
+ expect(provider.metadata.models['deepseek-chat']).toBeDefined();
300
+ expect(provider.metadata.models['deepseek-reasoner']).toBeDefined();
301
+ });
302
+
303
+ it('should have correct model capabilities', () => {
304
+ const chat = provider.metadata.models['deepseek-chat'];
305
+ expect(chat.supportsTools).toBe(true);
306
+ expect(chat.supportsStreaming).toBe(true);
307
+ expect(chat.strengths).toContain('reasoning');
308
+ expect(chat.strengths).toContain('code');
309
+
310
+ const reasoner = provider.metadata.models['deepseek-reasoner'];
311
+ expect(reasoner.strengths).toContain('reasoning');
312
+ expect(reasoner.strengths).toContain('math');
313
+ });
314
+
315
+ it('should make correct API call for complete', async () => {
316
+ const mockResponse = {
317
+ choices: [{ message: { role: 'assistant', content: 'Hello from DeepSeek!' }, finish_reason: 'stop' }],
318
+ usage: { prompt_tokens: 8, completion_tokens: 4 },
319
+ model: 'deepseek-chat',
320
+ };
321
+
322
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
323
+ ok: true,
324
+ json: async () => mockResponse,
325
+ } as Response);
326
+
327
+ const result = await provider.complete([{ role: 'user', content: 'Hello' }]);
328
+ expect(result.content).toBe('Hello from DeepSeek!');
329
+ expect(result.usage.inputTokens).toBe(8);
330
+ expect(result.usage.outputTokens).toBe(4);
331
+ });
332
+
333
+ it('should send correct headers and URL', async () => {
334
+ const mockResponse = {
335
+ choices: [{ message: { role: 'assistant', content: 'Hi' }, finish_reason: 'stop' }],
336
+ usage: { prompt_tokens: 1, completion_tokens: 1 },
337
+ model: 'deepseek-chat',
338
+ };
339
+
340
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
341
+ ok: true,
342
+ json: async () => mockResponse,
343
+ } as Response);
344
+
345
+ await provider.complete([{ role: 'user', content: 'Hi' }]);
346
+
347
+ expect(fetchSpy).toHaveBeenCalledWith(
348
+ 'https://api.deepseek.com/chat/completions',
349
+ expect.objectContaining({
350
+ method: 'POST',
351
+ headers: expect.objectContaining({
352
+ Authorization: 'Bearer test-deepseek-key',
353
+ }),
354
+ }),
355
+ );
356
+ });
357
+
358
+ it('should handle API errors', async () => {
359
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
360
+ ok: false,
361
+ status: 403,
362
+ statusText: 'Forbidden',
363
+ } as Response);
364
+
365
+ await expect(provider.complete([{ role: 'user', content: 'Hello' }]))
366
+ .rejects.toThrow('DeepSeek API error: 403');
367
+ });
368
+
369
+ it('should check availability', async () => {
370
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: true } as Response);
371
+ const available = await provider.metadata.isAvailable();
372
+ expect(available).toBe(true);
373
+ });
374
+
375
+ it('should return false when unreachable', async () => {
376
+ vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(new Error('Network error'));
377
+ const available = await provider.metadata.isAvailable();
378
+ expect(available).toBe(false);
379
+ });
380
+
381
+ it('should handle streaming errors', async () => {
382
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
383
+ ok: false,
384
+ status: 500,
385
+ statusText: 'Internal Server Error',
386
+ } as Response);
387
+
388
+ const chunks: Array<{ type: string; error?: string }> = [];
389
+ for await (const chunk of provider.stream([{ role: 'user', content: 'Hello' }])) {
390
+ chunks.push(chunk);
391
+ }
392
+
393
+ expect(chunks).toHaveLength(1);
394
+ expect(chunks[0].type).toBe('error');
395
+ expect(chunks[0].error).toContain('DeepSeek API error: 500');
396
+ });
397
+ });
398
+
399
+ // ──────────────────────────────────────────
400
+ // Cohere Provider
401
+ // ──────────────────────────────────────────
402
+
403
+ describe('CohereProvider', () => {
404
+ let provider: CohereProvider;
405
+
406
+ beforeEach(() => {
407
+ provider = new CohereProvider({ apiKey: 'test-cohere-key' });
408
+ });
409
+
410
+ afterEach(() => {
411
+ vi.restoreAllMocks();
412
+ });
413
+
414
+ it('should have correct metadata', () => {
415
+ expect(provider.name).toBe('cohere');
416
+ expect(provider.metadata.name).toBe('cohere');
417
+ expect(provider.metadata.displayName).toBe('Cohere');
418
+ expect(provider.metadata.models['command-r-plus']).toBeDefined();
419
+ expect(provider.metadata.models['command-r']).toBeDefined();
420
+ });
421
+
422
+ it('should have correct model capabilities', () => {
423
+ const model = provider.metadata.models['command-r-plus'];
424
+ expect(model.supportsTools).toBe(true);
425
+ expect(model.supportsStreaming).toBe(true);
426
+ expect(model.strengths).toContain('rag');
427
+ expect(model.strengths).toContain('multilingual');
428
+ expect(model.maxContextTokens).toBe(128000);
429
+ expect(model.isLocal).toBe(false);
430
+ });
431
+
432
+ it('should make correct API call for complete', async () => {
433
+ const mockResponse = {
434
+ message: {
435
+ role: 'assistant',
436
+ content: [{ type: 'text', text: 'Hello from Cohere!' }],
437
+ },
438
+ finish_reason: 'COMPLETE',
439
+ usage: {
440
+ tokens: { input_tokens: 12, output_tokens: 6 },
441
+ },
442
+ };
443
+
444
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
445
+ ok: true,
446
+ json: async () => mockResponse,
447
+ } as Response);
448
+
449
+ const result = await provider.complete([{ role: 'user', content: 'Hello' }]);
450
+ expect(result.content).toBe('Hello from Cohere!');
451
+ expect(result.usage.inputTokens).toBe(12);
452
+ expect(result.usage.outputTokens).toBe(6);
453
+ });
454
+
455
+ it('should send to correct URL', async () => {
456
+ const mockResponse = {
457
+ message: { role: 'assistant', content: [{ type: 'text', text: 'Hi' }] },
458
+ finish_reason: 'COMPLETE',
459
+ usage: { tokens: { input_tokens: 1, output_tokens: 1 } },
460
+ };
461
+
462
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
463
+ ok: true,
464
+ json: async () => mockResponse,
465
+ } as Response);
466
+
467
+ await provider.complete([{ role: 'user', content: 'Hi' }]);
468
+
469
+ expect(fetchSpy).toHaveBeenCalledWith(
470
+ 'https://api.cohere.com/v2/chat',
471
+ expect.objectContaining({
472
+ method: 'POST',
473
+ headers: expect.objectContaining({
474
+ Authorization: 'Bearer test-cohere-key',
475
+ }),
476
+ }),
477
+ );
478
+ });
479
+
480
+ it('should handle API errors', async () => {
481
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
482
+ ok: false,
483
+ status: 401,
484
+ statusText: 'Unauthorized',
485
+ } as Response);
486
+
487
+ await expect(provider.complete([{ role: 'user', content: 'Hello' }]))
488
+ .rejects.toThrow('Cohere API error: 401');
489
+ });
490
+
491
+ it('should use billed_units fallback for usage', async () => {
492
+ const mockResponse = {
493
+ message: {
494
+ role: 'assistant',
495
+ content: [{ type: 'text', text: 'Hi' }],
496
+ },
497
+ finish_reason: 'COMPLETE',
498
+ usage: {
499
+ billed_units: { input_tokens: 5, output_tokens: 3 },
500
+ },
501
+ };
502
+
503
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
504
+ ok: true,
505
+ json: async () => mockResponse,
506
+ } as Response);
507
+
508
+ const result = await provider.complete([{ role: 'user', content: 'Hi' }]);
509
+ expect(result.usage.inputTokens).toBe(5);
510
+ expect(result.usage.outputTokens).toBe(3);
511
+ });
512
+
513
+ it('should check availability', async () => {
514
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: true } as Response);
515
+ const available = await provider.metadata.isAvailable();
516
+ expect(available).toBe(true);
517
+ });
518
+
519
+ it('should return false when unreachable', async () => {
520
+ vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(new Error('Connection refused'));
521
+ const available = await provider.metadata.isAvailable();
522
+ expect(available).toBe(false);
523
+ });
524
+ });
525
+
526
+ // ──────────────────────────────────────────
527
+ // xAI Provider
528
+ // ──────────────────────────────────────────
529
+
530
+ describe('XAIProvider', () => {
531
+ let provider: XAIProvider;
532
+
533
+ beforeEach(() => {
534
+ provider = new XAIProvider({ apiKey: 'test-xai-key' });
535
+ });
536
+
537
+ afterEach(() => {
538
+ vi.restoreAllMocks();
539
+ });
540
+
541
+ it('should have correct metadata', () => {
542
+ expect(provider.name).toBe('xai');
543
+ expect(provider.metadata.name).toBe('xai');
544
+ expect(provider.metadata.displayName).toBe('xAI Grok');
545
+ expect(provider.metadata.models['grok-2']).toBeDefined();
546
+ expect(provider.metadata.models['grok-2-mini']).toBeDefined();
547
+ });
548
+
549
+ it('should have correct model capabilities', () => {
550
+ const grok2 = provider.metadata.models['grok-2'];
551
+ expect(grok2.supportsTools).toBe(true);
552
+ expect(grok2.supportsStreaming).toBe(true);
553
+ expect(grok2.maxContextTokens).toBe(131072);
554
+ expect(grok2.isLocal).toBe(false);
555
+ expect(grok2.strengths).toContain('reasoning');
556
+
557
+ const mini = provider.metadata.models['grok-2-mini'];
558
+ expect(mini.strengths).toContain('fast');
559
+ expect(mini.strengths).toContain('low-cost');
560
+ });
561
+
562
+ it('should make correct API call for complete', async () => {
563
+ const mockResponse = {
564
+ choices: [{ message: { role: 'assistant', content: 'Hello from Grok!' }, finish_reason: 'stop' }],
565
+ usage: { prompt_tokens: 7, completion_tokens: 3 },
566
+ model: 'grok-2',
567
+ };
568
+
569
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
570
+ ok: true,
571
+ json: async () => mockResponse,
572
+ } as Response);
573
+
574
+ const result = await provider.complete([{ role: 'user', content: 'Hello' }]);
575
+ expect(result.content).toBe('Hello from Grok!');
576
+ expect(result.usage.inputTokens).toBe(7);
577
+ expect(result.usage.outputTokens).toBe(3);
578
+ });
579
+
580
+ it('should send to correct URL', async () => {
581
+ const mockResponse = {
582
+ choices: [{ message: { role: 'assistant', content: 'Hi' }, finish_reason: 'stop' }],
583
+ usage: { prompt_tokens: 1, completion_tokens: 1 },
584
+ model: 'grok-2',
585
+ };
586
+
587
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
588
+ ok: true,
589
+ json: async () => mockResponse,
590
+ } as Response);
591
+
592
+ await provider.complete([{ role: 'user', content: 'Hi' }]);
593
+
594
+ expect(fetchSpy).toHaveBeenCalledWith(
595
+ 'https://api.x.ai/v1/chat/completions',
596
+ expect.objectContaining({
597
+ method: 'POST',
598
+ headers: expect.objectContaining({
599
+ Authorization: 'Bearer test-xai-key',
600
+ }),
601
+ }),
602
+ );
603
+ });
604
+
605
+ it('should handle API errors', async () => {
606
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
607
+ ok: false,
608
+ status: 503,
609
+ statusText: 'Service Unavailable',
610
+ } as Response);
611
+
612
+ await expect(provider.complete([{ role: 'user', content: 'Hello' }]))
613
+ .rejects.toThrow('xAI API error: 503');
614
+ });
615
+
616
+ it('should check availability', async () => {
617
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: true } as Response);
618
+ const available = await provider.metadata.isAvailable();
619
+ expect(available).toBe(true);
620
+ });
621
+
622
+ it('should return false when unreachable', async () => {
623
+ vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(new Error('Connection refused'));
624
+ const available = await provider.metadata.isAvailable();
625
+ expect(available).toBe(false);
626
+ });
627
+
628
+ it('should handle streaming errors', async () => {
629
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
630
+ ok: false,
631
+ status: 500,
632
+ statusText: 'Internal Server Error',
633
+ } as Response);
634
+
635
+ const chunks: Array<{ type: string; error?: string }> = [];
636
+ for await (const chunk of provider.stream([{ role: 'user', content: 'Hello' }])) {
637
+ chunks.push(chunk);
638
+ }
639
+
640
+ expect(chunks).toHaveLength(1);
641
+ expect(chunks[0].type).toBe('error');
642
+ expect(chunks[0].error).toContain('xAI API error: 500');
643
+ });
644
+ });
645
+
646
+ // ──────────────────────────────────────────
647
+ // ProviderFactory with new providers
648
+ // ──────────────────────────────────────────
649
+
650
+ describe('ProviderFactory with new providers', () => {
651
+ it('should create groq provider when API key present', () => {
652
+ const factory = new ProviderFactory({
653
+ primary: 'groq',
654
+ config: {
655
+ groq: { apiKey: 'test-groq-key' },
656
+ },
657
+ });
658
+ const provider = factory.getProvider('groq');
659
+ expect(provider.name).toBe('groq');
660
+ });
661
+
662
+ it('should create replicate provider when API token present', () => {
663
+ const factory = new ProviderFactory({
664
+ primary: 'replicate',
665
+ config: {
666
+ replicate: { apiToken: 'test-replicate-token' },
667
+ },
668
+ });
669
+ const provider = factory.getProvider('replicate');
670
+ expect(provider.name).toBe('replicate');
671
+ });
672
+
673
+ it('should create deepseek provider when API key present', () => {
674
+ const factory = new ProviderFactory({
675
+ primary: 'deepseek',
676
+ config: {
677
+ deepseek: { apiKey: 'test-deepseek-key' },
678
+ },
679
+ });
680
+ const provider = factory.getProvider('deepseek');
681
+ expect(provider.name).toBe('deepseek');
682
+ });
683
+
684
+ it('should create cohere provider when API key present', () => {
685
+ const factory = new ProviderFactory({
686
+ primary: 'cohere',
687
+ config: {
688
+ cohere: { apiKey: 'test-cohere-key' },
689
+ },
690
+ });
691
+ const provider = factory.getProvider('cohere');
692
+ expect(provider.name).toBe('cohere');
693
+ });
694
+
695
+ it('should create xai provider when API key present', () => {
696
+ const factory = new ProviderFactory({
697
+ primary: 'xai',
698
+ config: {
699
+ xai: { apiKey: 'test-xai-key' },
700
+ },
701
+ });
702
+ const provider = factory.getProvider('xai');
703
+ expect(provider.name).toBe('xai');
704
+ });
705
+
706
+ it('should list all configured providers including new ones', () => {
707
+ const factory = new ProviderFactory({
708
+ primary: 'groq',
709
+ config: {
710
+ groq: { apiKey: 'key1' },
711
+ deepseek: { apiKey: 'key2' },
712
+ cohere: { apiKey: 'key3' },
713
+ xai: { apiKey: 'key4' },
714
+ replicate: { apiToken: 'token1' },
715
+ },
716
+ });
717
+ const available = factory.listAvailable();
718
+ expect(available).toContain('groq');
719
+ expect(available).toContain('deepseek');
720
+ expect(available).toContain('cohere');
721
+ expect(available).toContain('xai');
722
+ expect(available).toContain('replicate');
723
+ });
724
+
725
+ it('should throw when getting unconfigured new provider', () => {
726
+ const factory = new ProviderFactory({
727
+ primary: 'groq',
728
+ config: {},
729
+ });
730
+ expect(() => factory.getProvider('groq')).toThrow('Provider not configured: groq');
731
+ });
732
+ });