Skip to content

Commit f0c9532

Browse files
authored
✨ feat: support Azure OpenAI (#177)
* 💄 style: 拆分独立的 LLM Tab * ✨ feat: 支持 Azure OpenAI 调用 * 🚨 ci: fix types * 🗃️ fix: 补充数据迁移逻辑 * 🚸 style: 优化对用户的表达感知 * 💄 style: fix layout * 🚨 ci: fix circular dependencies * ✅ test: fix test * 🎨 chore: clean storage
1 parent f117d0e commit f0c9532

35 files changed

+1006
-186
lines changed

next.config.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ const nextConfig = {
2727
async rewrites() {
2828
return [
2929
{
30-
source: '/api/openai-dev',
31-
destination: `${API_END_PORT_URL}/api/openai`,
30+
source: '/api/openai/chat-dev',
31+
destination: `${API_END_PORT_URL}/api/openai/chat`,
32+
},
33+
{
34+
source: '/api/openai/models-dev',
35+
destination: `${API_END_PORT_URL}/api/openai/models`,
3236
},
3337
{
3438
source: '/api/plugins-dev',

package.json

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,12 @@
6565
"dependencies": {
6666
"@ant-design/colors": "^7",
6767
"@ant-design/icons": "^5",
68+
"@azure/openai": "latest",
6869
"@emoji-mart/data": "^1",
6970
"@emoji-mart/react": "^1",
7071
"@icons-pack/react-simple-icons": "^9",
71-
"@lobehub/chat-plugin-sdk": "^1.17.0",
72-
"@lobehub/chat-plugins-gateway": "^1.5.0",
72+
"@lobehub/chat-plugin-sdk": "^1.17.7",
73+
"@lobehub/chat-plugins-gateway": "^1.5.1",
7374
"@lobehub/ui": "latest",
7475
"@vercel/analytics": "^1",
7576
"ahooks": "^3",
@@ -96,11 +97,11 @@
9697
"react-i18next": "^13",
9798
"react-intersection-observer": "^9",
9899
"react-layout-kit": "^1.7.1",
99-
"serpapi": "^2",
100100
"swr": "^2",
101101
"systemjs": "^6.14.2",
102102
"ts-md5": "^1",
103103
"use-merge-value": "^1",
104+
"utility-types": "^3",
104105
"uuid": "^9",
105106
"zustand": "^4.4",
106107
"zustand-utils": "^1"
@@ -142,12 +143,6 @@
142143
"typescript": "^5",
143144
"vitest": "latest"
144145
},
145-
"peerDependencies": {
146-
"antd": ">=5",
147-
"antd-style": ">=3",
148-
"react": ">=18",
149-
"react-dom": ">=18"
150-
},
151146
"publishConfig": {
152147
"access": "public",
153148
"registry": "https://registry.npmjs.org"

src/config/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ declare global {
33
namespace NodeJS {
44
interface ProcessEnv {
55
ACCESS_CODE?: string;
6+
AZURE_API_KEY?: string;
67
OPENAI_API_KEY?: string;
78
OPENAI_PROXY_URL?: string;
89
}
@@ -16,6 +17,7 @@ export const getServerConfig = () => {
1617

1718
return {
1819
ACCESS_CODE: process.env.ACCESS_CODE,
20+
AZURE_API_KEY: process.env.AZURE_API_KEY,
1921
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
2022
OPENAI_PROXY_URL: process.env.OPENAI_PROXY_URL,
2123
};

src/const/fetch.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
export const OPENAI_END_POINT = 'X-OPENAI-END_POINT';
2-
32
export const OPENAI_API_KEY_HEADER_KEY = 'X-OPENAI-API-KEY';
43

4+
export const USE_AZURE_OPENAI = 'X-USE_AZURE_OPENAI';
5+
6+
export const AZURE_OPENAI_API_VERSION = 'X-AZURE_OPENAI_API_VERSION';
7+
58
export const LOBE_CHAT_ACCESS_CODE = 'X-LOBE_CHAT_ACCESS_CODE';
69

7-
export const LOBE_PLUGIN_SETTINGS = 'X-LOBE_PLUGIN_SETTINGS';
10+
export const getOpenAIAuthFromRequest = (req: Request) => {
11+
const apiKey = req.headers.get(OPENAI_API_KEY_HEADER_KEY);
12+
const endpoint = req.headers.get(OPENAI_END_POINT);
13+
const accessCode = req.headers.get(LOBE_CHAT_ACCESS_CODE);
14+
const useAzureStr = req.headers.get(USE_AZURE_OPENAI);
15+
const apiVersion = req.headers.get(AZURE_OPENAI_API_VERSION);
16+
17+
const useAzure = !!useAzureStr;
18+
19+
return { accessCode, apiKey, apiVersion, endpoint, useAzure };
20+
};

src/const/llm.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* A white list of language models that are allowed to display and be used in the app.
3+
*/
4+
export const LanguageModelWhiteList = [
5+
// OpenAI
6+
'gpt-3.5-turbo',
7+
'gpt-3.5-turbo-16k',
8+
'gpt-4',
9+
'gpt-4-32k',
10+
];
11+
12+
export const DEFAULT_OPENAI_MODEL_LIST = [
13+
'gpt-3.5-turbo',
14+
'gpt-3.5-turbo-16k',
15+
'gpt-4',
16+
'gpt-4-32k',
17+
];

src/const/settings.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1+
import { DEFAULT_OPENAI_MODEL_LIST } from '@/const/llm';
12
import { DEFAULT_AGENT_META } from '@/const/meta';
23
import { LanguageModel } from '@/types/llm';
34
import { LobeAgentConfig } from '@/types/session';
4-
import { GlobalBaseSettings, GlobalDefaultAgent, GlobalSettings } from '@/types/settings';
5+
import {
6+
GlobalBaseSettings,
7+
GlobalDefaultAgent,
8+
GlobalLLMConfig,
9+
GlobalSettings,
10+
} from '@/types/settings';
511

612
export const DEFAULT_BASE_SETTINGS: GlobalBaseSettings = {
7-
OPENAI_API_KEY: '',
813
avatar: '',
9-
compressThreshold: 24,
10-
enableCompressThreshold: false,
11-
enableHistoryCount: false,
12-
enableMaxTokens: true,
13-
endpoint: '',
1414
fontSize: 14,
15-
historyCount: 24,
1615
language: 'zh-CN',
1716
neutralColor: '',
1817
password: '',
@@ -34,12 +33,21 @@ export const DEFAULT_AGENT_CONFIG: LobeAgentConfig = {
3433
systemRole: '',
3534
};
3635

36+
export const DEFAULT_LLM_CONFIG: GlobalLLMConfig = {
37+
openAI: {
38+
OPENAI_API_KEY: '',
39+
azureApiVersion: '2023-08-01-preview',
40+
models: DEFAULT_OPENAI_MODEL_LIST,
41+
},
42+
};
43+
3744
export const DEFAULT_AGENT: GlobalDefaultAgent = {
3845
config: DEFAULT_AGENT_CONFIG,
3946
meta: DEFAULT_AGENT_META,
4047
};
4148

4249
export const DEFAULT_SETTINGS: GlobalSettings = {
4350
defaultAgent: DEFAULT_AGENT,
51+
languageModel: DEFAULT_LLM_CONFIG,
4452
...DEFAULT_BASE_SETTINGS,
4553
};

src/locales/default/setting.ts

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,64 @@ export default {
2020
session: '会话设置',
2121
sessionWithName: '会话设置 · {{name}}',
2222
},
23+
llm: {
24+
AzureOpenAI: {
25+
endpoint: {
26+
desc: '从 Azure 门户检查资源时,可在“密钥和终结点”部分中找到此值',
27+
placeholder: 'https://docs-test-001.openai.azure.com',
28+
title: 'Azure API 地址',
29+
},
30+
models: {
31+
desc: '支持的模型',
32+
title: '模型列表',
33+
},
34+
title: 'Azure OpenAI 设置',
35+
token: {
36+
desc: '从 Azure 门户检查资源时,可在“密钥和终结点”部分中找到此值。 可以使用 KEY1 或 KEY2',
37+
placeholder: 'Azure API Key',
38+
title: 'API Key',
39+
},
40+
},
41+
OpenAI: {
42+
azureApiVersion: {
43+
desc: 'Azure 的 API 版本,遵循 YYYY-MM-DD 格式,查阅[最新版本](https://learn.microsoft.com/zh-cn/azure/ai-services/openai/reference#chat-completions)',
44+
fetch: '获取列表',
45+
title: 'Azure Api Version',
46+
},
47+
check: {
48+
button: '检查',
49+
desc: '测试 Api Key 与代理地址是否正确填写',
50+
pass: '检查通过',
51+
title: '连通性检查',
52+
},
53+
endpoint: {
54+
desc: '除默认地址外,必须包含 http(s)://',
55+
placeholder: 'https://api.openai.com/v1',
56+
title: '接口代理地址',
57+
},
58+
models: {
59+
count: '共支持 {{count}} 个模型',
60+
desc: '支持的模型',
61+
fetch: '获取模型列表',
62+
notSupport: 'Azure OpenAI 暂不支持查看模型列表',
63+
notSupportTip: '你需要自行确保部署名称与模型名称一致',
64+
refetch: '重新获取模型列表',
65+
title: '模型列表',
66+
},
67+
title: 'OpenAI 设置',
68+
token: {
69+
desc: '使用自己的 OpenAI Key',
70+
placeholder: 'OpenAI API Key',
71+
title: 'API Key',
72+
},
73+
useAzure: {
74+
desc: '使用 Azure 提供的 OpenAI 服务',
75+
fetch: '获取列表',
76+
title: 'Azure OpenAI',
77+
},
78+
},
79+
waitingForMore: '更多模型正在 <1>计划接入</1> 中,敬请期待 ✨',
80+
},
2381
settingAgent: {
2482
avatar: {
2583
title: '头像',
@@ -114,19 +172,6 @@ export default {
114172
title: '核采样',
115173
},
116174
},
117-
settingOpenAI: {
118-
endpoint: {
119-
desc: '除默认地址外,必须包含 http(s)://',
120-
placeholder: 'https://api.openai.com/v1',
121-
title: '接口代理地址',
122-
},
123-
title: 'OpenAI 设置',
124-
token: {
125-
desc: '使用自己的 OpenAI Key',
126-
placeholder: 'OpenAI API Key',
127-
title: 'API Key',
128-
},
129-
},
130175
settingPlugin: {
131176
add: '添加',
132177
addTooltip: '添加自定义插件',
@@ -173,5 +218,6 @@ export default {
173218
tab: {
174219
agent: '默认助手',
175220
common: '通用设置',
221+
llm: '语言模型',
176222
},
177223
};

src/pages/api/openai.ts renamed to src/pages/api/createChatCompletion.ts

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,17 @@
11
import { OpenAIStream, StreamingTextResponse } from 'ai';
2-
import OpenAI, { ClientOptions } from 'openai';
2+
import OpenAI from 'openai';
33

4-
import { getServerConfig } from '@/config/server';
54
import { createErrorResponse } from '@/pages/api/error';
65
import { ChatErrorType } from '@/types/fetch';
76
import { OpenAIStreamPayload } from '@/types/openai';
87

9-
// 创建 OpenAI 实例
10-
export const createOpenAI = (userApiKey: string | null, endpoint?: string | null) => {
11-
const { OPENAI_API_KEY, OPENAI_PROXY_URL } = getServerConfig();
12-
13-
const baseURL = endpoint ? endpoint : OPENAI_PROXY_URL ? OPENAI_PROXY_URL : undefined;
14-
15-
const config: ClientOptions = {
16-
apiKey: !userApiKey ? OPENAI_API_KEY : userApiKey,
17-
};
18-
19-
// a bug with openai: https://github.com/openai/openai-node/issues/283
20-
// TODO: should refactor when openai fix the bug
21-
if (baseURL) {
22-
config.baseURL = baseURL;
23-
}
24-
25-
return new OpenAI(config);
26-
};
27-
288
interface CreateChatCompletionOptions {
29-
OPENAI_API_KEY: string | null;
30-
endpoint?: string | null;
9+
openai: OpenAI;
3110
payload: OpenAIStreamPayload;
3211
}
3312

34-
export const createChatCompletion = async ({
35-
payload,
36-
OPENAI_API_KEY,
37-
endpoint,
38-
}: CreateChatCompletionOptions) => {
39-
// ============ 0.创建 OpenAI 实例 ============ //
40-
41-
const openai = createOpenAI(OPENAI_API_KEY, endpoint);
42-
43-
// ============ 1. 前置处理 messages ============ //
13+
export const createChatCompletion = async ({ payload, openai }: CreateChatCompletionOptions) => {
14+
// ============ 1. preprocess messages ============ //
4415
const { messages, ...params } = payload;
4516

4617
const formatMessages = messages.map((m) => ({
@@ -49,7 +20,7 @@ export const createChatCompletion = async ({
4920
role: m.role,
5021
}));
5122

52-
// ============ 2. 发送请求 ============ //
23+
// ============ 2. send api ============ //
5324

5425
try {
5526
const response = await openai.chat.completions.create({
@@ -63,7 +34,7 @@ export const createChatCompletion = async ({
6334
// Check if the error is an OpenAI APIError
6435
if (error instanceof OpenAI.APIError) {
6536
return createErrorResponse(ChatErrorType.OpenAIBizError, {
66-
endpoint: !!endpoint ? endpoint : undefined,
37+
endpoint: openai.baseURL,
6738
error: error.error ?? error.cause,
6839
});
6940
}
@@ -73,7 +44,7 @@ export const createChatCompletion = async ({
7344

7445
// return as a GatewayTimeout error
7546
return createErrorResponse(ChatErrorType.InternalServerError, {
76-
endpoint,
47+
endpoint: openai.baseURL,
7748
error: JSON.stringify(error),
7849
});
7950
}

src/pages/api/openai.api.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/pages/api/openai/chat.api.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import OpenAI from 'openai';
2+
3+
import { getOpenAIAuthFromRequest } from '@/const/fetch';
4+
import { ChatErrorType, ErrorType } from '@/types/fetch';
5+
import { OpenAIStreamPayload } from '@/types/openai';
6+
7+
import { checkAuth } from '../auth';
8+
import { createChatCompletion } from '../createChatCompletion';
9+
import { createErrorResponse } from '../error';
10+
import { createAzureOpenai } from './createAzureOpenai';
11+
import { createOpenai } from './createOpenai';
12+
13+
export const runtime = 'edge';
14+
15+
export default async function handler(req: Request) {
16+
const payload = (await req.json()) as OpenAIStreamPayload;
17+
18+
const { apiKey, accessCode, endpoint, useAzure, apiVersion } = getOpenAIAuthFromRequest(req);
19+
20+
const result = checkAuth({ accessCode, apiKey });
21+
22+
if (!result.auth) {
23+
return createErrorResponse(result.error as ErrorType);
24+
}
25+
26+
let openai: OpenAI;
27+
if (useAzure) {
28+
if (!apiVersion) return createErrorResponse(ChatErrorType.BadRequest);
29+
30+
// `https://test-001.openai.azure.com/openai/deployments/gpt-35-turbo`,
31+
const url = `${endpoint}/openai/deployments/${payload.model.replace('.', '')}`;
32+
33+
openai = createAzureOpenai({
34+
apiVersion,
35+
endpoint: url,
36+
userApiKey: apiKey,
37+
});
38+
} else {
39+
openai = createOpenai(apiKey, endpoint);
40+
}
41+
42+
return createChatCompletion({ openai, payload });
43+
}

0 commit comments

Comments
 (0)