@lobehub/lobehub 2.0.0-next.232 → 2.0.0-next.234

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 (112) hide show
  1. package/.github/workflows/bundle-analyzer.yml +1 -1
  2. package/.github/workflows/e2e.yml +62 -53
  3. package/.github/workflows/manual-build-desktop.yml +5 -5
  4. package/.github/workflows/pr-build-desktop.yml +4 -4
  5. package/.github/workflows/pr-build-docker.yml +2 -2
  6. package/.github/workflows/release-desktop-beta.yml +4 -4
  7. package/.github/workflows/release-docker.yml +2 -2
  8. package/.github/workflows/test.yml +44 -7
  9. package/CHANGELOG.md +59 -0
  10. package/CLAUDE.md +1 -1
  11. package/changelog/v1.json +14 -0
  12. package/docs/development/basic/feature-development.mdx +4 -5
  13. package/docs/development/basic/feature-development.zh-CN.mdx +4 -5
  14. package/docs/self-hosting/environment-variables/auth.mdx +7 -0
  15. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +7 -0
  16. package/e2e/README.md +6 -6
  17. package/e2e/src/features/community/detail-pages.feature +9 -9
  18. package/e2e/src/features/community/interactions.feature +13 -13
  19. package/e2e/src/features/community/smoke.feature +6 -6
  20. package/e2e/src/steps/agent/conversation-mgmt.steps.ts +196 -25
  21. package/e2e/src/steps/agent/conversation.steps.ts +58 -0
  22. package/e2e/src/steps/agent/message-ops.steps.ts +20 -15
  23. package/e2e/src/steps/community/detail-pages.steps.ts +60 -19
  24. package/e2e/src/steps/community/interactions.steps.ts +145 -32
  25. package/e2e/src/steps/hooks.ts +12 -2
  26. package/locales/en-US/setting.json +3 -0
  27. package/locales/zh-CN/file.json +4 -0
  28. package/locales/zh-CN/setting.json +3 -0
  29. package/package.json +5 -5
  30. package/packages/business/config/src/llm.ts +6 -1
  31. package/packages/const/src/index.ts +1 -0
  32. package/packages/const/src/lobehubSkill.ts +55 -0
  33. package/packages/const/src/settings/image.ts +1 -1
  34. package/packages/model-bank/src/aiModels/azure.ts +2 -2
  35. package/packages/model-bank/src/aiModels/google.ts +1 -0
  36. package/packages/model-bank/src/aiModels/lobehub.ts +33 -13
  37. package/packages/model-bank/src/aiModels/openai.ts +21 -4
  38. package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +4 -1
  39. package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +1 -1
  40. package/packages/ssrf-safe-fetch/index.test.ts +5 -34
  41. package/packages/ssrf-safe-fetch/index.ts +12 -2
  42. package/packages/types/package.json +1 -1
  43. package/packages/types/src/files/upload.ts +11 -1
  44. package/packages/types/src/message/common/tools.ts +1 -1
  45. package/packages/types/src/serverConfig.ts +1 -0
  46. package/public/not-compatible.html +1296 -0
  47. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/MultiImagesUpload/index.tsx +3 -3
  48. package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +3 -10
  49. package/src/app/[variants]/(main)/image/index.tsx +1 -1
  50. package/src/app/[variants]/(main)/resource/features/FileDetail.tsx +20 -12
  51. package/src/app/[variants]/(main)/resource/features/modal/FullscreenModal.tsx +2 -4
  52. package/src/app/[variants]/layout.tsx +50 -1
  53. package/src/envs/auth.ts +15 -0
  54. package/src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx +304 -0
  55. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +74 -10
  56. package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/ToolTitle.tsx +9 -0
  57. package/src/features/FileViewer/Renderer/Code/index.tsx +224 -0
  58. package/src/features/FileViewer/Renderer/Image/index.tsx +8 -1
  59. package/src/features/FileViewer/Renderer/PDF/index.tsx +3 -1
  60. package/src/features/FileViewer/Renderer/PDF/style.ts +2 -1
  61. package/src/features/FileViewer/index.tsx +135 -24
  62. package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +7 -4
  63. package/src/features/PageEditor/store/initialState.ts +2 -1
  64. package/src/features/ResourceManager/components/Editor/FileContent.tsx +1 -4
  65. package/src/features/ResourceManager/components/Editor/FileCopilot.tsx +64 -0
  66. package/src/features/ResourceManager/components/Editor/index.tsx +98 -31
  67. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +3 -2
  68. package/src/features/ResourceManager/components/Explorer/ListView/ColumnResizeHandle.tsx +119 -0
  69. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +67 -22
  70. package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +46 -11
  71. package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +140 -81
  72. package/src/features/ResourceManager/components/Explorer/ToolBar/SortDropdown.tsx +20 -12
  73. package/src/features/ResourceManager/components/Explorer/ToolBar/ViewSwitcher.tsx +18 -10
  74. package/src/features/ResourceManager/components/UploadDock/Item.tsx +38 -6
  75. package/src/features/ResourceManager/components/UploadDock/index.tsx +62 -41
  76. package/src/features/ResourceManager/index.tsx +1 -0
  77. package/src/helpers/toolEngineering/index.test.ts +3 -0
  78. package/src/helpers/toolEngineering/index.ts +12 -1
  79. package/src/hooks/useFetchAiImageConfig.ts +54 -10
  80. package/src/libs/trpc/utils/internalJwt.ts +2 -2
  81. package/src/locales/default/file.ts +4 -0
  82. package/src/locales/default/setting.ts +3 -0
  83. package/src/server/globalConfig/index.ts +1 -0
  84. package/src/server/modules/ModelRuntime/index.test.ts +214 -1
  85. package/src/server/modules/ModelRuntime/index.ts +43 -7
  86. package/src/server/routers/lambda/document.ts +44 -0
  87. package/src/server/routers/tools/market.ts +261 -0
  88. package/src/server/services/document/index.ts +22 -0
  89. package/src/services/document/index.ts +4 -0
  90. package/src/services/upload.ts +22 -2
  91. package/src/store/chat/slices/plugin/actions/internals.ts +15 -2
  92. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +104 -0
  93. package/src/store/file/slices/fileManager/action.test.ts +9 -3
  94. package/src/store/file/slices/fileManager/action.ts +165 -70
  95. package/src/store/file/slices/upload/action.ts +3 -0
  96. package/src/store/global/actions/general.ts +15 -0
  97. package/src/store/global/initialState.ts +13 -0
  98. package/src/store/image/slices/generationConfig/initialState.ts +5 -5
  99. package/src/store/image/slices/generationConfig/selectors.test.ts +11 -4
  100. package/src/store/serverConfig/selectors.ts +1 -0
  101. package/src/store/tool/initialState.ts +11 -2
  102. package/src/store/tool/selectors/index.ts +1 -0
  103. package/src/store/tool/selectors/tool.ts +3 -1
  104. package/src/store/tool/slices/lobehubSkillStore/action.ts +361 -0
  105. package/src/store/tool/slices/lobehubSkillStore/index.ts +4 -0
  106. package/src/store/tool/slices/lobehubSkillStore/initialState.ts +24 -0
  107. package/src/store/tool/slices/lobehubSkillStore/selectors.ts +145 -0
  108. package/src/store/tool/slices/lobehubSkillStore/types.ts +100 -0
  109. package/src/store/tool/store.ts +8 -2
  110. package/vitest.config.mts +11 -6
  111. package/src/features/FileViewer/Renderer/JavaScript/index.tsx +0 -66
  112. package/src/features/FileViewer/Renderer/TXT/index.tsx +0 -50
