mcp-rubber-duck 1.5.1 → 1.6.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 (69) hide show
  1. package/.claude/agents/pricing-updater.md +111 -0
  2. package/.claude/commands/update-pricing.md +22 -0
  3. package/.releaserc.json +4 -0
  4. package/CHANGELOG.md +14 -0
  5. package/dist/config/types.d.ts +72 -0
  6. package/dist/config/types.d.ts.map +1 -1
  7. package/dist/config/types.js +8 -0
  8. package/dist/config/types.js.map +1 -1
  9. package/dist/data/default-pricing.d.ts +18 -0
  10. package/dist/data/default-pricing.d.ts.map +1 -0
  11. package/dist/data/default-pricing.js +307 -0
  12. package/dist/data/default-pricing.js.map +1 -0
  13. package/dist/providers/enhanced-manager.d.ts +2 -1
  14. package/dist/providers/enhanced-manager.d.ts.map +1 -1
  15. package/dist/providers/enhanced-manager.js +20 -2
  16. package/dist/providers/enhanced-manager.js.map +1 -1
  17. package/dist/providers/manager.d.ts +3 -1
  18. package/dist/providers/manager.d.ts.map +1 -1
  19. package/dist/providers/manager.js +12 -1
  20. package/dist/providers/manager.js.map +1 -1
  21. package/dist/server.d.ts +2 -0
  22. package/dist/server.d.ts.map +1 -1
  23. package/dist/server.js +35 -4
  24. package/dist/server.js.map +1 -1
  25. package/dist/services/pricing.d.ts +56 -0
  26. package/dist/services/pricing.d.ts.map +1 -0
  27. package/dist/services/pricing.js +124 -0
  28. package/dist/services/pricing.js.map +1 -0
  29. package/dist/services/usage.d.ts +48 -0
  30. package/dist/services/usage.d.ts.map +1 -0
  31. package/dist/services/usage.js +243 -0
  32. package/dist/services/usage.js.map +1 -0
  33. package/dist/tools/get-usage-stats.d.ts +8 -0
  34. package/dist/tools/get-usage-stats.d.ts.map +1 -0
  35. package/dist/tools/get-usage-stats.js +92 -0
  36. package/dist/tools/get-usage-stats.js.map +1 -0
  37. package/package.json +1 -1
  38. package/src/config/types.ts +51 -0
  39. package/src/data/default-pricing.ts +368 -0
  40. package/src/providers/enhanced-manager.ts +41 -4
  41. package/src/providers/manager.ts +22 -1
  42. package/src/server.ts +42 -4
  43. package/src/services/pricing.ts +155 -0
  44. package/src/services/usage.ts +293 -0
  45. package/src/tools/get-usage-stats.ts +109 -0
  46. package/tests/approval.test.ts +440 -0
  47. package/tests/cache.test.ts +240 -0
  48. package/tests/config.test.ts +468 -0
  49. package/tests/consensus.test.ts +10 -0
  50. package/tests/conversation.test.ts +86 -0
  51. package/tests/duck-debate.test.ts +105 -1
  52. package/tests/duck-iterate.test.ts +30 -0
  53. package/tests/duck-judge.test.ts +93 -0
  54. package/tests/duck-vote.test.ts +46 -0
  55. package/tests/health.test.ts +129 -0
  56. package/tests/pricing.test.ts +335 -0
  57. package/tests/providers.test.ts +591 -0
  58. package/tests/safe-logger.test.ts +314 -0
  59. package/tests/tools/approve-mcp-request.test.ts +239 -0
  60. package/tests/tools/ask-duck.test.ts +159 -0
  61. package/tests/tools/chat-duck.test.ts +191 -0
  62. package/tests/tools/compare-ducks.test.ts +190 -0
  63. package/tests/tools/duck-council.test.ts +219 -0
  64. package/tests/tools/get-pending-approvals.test.ts +195 -0
  65. package/tests/tools/get-usage-stats.test.ts +236 -0
  66. package/tests/tools/list-ducks.test.ts +144 -0
  67. package/tests/tools/list-models.test.ts +163 -0
  68. package/tests/tools/mcp-status.test.ts +330 -0
  69. package/tests/usage.test.ts +661 -0
@@ -266,4 +266,595 @@ describe('ProviderManager', () => {
266
266
  const responses = await manager.duckCouncil('Hello');
267
267
  expect(responses).toHaveLength(2);
268
268
  });
