@lobehub/chat 1.138.3 → 1.138.5

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 (24) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/package.json +1 -1
  4. package/packages/database/src/repositories/aiInfra/index.test.ts +656 -0
  5. package/packages/model-runtime/src/core/contextBuilders/google.test.ts +585 -0
  6. package/packages/model-runtime/src/core/contextBuilders/google.ts +201 -0
  7. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +191 -179
  8. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +305 -47
  9. package/packages/model-runtime/src/providers/anthropic/generateObject.test.ts +93 -84
  10. package/packages/model-runtime/src/providers/anthropic/generateObject.ts +3 -3
  11. package/packages/model-runtime/src/providers/google/generateObject.test.ts +588 -83
  12. package/packages/model-runtime/src/providers/google/generateObject.ts +104 -6
  13. package/packages/model-runtime/src/providers/google/index.test.ts +0 -395
  14. package/packages/model-runtime/src/providers/google/index.ts +28 -194
  15. package/packages/model-runtime/src/providers/openai/index.test.ts +18 -17
  16. package/packages/model-runtime/src/types/structureOutput.ts +3 -4
  17. package/packages/types/src/aiChat.ts +0 -1
  18. package/src/app/(backend)/trpc/edge/[trpc]/route.ts +0 -2
  19. package/src/server/routers/edge/index.ts +2 -1
  20. package/src/server/routers/lambda/aiChat.ts +1 -2
  21. package/src/server/routers/lambda/index.ts +2 -0
  22. package/src/server/routers/lambda/upload.ts +16 -0
  23. package/src/services/__tests__/upload.test.ts +266 -18
  24. package/src/services/upload.ts +2 -2
@@ -419,6 +419,318 @@ describe('AiInfraRepos', () => {
419
419
  // For custom provider, when user enables search with no builtin settings, default to 'params'
420
420
  expect(merged?.settings).toEqual({ searchImpl: 'params' });
421
421
  });
