Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat: support Azure OpenAI #177

Merged
merged 9 commits into from
Sep 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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