Skip to content

Commit

Permalink
✨ feat: support Azure OpenAI (lobehub#177)
Browse files Browse the repository at this point in the history
* 💄 style: 拆分独立的 LLM Tab

* ✨ feat: 支持 Azure OpenAI 调用

* 🚨 ci: fix types

* 🗃️ fix: 补充数据迁移逻辑

* 🚸 style: 优化对用户的表达感知

* 💄 style: fix layout

* 🚨 ci: fix circular dependencies

* ✅ test: fix test

* 🎨 chore: clean storage
  • Loading branch information
arvinxx committed Sep 10, 2023
1 parent f117d0e commit f0c9532
Show file tree
Hide file tree
Showing 35 changed files with 1,006 additions and 186 deletions.
8 changes: 6 additions & 2 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ const nextConfig = {
async rewrites() {
return [
{
source: '/api/openai-dev',
destination: `${API_END_PORT_URL}/api/openai`,
source: '/api/openai/chat-dev',
destination: `${API_END_PORT_URL}/api/openai/chat`,
},
{
source: '/api/openai/models-dev',
destination: `${API_END_PORT_URL}/api/openai/models`,
},
{
source: '/api/plugins-dev',
Expand Down
13 changes: 4 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@
"dependencies": {
"@ant-design/colors": "^7",
"@ant-design/icons": "^5",
"@azure/openai": "latest",
"@emoji-mart/data": "^1",
"@emoji-mart/react": "^1",
"@icons-pack/react-simple-icons": "^9",
"@lobehub/chat-plugin-sdk": "^1.17.0",
"@lobehub/chat-plugins-gateway": "^1.5.0",
"@lobehub/chat-plugin-sdk": "^1.17.7",
"@lobehub/chat-plugins-gateway": "^1.5.1",
"@lobehub/ui": "latest",
"@vercel/analytics": "^1",
"ahooks": "^3",
Expand All @@ -96,11 +97,11 @@
"react-i18next": "^13",
"react-intersection-observer": "^9",
"react-layout-kit": "^1.7.1",
"serpapi": "^2",
"swr": "^2",
"systemjs": "^6.14.2",
"ts-md5": "^1",
"use-merge-value": "^1",
"utility-types": "^3",
"uuid": "^9",
"zustand": "^4.4",
"zustand-utils": "^1"
Expand Down Expand Up @@ -142,12 +143,6 @@
"typescript": "^5",
"vitest": "latest"
},
"peerDependencies": {
"antd": ">=5",
"antd-style": ">=3",
"react": ">=18",
"react-dom": ">=18"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
Expand Down
2 changes: 2 additions & 0 deletions src/config/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ declare global {
namespace NodeJS {
interface ProcessEnv {
ACCESS_CODE?: string;
AZURE_API_KEY?: string;
OPENAI_API_KEY?: string;
OPENAI_PROXY_URL?: string;
}
Expand All @@ -16,6 +17,7 @@ export const getServerConfig = () => {

return {
ACCESS_CODE: process.env.ACCESS_CODE,
AZURE_API_KEY: process.env.AZURE_API_KEY,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
OPENAI_PROXY_URL: process.env.OPENAI_PROXY_URL,
};
Expand Down
17 changes: 15 additions & 2 deletions src/const/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
export const OPENAI_END_POINT = 'X-OPENAI-END_POINT';

export const OPENAI_API_KEY_HEADER_KEY = 'X-OPENAI-API-KEY';

export const USE_AZURE_OPENAI = 'X-USE_AZURE_OPENAI';

export const AZURE_OPENAI_API_VERSION = 'X-AZURE_OPENAI_API_VERSION';

export const LOBE_CHAT_ACCESS_CODE = 'X-LOBE_CHAT_ACCESS_CODE';

export const LOBE_PLUGIN_SETTINGS = 'X-LOBE_PLUGIN_SETTINGS';
export const getOpenAIAuthFromRequest = (req: Request) => {
const apiKey = req.headers.get(OPENAI_API_KEY_HEADER_KEY);
const endpoint = req.headers.get(OPENAI_END_POINT);
const accessCode = req.headers.get(LOBE_CHAT_ACCESS_CODE);
const useAzureStr = req.headers.get(USE_AZURE_OPENAI);
const apiVersion = req.headers.get(AZURE_OPENAI_API_VERSION);

const useAzure = !!useAzureStr;

return { accessCode, apiKey, apiVersion, endpoint, useAzure };
};
17 changes: 17 additions & 0 deletions src/const/llm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* A white list of language models that are allowed to display and be used in the app.
*/
export const LanguageModelWhiteList = [
// OpenAI
'gpt-3.5-turbo',
'gpt-3.5-turbo-16k',
'gpt-4',
'gpt-4-32k',
];

export const DEFAULT_OPENAI_MODEL_LIST = [
'gpt-3.5-turbo',
'gpt-3.5-turbo-16k',
'gpt-4',
'gpt-4-32k',
];
24 changes: 16 additions & 8 deletions src/const/settings.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { DEFAULT_OPENAI_MODEL_LIST } from '@/const/llm';
import { DEFAULT_AGENT_META } from '@/const/meta';
import { LanguageModel } from '@/types/llm';
import { LobeAgentConfig } from '@/types/session';
import { GlobalBaseSettings, GlobalDefaultAgent, GlobalSettings } from '@/types/settings';
import {
GlobalBaseSettings,
GlobalDefaultAgent,
GlobalLLMConfig,
GlobalSettings,
} from '@/types/settings';

export const DEFAULT_BASE_SETTINGS: GlobalBaseSettings = {
OPENAI_API_KEY: '',
avatar: '',
compressThreshold: 24,
enableCompressThreshold: false,
enableHistoryCount: false,
enableMaxTokens: true,
endpoint: '',
fontSize: 14,
historyCount: 24,
language: 'zh-CN',
neutralColor: '',
password: '',
Expand All @@ -34,12 +33,21 @@ export const DEFAULT_AGENT_CONFIG: LobeAgentConfig = {
systemRole: '',
};

export const DEFAULT_LLM_CONFIG: GlobalLLMConfig = {
openAI: {
OPENAI_API_KEY: '',
azureApiVersion: '2023-08-01-preview',
models: DEFAULT_OPENAI_MODEL_LIST,
},
};

export const DEFAULT_AGENT: GlobalDefaultAgent = {
config: DEFAULT_AGENT_CONFIG,
meta: DEFAULT_AGENT_META,
};

export const DEFAULT_SETTINGS: GlobalSettings = {
defaultAgent: DEFAULT_AGENT,
languageModel: DEFAULT_LLM_CONFIG,
...DEFAULT_BASE_SETTINGS,
};
72 changes: 59 additions & 13 deletions src/locales/default/setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,64 @@ export default {
session: '会话设置',
sessionWithName: '会话设置 · {{name}}',
},
llm: {
AzureOpenAI: {
endpoint: {
desc: '从 Azure 门户检查资源时,可在“密钥和终结点”部分中找到此值',
placeholder: 'https://docs-test-001.openai.azure.com',
title: 'Azure API 地址',
},
models: {
desc: '支持的模型',
title: '模型列表',
},
title: 'Azure OpenAI 设置',
token: {
desc: '从 Azure 门户检查资源时,可在“密钥和终结点”部分中找到此值。 可以使用 KEY1 或 KEY2',
placeholder: 'Azure API Key',
title: 'API Key',
},
},
OpenAI: {
azureApiVersion: {
desc: 'Azure 的 API 版本,遵循 YYYY-MM-DD 格式,查阅[最新版本](https://learn.microsoft.com/zh-cn/azure/ai-services/openai/reference#chat-completions)',
fetch: '获取列表',
title: 'Azure Api Version',
},
check: {
button: '检查',
desc: '测试 Api Key 与代理地址是否正确填写',
pass: '检查通过',
title: '连通性检查',
},
endpoint: {
desc: '除默认地址外,必须包含 http(s)://',
placeholder: 'https://api.openai.com/v1',
title: '接口代理地址',
},
models: {
count: '共支持 {{count}} 个模型',
desc: '支持的模型',
fetch: '获取模型列表',
notSupport: 'Azure OpenAI 暂不支持查看模型列表',
notSupportTip: '你需要自行确保部署名称与模型名称一致',
refetch: '重新获取模型列表',
title: '模型列表',
},
title: 'OpenAI 设置',
token: {
desc: '使用自己的 OpenAI Key',
placeholder: 'OpenAI API Key',
title: 'API Key',
},
useAzure: {
desc: '使用 Azure 提供的 OpenAI 服务',
fetch: '获取列表',
title: 'Azure OpenAI',
},
},
waitingForMore: '更多模型正在 <1>计划接入</1> 中,敬请期待 ✨',
},
settingAgent: {
avatar: {
title: '头像',
Expand Down Expand Up @@ -114,19 +172,6 @@ export default {
title: '核采样',
},
},
settingOpenAI: {
endpoint: {
desc: '除默认地址外,必须包含 http(s)://',
placeholder: 'https://api.openai.com/v1',
title: '接口代理地址',
},
title: 'OpenAI 设置',
token: {
desc: '使用自己的 OpenAI Key',
placeholder: 'OpenAI API Key',
title: 'API Key',
},
},
settingPlugin: {
add: '添加',
addTooltip: '添加自定义插件',
Expand Down Expand Up @@ -173,5 +218,6 @@ export default {
tab: {
agent: '默认助手',
common: '通用设置',
llm: '语言模型',
},
};
43 changes: 7 additions & 36 deletions src/pages/api/openai.ts → src/pages/api/createChatCompletion.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,17 @@
import { OpenAIStream, StreamingTextResponse } from 'ai';
import OpenAI, { ClientOptions } from 'openai';
import OpenAI from 'openai';

import { getServerConfig } from '@/config/server';
import { createErrorResponse } from '@/pages/api/error';
import { ChatErrorType } from '@/types/fetch';
import { OpenAIStreamPayload } from '@/types/openai';

// 创建 OpenAI 实例
export const createOpenAI = (userApiKey: string | null, endpoint?: string | null) => {
const { OPENAI_API_KEY, OPENAI_PROXY_URL } = getServerConfig();

const baseURL = endpoint ? endpoint : OPENAI_PROXY_URL ? OPENAI_PROXY_URL : undefined;

const config: ClientOptions = {
apiKey: !userApiKey ? OPENAI_API_KEY : userApiKey,
};

// a bug with openai: https://github.com/openai/openai-node/issues/283
// TODO: should refactor when openai fix the bug
if (baseURL) {
config.baseURL = baseURL;
}

return new OpenAI(config);
};

interface CreateChatCompletionOptions {
OPENAI_API_KEY: string | null;
endpoint?: string | null;
openai: OpenAI;
payload: OpenAIStreamPayload;
}

export const createChatCompletion = async ({
payload,
OPENAI_API_KEY,
endpoint,
}: CreateChatCompletionOptions) => {
// ============ 0.创建 OpenAI 实例 ============ //

const openai = createOpenAI(OPENAI_API_KEY, endpoint);

// ============ 1. 前置处理 messages ============ //
export const createChatCompletion = async ({ payload, openai }: CreateChatCompletionOptions) => {
// ============ 1. preprocess messages ============ //
const { messages, ...params } = payload;

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

// ============ 2. 发送请求 ============ //
// ============ 2. send api ============ //

try {
const response = await openai.chat.completions.create({
Expand All @@ -63,7 +34,7 @@ export const createChatCompletion = async ({
// Check if the error is an OpenAI APIError
if (error instanceof OpenAI.APIError) {
return createErrorResponse(ChatErrorType.OpenAIBizError, {
endpoint: !!endpoint ? endpoint : undefined,
endpoint: openai.baseURL,
error: error.error ?? error.cause,
});
}
Expand All @@ -73,7 +44,7 @@ export const createChatCompletion = async ({

// return as a GatewayTimeout error
return createErrorResponse(ChatErrorType.InternalServerError, {
endpoint,
endpoint: openai.baseURL,
error: JSON.stringify(error),
});
}
Expand Down
24 changes: 0 additions & 24 deletions src/pages/api/openai.api.ts

This file was deleted.

43 changes: 43 additions & 0 deletions src/pages/api/openai/chat.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import OpenAI from 'openai';

import { getOpenAIAuthFromRequest } from '@/const/fetch';
import { ChatErrorType, ErrorType } from '@/types/fetch';
import { OpenAIStreamPayload } from '@/types/openai';

import { checkAuth } from '../auth';
import { createChatCompletion } from '../createChatCompletion';
import { createErrorResponse } from '../error';
import { createAzureOpenai } from './createAzureOpenai';
import { createOpenai } from './createOpenai';

export const runtime = 'edge';

export default async function handler(req: Request) {
const payload = (await req.json()) as OpenAIStreamPayload;

const { apiKey, accessCode, endpoint, useAzure, apiVersion } = getOpenAIAuthFromRequest(req);

const result = checkAuth({ accessCode, apiKey });

if (!result.auth) {
return createErrorResponse(result.error as ErrorType);
}

let openai: OpenAI;
if (useAzure) {
if (!apiVersion) return createErrorResponse(ChatErrorType.BadRequest);

// `https://test-001.openai.azure.com/openai/deployments/gpt-35-turbo`,
const url = `${endpoint}/openai/deployments/${payload.model.replace('.', '')}`;

openai = createAzureOpenai({
apiVersion,
endpoint: url,
userApiKey: apiKey,
});
} else {
openai = createOpenai(apiKey, endpoint);
}

return createChatCompletion({ openai, payload });
}
Loading

0 comments on commit f0c9532

Please sign in to comment.