422
+
423
+ // 测试场景:用户模型 abilitie 为空(Empty),而基础模型有搜索能力和设置
424
+ it('should retain builtin abilities and settings when user model has no abilities (empty) and builtin has settings', async () => {
425
+ const mockProviders = [
426
+ { enabled: true, id: 'openai', name: 'OpenAI', source: 'builtin' as const },
427
+ ];
428
+
429
+ const userModel: EnabledAiModel = {
430
+ id: 'gpt-4',
431
+ providerId: 'openai',
432
+ enabled: true,
433
+ type: 'chat',
434
+ abilities: {}, // Empty object, no search
435
+ };
436
+
437
+ const builtinModel = {
438
+ id: 'gpt-4',
439
+ enabled: true,
440
+ type: 'chat' as const,
441
+ abilities: { search: false }, // 使用 builtin abilities
442
+ settings: { searchImpl: 'params', searchProvider: 'google' }, // builtin has settings
443
+ };
444
+
445
+ vi.spyOn(repo, 'getAiProviderList').mockResolvedValue(mockProviders);
446
+ vi.spyOn(repo.aiModelModel, 'getAllModels').mockResolvedValue([userModel]);
447
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue([builtinModel]);
448
+
449
+ const result = await repo.getEnabledModels();
450
+
451
+ const merged = result.find((m) => m.id === 'gpt-4');
452
+ expect(merged).toBeDefined();
453
+ // 使用 builtin abilities
454
+ expect(merged?.abilities?.search).toEqual(false);
455
+ // 删去 builtin settings
456
+ expect(merged?.settings).toBeUndefined();
457
+ });
458
+
459
+ it('should retain builtin abilities and settings when user model has no abilities (empty) and builtin has settings', async () => {
460
+ const mockProviders = [
461
+ { enabled: true, id: 'openai', name: 'OpenAI', source: 'builtin' as const },
462
+ ];
463
+
464
+ const userModel: EnabledAiModel = {
465
+ id: 'gpt-4',
466
+ providerId: 'openai',
467
+ enabled: true,
468
+ type: 'chat',
469
+ abilities: {}, // Empty object, no search
470
+ };
471
+
472
+ const builtinModel = {
473
+ id: 'gpt-4',
474
+ enabled: true,
475
+ type: 'chat' as const,
476
+ abilities: { search: true }, // 使用 builtin abilities
477
+ settings: { searchImpl: 'params', searchProvider: 'google' }, // builtin has settings
478
+ };
479
+
480
+ vi.spyOn(repo, 'getAiProviderList').mockResolvedValue(mockProviders);
481
+ vi.spyOn(repo.aiModelModel, 'getAllModels').mockResolvedValue([userModel]);
482
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue([builtinModel]);
483
+
484
+ const result = await repo.getEnabledModels();
485
+
486
+ const merged = result.find((m) => m.id === 'gpt-4');
487
+ expect(merged).toBeDefined();
488
+ // 使用 builtin abilities
489
+ expect(merged?.abilities?.search).toEqual(true);
490
+ // 保留 builtin settings
491
+ expect(merged?.settings).toEqual({ searchImpl: 'params', searchProvider: 'google' });
492
+ });
493
+
494
+ // 测试场景:用户模型未启用搜索(abilities.search 为 undefined),而基础模型有搜索能力和设置
495
+ it('should retain builtin settings when user model has no abilities.search (undefined) and builtin has settings', async () => {
496
+ const mockProviders = [
497
+ { enabled: true, id: 'openai', name: 'OpenAI', source: 'builtin' as const },
498
+ ];
499
+
500
+ const userModel: EnabledAiModel = {
501
+ id: 'gpt-4',
502
+ providerId: 'openai',
503
+ enabled: true,
504
+ type: 'chat',
505
+ abilities: { vision: true }, // 启用 vision 能力, no search
506
+ };
507
+
508
+ const builtinModel = {
509
+ id: 'gpt-4',
510
+ enabled: true,
511
+ type: 'chat' as const,
512
+ abilities: { search: false }, // builtin abilities 不生效
513
+ settings: { searchImpl: 'params', searchProvider: 'google' }, // builtin has settings
514
+ };
515
+
516
+ vi.spyOn(repo, 'getAiProviderList').mockResolvedValue(mockProviders);
517
+ vi.spyOn(repo.aiModelModel, 'getAllModels').mockResolvedValue([userModel]);
518
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue([builtinModel]);
519
+
520
+ const result = await repo.getEnabledModels();
521
+
522
+ const merged = result.find((m) => m.id === 'gpt-4');
523
+ expect(merged).toBeDefined();
524
+ // abilities.search 仍 undefined(兼容老版本)
525
+ expect(merged?.abilities?.search).toBeUndefined();
526
+ // 保留 builtin settings
527
+ expect(merged?.settings).toEqual({ searchImpl: 'params', searchProvider: 'google' });
528
+ });
529
+
530
+ it('should retain builtin settings when user model has no abilities.search (undefined) and builtin has settings', async () => {
531
+ const mockProviders = [
532
+ { enabled: true, id: 'openai', name: 'OpenAI', source: 'builtin' as const },
533
+ ];
534
+
535
+ const userModel: EnabledAiModel = {
536
+ id: 'gpt-4',
537
+ providerId: 'openai',
538
+ enabled: true,
539
+ type: 'chat',
540
+ abilities: { vision: true }, // 启用 vision 能力, no search
541
+ };
542
+
543
+ const builtinModel = {
544
+ id: 'gpt-4',
545
+ enabled: true,
546
+ type: 'chat' as const,
547
+ abilities: { search: true }, // builtin abilities 不生效
548
+ settings: { searchImpl: 'params', searchProvider: 'google' }, // builtin has settings
549
+ };
550
+
551
+ vi.spyOn(repo, 'getAiProviderList').mockResolvedValue(mockProviders);
552
+ vi.spyOn(repo.aiModelModel, 'getAllModels').mockResolvedValue([userModel]);
553
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue([builtinModel]);
554
+
555
+ const result = await repo.getEnabledModels();
556
+
557
+ const merged = result.find((m) => m.id === 'gpt-4');
558
+ expect(merged).toBeDefined();
559
+ // abilities.search 仍 undefined(兼容老版本)
560
+ expect(merged?.abilities?.search).toBeUndefined();
561
+ // 保留 builtin settings
562
+ expect(merged?.settings).toEqual({ searchImpl: 'params', searchProvider: 'google' });
563
+ });
564
+
565
+ // 测试场景:用户模型未启用搜索(abilities.search 为 undefined),而基础模型也无搜索能力和设置
566
+ it('should retain no settings when user model has no abilities.search (undefined) and builtin has no settings', async () => {
567
+ const mockProviders = [
568
+ { enabled: true, id: 'openai', name: 'OpenAI', source: 'builtin' as const },
569
+ ];
570
+
571
+ const userModel: EnabledAiModel = {
572
+ id: 'gpt-4',
573
+ providerId: 'openai',
574
+ enabled: true,
575
+ type: 'chat',
576
+ abilities: {}, // 无 search
577
+ };
578
+
579
+ const builtinModel = {
580
+ id: 'gpt-4',
581
+ enabled: true,
582
+ type: 'chat' as const,
583
+ abilities: {},
584
+ // builtin 无 settings
585
+ };
586
+
587
+ vi.spyOn(repo, 'getAiProviderList').mockResolvedValue(mockProviders);
588
+ vi.spyOn(repo.aiModelModel, 'getAllModels').mockResolvedValue([userModel]);
589
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue([builtinModel]);
590
+
591
+ const result = await repo.getEnabledModels();
592
+
593
+ const merged = result.find((m) => m.id === 'gpt-4');
594
+ expect(merged).toBeDefined();
595
+ expect(merged?.abilities?.search).toBeUndefined();
596
+ // 无 settings
597
+ expect(merged?.settings).toBeUndefined();
598
+ });
599
+
600
+ // 测试:用户模型有 abilities.search: true
601
+ it('should inject defaults when user has search: true, no existing settings (builtin none)', async () => {
602
+ const mockProviders = [
603
+ { enabled: true, id: 'openai', name: 'OpenAI', source: 'builtin' as const },
604
+ ];
605
+
606
+ const userModel: EnabledAiModel = {
607
+ id: 'gpt-4',
608
+ providerId: 'openai',
609
+ enabled: true,
610
+ type: 'chat',
611
+ abilities: { search: true }, // 用户启用 search
612
+ };
613
+
614
+ const builtinModel = {
615
+ id: 'gpt-4',
616
+ enabled: true,
617
+ type: 'chat' as const,
618
+ abilities: {},
619
+ // 无 settings
620
+ };
621
+
622
+ vi.spyOn(repo, 'getAiProviderList').mockResolvedValue(mockProviders);
623
+ vi.spyOn(repo.aiModelModel, 'getAllModels').mockResolvedValue([userModel]);
624
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue([builtinModel]);
625
+
626
+ const result = await repo.getEnabledModels();
627
+
628
+ const merged = result.find((m) => m.id === 'gpt-4');
629
+ expect(merged).toBeDefined();
630
+ expect(merged?.abilities).toEqual({ search: true });
631
+ // 注入 defaults (openai: params)
632
+ expect(merged?.settings).toEqual({ searchImpl: 'params' });
633
+ });
634
+
635
+ it('should retain existing settings when user has search: true and builtin has settings', async () => {
636
+ const mockProviders = [
637
+ { enabled: true, id: 'openai', name: 'OpenAI', source: 'builtin' as const },
638
+ ];
639
+
640
+ const userModel: EnabledAiModel = {
641
+ id: 'gpt-4',
642
+ providerId: 'openai',
643
+ enabled: true,
644
+ type: 'chat',
645
+ abilities: { search: true },
646
+ };
647
+
648
+ const builtinModel = {
649
+ id: 'gpt-4',
650
+ enabled: true,
651
+ type: 'chat' as const,
652
+ settings: { searchImpl: 'tool' }, // builtin 有 settings
653
+ };
654
+
655
+ vi.spyOn(repo, 'getAiProviderList').mockResolvedValue(mockProviders);
656
+ vi.spyOn(repo.aiModelModel, 'getAllModels').mockResolvedValue([userModel]);
657
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue([builtinModel]);
658
+
659
+ const result = await repo.getEnabledModels();
660
+
661
+ const merged = result.find((m) => m.id === 'gpt-4');
662
+ expect(merged).toBeDefined();
663
+ expect(merged?.abilities).toEqual({ search: true });
664
+ // 使用 builtin settings
665
+ expect(merged?.settings).toEqual({ searchImpl: 'tool' });
666
+ });
667
+
668
+ // 测试:用户模型有 abilities.search: false
669
+ it('should remove settings when user has search: false and builtin has settings', async () => {
670
+ const mockProviders = [
671
+ { enabled: true, id: 'openai', name: 'OpenAI', source: 'builtin' as const },
672
+ ];
673
+
674
+ const userModel: EnabledAiModel = {
675
+ id: 'gpt-4',
676
+ providerId: 'openai',
677
+ enabled: true,
678
+ type: 'chat',
679
+ abilities: { search: false }, // 用户禁用 search
680
+ };
681
+
682
+ const builtinModel = {
683
+ id: 'gpt-4',
684
+ enabled: true,
685
+ type: 'chat' as const,
686
+ settings: { searchImpl: 'tool', extendParams: [] }, // builtin 有 settings
687
+ };
688
+
689
+ vi.spyOn(repo, 'getAiProviderList').mockResolvedValue(mockProviders);
690
+ vi.spyOn(repo.aiModelModel, 'getAllModels').mockResolvedValue([userModel]);
691
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue([builtinModel]);
692
+
693
+ const result = await repo.getEnabledModels();
694
+
695
+ const merged = result.find((m) => m.id === 'gpt-4');
696
+ expect(merged).toBeDefined();
697
+ expect(merged?.abilities).toEqual({ search: false });
698
+ // 移除 search 相关,保留其他
699
+ expect(merged?.settings).toEqual({ extendParams: [] });
700
+ });
701
+
702
+ it('should keep no settings when user has search: false and no existing settings', async () => {
703
+ const mockProviders = [
704
+ { enabled: true, id: 'openai', name: 'OpenAI', source: 'builtin' as const },
705
+ ];
706
+
707
+ const userModel: EnabledAiModel = {
708
+ id: 'gpt-4',
709
+ providerId: 'openai',
710
+ enabled: true,
711
+ type: 'chat',
712
+ abilities: { search: false },
713
+ };
714
+
715
+ const builtinModel = {
716
+ id: 'gpt-4',
717
+ enabled: true,
718
+ type: 'chat' as const,
719
+ // 无 settings
720
+ };
721
+
722
+ vi.spyOn(repo, 'getAiProviderList').mockResolvedValue(mockProviders);
723
+ vi.spyOn(repo.aiModelModel, 'getAllModels').mockResolvedValue([userModel]);
724
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue([builtinModel]);
725
+
726
+ const result = await repo.getEnabledModels();
727
+
728
+ const merged = result.find((m) => m.id === 'gpt-4');
729
+ expect(merged).toBeDefined();
730
+ expect(merged?.abilities).toEqual({ search: false });
731
+ // 无 settings
732
+ expect(merged?.settings).toBeUndefined();
733
+ });
422
734
  });