@@ -1,12 +1,26 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useMemo } from 'react';
2
2
 
3
3
  import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
4
4
  import { useGlobalStore } from '@/store/global';
5
5
  import { systemStatusSelectors } from '@/store/global/selectors';
6
6
  import { useImageStore } from '@/store/image';
7
+ import {
8
+ DEFAULT_AI_IMAGE_MODEL,
9
+ DEFAULT_AI_IMAGE_PROVIDER,
10
+ } from '@/store/image/slices/generationConfig/initialState';
7
11
  import { useUserStore } from '@/store/user';
8
12
  import { authSelectors } from '@/store/user/selectors';
9
13
 
14
+ const checkModelEnabled = (
15
+ enabledImageModelList: ReturnType<typeof aiProviderSelectors.enabledImageModelList>,
16
+ provider: string,
17
+ model: string,
18
+ ) => {
19
+ return enabledImageModelList.some(
20
+ (p) => p.id === provider && p.children.some((m) => m.id === model),
21
+ );
22
+ };
23
+
10
24
  export const useFetchAiImageConfig = () => {
11
25
  const isStatusInit = useGlobalStore(systemStatusSelectors.isStatusInit);
12
26
  const isInitAiProviderRuntimeState = useAiInfraStore(
@@ -29,16 +43,46 @@ export const useFetchAiImageConfig = () => {
29
43
  const isInitializedImageConfig = useImageStore((s) => s.isInit);
30
44
  const initializeImageConfig = useImageStore((s) => s.initializeImageConfig);
31
45
 
46
+ const enabledImageModelList = useAiInfraStore(aiProviderSelectors.enabledImageModelList);
47
+
48
+ // Determine which model/provider to use for initialization
49
+ const initParams = useMemo(() => {
50
+ // 1. Try lastSelected if enabled
51
+ if (
52
+ lastSelectedImageModel &&
53
+ lastSelectedImageProvider &&
54
+ checkModelEnabled(enabledImageModelList, lastSelectedImageProvider, lastSelectedImageModel)
55
+ ) {
56
+ return { model: lastSelectedImageModel, provider: lastSelectedImageProvider };
57
+ }
58
+
59
+ // 2. Try default model from any enabled provider (prefer default provider first)
60
+ if (
61
+ checkModelEnabled(enabledImageModelList, DEFAULT_AI_IMAGE_PROVIDER, DEFAULT_AI_IMAGE_MODEL)
62
+ ) {
63
+ return { model: undefined, provider: undefined }; // Use initialState defaults
64
+ }
65
+ const providerWithDefaultModel = enabledImageModelList.find((p) =>
66
+ p.children.some((m) => m.id === DEFAULT_AI_IMAGE_MODEL),
67
+ );
68
+ if (providerWithDefaultModel) {
69
+ return { model: DEFAULT_AI_IMAGE_MODEL, provider: providerWithDefaultModel.id };
70
+ }
71
+
72
+ // 3. Fallback to first enabled model
73
+ const firstProvider = enabledImageModelList[0];
74
+ const firstModel = firstProvider?.children[0];
75
+ if (firstProvider && firstModel) {
76
+ return { model: firstModel.id, provider: firstProvider.id };
77
+ }
78
+
79
+ // No enabled models
80
+ return { model: undefined, provider: undefined };
81
+ }, [lastSelectedImageModel, lastSelectedImageProvider, enabledImageModelList]);
82
+
32
83
  useEffect(() => {
33
84
  if (!isInitializedImageConfig && isReadyForInit) {
34
- initializeImageConfig(isLogin, lastSelectedImageModel, lastSelectedImageProvider);
85
+ initializeImageConfig(isLogin, initParams.model, initParams.provider);
35
86
  }
36
- }, [
37
- isReadyForInit,
38
- isInitializedImageConfig,
39
- isLogin,
40
- lastSelectedImageModel,
41
- lastSelectedImageProvider,
42
- initializeImageConfig,
43
- ]);
87
+ }, [isReadyForInit, isInitializedImageConfig, isLogin, initParams, initializeImageConfig]);
44
88
  };