269
+
270
+ it('should get all providers info', () => {
271
+ const providers = manager.getAllProviders();
272
+ expect(providers).toHaveLength(2);
273
+ expect(providers[0].name).toBe('test1');
274
+ expect(providers[0].info.nickname).toBe('Duck 1');
275
+ expect(providers[1].name).toBe('test2');
276
+ expect(providers[1].info.nickname).toBe('Duck 2');
277
+ });
278
+
279
+ it('should validate model for provider', () => {
280
+ // Valid model should return true
281
+ const isValid = manager.validateModel('test1', 'model1');
282
+ expect(isValid).toBe(true);
283
+ });
284
+
285
+ it('should return false for invalid model', () => {
286
+ const isValid = manager.validateModel('test1', 'nonexistent-model');
287
+ expect(isValid).toBe(false);
288
+ });
289
+
290
+ it('should handle error in compareDucks gracefully', async () => {
291
+ // Make one provider fail
292
+ const provider1 = manager.getProvider('test1');
293
+ provider1['client'].chat.completions.create = jest.fn().mockRejectedValue(new Error('API Error'));
294
+
295
+ const responses = await manager.compareDucks('Hello', ['test1', 'test2']);
296
+
297
+ // Should still return responses, with error message for failed provider
298
+ expect(responses).toHaveLength(2);
299
+ expect(responses[0].content).toContain('Error');
300
+ expect(responses[1].content).toBe('Mocked response');
301
+ });
302
+
303
+ it('should throw error when no valid providers in compareDucks', async () => {
304
+ await expect(manager.compareDucks('Hello', ['nonexistent'])).rejects.toThrow(
305
+ 'No valid providers specified'
306
+ );
307
+ });
308
+
309
+ it('should check health of all providers', async () => {
310
+ const results = await manager.checkHealth();
311
+ expect(results).toHaveLength(2);
312
+ expect(results[0].provider).toBe('test1');
313
+ expect(results[1].provider).toBe('test2');
314
+ });
315
+
316
+ it('should check health of specific provider', async () => {
317
+ const results = await manager.checkHealth('test1');
318
+ expect(results).toHaveLength(1);
319
+ expect(results[0].provider).toBe('test1');
320
+ });
321
+
322
+ it('should handle health check that returns false', async () => {
323
+ // Mock the health check to return false (not throw)
324
+ const provider1 = manager.getProvider('test1');
325
+ provider1['client'].chat.completions.create = jest.fn().mockResolvedValue({
326
+ choices: [{ message: { content: '' } }], // Empty content = unhealthy
327
+ });
328
+
329
+ const results = await manager.checkHealth('test1');
330
+
331
+ expect(results).toHaveLength(1);
332
+ expect(results[0].healthy).toBe(false);
333
+ });
334
+
335
+ it('should throw error when getting models for nonexistent provider', async () => {
336
+ await expect(manager.getAvailableModels('nonexistent')).rejects.toThrow(
337
+ 'Provider nonexistent not found'
338
+ );
339
+ });
340
+
341
+ it('should return false when validating model for nonexistent provider', () => {
342
+ const isValid = manager.validateModel('nonexistent', 'model1');
343
+ expect(isValid).toBe(false);
344
+ });
345
+
346
+ it('should return true for any model when provider has no models list', () => {
347
+ // Create manager with a provider that has no models configured
348
+ const noModelsConfigManager = {
349
+ getConfig: jest.fn().mockReturnValue({
350
+ providers: {
351
+ testNoModels: {
352
+ api_key: 'key1',
353
+ base_url: 'https://api.test.com/v1',
354
+ default_model: 'model1',
355
+ nickname: 'Test No Models',
356
+ // No models array - undefined
357
+ },
358
+ },
359
+ default_provider: 'testNoModels',
360
+ cache_ttl: 300,
361
+ enable_failover: false,
362
+ default_temperature: 0.7,
363
+ }),
364
+ } as any;
365
+
366
+ const noModelsManager = new ProviderManager(noModelsConfigManager);
367
+
368
+ // When provider has no availableModels, should return true (let API validate)
369
+ const isValid = noModelsManager.validateModel('testNoModels', 'any-model-name');
370
+ expect(isValid).toBe(true);
371
+ });
372
+ });
373
+
374
+ describe('ProviderManager Error Cases', () => {
375
+ it('should throw error when no default provider and none specified', () => {
376
+ const mockConfigManager = {
377
+ getConfig: jest.fn().mockReturnValue({
378
+ providers: {
379
+ test1: {
380
+ api_key: 'key1',
381
+ base_url: 'https://api1.test.com/v1',
382
+ default_model: 'model1',
383
+ nickname: 'Duck 1',
384
+ },
385
+ },
386
+ // No default_provider set
387
+ cache_ttl: 300,
388
+ enable_failover: false,
389
+ default_temperature: 0.7,
390
+ }),
391
+ } as any;
392
+
393
+ const manager = new ProviderManager(mockConfigManager);
394
+
395
+ // Override the client
396
+ const provider = manager.getProvider('test1');
397
+ provider['client'].chat.completions.create = mockCreate;
398
+
399
+ // This should work since we're specifying the provider
400
+ expect(() => manager.getProvider('test1')).not.toThrow();
401
+ });
402
+
403
+ it('should throw error when getProvider called without name and no default', () => {
404
+ const mockConfigManager = {
405
+ getConfig: jest.fn().mockReturnValue({
406
+ providers: {
407
+ test1: {
408
+ api_key: 'key1',
409
+ base_url: 'https://api1.test.com/v1',
410
+ default_model: 'model1',
411
+ nickname: 'Duck 1',
412
+ },
413
+ },
414
+ // No default_provider set
415
+ cache_ttl: 300,
416
+ enable_failover: false,
417
+ default_temperature: 0.7,
418
+ }),
419
+ } as any;
420
+
421
+ const manager = new ProviderManager(mockConfigManager);
422
+
423
+ expect(() => manager.getProvider()).toThrow(
424
+ 'No provider specified and no default provider configured'
425
+ );
426
+ });
427
+ });
428
+
429
+ describe('ProviderManager Health Check Exception', () => {
430
+ let manager: ProviderManager;
431
+ let mockConfigManager: jest.Mocked<ConfigManager>;
432
+
433
+ beforeEach(() => {
434
+ jest.clearAllMocks();
435
+
436
+ mockConfigManager = {
437
+ getConfig: jest.fn().mockReturnValue({
438
+ providers: {
439
+ test1: {
440
+ api_key: 'key1',
441
+ base_url: 'https://api1.test.com/v1',
442
+ default_model: 'model1',
443
+ nickname: 'Duck 1',
444
+ },
445
+ },
446
+ default_provider: 'test1',
447
+ cache_ttl: 300,
448
+ enable_failover: false,
449
+ default_temperature: 0.7,
450
+ }),
451
+ } as any;
452
+
453
+ manager = new ProviderManager(mockConfigManager);
454
+ });
455
+
456
+ it('should handle health check that fails with rejected promise', async () => {
457
+ const provider = manager.getProvider('test1');
458
+ provider['client'].chat.completions.create = jest.fn().mockRejectedValue(
459
+ new Error('Network timeout')
460
+ );
461
+
462
+ const results = await manager.checkHealth('test1');
463
+
464
+ expect(results).toHaveLength(1);
465
+ // DuckProvider.healthCheck() catches errors internally and returns false
466
+ // so error is not propagated to manager
467
+ expect(results[0].healthy).toBe(false);
468
+ });
469
+
470
+ it('should handle health check when provider.healthCheck itself throws', async () => {
471
+ const provider = manager.getProvider('test1');
472
+ // Override healthCheck to actually throw (unlike normal behavior)
473
+ provider.healthCheck = jest.fn().mockRejectedValue(new Error('Unexpected error'));
474
+
475
+ const results = await manager.checkHealth('test1');
476
+
477
+ expect(results).toHaveLength(1);
478
+ expect(results[0].healthy).toBe(false);
479
+ expect(results[0].error).toBe('Unexpected error');
480
+ });
481
+
482
+ it('should handle non-Error exception from healthCheck', async () => {
483
+ const provider = manager.getProvider('test1');
484
+ // Override healthCheck to throw a non-Error
485
+ provider.healthCheck = jest.fn().mockRejectedValue('String error');
486
+
487
+ const results = await manager.checkHealth('test1');
488
+
489
+ expect(results).toHaveLength(1);
490
+ expect(results[0].healthy).toBe(false);
491
+ expect(results[0].error).toBe('String error');
492
+ });
493
+ });
494
+
495
+ describe('ProviderManager Failover', () => {
496
+ let manager: ProviderManager;
497
+ let mockConfigManager: jest.Mocked<ConfigManager>;
498
+
499
+ beforeEach(() => {
500
+ jest.clearAllMocks();
501
+
502
+ mockCreate.mockResolvedValue({
503
+ choices: [{ message: { content: 'Mocked response' }, finish_reason: 'stop' }],
504
+ usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
505
+ model: 'model2',
506
+ });
507
+
508
+ mockConfigManager = {
509
+ getConfig: jest.fn().mockReturnValue({
510
+ providers: {
511
+ test1: {
512
+ api_key: 'key1',
513
+ base_url: 'https://api1.test.com/v1',
514
+ default_model: 'model1',
515
+ nickname: 'Duck 1',
516
+ },
517
+ test2: {
518
+ api_key: 'key2',
519
+ base_url: 'https://api2.test.com/v1',
520
+ default_model: 'model2',
521
+ nickname: 'Duck 2',
522
+ },
523
+ },
524
+ default_provider: 'test1',
525
+ cache_ttl: 300,
526
+ enable_failover: true, // Enable failover
527
+ default_temperature: 0.7,
528
+ }),
529
+ } as any;
530
+
531
+ manager = new ProviderManager(mockConfigManager);
532
+
533
+ // Set up mock clients
534
+ const provider1 = manager.getProvider('test1');
535
+ const provider2 = manager.getProvider('test2');
536
+ provider2['client'].chat.completions.create = mockCreate;
537
+ provider1['client'].chat.completions.create = jest.fn().mockRejectedValue(
538
+ new Error('Primary provider failed')
539
+ );
540
+ });
541
+
542
+ it('should failover to another provider when primary fails', async () => {
543
+ // Call askDuck without specifying provider (use default)
544
+ const response = await manager.askDuck(undefined, 'Hello');
545
+
546
+ expect(response.provider).toBe('test2');
547
+ expect(response.content).toBe('Mocked response');
548
+ });
549
+
550
+ it('should throw when all providers fail during failover', async () => {
551
+ // Make both providers fail
552
+ const provider2 = manager.getProvider('test2');
553
+ provider2['client'].chat.completions.create = jest.fn().mockRejectedValue(
554
+ new Error('Secondary also failed')
555
+ );
556
+
557
+ await expect(manager.askDuck(undefined, 'Hello')).rejects.toThrow(
558
+ 'All ducks have flown away!'
559
+ );
560
+ });
561
+
562
+ it('should not failover when provider is explicitly specified', async () => {
563
+ await expect(manager.askDuck('test1', 'Hello')).rejects.toThrow(
564
+ 'Primary provider failed'
565
+ );
566
+ });
567
+ });
568
+
569
+ describe('ProviderManager getAllModels', () => {
570
+ let manager: ProviderManager;
571
+ let mockConfigManager: jest.Mocked<ConfigManager>;
572
+
573
+ beforeEach(() => {
574
+ jest.clearAllMocks();
575
+
576
+ mockConfigManager = {
577
+ getConfig: jest.fn().mockReturnValue({
578
+ providers: {
579
+ test1: {
580
+ api_key: 'key1',
581
+ base_url: 'https://api1.test.com/v1',
582
+ default_model: 'model1',
583
+ nickname: 'Duck 1',
584
+ models: ['model1'],
585
+ },
586
+ test2: {
587
+ api_key: 'key2',
588
+ base_url: 'https://api2.test.com/v1',
589
+ default_model: 'model2',
590
+ nickname: 'Duck 2',
591
+ models: ['model2', 'model2b'],
592
+ },
593
+ },
594
+ default_provider: 'test1',
595
+ cache_ttl: 300,
596
+ enable_failover: false,
597
+ default_temperature: 0.7,
598
+ }),
599
+ } as any;
600
+
601
+ manager = new ProviderManager(mockConfigManager);
602
+ });
603
+
604
+ it('should return models from all providers', async () => {
605
+ // Mock listModels to use fallback (already configured models)
606
+ const provider1 = manager.getProvider('test1');
607
+ const provider2 = manager.getProvider('test2');
608
+ provider1['client'].models = { list: jest.fn().mockRejectedValue(new Error('API error')) } as any;
609
+ provider2['client'].models = { list: jest.fn().mockRejectedValue(new Error('API error')) } as any;
610
+
611
+ const allModels = await manager.getAllModels();
612
+
613
+ expect(allModels.size).toBe(2);
614
+ expect(allModels.get('test1')).toHaveLength(1);
615
+ expect(allModels.get('test2')).toHaveLength(2);
616
+ });
617
+
618
+ it('should return empty array for providers that fail', async () => {
619
+ const provider1 = manager.getProvider('test1');
620
+ // Make listModels throw by accessing undefined (simulating real error)
621
+ provider1.listModels = jest.fn().mockRejectedValue(new Error('Fatal error'));
622
+
623
+ const allModels = await manager.getAllModels();
624
+
625
+ expect(allModels.get('test1')).toEqual([]);
626
+ });
627
+ });
628
+
629
+ describe('DuckProvider Health Check', () => {
630
+ let provider: DuckProvider;
631
+
632
+ beforeEach(() => {
633
+ jest.clearAllMocks();
634
+
635
+ mockCreate.mockResolvedValue({
636
+ choices: [{
637
+ message: { content: 'healthy' },
638
+ finish_reason: 'stop',
639
+ }],
640
+ });
641
+
642
+ provider = new DuckProvider('test', 'Test Duck', {
643
+ apiKey: 'test-key',
644
+ baseURL: 'https://api.test.com/v1',
645
+ model: 'test-model',
646
+ });
647
+
648
+ provider['client'].chat.completions.create = mockCreate;
649
+ });
650
+
651
+ it('should return true when health check succeeds', async () => {
652
+ const isHealthy = await provider.healthCheck();
653
+ expect(isHealthy).toBe(true);
654
+ expect(mockCreate).toHaveBeenCalled();
655
+ });
656
+
657
+ it('should return false when health check fails', async () => {
658
+ mockCreate.mockRejectedValue(new Error('Connection failed'));
659
+
660
+ const isHealthy = await provider.healthCheck();
661
+ expect(isHealthy).toBe(false);
662
+ });
663
+
664
+ it('should return false when response has no content', async () => {
665
+ mockCreate.mockResolvedValue({
666
+ choices: [{
667
+ message: { content: '' },
668
+ finish_reason: 'stop',
669
+ }],
670
+ });
671
+
672
+ const isHealthy = await provider.healthCheck();
673
+ expect(isHealthy).toBe(false);
674
+ });
675
+
676
+ it('should return false when response has null content', async () => {
677
+ mockCreate.mockResolvedValue({
678
+ choices: [{
679
+ message: { content: null },
680
+ finish_reason: 'stop',
681
+ }],
682
+ });
683
+
684
+ const isHealthy = await provider.healthCheck();
685
+ expect(isHealthy).toBe(false);
686
+ });
687
+ });
688
+
689
+ describe('DuckProvider listModels', () => {
690
+ let provider: DuckProvider;
691
+
692
+ beforeEach(() => {
693
+ jest.clearAllMocks();
694
+
695
+ provider = new DuckProvider('test', 'Test Duck', {
696
+ apiKey: 'test-key',
697
+ baseURL: 'https://api.test.com/v1',
698
+ model: 'test-model',
699
+ availableModels: ['model-a', 'model-b'],
700
+ });
701
+ });
702
+
703
+ it('should return models from API on success', async () => {
704
+ // Create async iterator for models
705
+ const mockModels = [
706
+ { id: 'gpt-4', created: 1234567890, owned_by: 'openai', object: 'model' },
707
+ { id: 'gpt-3.5', created: 1234567880, owned_by: 'openai', object: 'model' },
708
+ ];
709
+ const asyncIterator = (async function* () {
710
+ for (const model of mockModels) {
711
+ yield model;
712
+ }
713
+ })();
714
+
715
+ provider['client'].models = {
716
+ list: jest.fn().mockResolvedValue(asyncIterator),
717
+ } as any;
718
+
719
+ const models = await provider.listModels();
720
+
721
+ expect(models).toHaveLength(2);
722
+ expect(models[0].id).toBe('gpt-4');
723
+ expect(models[1].id).toBe('gpt-3.5');
724
+ });
725
+
726
+ it('should fallback to configured models when API fails', async () => {
727
+ provider['client'].models = {
728
+ list: jest.fn().mockRejectedValue(new Error('API error')),
729
+ } as any;
730
+
731
+ const models = await provider.listModels();
732
+
733
+ expect(models).toHaveLength(2);
734
+ expect(models[0].id).toBe('model-a');
735
+ expect(models[0].description).toBe('Configured model (not fetched from API)');
736
+ expect(models[1].id).toBe('model-b');
737
+ });
738
+
739
+ it('should fallback to default model when API fails and no configured models', async () => {
740
+ // Create provider without availableModels
741
+ const providerNoModels = new DuckProvider('test', 'Test Duck', {
742
+ apiKey: 'test-key',
743
+ baseURL: 'https://api.test.com/v1',
744
+ model: 'default-model',
745
+ });
746
+
747
+ providerNoModels['client'].models = {
748
+ list: jest.fn().mockRejectedValue(new Error('API error')),
749
+ } as any;
750
+
751
+ const models = await providerNoModels.listModels();
752
+
753
+ expect(models).toHaveLength(1);
754
+ expect(models[0].id).toBe('default-model');
755
+ expect(models[0].description).toBe('Default configured model');
756
+ });
757
+
758
+ it('should fallback to default model when API fails and configured models is empty', async () => {
759
+ // Create provider with empty availableModels
760
+ const providerEmptyModels = new DuckProvider('test', 'Test Duck', {
761
+ apiKey: 'test-key',
762
+ baseURL: 'https://api.test.com/v1',
763
+ model: 'fallback-model',
764
+ availableModels: [],
765
+ });
766
+
767
+ providerEmptyModels['client'].models = {
768
+ list: jest.fn().mockRejectedValue(new Error('API error')),
769
+ } as any;
770
+
771
+ const models = await providerEmptyModels.listModels();
772
+
773
+ expect(models).toHaveLength(1);
774
+ expect(models[0].id).toBe('fallback-model');
775
+ expect(models[0].description).toBe('Default configured model');
776
+ });
777
+
778
+ it('should handle non-Error thrown from API', async () => {
779
+ provider['client'].models = {
780
+ list: jest.fn().mockRejectedValue('String error'),
781
+ } as any;
782
+
783
+ const models = await provider.listModels();
784
+
785
+ // Should still fallback to configured models
786
+ expect(models).toHaveLength(2);
787
+ expect(models[0].id).toBe('model-a');
788
+ });
789
+ });
790
+
791
+ describe('DuckProvider Error Handling', () => {
792
+ let provider: DuckProvider;
793
+
794
+ beforeEach(() => {
795
+ jest.clearAllMocks();
796
+
797
+ provider = new DuckProvider('test', 'Test Duck', {
798
+ apiKey: 'test-key',
799
+ baseURL: 'https://api.test.com/v1',
800
+ model: 'test-model',
801
+ });
802
+
803
+ provider['client'].chat.completions.create = mockCreate;
804
+ });
805
+
806
+ it('should throw error when API call fails', async () => {
807
+ mockCreate.mockRejectedValue(new Error('API rate limited'));
808
+
809
+ await expect(
810
+ provider.chat({
811
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
812
+ })
813
+ ).rejects.toThrow('API rate limited');
814
+ });
815
+
816
+ it('should throw error when response has empty choices array', async () => {
817
+ mockCreate.mockResolvedValue({
818
+ choices: [],
819
+ usage: { prompt_tokens: 10, completion_tokens: 0, total_tokens: 10 },
820
+ model: 'test-model',
821
+ });
822
+
823
+ // When choices is empty, accessing choices[0].message throws
824
+ await expect(
825
+ provider.chat({
826
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
827
+ })
828
+ ).rejects.toThrow("couldn't respond");
829
+ });
830
+
831
+ it('should handle system prompt in options', async () => {
832
+ mockCreate.mockResolvedValue({
833
+ choices: [{
834
+ message: { content: 'Response with system prompt' },
835
+ finish_reason: 'stop',
836
+ }],
837
+ usage: { prompt_tokens: 15, completion_tokens: 5, total_tokens: 20 },
838
+ model: 'test-model',
839
+ });
840
+
841
+ const providerWithSystem = new DuckProvider('test', 'Test Duck', {
842
+ apiKey: 'test-key',
843
+ baseURL: 'https://api.test.com/v1',
844
+ model: 'test-model',
845
+ systemPrompt: 'You are a helpful assistant',
846
+ });
847
+ providerWithSystem['client'].chat.completions.create = mockCreate;
848
+
849
+ const response = await providerWithSystem.chat({
850
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
851
+ });
852
+
853
+ expect(response.content).toBe('Response with system prompt');
854
+
855
+ // Verify system prompt was included in the call
856
+ const callArgs = mockCreate.mock.calls[0][0];
857
+ expect(callArgs.messages[0].role).toBe('system');
858
+ expect(callArgs.messages[0].content).toBe('You are a helpful assistant');
859
+ });
269
860
  });