@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.
- package/.github/workflows/bundle-analyzer.yml +1 -1
- package/.github/workflows/e2e.yml +62 -53
- package/.github/workflows/manual-build-desktop.yml +5 -5
- package/.github/workflows/pr-build-desktop.yml +4 -4
- package/.github/workflows/pr-build-docker.yml +2 -2
- package/.github/workflows/release-desktop-beta.yml +4 -4
- package/.github/workflows/release-docker.yml +2 -2
- package/.github/workflows/test.yml +44 -7
- package/CHANGELOG.md +59 -0
- package/CLAUDE.md +1 -1
- package/changelog/v1.json +14 -0
- package/docs/development/basic/feature-development.mdx +4 -5
- package/docs/development/basic/feature-development.zh-CN.mdx +4 -5
- package/docs/self-hosting/environment-variables/auth.mdx +7 -0
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +7 -0
- package/e2e/README.md +6 -6
- package/e2e/src/features/community/detail-pages.feature +9 -9
- package/e2e/src/features/community/interactions.feature +13 -13
- package/e2e/src/features/community/smoke.feature +6 -6
- package/e2e/src/steps/agent/conversation-mgmt.steps.ts +196 -25
- package/e2e/src/steps/agent/conversation.steps.ts +58 -0
- package/e2e/src/steps/agent/message-ops.steps.ts +20 -15
- package/e2e/src/steps/community/detail-pages.steps.ts +60 -19
- package/e2e/src/steps/community/interactions.steps.ts +145 -32
- package/e2e/src/steps/hooks.ts +12 -2
- package/locales/en-US/setting.json +3 -0
- package/locales/zh-CN/file.json +4 -0
- package/locales/zh-CN/setting.json +3 -0
- package/package.json +5 -5
- package/packages/business/config/src/llm.ts +6 -1
- package/packages/const/src/index.ts +1 -0
- package/packages/const/src/lobehubSkill.ts +55 -0
- package/packages/const/src/settings/image.ts +1 -1
- package/packages/model-bank/src/aiModels/azure.ts +2 -2
- package/packages/model-bank/src/aiModels/google.ts +1 -0
- package/packages/model-bank/src/aiModels/lobehub.ts +33 -13
- package/packages/model-bank/src/aiModels/openai.ts +21 -4
- package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +4 -1
- package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +1 -1
- package/packages/ssrf-safe-fetch/index.test.ts +5 -34
- package/packages/ssrf-safe-fetch/index.ts +12 -2
- package/packages/types/package.json +1 -1
- package/packages/types/src/files/upload.ts +11 -1
- package/packages/types/src/message/common/tools.ts +1 -1
- package/packages/types/src/serverConfig.ts +1 -0
- package/public/not-compatible.html +1296 -0
- package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/MultiImagesUpload/index.tsx +3 -3
- package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +3 -10
- package/src/app/[variants]/(main)/image/index.tsx +1 -1
- package/src/app/[variants]/(main)/resource/features/FileDetail.tsx +20 -12
- package/src/app/[variants]/(main)/resource/features/modal/FullscreenModal.tsx +2 -4
- package/src/app/[variants]/layout.tsx +50 -1
- package/src/envs/auth.ts +15 -0
- package/src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx +304 -0
- package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +74 -10
- package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/ToolTitle.tsx +9 -0
- package/src/features/FileViewer/Renderer/Code/index.tsx +224 -0
- package/src/features/FileViewer/Renderer/Image/index.tsx +8 -1
- package/src/features/FileViewer/Renderer/PDF/index.tsx +3 -1
- package/src/features/FileViewer/Renderer/PDF/style.ts +2 -1
- package/src/features/FileViewer/index.tsx +135 -24
- package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +7 -4
- package/src/features/PageEditor/store/initialState.ts +2 -1
- package/src/features/ResourceManager/components/Editor/FileContent.tsx +1 -4
- package/src/features/ResourceManager/components/Editor/FileCopilot.tsx +64 -0
- package/src/features/ResourceManager/components/Editor/index.tsx +98 -31
- package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +3 -2
- package/src/features/ResourceManager/components/Explorer/ListView/ColumnResizeHandle.tsx +119 -0
- package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +67 -22
- package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +46 -11
- package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +140 -81
- package/src/features/ResourceManager/components/Explorer/ToolBar/SortDropdown.tsx +20 -12
- package/src/features/ResourceManager/components/Explorer/ToolBar/ViewSwitcher.tsx +18 -10
- package/src/features/ResourceManager/components/UploadDock/Item.tsx +38 -6
- package/src/features/ResourceManager/components/UploadDock/index.tsx +62 -41
- package/src/features/ResourceManager/index.tsx +1 -0
- package/src/helpers/toolEngineering/index.test.ts +3 -0
- package/src/helpers/toolEngineering/index.ts +12 -1
- package/src/hooks/useFetchAiImageConfig.ts +54 -10
- package/src/libs/trpc/utils/internalJwt.ts +2 -2
- package/src/locales/default/file.ts +4 -0
- package/src/locales/default/setting.ts +3 -0
- package/src/server/globalConfig/index.ts +1 -0
- package/src/server/modules/ModelRuntime/index.test.ts +214 -1
- package/src/server/modules/ModelRuntime/index.ts +43 -7
- package/src/server/routers/lambda/document.ts +44 -0
- package/src/server/routers/tools/market.ts +261 -0
- package/src/server/services/document/index.ts +22 -0
- package/src/services/document/index.ts +4 -0
- package/src/services/upload.ts +22 -2
- package/src/store/chat/slices/plugin/actions/internals.ts +15 -2
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +104 -0
- package/src/store/file/slices/fileManager/action.test.ts +9 -3
- package/src/store/file/slices/fileManager/action.ts +165 -70
- package/src/store/file/slices/upload/action.ts +3 -0
- package/src/store/global/actions/general.ts +15 -0
- package/src/store/global/initialState.ts +13 -0
- package/src/store/image/slices/generationConfig/initialState.ts +5 -5
- package/src/store/image/slices/generationConfig/selectors.test.ts +11 -4
- package/src/store/serverConfig/selectors.ts +1 -0
- package/src/store/tool/initialState.ts +11 -2
- package/src/store/tool/selectors/index.ts +1 -0
- package/src/store/tool/selectors/tool.ts +3 -1
- package/src/store/tool/slices/lobehubSkillStore/action.ts +361 -0
- package/src/store/tool/slices/lobehubSkillStore/index.ts +4 -0
- package/src/store/tool/slices/lobehubSkillStore/initialState.ts +24 -0
- package/src/store/tool/slices/lobehubSkillStore/selectors.ts +145 -0
- package/src/store/tool/slices/lobehubSkillStore/types.ts +100 -0
- package/src/store/tool/store.ts +8 -2
- package/vitest.config.mts +11 -6
- package/src/features/FileViewer/Renderer/JavaScript/index.tsx +0 -66
- 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,
|
|
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
|
|
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(
|
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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(
|
|
391
|
+
const payload = buildPayloadFromKeyVaults(keyVaults, runtimeProvider);
|
|
356
392
|
|
|
357
|
-
//
|
|
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 }) => {
|