@@ -66,7 +66,7 @@ const getVerificationKey = async () => {
66
66
 
67
67
  /**
68
68
  * Sign JWT for internal lambda → async calls
69
- * Uses JWKS private key with short expiration (3s)
69
+ * Uses JWKS private key with configurable expiration (default: 30s)
70
70
  * The JWT only proves the request is from lambda, payload is sent via LOBE_CHAT_AUTH_HEADER
71
71
  */
72
72
  export const signInternalJWT = async (): Promise<string> => {
@@ -75,7 +75,7 @@ export const signInternalJWT = async (): Promise<string> => {
75
75
  return new SignJWT({ purpose: INTERNAL_JWT_PURPOSE })
76
76
  .setProtectedHeader({ alg: 'RS256', kid })
77
77
  .setIssuedAt()
78
- .setExpirationTime('3s')
78
+ .setExpirationTime(authEnv.INTERNAL_JWT_EXPIRATION)
79
79
  .sign(key);
80
80
  };
81
81
 
@@ -39,6 +39,7 @@ export default {
39
39
  'header.actions.notionGuide.title': 'Import from Notion',
40
40
  'header.actions.uploadFile': 'Upload File',
41
41
  'header.actions.uploadFolder': 'Upload Folder',
42
+ 'header.actions.uploadFolder.creatingFolders': 'Creating folder structure...',
42
43
  'header.newPageButton': 'New Page',
43
44
  'header.uploadButton': 'Upload',
44
45
  'home.getStarted': 'Get Started',
@@ -129,6 +130,8 @@ export default {
129
130
  'title': 'Resources',
130
131
  'toggleLeftPanel': 'Show/Hide Left Panel',
131
132
  'uploadDock.body.collapse': 'Collapse',
133
+ 'uploadDock.body.item.cancel': 'Cancel',
134
+ 'uploadDock.body.item.cancelled': 'Cancelled',
132
135
  'uploadDock.body.item.done': 'Uploaded',
133
136
  'uploadDock.body.item.error': 'Upload failed, please try again',
134
137
  'uploadDock.body.item.pending': 'Preparing to upload...',
@@ -137,6 +140,7 @@ export default {
137
140
  'uploadDock.fileQueueInfo':
138
141
  'Uploading the first {{count}} files, {{remaining}} remaining in queue',
139
142
  'uploadDock.totalCount': 'Total {{count}} items',
143
+ 'uploadDock.uploadStatus.cancelled': 'Upload cancelled',
140
144
  'uploadDock.uploadStatus.error': 'Upload error',
141
145
  'uploadDock.uploadStatus.pending': 'Waiting to upload',
142
146
  'uploadDock.uploadStatus.processing': 'Uploading',
@@ -603,6 +603,9 @@ export default {
603
603
  'tools.klavis.servers': 'servers',
604
604
  'tools.klavis.tools': 'tools',
605
605
  'tools.klavis.verifyAuth': 'I have completed authentication',
606
+ 'tools.lobehubSkill.authorize': 'Authorize',
607
+ 'tools.lobehubSkill.connect': 'Connect',
608
+ 'tools.lobehubSkill.error': 'Error',
606
609
  'tools.notInstalled': 'Not Installed',
607
610
  'tools.notInstalledWarning':
608
611
  'This skill is not currently installed, which may affect agent functionality.',
@@ -76,6 +76,7 @@ export const getServerGlobalConfig = async () => {
76
76
  },
77
77
  enableEmailVerification: authEnv.AUTH_EMAIL_VERIFICATION,
78
78
  enableKlavis: !!klavisEnv.KLAVIS_API_KEY,
79
+ enableLobehubSkill: !!(appEnv.MARKET_TRUSTED_CLIENT_SECRET && appEnv.MARKET_TRUSTED_CLIENT_ID),
79
80
  enableMagicLink: authEnv.ENABLE_MAGIC_LINK,
80
81
  enableMarketTrustedClient: !!(
81
82
  appEnv.MARKET_TRUSTED_CLIENT_SECRET && appEnv.MARKET_TRUSTED_CLIENT_ID
@@ -26,7 +26,7 @@ import { ClientSecretPayload } from '@lobechat/types';
26
26
  import { ModelProvider } from 'model-bank';
27
27
  import { describe, expect, it, vi } from 'vitest';
28
28
 
29
- import { initModelRuntimeWithUserPayload } from './index';
29
+ import { buildPayloadFromKeyVaults, initModelRuntimeWithUserPayload } from './index';
30
30
 
31
31
  // 模拟依赖项
32
32
  vi.mock('@/envs/llm', () => ({
@@ -496,3 +496,216 @@ describe('initModelRuntimeWithUserPayload method', () => {
496
496
  });
497
497
  });
498
498
  });
499
+
500
+ /**
501
+ * Test cases for buildPayloadFromKeyVaults function
502
+ * This function builds ClientSecretPayload based on runtimeProvider (sdkType)
503
+ * to ensure provider-specific fields are correctly forwarded
504
+ */
505
+ describe('buildPayloadFromKeyVaults', () => {
506
+ describe('should build payload with correct fields based on runtimeProvider', () => {
507
+ it('OpenAI compatible: returns apiKey, baseURL and runtimeProvider', () => {
508
+ const keyVaults = {
509
+ apiKey: 'test-api-key',
510
+ baseURL: 'https://custom-endpoint.com/v1',
511
+ };
512
+ const payload = buildPayloadFromKeyVaults(keyVaults, ModelProvider.OpenAI);
513
+
514
+ expect(payload).toEqual({
515
+ apiKey: 'test-api-key',
516
+ baseURL: 'https://custom-endpoint.com/v1',
517
+ runtimeProvider: ModelProvider.OpenAI,
518
+ });
519
+ });
520
+
521
+ it('Azure: returns apiKey, baseURL, azureApiVersion and runtimeProvider', () => {
522
+ const keyVaults = {
523
+ apiKey: 'azure-api-key',
524
+ baseURL: 'https://my-azure.openai.azure.com',
525
+ apiVersion: '2024-06-01',
526
+ endpoint: 'https://fallback-endpoint.com',
527
+ };
528
+ const payload = buildPayloadFromKeyVaults(keyVaults, ModelProvider.Azure);
529
+
530
+ expect(payload).toEqual({
531
+ apiKey: 'azure-api-key',
532
+ azureApiVersion: '2024-06-01',
533
+ baseURL: 'https://my-azure.openai.azure.com',
534
+ runtimeProvider: ModelProvider.Azure,
535
+ });
536
+ });
537
+
538
+ it('Azure: uses endpoint as fallback when baseURL is not provided', () => {
539
+ const keyVaults = {
540
+ apiKey: 'azure-api-key',
541
+ endpoint: 'https://fallback-endpoint.com',
542
+ apiVersion: '2024-06-01',
543
+ };
544
+ const payload = buildPayloadFromKeyVaults(keyVaults, ModelProvider.Azure);
545
+
546
+ expect(payload.baseURL).toBe('https://fallback-endpoint.com');
547
+ });
548
+
549
+ it('Cloudflare: returns apiKey, cloudflareBaseURLOrAccountID and runtimeProvider', () => {
550
+ const keyVaults = {
551
+ apiKey: 'cloudflare-api-key',
552
+ baseURLOrAccountID: 'my-account-id',
553
+ };
554
+ const payload = buildPayloadFromKeyVaults(keyVaults, ModelProvider.Cloudflare);
555
+
556
+ expect(payload).toEqual({
557
+ apiKey: 'cloudflare-api-key',
558
+ cloudflareBaseURLOrAccountID: 'my-account-id',
559
+ runtimeProvider: ModelProvider.Cloudflare,
560
+ });
561
+ });
562
+
563
+ it('Bedrock: returns AWS credentials and runtimeProvider', () => {
564
+ const keyVaults = {
565
+ accessKeyId: 'aws-access-key',
566
+ secretAccessKey: 'aws-secret-key',
567
+ region: 'us-east-1',
568
+ sessionToken: 'session-token',
569
+ };
570
+ const payload = buildPayloadFromKeyVaults(keyVaults, ModelProvider.Bedrock);
571
+
572
+ expect(payload).toEqual({
573
+ apiKey: 'aws-secret-keyaws-access-key',
574
+ awsAccessKeyId: 'aws-access-key',
575
+ awsRegion: 'us-east-1',
576
+ awsSecretAccessKey: 'aws-secret-key',
577
+ awsSessionToken: 'session-token',
578
+ runtimeProvider: ModelProvider.Bedrock,
579
+ });
580
+ });
581
+
582
+ it('Ollama: returns baseURL and runtimeProvider', () => {
583
+ const keyVaults = {
584
+ baseURL: 'http://localhost:11434',
585
+ };
586
+ const payload = buildPayloadFromKeyVaults(keyVaults, ModelProvider.Ollama);
587
+
588
+ expect(payload).toEqual({
589
+ baseURL: 'http://localhost:11434',
590
+ runtimeProvider: ModelProvider.Ollama,
591
+ });
592
+ });
593
+
594
+ it('VertexAI: returns apiKey, baseURL, vertexAIRegion and runtimeProvider', () => {
595
+ const keyVaults = {
596
+ apiKey: 'vertex-credentials-json',
597
+ baseURL: 'https://vertex-endpoint.com',
598
+ region: 'us-central1',
599
+ };
600
+ const payload = buildPayloadFromKeyVaults(keyVaults, ModelProvider.VertexAI);
601
+
602
+ expect(payload).toEqual({
603
+ apiKey: 'vertex-credentials-json',
604
+ baseURL: 'https://vertex-endpoint.com',
605
+ runtimeProvider: ModelProvider.VertexAI,
606
+ vertexAIRegion: 'us-central1',
607
+ });
608
+ });
609
+
610
+ it('ComfyUI: returns all auth fields and runtimeProvider', () => {
611
+ const keyVaults = {
612
+ apiKey: 'comfyui-api-key',
613
+ authType: 'bearer',
614
+ baseURL: 'http://localhost:8188',
615
+ customHeaders: { 'X-Custom': 'header' },
616
+ password: 'pass',
617
+ username: 'user',
618
+ } as const;
619
+ const payload = buildPayloadFromKeyVaults(keyVaults, ModelProvider.ComfyUI);
620
+
621
+ expect(payload).toEqual({
622
+ apiKey: 'comfyui-api-key',
623
+ authType: 'bearer',
624
+ baseURL: 'http://localhost:8188',
625
+ customHeaders: { 'X-Custom': 'header' },
626
+ password: 'pass',
627
+ runtimeProvider: ModelProvider.ComfyUI,
628
+ username: 'user',
629
+ });
630
+ });
631
+
632
+ it('Unknown provider: falls back to default with apiKey, baseURL and runtimeProvider', () => {
633
+ const keyVaults = {
634
+ apiKey: 'unknown-api-key',
635
+ baseURL: 'https://unknown-endpoint.com',
636
+ };
637
+ const payload = buildPayloadFromKeyVaults(keyVaults, 'unknown-provider');
638
+
639
+ expect(payload).toEqual({
640
+ apiKey: 'unknown-api-key',
641
+ baseURL: 'https://unknown-endpoint.com',
642
+ runtimeProvider: 'unknown-provider',
643
+ });
644
+ });
645
+ });
646
+
647
+ describe('custom provider with sdkType should include provider-specific fields', () => {
648
+ it('custom provider with Azure sdkType includes azureApiVersion', () => {
649
+ const keyVaults = {
650
+ apiKey: 'custom-azure-key',
651
+ baseURL: 'https://custom-azure.openai.azure.com',
652
+ apiVersion: '2024-06-01',
653
+ };
654
+ // Simulates a custom provider where runtimeProvider is resolved to 'azure'
655
+ const payload = buildPayloadFromKeyVaults(keyVaults, ModelProvider.Azure);
656
+
657
+ expect(payload.azureApiVersion).toBe('2024-06-01');
658
+ expect(payload.runtimeProvider).toBe(ModelProvider.Azure);
659
+ });
660
+
661
+ it('custom provider with Cloudflare sdkType includes cloudflareBaseURLOrAccountID', () => {
662
+ const keyVaults = {
663
+ apiKey: 'custom-cloudflare-key',
664
+ baseURLOrAccountID: 'custom-account-id',
665
+ };
666
+ // Simulates a custom provider where runtimeProvider is resolved to 'cloudflare'
667
+ const payload = buildPayloadFromKeyVaults(keyVaults, ModelProvider.Cloudflare);
668
+
669
+ expect(payload.cloudflareBaseURLOrAccountID).toBe('custom-account-id');
670
+ expect(payload.runtimeProvider).toBe(ModelProvider.Cloudflare);
671
+ });
672
+
673
+ it('custom provider with Bedrock sdkType includes AWS credentials', () => {
674
+ const keyVaults = {
675
+ accessKeyId: 'custom-aws-id',
676
+ secretAccessKey: 'custom-aws-secret',
677
+ region: 'eu-west-1',
678
+ };
679
+ // Simulates a custom provider where runtimeProvider is resolved to 'bedrock'
680
+ const payload = buildPayloadFromKeyVaults(keyVaults, ModelProvider.Bedrock);
681
+
682
+ expect(payload.awsAccessKeyId).toBe('custom-aws-id');
683
+ expect(payload.awsSecretAccessKey).toBe('custom-aws-secret');
684
+ expect(payload.awsRegion).toBe('eu-west-1');
685
+ expect(payload.runtimeProvider).toBe(ModelProvider.Bedrock);
686
+ });
687
+
688
+ it('custom provider with Ollama sdkType includes baseURL', () => {
689
+ const keyVaults = {
690
+ baseURL: 'http://custom-ollama:11434',
691
+ };
692
+ // Simulates a custom provider where runtimeProvider is resolved to 'ollama'
693
+ const payload = buildPayloadFromKeyVaults(keyVaults, ModelProvider.Ollama);
694
+
695
+ expect(payload.baseURL).toBe('http://custom-ollama:11434');
696
+ expect(payload.runtimeProvider).toBe(ModelProvider.Ollama);
697
+ });
698
+
699
+ it('custom provider with VertexAI sdkType includes vertexAIRegion', () => {
700
+ const keyVaults = {
701
+ apiKey: 'custom-vertex-creds',
702
+ region: 'asia-northeast1',
703
+ };
704
+ // Simulates a custom provider where runtimeProvider is resolved to 'vertexai'
705
+ const payload = buildPayloadFromKeyVaults(keyVaults, ModelProvider.VertexAI);
706
+
707
+ expect(payload.vertexAIRegion).toBe('asia-northeast1');
708
+ expect(payload.runtimeProvider).toBe(ModelProvider.VertexAI);
709
+ });
710
+ });
711
+ });
@@ -32,6 +32,24 @@ type ProviderKeyVaults = OpenAICompatibleKeyVault &
32
32
  ComfyUIKeyVault &
33
33
  VertexAIKeyVault;
34
34
 
35
+ /**
36
+ * Resolve the runtime provider for a given provider.
37
+ *
38
+ * This is the server-side equivalent of the frontend's resolveRuntimeProvider function.
39
+ * For builtin providers, returns the provider as-is.
40
+ * For custom providers, returns the sdkType from settings (defaults to 'openai').
41
+ *
42
+ * @param provider - The provider id
43
+ * @param sdkType - The sdkType from provider settings
44
+ * @returns The resolved runtime provider
45
+ */
46
+ const resolveRuntimeProvider = (provider: string, sdkType?: string): string => {
47
+ const isBuiltin = Object.values(ModelProvider).includes(provider as ModelProvider);
48
+ if (isBuiltin) return provider;
49
+
50
+ return sdkType || 'openai';
51
+ };
52
+
35
53
  /**
36
54
  * Build ClientSecretPayload from keyVaults stored in database
37
55
  *
@@ -39,15 +57,21 @@ type ProviderKeyVaults = OpenAICompatibleKeyVault &
39
57
  * It converts the keyVaults object from database to the ClientSecretPayload format
40
58
  * expected by initModelRuntimeWithUserPayload.
41
59
  *
42
- * @param provider - The model provider
60
+ * For custom providers, we use runtimeProvider (sdkType) to determine which fields
61
+ * to include in the payload. This ensures that provider-specific fields like
62
+ * cloudflareBaseURLOrAccountID or azureApiVersion are correctly forwarded.
63
+ *
43
64
  * @param keyVaults - The keyVaults object from database (already decrypted)
65
+ * @param runtimeProvider - The runtime provider (sdkType) to use for building payload
44
66
  * @returns ClientSecretPayload for the provider
45
67
  */
46
68
  export const buildPayloadFromKeyVaults = (
47
- provider: string,
48
69
  keyVaults: ProviderKeyVaults,
70
+ runtimeProvider: string,
49
71
  ): ClientSecretPayload => {
50
- switch (provider) {
72
+ // Use runtimeProvider to determine which fields to include
73
+ // This handles both builtin providers and custom providers with sdkType
74
+ switch (runtimeProvider) {
51
75
  case ModelProvider.Bedrock: {
52
76
  const { accessKeyId, region, secretAccessKey, sessionToken } = keyVaults;
53
77
  const apiKey = (secretAccessKey || '') + (accessKeyId || '');
@@ -58,6 +82,7 @@ export const buildPayloadFromKeyVaults = (
58
82
  awsRegion: region,
59
83
  awsSecretAccessKey: secretAccessKey,
60
84
  awsSessionToken: sessionToken,
85
+ runtimeProvider,
61
86
  };
62
87
  }
63
88
 
@@ -66,17 +91,19 @@ export const buildPayloadFromKeyVaults = (
66
91
  apiKey: keyVaults.apiKey,
67
92
  azureApiVersion: keyVaults.apiVersion,
68
93
  baseURL: keyVaults.baseURL || keyVaults.endpoint,
94
+ runtimeProvider,
69
95
  };
70
96
  }
71
97
 
72
98
  case ModelProvider.Ollama: {
73
- return { baseURL: keyVaults.baseURL };
99
+ return { baseURL: keyVaults.baseURL, runtimeProvider };
74
100
  }
75
101
 
76
102
  case ModelProvider.Cloudflare: {
77
103
  return {
78
104
  apiKey: keyVaults.apiKey,
79
105
  cloudflareBaseURLOrAccountID: keyVaults.baseURLOrAccountID,
106
+ runtimeProvider,
80
107
  };
81
108
  }
82
109
 
@@ -87,6 +114,7 @@ export const buildPayloadFromKeyVaults = (
87
114
  baseURL: keyVaults.baseURL,
88
115
  customHeaders: keyVaults.customHeaders,
89
116
  password: keyVaults.password,
117
+ runtimeProvider,
90
118
  username: keyVaults.username,
91
119
  };
92
120
  }
@@ -95,6 +123,7 @@ export const buildPayloadFromKeyVaults = (
95
123
  return {
96
124
  apiKey: keyVaults.apiKey,
97
125
  baseURL: keyVaults.baseURL,
126
+ runtimeProvider,
98
127
  vertexAIRegion: keyVaults.region,
99
128
  };
100
129
  }
@@ -103,6 +132,7 @@ export const buildPayloadFromKeyVaults = (
103
132
  return {
104
133
  apiKey: keyVaults.apiKey,
105
134
  baseURL: keyVaults.baseURL,
135
+ runtimeProvider,
106
136
  };
107
137
  }
108
138
  }
@@ -350,10 +380,16 @@ export const initModelRuntimeFromDB = async (
350
380
  KeyVaultsGateKeeper.getUserKeyVaults,
351
381
  );
352
382
 
353
- // 2. Build ClientSecretPayload from keyVaults
383
+ // 2. Resolve the runtime provider for custom providers
384
+ // For custom providers, use sdkType from settings (defaults to 'openai')
385
+ const sdkType = providerConfig?.settings?.sdkType;
386
+ const runtimeProvider = resolveRuntimeProvider(provider, sdkType);
387
+
388
+ // 3. Build ClientSecretPayload from keyVaults based on runtimeProvider
389
+ // This ensures provider-specific fields (e.g., cloudflareBaseURLOrAccountID) are included
354
390
  const keyVaults = (providerConfig?.keyVaults || {}) as ProviderKeyVaults;
355
- const payload = buildPayloadFromKeyVaults(provider, keyVaults);
391
+ const payload = buildPayloadFromKeyVaults(keyVaults, runtimeProvider);
356
392
 
357
- // 3. Initialize ModelRuntime with the payload
393
+ // 4. Initialize ModelRuntime with the payload
358
394
  return initModelRuntimeWithUserPayload(provider, payload);
359
395
  };
@@ -55,6 +55,50 @@ export const documentRouter = router({
55
55
  });
56
56
  }),
57
57
 
58
+ createDocuments: documentProcedure
59
+ .input(
60
+ z.object({
61
+ documents: z.array(
62
+ z.object({
63
+ content: z.string().optional(),
64
+ editorData: z.string(),
65
+ fileType: z.string().optional(),
66
+ knowledgeBaseId: z.string().optional(),
67
+ metadata: z.record(z.any()).optional(),
68
+ parentId: z.string().optional(),
69
+ slug: z.string().optional(),
70
+ title: z.string(),
71
+ }),
72
+ ),
73
+ }),
74
+ )
75
+ .mutation(async ({ ctx, input }) => {
76
+ // Process each document: resolve parentId and parse editorData
77
+ const processedDocuments = await Promise.all(
78
+ input.documents.map(async (doc) => {
79
+ // Resolve parentId if it's a slug
80
+ let resolvedParentId = doc.parentId;
81
+ if (doc.parentId) {
82
+ const docBySlug = await ctx.documentModel.findBySlug(doc.parentId);
83
+ if (docBySlug) {
84
+ resolvedParentId = docBySlug.id;
85
+ }
86
+ }
87
+
88
+ // Parse editorData from JSON string to object
89
+ const editorData = JSON.parse(doc.editorData);
90
+
91
+ return {
92
+ ...doc,
93
+ editorData,
94
+ parentId: resolvedParentId,
95
+ };
96
+ }),
97
+ );
98
+
99
+ return ctx.documentService.createDocuments(processedDocuments);
100
+ }),
101
+
58
102
  deleteDocument: documentProcedure
59
103
  .input(z.object({ id: z.string() }))
60
104
  .mutation(async ({ ctx, input }) => {