423
735
 
424
736
  describe('getAiProviderModelList', () => {
@@ -614,6 +926,350 @@ describe('AiInfraRepos', () => {
614
926
  // For custom provider, when user enables search with no builtin settings, default to 'params'
615
927
  expect(merged.settings).toEqual({ searchImpl: 'params' });
616
928
  });
929
+
930
+ // 测试场景:用户模型 abilitie 为空(Empty),而基础模型有搜索能力和设置
931
+ it('should retain builtin abilities and settings when user model has no abilities (empty) and builtin has settings', async () => {
932
+ const providerId = 'openai';
933
+
934
+ const userModels: AiProviderModelListItem[] = [
935
+ {
936
+ id: 'gpt-4',
937
+ type: 'chat',
938
+ enabled: true,
939
+ abilities: {}, // Empty object, no search
940
+ },
941
+ ];
942
+
943
+ const builtinModels: AiProviderModelListItem[] = [
944
+ {
945
+ id: 'gpt-4',
946
+ type: 'chat',
947
+ enabled: true,
948
+ abilities: { search: false }, // 使用 builtin abilities
949
+ settings: { searchImpl: 'params', searchProvider: 'google' }, // builtin has settings
950
+ },
951
+ ];
952
+
953
+ vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue(userModels);
954
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue(builtinModels);
955
+
956
+ const result = await repo.getAiProviderModelList(providerId);
957
+
958
+ const merged = result.find((m) => m.id === 'gpt-4');
959
+ expect(merged).toBeDefined();
960
+ // 使用 builtin abilities
961
+ expect(merged?.abilities?.search).toEqual(false);
962
+ // 保留 builtin settings
963
+ expect(merged?.settings).toBeUndefined();
964
+ });
965
+
966
+ it('should retain builtin abilities and settings when user model has no abilities (empty) and builtin has settings', async () => {
967
+ const providerId = 'openai';
968
+
969
+ const userModels: AiProviderModelListItem[] = [
970
+ {
971
+ id: 'gpt-4',
972
+ type: 'chat',
973
+ enabled: true,
974
+ abilities: {}, // Empty object, no search
975
+ },
976
+ ];
977
+
978
+ const builtinModels: AiProviderModelListItem[] = [
979
+ {
980
+ id: 'gpt-4',
981
+ type: 'chat',
982
+ enabled: true,
983
+ abilities: { search: true }, // 使用 builtin abilities
984
+ settings: { searchImpl: 'params', searchProvider: 'google' }, // builtin has settings
985
+ },
986
+ ];
987
+
988
+ vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue(userModels);
989
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue(builtinModels);
990
+
991
+ const result = await repo.getAiProviderModelList(providerId);
992
+
993
+ const merged = result.find((m) => m.id === 'gpt-4');
994
+ expect(merged).toBeDefined();
995
+ // 使用 builtin abilities
996
+ expect(merged?.abilities?.search).toEqual(true);
997
+ // 保留 builtin settings
998
+ expect(merged?.settings).toEqual({ searchImpl: 'params', searchProvider: 'google' });
999
+ });
1000
+
1001
+ // 测试场景:用户模型未启用搜索(abilities.search 为 undefined),而基础模型有搜索能力和设置
1002
+ it('should retain builtin settings when user model has no abilities (empty) and builtin has settings', async () => {
1003
+ const providerId = 'openai';
1004
+
1005
+ const userModels: AiProviderModelListItem[] = [
1006
+ {
1007
+ id: 'gpt-4',
1008
+ type: 'chat',
1009
+ enabled: true,
1010
+ abilities: { vision: true }, // 启用 vision 能力, no search
1011
+ },
1012
+ ];
1013
+
1014
+ const builtinModels: AiProviderModelListItem[] = [
1015
+ {
1016
+ id: 'gpt-4',
1017
+ type: 'chat',
1018
+ enabled: true,
1019
+ abilities: { search: false }, // builtin abilities 会被 merge
1020
+ settings: { searchImpl: 'params', searchProvider: 'google' }, // builtin has settings
1021
+ },
1022
+ ];
1023
+
1024
+ vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue(userModels);
1025
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue(builtinModels);
1026
+
1027
+ const result = await repo.getAiProviderModelList(providerId);
1028
+
1029
+ const merged = result.find((m) => m.id === 'gpt-4');
1030
+ expect(merged).toBeDefined();
1031
+ // abilities.search 会被 merge 为 false,此处和 getEnabledAiModel 不同
1032
+ expect(merged?.abilities?.search).toEqual(false);
1033
+ // 删去 builtin settings
1034
+ expect(merged?.settings).toBeUndefined();
1035
+ });
1036
+
1037
+ it('should retain builtin settings when user model has no abilities (empty) and builtin has settings', async () => {
1038
+ const providerId = 'openai';
1039
+
1040
+ const userModels: AiProviderModelListItem[] = [
1041
+ {
1042
+ id: 'gpt-4',
1043
+ type: 'chat',
1044
+ enabled: true,
1045
+ abilities: { vision: true }, // 启用 vision 能力, no search
1046
+ },
1047
+ ];
1048
+
1049
+ const builtinModels: AiProviderModelListItem[] = [
1050
+ {
1051
+ id: 'gpt-4',
1052
+ type: 'chat',
1053
+ enabled: true,
1054
+ abilities: { search: true }, // builtin abilities 会被 merge
1055
+ settings: { searchImpl: 'params', searchProvider: 'google' }, // builtin has settings
1056
+ },
1057
+ ];
1058
+
1059
+ vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue(userModels);
1060
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue(builtinModels);
1061
+
1062
+ const result = await repo.getAiProviderModelList(providerId);
1063
+
1064
+ const merged = result.find((m) => m.id === 'gpt-4');
1065
+ expect(merged).toBeDefined();
1066
+ // abilities.search 会被 merge 为 true,此处和 getEnabledAiModel 不同
1067
+ expect(merged?.abilities?.search).toEqual(true);
1068
+ // 保留 builtin settings
1069
+ expect(merged?.settings).toEqual({ searchImpl: 'params', searchProvider: 'google' });
1070
+ });
1071
+
1072
+ // 测试:用户模型无 abilities.search(undefined),保留 builtin settings(mergeArrayById 优先用户,但用户无则 builtin)
1073
+ it('should retain builtin settings when user model has no abilities.search (undefined) and builtin has settings', async () => {
1074
+ const providerId = 'openai';
1075
+
1076
+ const userModels: AiProviderModelListItem[] = [
1077
+ {
1078
+ id: 'gpt-4',
1079
+ type: 'chat',
1080
+ enabled: true,
1081
+ abilities: {}, // 无 search
1082
+ },
1083
+ ];
1084
+
1085
+ const builtinModels: AiProviderModelListItem[] = [
1086
+ {
1087
+ id: 'gpt-4',
1088
+ type: 'chat',
1089
+ enabled: true,
1090
+ abilities: {},
1091
+ settings: { searchImpl: 'params', searchProvider: 'google' }, // builtin 有
1092
+ },
1093
+ ];
1094
+
1095
+ vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue(userModels);
1096
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue(builtinModels);
1097
+
1098
+ const result = await repo.getAiProviderModelList(providerId);
1099
+
1100
+ const merged = result.find((m) => m.id === 'gpt-4');
1101
+ expect(merged).toBeDefined();
1102
+ expect(merged?.abilities?.search).toBeUndefined();
1103
+ // 保留 builtin settings
1104
+ expect(merged?.settings).toEqual({ searchImpl: 'params', searchProvider: 'google' });
1105
+ });
1106
+
1107
+ it('should retain no settings when user model has no abilities.search (undefined) and builtin has no settings', async () => {
1108
+ const providerId = 'openai';
1109
+
1110
+ const userModels: AiProviderModelListItem[] = [
1111
+ {
1112
+ id: 'gpt-4',
1113
+ type: 'chat',
1114
+ enabled: true,
1115
+ abilities: {}, // 无 search
1116
+ },
1117
+ ];
1118
+
1119
+ const builtinModels: AiProviderModelListItem[] = [
1120
+ {
1121
+ id: 'gpt-4',
1122
+ type: 'chat',
1123
+ enabled: true,
1124
+ // 无 settings
1125
+ },
1126
+ ];
1127
+
1128
+ vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue(userModels);
1129
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue(builtinModels);
1130
+
1131
+ const result = await repo.getAiProviderModelList(providerId);
1132
+
1133
+ const merged = result.find((m) => m.id === 'gpt-4');
1134
+ expect(merged).toBeDefined();
1135
+ expect(merged?.abilities?.search).toBeUndefined();
1136
+ // 无 settings
1137
+ expect(merged?.settings).toBeUndefined();
1138
+ });
1139
+
1140
+ // 测试:用户模型有 abilities.search: true
1141
+ it('should inject defaults when user has search: true, no existing settings (builtin none)', async () => {
1142
+ const providerId = 'openai';
1143
+
1144
+ const userModels: AiProviderModelListItem[] = [
1145
+ {
1146
+ id: 'gpt-4',
1147
+ type: 'chat',
1148
+ enabled: true,
1149
+ abilities: { search: true }, // 用户启用
1150
+ },
1151
+ ];
1152
+
1153
+ const builtinModels: AiProviderModelListItem[] = [
1154
+ {
1155
+ id: 'gpt-4',
1156
+ type: 'chat',
1157
+ enabled: true,
1158
+ // 无 settings
1159
+ },
1160
+ ];
1161
+
1162
+ vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue(userModels);
1163
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue(builtinModels);
1164
+
1165
+ const result = await repo.getAiProviderModelList(providerId);
1166
+
1167
+ const merged = result.find((m) => m.id === 'gpt-4');
1168
+ expect(merged).toBeDefined();
1169
+ expect(merged?.abilities).toEqual({ search: true });
1170
+ // 注入 defaults
1171
+ expect(merged?.settings).toEqual({ searchImpl: 'params' });
1172
+ });
1173
+
1174
+ it('should retain existing settings when user has search: true and builtin has settings', async () => {
1175
+ const providerId = 'openai';
1176
+
1177
+ const userModels: AiProviderModelListItem[] = [
1178
+ {
1179
+ id: 'gpt-4',
1180
+ type: 'chat',
1181
+ enabled: true,
1182
+ abilities: { search: true },
1183
+ },
1184
+ ];
1185
+
1186
+ const builtinModels: AiProviderModelListItem[] = [
1187
+ {
1188
+ id: 'gpt-4',
1189
+ type: 'chat',
1190
+ enabled: true,
1191
+ settings: { searchImpl: 'tool' },
1192
+ },
1193
+ ];
1194
+
1195
+ vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue(userModels);
1196
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue(builtinModels);
1197
+
1198
+ const result = await repo.getAiProviderModelList(providerId);
1199
+
1200
+ const merged = result.find((m) => m.id === 'gpt-4');
1201
+ expect(merged).toBeDefined();
1202
+ expect(merged?.abilities).toEqual({ search: true });
1203
+ // 使用 builtin settings
1204
+ expect(merged?.settings).toEqual({ searchImpl: 'tool' });
1205
+ });
1206
+
1207
+ // 测试:用户模型有 abilities.search: false
1208
+ it('should remove settings when user has search: false and builtin has settings', async () => {
1209
+ const providerId = 'openai';
1210
+
1211
+ const userModels: AiProviderModelListItem[] = [
1212
+ {
1213
+ id: 'gpt-4',
1214
+ type: 'chat',
1215
+ enabled: true,
1216
+ abilities: { search: false }, // 用户禁用
1217
+ },
1218
+ ];
1219
+
1220
+ const builtinModels: AiProviderModelListItem[] = [
1221
+ {
1222
+ id: 'gpt-4',
1223
+ type: 'chat',
1224
+ enabled: true,
1225
+ settings: { searchImpl: 'tool', extendParams: [] },
1226
+ },
1227
+ ];
1228
+
1229
+ vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue(userModels);
1230
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue(builtinModels);
1231
+
1232
+ const result = await repo.getAiProviderModelList(providerId);
1233
+
1234
+ const merged = result.find((m) => m.id === 'gpt-4');
1235
+ expect(merged).toBeDefined();
1236
+ expect(merged?.abilities).toEqual({ search: false });
1237
+ // 移除 search 相关,保留其他
1238
+ expect(merged?.settings).toEqual({ extendParams: [] });
1239
+ });
1240
+
1241
+ it('should keep no settings when user has search: false and no existing settings', async () => {
1242
+ const providerId = 'openai';
1243
+
1244
+ const userModels: AiProviderModelListItem[] = [
1245
+ {
1246
+ id: 'gpt-4',
1247
+ type: 'chat',
1248
+ enabled: true,
1249
+ abilities: { search: false },
1250
+ },
1251
+ ];
1252
+
1253
+ const builtinModels: AiProviderModelListItem[] = [
1254
+ {
1255
+ id: 'gpt-4',
1256
+ type: 'chat',
1257
+ enabled: true,
1258
+ // 无 settings
1259
+ },
1260
+ ];
1261
+
1262
+ vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue(userModels);
1263
+ vi.spyOn(repo as any, 'fetchBuiltinModels').mockResolvedValue(builtinModels);
1264
+
1265
+ const result = await repo.getAiProviderModelList(providerId);
1266
+
1267
+ const merged = result.find((m) => m.id === 'gpt-4');
1268
+ expect(merged).toBeDefined();
1269
+ expect(merged?.abilities).toEqual({ search: false });
1270
+ // 无 settings
1271
+ expect(merged?.settings).toBeUndefined();
1272
+ });
617
1273
  });
618
1274
 
619
1275
  describe('getAiProviderRuntimeState', () => {