@lobehub/chat 0.122.9 → 0.123.1
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/.env.example +22 -2
- package/.eslintrc.js +1 -0
- package/CHANGELOG.md +59 -0
- package/Dockerfile +7 -1
- package/README.md +12 -10
- package/README.zh-CN.md +12 -10
- package/docs/Deployment/Docker-Deployment.md +1 -1
- package/docs/Deployment/Docker-Deployment.zh-CN.md +1 -1
- package/docs/Deployment/Environment-Variable.md +65 -23
- package/docs/Deployment/Environment-Variable.zh-CN.md +64 -22
- package/locales/ar/chat.json +12 -1
- package/locales/ar/common.json +16 -0
- package/locales/ar/error.json +35 -6
- package/locales/ar/setting.json +53 -6
- package/locales/de-DE/chat.json +12 -1
- package/locales/de-DE/common.json +16 -0
- package/locales/de-DE/error.json +35 -6
- package/locales/de-DE/setting.json +53 -6
- package/locales/en-US/chat.json +13 -2
- package/locales/en-US/common.json +16 -0
- package/locales/en-US/error.json +35 -6
- package/locales/en-US/setting.json +53 -6
- package/locales/es-ES/chat.json +12 -1
- package/locales/es-ES/common.json +16 -0
- package/locales/es-ES/error.json +35 -6
- package/locales/es-ES/setting.json +53 -6
- package/locales/fr-FR/chat.json +13 -2
- package/locales/fr-FR/common.json +16 -0
- package/locales/fr-FR/error.json +35 -6
- package/locales/fr-FR/setting.json +53 -6
- package/locales/it-IT/chat.json +12 -1
- package/locales/it-IT/common.json +16 -0
- package/locales/it-IT/error.json +35 -6
- package/locales/it-IT/setting.json +53 -6
- package/locales/ja-JP/chat.json +12 -1
- package/locales/ja-JP/common.json +16 -0
- package/locales/ja-JP/error.json +35 -6
- package/locales/ja-JP/setting.json +53 -6
- package/locales/ko-KR/chat.json +12 -1
- package/locales/ko-KR/common.json +16 -0
- package/locales/ko-KR/error.json +35 -6
- package/locales/ko-KR/setting.json +53 -6
- package/locales/nl-NL/chat.json +12 -1
- package/locales/nl-NL/common.json +16 -0
- package/locales/nl-NL/error.json +35 -6
- package/locales/nl-NL/setting.json +53 -6
- package/locales/pl-PL/chat.json +12 -1
- package/locales/pl-PL/common.json +16 -0
- package/locales/pl-PL/error.json +35 -6
- package/locales/pl-PL/setting.json +53 -6
- package/locales/pt-BR/chat.json +12 -1
- package/locales/pt-BR/common.json +16 -0
- package/locales/pt-BR/error.json +35 -6
- package/locales/pt-BR/setting.json +53 -6
- package/locales/ru-RU/chat.json +12 -1
- package/locales/ru-RU/common.json +16 -0
- package/locales/ru-RU/error.json +35 -6
- package/locales/ru-RU/setting.json +53 -6
- package/locales/tr-TR/chat.json +12 -1
- package/locales/tr-TR/common.json +16 -0
- package/locales/tr-TR/error.json +35 -6
- package/locales/tr-TR/setting.json +53 -6
- package/locales/vi-VN/chat.json +12 -1
- package/locales/vi-VN/common.json +16 -0
- package/locales/vi-VN/error.json +35 -6
- package/locales/vi-VN/setting.json +53 -6
- package/locales/zh-CN/chat.json +12 -1
- package/locales/zh-CN/common.json +16 -0
- package/locales/zh-CN/error.json +34 -5
- package/locales/zh-CN/setting.json +59 -12
- package/locales/zh-TW/chat.json +12 -1
- package/locales/zh-TW/common.json +16 -0
- package/locales/zh-TW/error.json +35 -6
- package/locales/zh-TW/setting.json +53 -6
- package/next.config.mjs +8 -2
- package/package.json +8 -2
- package/src/app/api/chat/[provider]/agentRuntime.ts +129 -0
- package/src/app/api/chat/[provider]/route.ts +64 -0
- package/src/app/api/chat/auth.ts +42 -0
- package/src/app/api/chat/google/route.ts +86 -0
- package/src/app/api/config/route.ts +6 -1
- package/src/app/api/config.ts +3 -3
- package/src/app/api/errorResponse.test.ts +8 -8
- package/src/app/api/errorResponse.ts +43 -6
- package/src/app/api/openai/createBizOpenAI/createAzureOpenai.ts +1 -1
- package/src/app/api/openai/createBizOpenAI/createOpenai.ts +1 -1
- package/src/app/api/openai/createBizOpenAI/index.ts +2 -2
- package/src/app/api/plugin/gateway/route.ts +4 -1
- package/src/app/chat/(desktop)/features/ChatHeader.tsx +6 -4
- package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +13 -3
- package/src/app/chat/(desktop)/features/ChatInput/Header/index.tsx +16 -24
- package/src/app/chat/(desktop)/features/SideBar/index.tsx +1 -0
- package/src/app/chat/features/ChatHeader/ShareButton/Preview.tsx +3 -3
- package/src/app/chat/features/SessionListContent/List/Item/index.tsx +2 -3
- package/src/app/settings/llm/Azure/index.tsx +145 -0
- package/src/app/settings/llm/Bedrock/index.tsx +109 -0
- package/src/app/settings/llm/{LLM/Checker.tsx → Checker.tsx} +11 -14
- package/src/app/settings/llm/Google/index.tsx +74 -0
- package/src/app/settings/llm/{LLM → OpenAI}/index.tsx +26 -44
- package/src/app/settings/llm/Zhipu/index.tsx +78 -0
- package/src/app/settings/llm/const.ts +13 -0
- package/src/app/settings/llm/page.tsx +42 -2
- package/src/app/settings/llm/useSyncSettings.ts +23 -0
- package/src/chains/__tests__/langDetect.test.ts +2 -2
- package/src/chains/__tests__/pickEmoji.test.ts +2 -2
- package/src/chains/langDetect.ts +2 -2
- package/src/chains/pickEmoji.ts +2 -2
- package/src/chains/summaryAgentName.ts +2 -2
- package/src/chains/summaryDescription.ts +2 -2
- package/src/chains/summaryTags.ts +2 -2
- package/src/chains/summaryTitle.ts +2 -2
- package/src/chains/translate.ts +2 -2
- package/src/components/ModelIcon/index.tsx +37 -0
- package/src/components/ModelProviderIcon/index.tsx +44 -0
- package/src/components/ModelSelect/index.tsx +133 -0
- package/src/components/ModelTag/ModelIcon.tsx +35 -0
- package/src/components/ModelTag/index.tsx +13 -0
- package/src/config/modelProviders/bedrock.ts +43 -0
- package/src/config/modelProviders/google.ts +20 -0
- package/src/config/modelProviders/index.ts +18 -0
- package/src/config/modelProviders/openai.ts +110 -0
- package/src/config/modelProviders/zhipu.ts +34 -0
- package/src/config/{server.ts → server/app.ts} +1 -24
- package/src/config/server/index.ts +13 -0
- package/src/config/server/provider.ts +78 -0
- package/src/const/auth.ts +30 -0
- package/src/const/fetch.ts +3 -0
- package/src/const/settings.ts +24 -4
- package/src/database/core/migrations/migrateSettingsToUser/index.test.ts +2 -1
- package/src/database/core/migrations/migrateSettingsToUser/index.ts +1 -18
- package/src/database/core/migrations/migrateSettingsToUser/type.ts +100 -0
- package/src/database/models/message.ts +3 -1
- package/src/database/schemas/message.ts +1 -0
- package/src/database/schemas/session.ts +1 -0
- package/src/features/AgentSetting/AgentConfig/ModelSelect.tsx +71 -0
- package/src/features/AgentSetting/AgentConfig/index.tsx +171 -161
- package/src/features/AgentSetting/AgentConfig/useSyncConfig.ts +23 -0
- package/src/features/AgentSetting/AgentPrompt/TokenTag.tsx +39 -0
- package/src/features/AgentSetting/AgentPrompt/index.tsx +4 -26
- package/src/features/AgentSetting/store/index.ts +10 -2
- package/src/features/ChatInput/ActionBar/FileUpload.tsx +15 -5
- package/src/features/ChatInput/ActionBar/ModelSwitch.tsx +56 -14
- package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +42 -7
- package/src/features/ChatInput/ActionBar/Token/index.tsx +5 -2
- package/src/features/ChatInput/ActionBar/Tools/index.tsx +11 -1
- package/src/features/ChatInput/useChatInput.ts +5 -1
- package/src/features/Conversation/Error/APIKeyForm/Bedrock.tsx +80 -0
- package/src/features/Conversation/Error/APIKeyForm/Google.tsx +61 -0
- package/src/features/Conversation/Error/APIKeyForm/OpenAI.tsx +63 -0
- package/src/features/Conversation/Error/APIKeyForm/Zhipu.tsx +62 -0
- package/src/features/Conversation/Error/APIKeyForm/index.tsx +72 -0
- package/src/features/Conversation/Error/AccessCodeForm.tsx +63 -0
- package/src/features/Conversation/Error/ErrorJsonViewer.tsx +26 -0
- package/src/features/Conversation/Error/InvalidAPIKey.tsx +16 -0
- package/src/features/Conversation/Error/InvalidAccessCode.tsx +50 -0
- package/src/features/Conversation/Error/OpenAiBizError.tsx +9 -17
- package/src/features/Conversation/Error/{Plugin/PluginSettings.tsx → PluginSettings.tsx} +8 -3
- package/src/features/Conversation/Error/index.tsx +75 -70
- package/src/features/Conversation/Error/style.tsx +9 -3
- package/src/features/Conversation/Extras/Assistant.tsx +2 -3
- package/src/features/Conversation/Plugins/Inspector/index.tsx +8 -2
- package/src/features/Conversation/components/ChatItem/index.tsx +9 -24
- package/src/features/Conversation/types/index.tsx +2 -5
- package/src/hooks/_header.ts +35 -0
- package/src/libs/agent-runtime/BaseAI.ts +9 -0
- package/src/libs/agent-runtime/azureOpenai/index.ts +72 -0
- package/src/libs/agent-runtime/bedrock/index.ts +134 -0
- package/src/libs/agent-runtime/error.ts +26 -0
- package/src/libs/agent-runtime/google/index.ts +160 -0
- package/src/libs/agent-runtime/index.ts +9 -0
- package/src/libs/agent-runtime/openai/index.test.ts +176 -0
- package/src/libs/agent-runtime/openai/index.ts +107 -0
- package/src/libs/agent-runtime/types/chat.ts +119 -0
- package/src/libs/agent-runtime/types/index.ts +2 -0
- package/src/libs/agent-runtime/types/type.ts +34 -0
- package/src/libs/agent-runtime/utils/createError.ts +10 -0
- package/src/libs/agent-runtime/utils/debugStream.ts +18 -0
- package/src/libs/agent-runtime/utils/env.ts +1 -0
- package/src/libs/agent-runtime/utils/handleOpenAIError.ts +39 -0
- package/src/libs/agent-runtime/utils/uriParser.ts +16 -0
- package/src/libs/agent-runtime/zhipu/authToken.ts +22 -0
- package/src/libs/agent-runtime/zhipu/index.ts +125 -0
- package/src/locales/default/chat.ts +12 -2
- package/src/locales/default/common.ts +16 -0
- package/src/locales/default/error.ts +42 -5
- package/src/locales/default/setting.ts +61 -12
- package/src/services/__tests__/chat.test.ts +8 -2
- package/src/services/__tests__/message.test.ts +1 -1
- package/src/services/_auth.ts +106 -0
- package/src/services/_header.ts +1 -0
- package/src/services/_url.ts +0 -2
- package/src/services/chat.ts +34 -14
- package/src/store/chat/slices/message/action.ts +9 -6
- package/src/store/global/slices/common/action.ts +8 -2
- package/src/store/global/slices/settings/action.test.ts +2 -2
- package/src/store/global/slices/settings/action.ts +11 -4
- package/src/store/global/slices/settings/selectors/__snapshots__/modelProvider.test.ts.snap +24 -92
- package/src/store/global/slices/settings/selectors/__snapshots__/selectors.test.ts.snap +2 -0
- package/src/store/global/slices/settings/selectors/modelProvider.test.ts +127 -50
- package/src/store/global/slices/settings/selectors/modelProvider.ts +145 -43
- package/src/store/session/slices/agent/selectors.test.ts +0 -48
- package/src/store/session/slices/agent/selectors.ts +7 -12
- package/src/types/agent/index.ts +6 -2
- package/src/types/fetch.ts +4 -2
- package/src/types/files.ts +3 -3
- package/src/types/llm.ts +34 -0
- package/src/types/message/index.ts +7 -3
- package/src/types/openai/chat.ts +6 -11
- package/src/types/settings/index.ts +1 -0
- package/src/types/settings/modelProvider.ts +37 -1
- package/src/utils/__snapshots__/parseModels.test.ts.snap +63 -0
- package/src/utils/parseModels.test.ts +24 -0
- package/src/utils/parseModels.ts +37 -0
- package/src/app/api/openai/chat/createChatCompletion.test.ts +0 -166
- package/src/app/api/openai/chat/createChatCompletion.ts +0 -76
- package/src/app/api/openai/chat/route.ts +0 -19
- package/src/app/api/openai/models/route.ts +0 -17
- package/src/app/settings/llm/LLM/ModelList.tsx +0 -51
- package/src/app/settings/llm/LLM/getModelList.ts +0 -9
- package/src/app/settings/llm/index.tsx +0 -36
- package/src/const/llm.ts +0 -32
- package/src/features/Conversation/Error/ApiKeyForm.tsx +0 -87
- package/src/features/Conversation/Error/ErrorActionContainer.tsx +0 -23
- package/src/features/Conversation/Error/InvalidAccess.tsx +0 -90
- package/src/features/Conversation/Error/OpenAPIKey.tsx +0 -13
- package/src/features/Conversation/Error/Plugin/PluginError.tsx +0 -30
- package/src/services/modelList.ts +0 -15
- /package/src/app/chat/(desktop)/features/ChatInput/{Header → Footer}/DragUpload.tsx +0 -0
- /package/src/{app/api/openai/chat → libs/agent-runtime/utils}/desensitizeUrl.test.ts +0 -0
- /package/src/{app/api/openai/chat → libs/agent-runtime/utils}/desensitizeUrl.ts +0 -0
- /package/src/types/{translate.ts → message/translate.ts} +0 -0
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import OpenAI, { APIError } from 'openai';
|
|
2
|
-
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { createChatCompletion } from './createChatCompletion';
|
|
5
|
-
|
|
6
|
-
// Mock the console.error to avoid polluting test output
|
|
7
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
8
|
-
|
|
9
|
-
describe('createChatCompletion', () => {
|
|
10
|
-
let openaiInstance: OpenAI;
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
openaiInstance = new OpenAI({ apiKey: 'test', dangerouslyAllowBrowser: true });
|
|
14
|
-
|
|
15
|
-
// 使用 vi.spyOn 来模拟 chat.completions.create 方法
|
|
16
|
-
vi.spyOn(openaiInstance.chat.completions, 'create').mockResolvedValue(
|
|
17
|
-
new ReadableStream() as any,
|
|
18
|
-
);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
vi.clearAllMocks();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('should return a StreamingTextResponse on successful API call', async () => {
|
|
26
|
-
// Arrange
|
|
27
|
-
const mockStream = new ReadableStream();
|
|
28
|
-
const mockResponse = Promise.resolve(mockStream);
|
|
29
|
-
|
|
30
|
-
(openaiInstance.chat.completions.create as Mock).mockResolvedValue(mockResponse);
|
|
31
|
-
|
|
32
|
-
// Act
|
|
33
|
-
const result = await createChatCompletion({
|
|
34
|
-
openai: openaiInstance,
|
|
35
|
-
payload: {
|
|
36
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
|
37
|
-
model: 'text-davinci-003',
|
|
38
|
-
temperature: 0,
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// Assert
|
|
43
|
-
expect(result).toBeInstanceOf(Response); // 注意这里的改动
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should return an openai error response when OpenAI.APIError is thrown', async () => {
|
|
47
|
-
// Arrange
|
|
48
|
-
const apiError = new OpenAI.APIError(
|
|
49
|
-
400,
|
|
50
|
-
{
|
|
51
|
-
status: 400,
|
|
52
|
-
error: {
|
|
53
|
-
message: 'Bad Request',
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
'Error message',
|
|
57
|
-
{},
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
vi.spyOn(openaiInstance.chat.completions, 'create').mockRejectedValue(apiError);
|
|
61
|
-
|
|
62
|
-
// Act
|
|
63
|
-
const result = await createChatCompletion({
|
|
64
|
-
openai: openaiInstance,
|
|
65
|
-
payload: {
|
|
66
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
|
67
|
-
model: 'text-davinci-003',
|
|
68
|
-
temperature: 0,
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Assert
|
|
73
|
-
expect(result).toBeInstanceOf(Response);
|
|
74
|
-
expect(result.status).toBe(577); // Your custom error status code
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should return an cause response when OpenAI.APIError is thrown with cause', async () => {
|
|
78
|
-
// Arrange
|
|
79
|
-
const errorInfo = {
|
|
80
|
-
stack: 'abc',
|
|
81
|
-
cause: {
|
|
82
|
-
message: 'api is undefined',
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
|
|
86
|
-
|
|
87
|
-
vi.spyOn(openaiInstance.chat.completions, 'create').mockRejectedValue(apiError);
|
|
88
|
-
|
|
89
|
-
// Act
|
|
90
|
-
const result = await createChatCompletion({
|
|
91
|
-
openai: openaiInstance,
|
|
92
|
-
payload: {
|
|
93
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
|
94
|
-
model: 'text-davinci-003',
|
|
95
|
-
temperature: 0,
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// Assert
|
|
100
|
-
expect(result).toBeInstanceOf(Response);
|
|
101
|
-
expect(result.status).toBe(577); // Your custom error status code
|
|
102
|
-
|
|
103
|
-
const content = await result.json();
|
|
104
|
-
expect(content.body).toHaveProperty('endpoint');
|
|
105
|
-
expect(content.body.endpoint).toEqual('https://api.openai.com/v1');
|
|
106
|
-
expect(content.body.error).toEqual(errorInfo);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should return an cause response with desensitize Url', async () => {
|
|
110
|
-
// Arrange
|
|
111
|
-
const errorInfo = {
|
|
112
|
-
stack: 'abc',
|
|
113
|
-
cause: { message: 'api is undefined' },
|
|
114
|
-
};
|
|
115
|
-
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
|
|
116
|
-
|
|
117
|
-
openaiInstance = new OpenAI({
|
|
118
|
-
apiKey: 'test',
|
|
119
|
-
dangerouslyAllowBrowser: true,
|
|
120
|
-
baseURL: 'https://api.abc.com/v1',
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
vi.spyOn(openaiInstance.chat.completions, 'create').mockRejectedValue(apiError);
|
|
124
|
-
|
|
125
|
-
// Act
|
|
126
|
-
const result = await createChatCompletion({
|
|
127
|
-
openai: openaiInstance,
|
|
128
|
-
payload: {
|
|
129
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
|
130
|
-
model: 'gpt-3.5-turbo',
|
|
131
|
-
temperature: 0,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Assert
|
|
136
|
-
expect(result).toBeInstanceOf(Response);
|
|
137
|
-
expect(result.status).toBe(577); // Your custom error status code
|
|
138
|
-
|
|
139
|
-
const content = await result.json();
|
|
140
|
-
expect(content.body.endpoint).toEqual('https://api.***.com/v1');
|
|
141
|
-
expect(content.body.error).toEqual(errorInfo);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('should return a 500 error response for non-OpenAI errors', async () => {
|
|
145
|
-
// Arrange
|
|
146
|
-
const genericError = new Error('Generic Error');
|
|
147
|
-
|
|
148
|
-
vi.spyOn(openaiInstance.chat.completions, 'create').mockRejectedValue(genericError);
|
|
149
|
-
|
|
150
|
-
// Act
|
|
151
|
-
const result = await createChatCompletion({
|
|
152
|
-
openai: openaiInstance,
|
|
153
|
-
payload: {
|
|
154
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
|
155
|
-
model: 'text-davinci-003',
|
|
156
|
-
temperature: 0,
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// Assert
|
|
161
|
-
expect(result.status).toBe(500);
|
|
162
|
-
const content = await result.json();
|
|
163
|
-
expect(content.body).toHaveProperty('endpoint');
|
|
164
|
-
expect(content.body).toHaveProperty('error');
|
|
165
|
-
});
|
|
166
|
-
});
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { OpenAIStream, StreamingTextResponse } from 'ai';
|
|
2
|
-
import OpenAI from 'openai';
|
|
3
|
-
|
|
4
|
-
import { ChatErrorType } from '@/types/fetch';
|
|
5
|
-
import { OpenAIChatStreamPayload } from '@/types/openai/chat';
|
|
6
|
-
|
|
7
|
-
import { createErrorResponse } from '../../errorResponse';
|
|
8
|
-
import { desensitizeUrl } from './desensitizeUrl';
|
|
9
|
-
|
|
10
|
-
interface CreateChatCompletionOptions {
|
|
11
|
-
openai: OpenAI;
|
|
12
|
-
payload: OpenAIChatStreamPayload;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const createChatCompletion = async ({ payload, openai }: CreateChatCompletionOptions) => {
|
|
16
|
-
// ============ 1. preprocess messages ============ //
|
|
17
|
-
const { messages, ...params } = payload;
|
|
18
|
-
|
|
19
|
-
// ============ 2. send api ============ //
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const response = await openai.chat.completions.create(
|
|
23
|
-
{
|
|
24
|
-
messages,
|
|
25
|
-
...params,
|
|
26
|
-
stream: true,
|
|
27
|
-
} as unknown as OpenAI.ChatCompletionCreateParamsStreaming,
|
|
28
|
-
{ headers: { Accept: '*/*' } },
|
|
29
|
-
);
|
|
30
|
-
const stream = OpenAIStream(response);
|
|
31
|
-
return new StreamingTextResponse(stream);
|
|
32
|
-
} catch (error) {
|
|
33
|
-
let desensitizedEndpoint = openai.baseURL;
|
|
34
|
-
|
|
35
|
-
// refs: https://github.com/lobehub/lobe-chat/issues/842
|
|
36
|
-
if (openai.baseURL !== 'https://api.openai.com/v1') {
|
|
37
|
-
desensitizedEndpoint = desensitizeUrl(openai.baseURL);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Check if the error is an OpenAI APIError
|
|
41
|
-
if (error instanceof OpenAI.APIError) {
|
|
42
|
-
let errorResult: any;
|
|
43
|
-
|
|
44
|
-
// if error is definitely OpenAI APIError, there will be an error object
|
|
45
|
-
if (error.error) {
|
|
46
|
-
errorResult = error.error;
|
|
47
|
-
}
|
|
48
|
-
// Or if there is a cause, we use error cause
|
|
49
|
-
// This often happened when there is a bug of the `openai` package.
|
|
50
|
-
else if (error.cause) {
|
|
51
|
-
errorResult = error.cause;
|
|
52
|
-
}
|
|
53
|
-
// if there is no other request error, the error object is a Response like object
|
|
54
|
-
else {
|
|
55
|
-
errorResult = { headers: error.headers, stack: error.stack, status: error.status };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// track the error at server side
|
|
59
|
-
console.error(errorResult);
|
|
60
|
-
|
|
61
|
-
return createErrorResponse(ChatErrorType.OpenAIBizError, {
|
|
62
|
-
endpoint: desensitizedEndpoint,
|
|
63
|
-
error: errorResult,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// track the non-openai error
|
|
68
|
-
console.error(error);
|
|
69
|
-
|
|
70
|
-
// return as a GatewayTimeout error
|
|
71
|
-
return createErrorResponse(ChatErrorType.InternalServerError, {
|
|
72
|
-
endpoint: desensitizedEndpoint,
|
|
73
|
-
error: JSON.stringify(error),
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { OpenAIChatStreamPayload } from '@/types/openai/chat';
|
|
2
|
-
|
|
3
|
-
import { getPreferredRegion } from '../../config';
|
|
4
|
-
import { createBizOpenAI } from '../createBizOpenAI';
|
|
5
|
-
import { createChatCompletion } from './createChatCompletion';
|
|
6
|
-
|
|
7
|
-
export const runtime = 'edge';
|
|
8
|
-
export const preferredRegion = getPreferredRegion();
|
|
9
|
-
|
|
10
|
-
export const POST = async (req: Request) => {
|
|
11
|
-
const payload = (await req.json()) as OpenAIChatStreamPayload;
|
|
12
|
-
|
|
13
|
-
const openaiOrErrResponse = createBizOpenAI(req, payload.model);
|
|
14
|
-
|
|
15
|
-
// if resOrOpenAI is a Response, it means there is an error,just return it
|
|
16
|
-
if (openaiOrErrResponse instanceof Response) return openaiOrErrResponse;
|
|
17
|
-
|
|
18
|
-
return createChatCompletion({ openai: openaiOrErrResponse, payload });
|
|
19
|
-
};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { getOpenAIAuthFromRequest } from '@/const/fetch';
|
|
2
|
-
|
|
3
|
-
import { createOpenai } from '../createBizOpenAI/createOpenai';
|
|
4
|
-
|
|
5
|
-
export const runtime = 'edge';
|
|
6
|
-
|
|
7
|
-
export const POST = async (req: Request) => {
|
|
8
|
-
const { apiKey, endpoint } = getOpenAIAuthFromRequest(req);
|
|
9
|
-
|
|
10
|
-
const openAI = createOpenai(apiKey, endpoint);
|
|
11
|
-
|
|
12
|
-
const res = await openAI.models.list();
|
|
13
|
-
|
|
14
|
-
const modelList = res.data.map((i) => i.id);
|
|
15
|
-
|
|
16
|
-
return new Response(JSON.stringify(modelList));
|
|
17
|
-
};
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { ActionIcon } from '@lobehub/ui';
|
|
2
|
-
import { Button, Dropdown, Skeleton } from 'antd';
|
|
3
|
-
import { MenuItemType } from 'antd/lib/menu/hooks/useItems';
|
|
4
|
-
import { RotateCwIcon } from 'lucide-react';
|
|
5
|
-
import { memo } from 'react';
|
|
6
|
-
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { Flexbox } from 'react-layout-kit';
|
|
8
|
-
|
|
9
|
-
import { getModelList } from './getModelList';
|
|
10
|
-
|
|
11
|
-
interface ModelListProps {
|
|
12
|
-
value?: string[];
|
|
13
|
-
}
|
|
14
|
-
const ModelList = memo<ModelListProps>(({ value }) => {
|
|
15
|
-
const { t } = useTranslation('setting');
|
|
16
|
-
|
|
17
|
-
const isLoading = !value;
|
|
18
|
-
|
|
19
|
-
if (isLoading)
|
|
20
|
-
return (
|
|
21
|
-
<Skeleton active paragraph={false} title={{ style: { marginBottom: 0 }, width: '50%' }} />
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
const isEmpty = value?.length === 0;
|
|
25
|
-
|
|
26
|
-
return isEmpty ? (
|
|
27
|
-
<Button
|
|
28
|
-
onClick={() => {
|
|
29
|
-
getModelList('openAI');
|
|
30
|
-
}}
|
|
31
|
-
>
|
|
32
|
-
{t('llm.OpenAI.models.fetch')}
|
|
33
|
-
</Button>
|
|
34
|
-
) : (
|
|
35
|
-
<Dropdown
|
|
36
|
-
menu={{
|
|
37
|
-
items: (value || []).map<MenuItemType>((v) => ({
|
|
38
|
-
key: v,
|
|
39
|
-
label: v,
|
|
40
|
-
})),
|
|
41
|
-
}}
|
|
42
|
-
>
|
|
43
|
-
<Flexbox align={'center'} gap={8} horizontal>
|
|
44
|
-
{t('llm.OpenAI.models.count', { count: value.length })}
|
|
45
|
-
<ActionIcon icon={RotateCwIcon} size={'small'} title={t('llm.OpenAI.models.refetch')} />
|
|
46
|
-
</Flexbox>
|
|
47
|
-
</Dropdown>
|
|
48
|
-
);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
export default ModelList;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { fetchModelList } from '@/services/modelList';
|
|
2
|
-
import { useGlobalStore } from '@/store/global';
|
|
3
|
-
|
|
4
|
-
export const getModelList = async (brand = 'openAI') => {
|
|
5
|
-
const setSettings = useGlobalStore.getState().setSettings;
|
|
6
|
-
const models = await fetchModelList();
|
|
7
|
-
|
|
8
|
-
setSettings({ languageModel: { [brand]: { models } } });
|
|
9
|
-
};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import Link from 'next/link';
|
|
4
|
-
import { memo } from 'react';
|
|
5
|
-
import { Trans, useTranslation } from 'react-i18next';
|
|
6
|
-
|
|
7
|
-
import Footer from '@/app/settings/features/Footer';
|
|
8
|
-
import PageTitle from '@/components/PageTitle';
|
|
9
|
-
import { useSwitchSideBarOnInit } from '@/store/global/hooks/useSwitchSettingsOnInit';
|
|
10
|
-
import { SettingsTabs } from '@/store/global/initialState';
|
|
11
|
-
|
|
12
|
-
import LLM from './LLM';
|
|
13
|
-
|
|
14
|
-
export default memo(() => {
|
|
15
|
-
useSwitchSideBarOnInit(SettingsTabs.LLM);
|
|
16
|
-
const { t } = useTranslation('setting');
|
|
17
|
-
return (
|
|
18
|
-
<>
|
|
19
|
-
<PageTitle title={t('tab.llm')} />
|
|
20
|
-
<LLM />
|
|
21
|
-
<Footer>
|
|
22
|
-
<Trans i18nKey="llm.waitingForMore" ns={'setting'}>
|
|
23
|
-
更多模型正在
|
|
24
|
-
<Link
|
|
25
|
-
aria-label={'todo'}
|
|
26
|
-
href="https://github.com/lobehub/lobe-chat/issues/151"
|
|
27
|
-
target="_blank"
|
|
28
|
-
>
|
|
29
|
-
计划接入
|
|
30
|
-
</Link>
|
|
31
|
-
中 ,敬请期待 ✨
|
|
32
|
-
</Trans>
|
|
33
|
-
</Footer>
|
|
34
|
-
</>
|
|
35
|
-
);
|
|
36
|
-
});
|
package/src/const/llm.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { LanguageModel } from '@/types/llm';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A white list of language models that are allowed to display and be used in the app.
|
|
5
|
-
*/
|
|
6
|
-
export const LanguageModelWhiteList = [
|
|
7
|
-
// OpenAI
|
|
8
|
-
'gpt-3.5-turbo',
|
|
9
|
-
'gpt-3.5-turbo-16k',
|
|
10
|
-
'gpt-4',
|
|
11
|
-
'gpt-4-32k',
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
export const DEFAULT_OPENAI_MODEL_LIST: string[] = Object.values(LanguageModel);
|
|
15
|
-
|
|
16
|
-
// vision model white list, these models will change the content from string to array
|
|
17
|
-
// refs: https://github.com/lobehub/lobe-chat/issues/790
|
|
18
|
-
export const isVisionModel = (model?: string) => !!model && model.includes('vision');
|
|
19
|
-
|
|
20
|
-
// refs: https://github.com/lobehub/lobe-chat/issues/837
|
|
21
|
-
export const GPT4_VISION_MODEL_DEFAULT_MAX_TOKENS = 2048;
|
|
22
|
-
|
|
23
|
-
// refs to: https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo
|
|
24
|
-
export const ModelTokens: Record<LanguageModel, number> = {
|
|
25
|
-
[LanguageModel.GPT3_5]: 4096,
|
|
26
|
-
[LanguageModel.GPT3_5_1106]: 16_385,
|
|
27
|
-
[LanguageModel.GPT3_5_16K]: 16_385,
|
|
28
|
-
[LanguageModel.GPT4]: 8196,
|
|
29
|
-
[LanguageModel.GPT4_PREVIEW]: 128_000,
|
|
30
|
-
[LanguageModel.GPT4_VISION_PREVIEW]: 128_000,
|
|
31
|
-
[LanguageModel.GPT4_32K]: 32_768,
|
|
32
|
-
};
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { Icon } from '@lobehub/ui';
|
|
2
|
-
import { Button, Input } from 'antd';
|
|
3
|
-
import { Network } from 'lucide-react';
|
|
4
|
-
import { memo, useState } from 'react';
|
|
5
|
-
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import { Center, Flexbox } from 'react-layout-kit';
|
|
7
|
-
|
|
8
|
-
import { useChatStore } from '@/store/chat';
|
|
9
|
-
import { useGlobalStore } from '@/store/global';
|
|
10
|
-
import { modelProviderSelectors } from '@/store/global/selectors';
|
|
11
|
-
|
|
12
|
-
import { FormAction } from './style';
|
|
13
|
-
|
|
14
|
-
const APIKeyForm = memo<{ id: string }>(({ id }) => {
|
|
15
|
-
const { t } = useTranslation('error');
|
|
16
|
-
const [showProxy, setShow] = useState(false);
|
|
17
|
-
|
|
18
|
-
const [apiKey, proxyUrl, setConfig] = useGlobalStore((s) => [
|
|
19
|
-
modelProviderSelectors.openAIAPI(s),
|
|
20
|
-
modelProviderSelectors.openAIProxyUrl(s),
|
|
21
|
-
s.setOpenAIConfig,
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
const [resend, deleteMessage] = useChatStore((s) => [s.resendMessage, s.deleteMessage]);
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<Center gap={16} style={{ maxWidth: 300 }}>
|
|
28
|
-
<FormAction
|
|
29
|
-
avatar={'🔑'}
|
|
30
|
-
description={t('unlock.apikey.description')}
|
|
31
|
-
title={t('unlock.apikey.title')}
|
|
32
|
-
>
|
|
33
|
-
<Input.Password
|
|
34
|
-
autoComplete={'new-password'}
|
|
35
|
-
onChange={(e) => {
|
|
36
|
-
setConfig({ OPENAI_API_KEY: e.target.value });
|
|
37
|
-
}}
|
|
38
|
-
placeholder={'sk-*****************************************'}
|
|
39
|
-
type={'block'}
|
|
40
|
-
value={apiKey}
|
|
41
|
-
/>
|
|
42
|
-
{showProxy ? (
|
|
43
|
-
<Input
|
|
44
|
-
onChange={(e) => {
|
|
45
|
-
setConfig({ endpoint: e.target.value });
|
|
46
|
-
}}
|
|
47
|
-
placeholder={'https://api.openai.com/v1'}
|
|
48
|
-
type={'block'}
|
|
49
|
-
value={proxyUrl}
|
|
50
|
-
/>
|
|
51
|
-
) : (
|
|
52
|
-
<Button
|
|
53
|
-
icon={<Icon icon={Network} />}
|
|
54
|
-
onClick={() => {
|
|
55
|
-
setShow(true);
|
|
56
|
-
}}
|
|
57
|
-
type={'text'}
|
|
58
|
-
>
|
|
59
|
-
{t('unlock.apikey.addProxyUrl')}
|
|
60
|
-
</Button>
|
|
61
|
-
)}
|
|
62
|
-
</FormAction>
|
|
63
|
-
<Flexbox gap={12} width={'100%'}>
|
|
64
|
-
<Button
|
|
65
|
-
block
|
|
66
|
-
onClick={() => {
|
|
67
|
-
resend(id);
|
|
68
|
-
deleteMessage(id);
|
|
69
|
-
}}
|
|
70
|
-
style={{ marginTop: 8 }}
|
|
71
|
-
type={'primary'}
|
|
72
|
-
>
|
|
73
|
-
{t('unlock.confirm')}
|
|
74
|
-
</Button>
|
|
75
|
-
<Button
|
|
76
|
-
onClick={() => {
|
|
77
|
-
deleteMessage(id);
|
|
78
|
-
}}
|
|
79
|
-
>
|
|
80
|
-
{t('unlock.closeMessage')}
|
|
81
|
-
</Button>
|
|
82
|
-
</Flexbox>
|
|
83
|
-
</Center>
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
export default APIKeyForm;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { createStyles } from 'antd-style';
|
|
2
|
-
import { ReactNode, memo } from 'react';
|
|
3
|
-
import { Center } from 'react-layout-kit';
|
|
4
|
-
|
|
5
|
-
const useStyles = createStyles(({ css, token }) => ({
|
|
6
|
-
container: css`
|
|
7
|
-
background: ${token.colorBgContainer};
|
|
8
|
-
border: 1px solid ${token.colorSplit};
|
|
9
|
-
border-radius: 8px;
|
|
10
|
-
`,
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
const ErrorActionContainer = memo<{ children: ReactNode }>(({ children }) => {
|
|
14
|
-
const { styles } = useStyles();
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<Center className={styles.container} gap={24} padding={24}>
|
|
18
|
-
{children}
|
|
19
|
-
</Center>
|
|
20
|
-
);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
export default ErrorActionContainer;
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { Icon } from '@lobehub/ui';
|
|
2
|
-
import { Button, Input, Segmented } from 'antd';
|
|
3
|
-
import { AsteriskSquare, KeySquare } from 'lucide-react';
|
|
4
|
-
import { memo, useState } from 'react';
|
|
5
|
-
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import { Flexbox } from 'react-layout-kit';
|
|
7
|
-
|
|
8
|
-
import { useChatStore } from '@/store/chat';
|
|
9
|
-
import { useGlobalStore } from '@/store/global';
|
|
10
|
-
import { settingsSelectors } from '@/store/global/selectors';
|
|
11
|
-
|
|
12
|
-
import { RenderErrorMessage } from '../types';
|
|
13
|
-
import APIKeyForm from './ApiKeyForm';
|
|
14
|
-
import { ErrorActionContainer, FormAction } from './style';
|
|
15
|
-
|
|
16
|
-
enum Tab {
|
|
17
|
-
Api = 'api',
|
|
18
|
-
Password = 'password',
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const InvalidAccess: RenderErrorMessage['Render'] = memo(({ id }) => {
|
|
22
|
-
const { t } = useTranslation('error');
|
|
23
|
-
const [mode, setMode] = useState<Tab>(Tab.Password);
|
|
24
|
-
const [password, setSettings] = useGlobalStore((s) => [
|
|
25
|
-
settingsSelectors.currentSettings(s).password,
|
|
26
|
-
s.setSettings,
|
|
27
|
-
]);
|
|
28
|
-
const [resend, deleteMessage] = useChatStore((s) => [s.resendMessage, s.deleteMessage]);
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<ErrorActionContainer>
|
|
32
|
-
<Segmented
|
|
33
|
-
block
|
|
34
|
-
onChange={(value) => setMode(value as Tab)}
|
|
35
|
-
options={[
|
|
36
|
-
{
|
|
37
|
-
icon: <Icon icon={AsteriskSquare} />,
|
|
38
|
-
label: t('password', { ns: 'common' }),
|
|
39
|
-
value: Tab.Password,
|
|
40
|
-
},
|
|
41
|
-
{ icon: <Icon icon={KeySquare} />, label: 'OpenAI API Key', value: Tab.Api },
|
|
42
|
-
]}
|
|
43
|
-
style={{ width: '100%' }}
|
|
44
|
-
value={mode}
|
|
45
|
-
/>
|
|
46
|
-
<Flexbox gap={24}>
|
|
47
|
-
{mode === Tab.Password && (
|
|
48
|
-
<>
|
|
49
|
-
<FormAction
|
|
50
|
-
avatar={'🗳'}
|
|
51
|
-
description={t('unlock.password.description')}
|
|
52
|
-
title={t('unlock.password.title')}
|
|
53
|
-
>
|
|
54
|
-
<Input.Password
|
|
55
|
-
autoComplete={'new-password'}
|
|
56
|
-
onChange={(e) => {
|
|
57
|
-
setSettings({ password: e.target.value });
|
|
58
|
-
}}
|
|
59
|
-
placeholder={t('unlock.password.placeholder')}
|
|
60
|
-
type={'block'}
|
|
61
|
-
value={password}
|
|
62
|
-
/>
|
|
63
|
-
</FormAction>
|
|
64
|
-
<Flexbox gap={12}>
|
|
65
|
-
<Button
|
|
66
|
-
onClick={() => {
|
|
67
|
-
resend(id);
|
|
68
|
-
deleteMessage(id);
|
|
69
|
-
}}
|
|
70
|
-
type={'primary'}
|
|
71
|
-
>
|
|
72
|
-
{t('unlock.confirm')}
|
|
73
|
-
</Button>
|
|
74
|
-
<Button
|
|
75
|
-
onClick={() => {
|
|
76
|
-
deleteMessage(id);
|
|
77
|
-
}}
|
|
78
|
-
>
|
|
79
|
-
{t('unlock.closeMessage')}
|
|
80
|
-
</Button>
|
|
81
|
-
</Flexbox>
|
|
82
|
-
</>
|
|
83
|
-
)}
|
|
84
|
-
{mode === Tab.Api && <APIKeyForm id={id} />}
|
|
85
|
-
</Flexbox>
|
|
86
|
-
</ErrorActionContainer>
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
export default InvalidAccess;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { memo } from 'react';
|
|
2
|
-
|
|
3
|
-
import { RenderErrorMessage } from '../types';
|
|
4
|
-
import APIKeyForm from './ApiKeyForm';
|
|
5
|
-
import { ErrorActionContainer } from './style';
|
|
6
|
-
|
|
7
|
-
const OpenAPIKey: RenderErrorMessage['Render'] = memo(({ id }) => (
|
|
8
|
-
<ErrorActionContainer>
|
|
9
|
-
<APIKeyForm id={id} />
|
|
10
|
-
</ErrorActionContainer>
|
|
11
|
-
));
|
|
12
|
-
|
|
13
|
-
export default OpenAPIKey;
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { Highlighter } from '@lobehub/ui';
|
|
2
|
-
import { memo } from 'react';
|
|
3
|
-
import { Flexbox } from 'react-layout-kit';
|
|
4
|
-
|
|
5
|
-
import { RenderErrorMessage } from '../../types';
|
|
6
|
-
|
|
7
|
-
interface OpenAIError {
|
|
8
|
-
code: 'invalid_api_key' | string;
|
|
9
|
-
message: string;
|
|
10
|
-
param?: any;
|
|
11
|
-
type: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface OpenAIErrorResponse {
|
|
15
|
-
error: OpenAIError;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const PluginError: RenderErrorMessage['Render'] = memo(({ error, id }) => {
|
|
19
|
-
const errorBody: OpenAIErrorResponse = (error as any)?.body;
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<Flexbox id={id} style={{ maxWidth: 600 }}>
|
|
23
|
-
<Highlighter copyButtonSize={'small'} language={'json'} type={'pure'}>
|
|
24
|
-
{JSON.stringify(errorBody, null, 2)}
|
|
25
|
-
</Highlighter>
|
|
26
|
-
</Flexbox>
|
|
27
|
-
);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
export default PluginError;
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { LanguageModelWhiteList } from '@/const/llm';
|
|
2
|
-
|
|
3
|
-
import { createHeaderWithOpenAI } from './_header';
|
|
4
|
-
import { OPENAI_URLS } from './_url';
|
|
5
|
-
|
|
6
|
-
export const fetchModelList = async (): Promise<string[]> => {
|
|
7
|
-
const res = await fetch(OPENAI_URLS.models, {
|
|
8
|
-
headers: createHeaderWithOpenAI(),
|
|
9
|
-
method: 'POST',
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
const modelList: string[] = await res.json();
|
|
13
|
-
|
|
14
|
-
return LanguageModelWhiteList.filter((i) => modelList.includes(i));
|
|
15
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|