feat: close #935 add azure support

This commit is contained in:
Yidadaa 2023-11-10 02:43:30 +08:00
parent fd2f441e02
commit b7ffca031e
17 changed files with 478 additions and 150 deletions

View File

@ -28,7 +28,7 @@ export function auth(req: NextRequest) {
const authToken = req.headers.get("Authorization") ?? ""; const authToken = req.headers.get("Authorization") ?? "";
// check if it is openai api key or user token // check if it is openai api key or user token
const { accessCode, apiKey: token } = parseApiKey(authToken); const { accessCode, apiKey } = parseApiKey(authToken);
const hashedCode = md5.hash(accessCode ?? "").trim(); const hashedCode = md5.hash(accessCode ?? "").trim();
@ -39,7 +39,7 @@ export function auth(req: NextRequest) {
console.log("[User IP] ", getIP(req)); console.log("[User IP] ", getIP(req));
console.log("[Time] ", new Date().toLocaleString()); console.log("[Time] ", new Date().toLocaleString());
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) { if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
return { return {
error: true, error: true,
msg: !accessCode ? "empty access code" : "wrong access code", msg: !accessCode ? "empty access code" : "wrong access code",
@ -47,11 +47,17 @@ export function auth(req: NextRequest) {
} }
// if user does not provide an api key, inject system api key // if user does not provide an api key, inject system api key
if (!token) { if (!apiKey) {
const apiKey = serverConfig.apiKey; const serverApiKey = serverConfig.isAzure
if (apiKey) { ? serverConfig.azureApiKey
: serverConfig.apiKey;
if (serverApiKey) {
console.log("[Auth] use system api key"); console.log("[Auth] use system api key");
req.headers.set("Authorization", `Bearer ${apiKey}`); req.headers.set(
"Authorization",
`${serverConfig.isAzure ? "" : "Bearer "}${serverApiKey}`,
);
} else { } else {
console.log("[Auth] admin did not provide an api key"); console.log("[Auth] admin did not provide an api key");
} }

View File

@ -1,19 +1,24 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "../config/server"; import { getServerSideConfig } from "../config/server";
import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant"; import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant";
import { collectModelTable, collectModels } from "../utils/model"; import { collectModelTable } from "../utils/model";
import { makeAzurePath } from "../azure";
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
export async function requestOpenai(req: NextRequest) { export async function requestOpenai(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();
const authValue = req.headers.get("Authorization") ?? ""; const authValue = req.headers.get("Authorization") ?? "";
const openaiPath = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll( const authHeaderName = serverConfig.isAzure ? "api-key" : "Authorization";
let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
"/api/openai/", "/api/openai/",
"", "",
); );
let baseUrl = serverConfig.baseUrl ?? OPENAI_BASE_URL; let baseUrl =
serverConfig.azureUrl ?? serverConfig.baseUrl ?? OPENAI_BASE_URL;
if (!baseUrl.startsWith("http")) { if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`; baseUrl = `https://${baseUrl}`;
@ -23,7 +28,7 @@ export async function requestOpenai(req: NextRequest) {
baseUrl = baseUrl.slice(0, -1); baseUrl = baseUrl.slice(0, -1);
} }
console.log("[Proxy] ", openaiPath); console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl); console.log("[Base Url]", baseUrl);
console.log("[Org ID]", serverConfig.openaiOrgId); console.log("[Org ID]", serverConfig.openaiOrgId);
@ -34,14 +39,24 @@ export async function requestOpenai(req: NextRequest) {
10 * 60 * 1000, 10 * 60 * 1000,
); );
const fetchUrl = `${baseUrl}/${openaiPath}`; if (serverConfig.isAzure) {
if (!serverConfig.azureApiVersion) {
return NextResponse.json({
error: true,
message: `missing AZURE_API_VERSION in server env vars`,
});
}
path = makeAzurePath(path, serverConfig.azureApiVersion);
}
const fetchUrl = `${baseUrl}/${path}`;
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Cache-Control": "no-store", "Cache-Control": "no-store",
Authorization: authValue, [authHeaderName]: authValue,
...(process.env.OPENAI_ORG_ID && { ...(serverConfig.openaiOrgId && {
"OpenAI-Organization": process.env.OPENAI_ORG_ID, "OpenAI-Organization": serverConfig.openaiOrgId,
}), }),
}, },
method: req.method, method: req.method,

9
app/azure.ts Normal file
View File

@ -0,0 +1,9 @@
export function makeAzurePath(path: string, apiVersion: string) {
// should omit /v1 prefix
path = path.replaceAll("v1/", "");
// should add api-key to query string
path += `${path.includes("?") ? "&" : "?"}api-version=${apiVersion}`;
return path;
}

View File

@ -1,5 +1,5 @@
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { ACCESS_CODE_PREFIX } from "../constant"; import { ACCESS_CODE_PREFIX, Azure, ServiceProvider } from "../constant";
import { ChatMessage, ModelType, useAccessStore } from "../store"; import { ChatMessage, ModelType, useAccessStore } from "../store";
import { ChatGPTApi } from "./platforms/openai"; import { ChatGPTApi } from "./platforms/openai";
@ -127,22 +127,26 @@ export const api = new ClientApi();
export function getHeaders() { export function getHeaders() {
const accessStore = useAccessStore.getState(); const accessStore = useAccessStore.getState();
let headers: Record<string, string> = { const headers: Record<string, string> = {
"Content-Type": "application/json", "Content-Type": "application/json",
"x-requested-with": "XMLHttpRequest", "x-requested-with": "XMLHttpRequest",
}; };
const makeBearer = (token: string) => `Bearer ${token.trim()}`; const isAzure = accessStore.provider === ServiceProvider.Azure;
const authHeader = isAzure ? "api-key" : "Authorization";
const apiKey = isAzure ? accessStore.azureApiKey : accessStore.openaiApiKey;
const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`;
const validString = (x: string) => x && x.length > 0; const validString = (x: string) => x && x.length > 0;
// use user's api key first // use user's api key first
if (validString(accessStore.token)) { if (validString(apiKey)) {
headers.Authorization = makeBearer(accessStore.token); headers[authHeader] = makeBearer(apiKey);
} else if ( } else if (
accessStore.enabledAccessControl() && accessStore.enabledAccessControl() &&
validString(accessStore.accessCode) validString(accessStore.accessCode)
) { ) {
headers.Authorization = makeBearer( headers[authHeader] = makeBearer(
ACCESS_CODE_PREFIX + accessStore.accessCode, ACCESS_CODE_PREFIX + accessStore.accessCode,
); );
} }

View File

@ -1,8 +1,10 @@
import { import {
ApiPath,
DEFAULT_API_HOST, DEFAULT_API_HOST,
DEFAULT_MODELS, DEFAULT_MODELS,
OpenaiPath, OpenaiPath,
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
ServiceProvider,
} from "@/app/constant"; } from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
@ -14,6 +16,7 @@ import {
} from "@fortaine/fetch-event-source"; } from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client"; import { getClientConfig } from "@/app/config/client";
import { makeAzurePath } from "@/app/azure";
export interface OpenAIListModelResponse { export interface OpenAIListModelResponse {
object: string; object: string;
@ -28,20 +31,35 @@ export class ChatGPTApi implements LLMApi {
private disableListModels = true; private disableListModels = true;
path(path: string): string { path(path: string): string {
let openaiUrl = useAccessStore.getState().openaiUrl; const accessStore = useAccessStore.getState();
const apiPath = "/api/openai";
if (openaiUrl.length === 0) { const isAzure = accessStore.provider === ServiceProvider.Azure;
if (isAzure && !accessStore.isValidAzure()) {
throw Error(
"incomplete azure config, please check it in your settings page",
);
}
let baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp; const isApp = !!getClientConfig()?.isApp;
openaiUrl = isApp ? DEFAULT_API_HOST : apiPath; baseUrl = isApp ? DEFAULT_API_HOST : ApiPath.OpenAI;
} }
if (openaiUrl.endsWith("/")) {
openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1); if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
} }
if (!openaiUrl.startsWith("http") && !openaiUrl.startsWith(apiPath)) { if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.OpenAI)) {
openaiUrl = "https://" + openaiUrl; baseUrl = "https://" + baseUrl;
} }
return [openaiUrl, path].join("/");
if (isAzure) {
path = makeAzurePath(path, accessStore.azureApiVersion);
}
return [baseUrl, path].join("/");
} }
extractMessage(res: any) { extractMessage(res: any) {
@ -156,14 +174,20 @@ export class ChatGPTApi implements LLMApi {
} }
const text = msg.data; const text = msg.data;
try { try {
const json = JSON.parse(text); const json = JSON.parse(text) as {
const delta = json.choices[0].delta.content; choices: Array<{
delta: {
content: string;
};
}>;
};
const delta = json.choices[0]?.delta?.content;
if (delta) { if (delta) {
responseText += delta; responseText += delta;
options.onUpdate?.(responseText, delta); options.onUpdate?.(responseText, delta);
} }
} catch (e) { } catch (e) {
console.error("[Request] parse error", text, msg); console.error("[Request] parse error", text);
} }
}, },
onclose() { onclose() {

View File

@ -18,7 +18,7 @@ export function AuthPage() {
const goChat = () => navigate(Path.Chat); const goChat = () => navigate(Path.Chat);
const resetAccessCode = () => { const resetAccessCode = () => {
accessStore.update((access) => { accessStore.update((access) => {
access.token = ""; access.openaiApiKey = "";
access.accessCode = ""; access.accessCode = "";
}); });
}; // Reset access code to empty string }; // Reset access code to empty string
@ -57,10 +57,10 @@ export function AuthPage() {
className={styles["auth-input"]} className={styles["auth-input"]}
type="password" type="password"
placeholder={Locale.Settings.Token.Placeholder} placeholder={Locale.Settings.Token.Placeholder}
value={accessStore.token} value={accessStore.openaiApiKey}
onChange={(e) => { onChange={(e) => {
accessStore.update( accessStore.update(
(access) => (access.token = e.currentTarget.value), (access) => (access.openaiApiKey = e.currentTarget.value),
); );
}} }}
/> />

View File

@ -998,7 +998,9 @@ function _Chat() {
).then((res) => { ).then((res) => {
if (!res) return; if (!res) return;
if (payload.key) { if (payload.key) {
accessStore.update((access) => (access.token = payload.key!)); accessStore.update(
(access) => (access.openaiApiKey = payload.key!),
);
} }
if (payload.url) { if (payload.url) {
accessStore.update((access) => (access.openaiUrl = payload.url!)); accessStore.update((access) => (access.openaiUrl = payload.url!));

View File

@ -51,10 +51,13 @@ import Locale, {
import { copyToClipboard } from "../utils"; import { copyToClipboard } from "../utils";
import Link from "next/link"; import Link from "next/link";
import { import {
Azure,
OPENAI_BASE_URL, OPENAI_BASE_URL,
Path, Path,
RELEASE_URL, RELEASE_URL,
STORAGE_KEY, STORAGE_KEY,
ServiceProvider,
SlotID,
UPDATE_URL, UPDATE_URL,
} from "../constant"; } from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt"; import { Prompt, SearchService, usePromptStore } from "../store/prompt";
@ -580,8 +583,16 @@ export function Settings() {
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const shouldHideBalanceQuery = useMemo(() => { const shouldHideBalanceQuery = useMemo(() => {
const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL); const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL);
return accessStore.hideBalanceQuery || isOpenAiUrl; return (
}, [accessStore.hideBalanceQuery, accessStore.openaiUrl]); accessStore.hideBalanceQuery ||
isOpenAiUrl ||
accessStore.provider === ServiceProvider.Azure
);
}, [
accessStore.hideBalanceQuery,
accessStore.openaiUrl,
accessStore.provider,
]);
const usage = { const usage = {
used: updateStore.used, used: updateStore.used,
@ -877,16 +888,16 @@ export function Settings() {
</ListItem> </ListItem>
</List> </List>
<List> <List id={SlotID.CustomModel}>
{showAccessCode ? ( {showAccessCode && (
<ListItem <ListItem
title={Locale.Settings.AccessCode.Title} title={Locale.Settings.Access.AccessCode.Title}
subTitle={Locale.Settings.AccessCode.SubTitle} subTitle={Locale.Settings.Access.AccessCode.SubTitle}
> >
<PasswordInput <PasswordInput
value={accessStore.accessCode} value={accessStore.accessCode}
type="text" type="text"
placeholder={Locale.Settings.AccessCode.Placeholder} placeholder={Locale.Settings.Access.AccessCode.Placeholder}
onChange={(e) => { onChange={(e) => {
accessStore.update( accessStore.update(
(access) => (access.accessCode = e.currentTarget.value), (access) => (access.accessCode = e.currentTarget.value),
@ -894,44 +905,152 @@ export function Settings() {
}} }}
/> />
</ListItem> </ListItem>
) : (
<></>
)} )}
{!accessStore.hideUserApiKey ? ( {!accessStore.hideUserApiKey && (
<> <>
<ListItem <ListItem
title={Locale.Settings.Endpoint.Title} title={Locale.Settings.Access.CustomEndpoint.Title}
subTitle={Locale.Settings.Endpoint.SubTitle} subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
> >
<input <input
type="text" type="checkbox"
value={accessStore.openaiUrl} checked={accessStore.useCustomConfig}
placeholder="https://api.openai.com/"
onChange={(e) => onChange={(e) =>
accessStore.update( accessStore.update(
(access) => (access.openaiUrl = e.currentTarget.value), (access) =>
(access.useCustomConfig = e.currentTarget.checked),
) )
} }
></input> ></input>
</ListItem> </ListItem>
<ListItem {accessStore.useCustomConfig && (
title={Locale.Settings.Token.Title} <>
subTitle={Locale.Settings.Token.SubTitle} <ListItem
> title={Locale.Settings.Access.Provider.Title}
<PasswordInput subTitle={Locale.Settings.Access.Provider.SubTitle}
value={accessStore.token} >
type="text" <Select
placeholder={Locale.Settings.Token.Placeholder} value={accessStore.provider}
onChange={(e) => { onChange={(e) => {
accessStore.update( accessStore.update(
(access) => (access.token = e.currentTarget.value), (access) =>
); (access.provider = e.target
}} .value as ServiceProvider),
/> );
</ListItem> }}
>
{Object.entries(ServiceProvider).map(([k, v]) => (
<option value={v} key={k}>
{k}
</option>
))}
</Select>
</ListItem>
{accessStore.provider === "OpenAI" ? (
<>
<ListItem
title={Locale.Settings.Access.OpenAI.Endpoint.Title}
subTitle={
Locale.Settings.Access.OpenAI.Endpoint.SubTitle
}
>
<input
type="text"
value={accessStore.openaiUrl}
placeholder={OPENAI_BASE_URL}
onChange={(e) =>
accessStore.update(
(access) =>
(access.openaiUrl = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Access.OpenAI.ApiKey.Title}
subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
>
<PasswordInput
value={accessStore.openaiApiKey}
type="text"
placeholder={
Locale.Settings.Access.OpenAI.ApiKey.Placeholder
}
onChange={(e) => {
accessStore.update(
(access) =>
(access.openaiApiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
</>
) : (
<>
<ListItem
title={Locale.Settings.Access.Azure.Endpoint.Title}
subTitle={
Locale.Settings.Access.Azure.Endpoint.SubTitle +
Azure.ExampleEndpoint
}
>
<input
type="text"
value={accessStore.azureUrl}
placeholder={Azure.ExampleEndpoint}
onChange={(e) =>
accessStore.update(
(access) =>
(access.azureUrl = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Access.Azure.ApiKey.Title}
subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
>
<PasswordInput
value={accessStore.azureApiKey}
type="text"
placeholder={
Locale.Settings.Access.Azure.ApiKey.Placeholder
}
onChange={(e) => {
accessStore.update(
(access) =>
(access.azureApiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
<ListItem
title={Locale.Settings.Access.Azure.ApiVerion.Title}
subTitle={
Locale.Settings.Access.Azure.ApiVerion.SubTitle
}
>
<input
type="text"
value={accessStore.azureApiVersion}
placeholder="2023-08-01-preview"
onChange={(e) =>
accessStore.update(
(access) =>
(access.azureApiVersion =
e.currentTarget.value),
)
}
></input>
</ListItem>
</>
)}
</>
)}
</> </>
) : null} )}
{!shouldHideBalanceQuery ? ( {!shouldHideBalanceQuery ? (
<ListItem <ListItem
@ -960,8 +1079,8 @@ export function Settings() {
) : null} ) : null}
<ListItem <ListItem
title={Locale.Settings.CustomModel.Title} title={Locale.Settings.Access.CustomModel.Title}
subTitle={Locale.Settings.CustomModel.SubTitle} subTitle={Locale.Settings.Access.CustomModel.SubTitle}
> >
<input <input
type="text" type="text"

View File

@ -235,7 +235,7 @@
.select-with-icon-select { .select-with-icon-select {
height: 100%; height: 100%;
border: var(--border-in-light); border: var(--border-in-light);
padding: 10px 25px 10px 10px; padding: 10px 35px 10px 10px;
border-radius: 10px; border-radius: 10px;
appearance: none; appearance: none;
cursor: pointer; cursor: pointer;

View File

@ -70,14 +70,12 @@ export function ListItem(props: {
); );
} }
export function List(props: { export function List(props: { children: React.ReactNode; id?: string }) {
children: return (
| Array<JSX.Element | null | undefined> <div className={styles.list} id={props.id}>
| JSX.Element {props.children}
| null </div>
| undefined; );
}) {
return <div className={styles.list}>{props.children}</div>;
} }
export function Loading() { export function Loading() {

View File

@ -4,19 +4,28 @@ import { DEFAULT_MODELS } from "../constant";
declare global { declare global {
namespace NodeJS { namespace NodeJS {
interface ProcessEnv { interface ProcessEnv {
PROXY_URL?: string; // docker only
OPENAI_API_KEY?: string; OPENAI_API_KEY?: string;
CODE?: string; CODE?: string;
BASE_URL?: string; BASE_URL?: string;
PROXY_URL?: string; OPENAI_ORG_ID?: string; // openai only
OPENAI_ORG_ID?: string;
VERCEL?: string; VERCEL?: string;
HIDE_USER_API_KEY?: string; // disable user's api key input
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
BUILD_MODE?: "standalone" | "export"; BUILD_MODE?: "standalone" | "export";
BUILD_APP?: string; // is building desktop app BUILD_APP?: string; // is building desktop app
HIDE_USER_API_KEY?: string; // disable user's api key input
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not
DISABLE_FAST_LINK?: string; // disallow parse settings from url or not DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
CUSTOM_MODELS?: string; // to control custom models CUSTOM_MODELS?: string; // to control custom models
// azure only
AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
AZURE_API_KEY?: string;
AZURE_API_VERSION?: string;
} }
} }
} }
@ -41,7 +50,7 @@ export const getServerSideConfig = () => {
); );
} }
let disableGPT4 = !!process.env.DISABLE_GPT4; const disableGPT4 = !!process.env.DISABLE_GPT4;
let customModels = process.env.CUSTOM_MODELS ?? ""; let customModels = process.env.CUSTOM_MODELS ?? "";
if (disableGPT4) { if (disableGPT4) {
@ -51,15 +60,25 @@ export const getServerSideConfig = () => {
.join(","); .join(",");
} }
const isAzure = !!process.env.AZURE_URL;
return { return {
baseUrl: process.env.BASE_URL,
apiKey: process.env.OPENAI_API_KEY, apiKey: process.env.OPENAI_API_KEY,
openaiOrgId: process.env.OPENAI_ORG_ID,
isAzure,
azureUrl: process.env.AZURE_URL,
azureApiKey: process.env.AZURE_API_KEY,
azureApiVersion: process.env.AZURE_API_VERSION,
needCode: ACCESS_CODES.size > 0,
code: process.env.CODE, code: process.env.CODE,
codes: ACCESS_CODES, codes: ACCESS_CODES,
needCode: ACCESS_CODES.size > 0,
baseUrl: process.env.BASE_URL,
proxyUrl: process.env.PROXY_URL, proxyUrl: process.env.PROXY_URL,
openaiOrgId: process.env.OPENAI_ORG_ID,
isVercel: !!process.env.VERCEL, isVercel: !!process.env.VERCEL,
hideUserApiKey: !!process.env.HIDE_USER_API_KEY, hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
disableGPT4, disableGPT4,
hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY, hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,

View File

@ -23,10 +23,12 @@ export enum Path {
export enum ApiPath { export enum ApiPath {
Cors = "/api/cors", Cors = "/api/cors",
OpenAI = "/api/openai",
} }
export enum SlotID { export enum SlotID {
AppBody = "app-body", AppBody = "app-body",
CustomModel = "custom-model",
} }
export enum FileName { export enum FileName {
@ -60,6 +62,11 @@ export const REQUEST_TIMEOUT_MS = 60000;
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
export enum ServiceProvider {
OpenAI = "OpenAI",
Azure = "Azure",
}
export const OpenaiPath = { export const OpenaiPath = {
ChatPath: "v1/chat/completions", ChatPath: "v1/chat/completions",
UsagePath: "dashboard/billing/usage", UsagePath: "dashboard/billing/usage",
@ -67,6 +74,10 @@ export const OpenaiPath = {
ListModelPath: "v1/models", ListModelPath: "v1/models",
}; };
export const Azure = {
ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}",
};
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
export const DEFAULT_SYSTEM_TEMPLATE = ` export const DEFAULT_SYSTEM_TEMPLATE = `
You are ChatGPT, a large language model trained by OpenAI. You are ChatGPT, a large language model trained by OpenAI.

View File

@ -258,11 +258,6 @@ const cn = {
Title: "历史消息长度压缩阈值", Title: "历史消息长度压缩阈值",
SubTitle: "当未压缩的历史消息超过该值时,将进行压缩", SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
}, },
Token: {
Title: "API Key",
SubTitle: "使用自己的 Key 可绕过密码访问限制",
Placeholder: "OpenAI API Key",
},
Usage: { Usage: {
Title: "余额查询", Title: "余额查询",
@ -273,19 +268,56 @@ const cn = {
Check: "重新检查", Check: "重新检查",
NoAccess: "输入 API Key 或访问密码查看余额", NoAccess: "输入 API Key 或访问密码查看余额",
}, },
AccessCode: {
Title: "访问密码", Access: {
SubTitle: "管理员已开启加密访问", AccessCode: {
Placeholder: "请输入访问密码", Title: "访问密码",
}, SubTitle: "管理员已开启加密访问",
Endpoint: { Placeholder: "请输入访问密码",
Title: "接口地址", },
SubTitle: "除默认地址外,必须包含 http(s)://", CustomEndpoint: {
}, Title: "自定义接口",
CustomModel: { SubTitle: "是否使用自定义 Azure 或 OpenAI 服务",
Title: "自定义模型名", },
SubTitle: "增加自定义模型可选项,使用英文逗号隔开", Provider: {
Title: "模型服务商",
SubTitle: "切换不同的服务商",
},
OpenAI: {
ApiKey: {
Title: "API Key",
SubTitle: "使用自定义 OpenAI Key 绕过密码访问限制",
Placeholder: "OpenAI API Key",
},
Endpoint: {
Title: "接口地址",
SubTitle: "除默认地址外,必须包含 http(s)://",
},
},
Azure: {
ApiKey: {
Title: "接口密钥",
SubTitle: "使用自定义 Azure Key 绕过密码访问限制",
Placeholder: "Azure API Key",
},
Endpoint: {
Title: "接口地址",
SubTitle: "样例:",
},
ApiVerion: {
Title: "接口版本 (azure api version)",
SubTitle: "选择指定的部分版本",
},
},
CustomModel: {
Title: "自定义模型名",
SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
},
}, },
Model: "模型 (model)", Model: "模型 (model)",
Temperature: { Temperature: {
Title: "随机性 (temperature)", Title: "随机性 (temperature)",

View File

@ -262,11 +262,7 @@ const en: LocaleType = {
SubTitle: SubTitle:
"Will compress if uncompressed messages length exceeds the value", "Will compress if uncompressed messages length exceeds the value",
}, },
Token: {
Title: "API Key",
SubTitle: "Use your key to ignore access code limit",
Placeholder: "OpenAI API Key",
},
Usage: { Usage: {
Title: "Account Balance", Title: "Account Balance",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@ -276,19 +272,55 @@ const en: LocaleType = {
Check: "Check", Check: "Check",
NoAccess: "Enter API Key to check balance", NoAccess: "Enter API Key to check balance",
}, },
AccessCode: { Access: {
Title: "Access Code", AccessCode: {
SubTitle: "Access control enabled", Title: "Access Code",
Placeholder: "Need Access Code", SubTitle: "Access control Enabled",
}, Placeholder: "Enter Code",
Endpoint: { },
Title: "Endpoint", CustomEndpoint: {
SubTitle: "Custom endpoint must start with http(s)://", Title: "Custom Endpoint",
}, SubTitle: "Use custom Azure or OpenAI service",
CustomModel: { },
Title: "Custom Models", Provider: {
SubTitle: "Add extra model options, separate by comma", Title: "Model Provider",
SubTitle: "Select Azure or OpenAI",
},
OpenAI: {
ApiKey: {
Title: "OpenAI API Key",
SubTitle: "User custom OpenAI Api Key",
Placeholder: "sk-xxx",
},
Endpoint: {
Title: "OpenAI Endpoint",
SubTitle: "Must starts with http(s):// or use /api/openai as default",
},
},
Azure: {
ApiKey: {
Title: "Azure Api Key",
SubTitle: "Check your api key from Azure console",
Placeholder: "Azure Api Key",
},
Endpoint: {
Title: "Azure Endpoint",
SubTitle: "Example: ",
},
ApiVerion: {
Title: "Azure Api Version",
SubTitle: "Check your api version from azure console",
},
},
CustomModel: {
Title: "Custom Models",
SubTitle: "Custom model options, seperated by comma",
},
}, },
Model: "Model", Model: "Model",
Temperature: { Temperature: {
Title: "Temperature", Title: "Temperature",

View File

@ -1,25 +1,41 @@
import { DEFAULT_API_HOST, DEFAULT_MODELS, StoreKey } from "../constant"; import {
ApiPath,
DEFAULT_API_HOST,
ServiceProvider,
StoreKey,
} from "../constant";
import { getHeaders } from "../client/api"; import { getHeaders } from "../client/api";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { createPersistStore } from "../utils/store"; import { createPersistStore } from "../utils/store";
import { ensure } from "../utils/clone";
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
const DEFAULT_OPENAI_URL = const DEFAULT_OPENAI_URL =
getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/"; getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : ApiPath.OpenAI;
console.log("[API] default openai url", DEFAULT_OPENAI_URL);
const DEFAULT_ACCESS_STATE = { const DEFAULT_ACCESS_STATE = {
token: "",
accessCode: "", accessCode: "",
useCustomConfig: false,
provider: ServiceProvider.OpenAI,
// openai
openaiUrl: DEFAULT_OPENAI_URL,
openaiApiKey: "",
// azure
azureUrl: "",
azureApiKey: "",
azureApiVersion: "2023-08-01-preview",
// server config
needCode: true, needCode: true,
hideUserApiKey: false, hideUserApiKey: false,
hideBalanceQuery: false, hideBalanceQuery: false,
disableGPT4: false, disableGPT4: false,
disableFastLink: false, disableFastLink: false,
customModels: "", customModels: "",
openaiUrl: DEFAULT_OPENAI_URL,
}; };
export const useAccessStore = createPersistStore( export const useAccessStore = createPersistStore(
@ -31,12 +47,24 @@ export const useAccessStore = createPersistStore(
return get().needCode; return get().needCode;
}, },
isValidOpenAI() {
return ensure(get(), ["openaiUrl", "openaiApiKey"]);
},
isValidAzure() {
return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]);
},
isAuthorized() { isAuthorized() {
this.fetch(); this.fetch();
// has token or has code or disabled access control // has token or has code or disabled access control
return ( return (
!!get().token || !!get().accessCode || !this.enabledAccessControl() this.isValidOpenAI() ||
this.isValidAzure() ||
!this.enabledAccessControl() ||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
); );
}, },
fetch() { fetch() {
@ -64,6 +92,19 @@ export const useAccessStore = createPersistStore(
}), }),
{ {
name: StoreKey.Access, name: StoreKey.Access,
version: 1, version: 2,
migrate(persistedState, version) {
if (version < 2) {
const state = persistedState as {
token: string;
openaiApiKey: string;
azureApiVersion: string;
};
state.openaiApiKey = state.token;
state.azureApiVersion = "2023-08-01-preview";
}
return persistedState as any;
},
}, },
); );

View File

@ -1,3 +1,10 @@
export function deepClone<T>(obj: T) { export function deepClone<T>(obj: T) {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
} }
export function ensure<T extends object>(
obj: T,
keys: Array<[keyof T][number]>,
) {
return keys.every((k) => obj[k] !== undefined && obj[k] !== null);
}

View File

@ -1,5 +1,5 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { combine, persist } from "zustand/middleware";
import { Updater } from "../typing"; import { Updater } from "../typing";
import { deepClone } from "./clone"; import { deepClone } from "./clone";
@ -23,33 +23,42 @@ type SetStoreState<T> = (
replace?: boolean | undefined, replace?: boolean | undefined,
) => void; ) => void;
export function createPersistStore<T, M>( export function createPersistStore<T extends object, M>(
defaultState: T, state: T,
methods: ( methods: (
set: SetStoreState<T & MakeUpdater<T>>, set: SetStoreState<T & MakeUpdater<T>>,
get: () => T & MakeUpdater<T>, get: () => T & MakeUpdater<T>,
) => M, ) => M,
persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>, persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
) { ) {
return create<T & M & MakeUpdater<T>>()( return create(
persist((set, get) => { persist(
return { combine(
...defaultState, {
...methods(set as any, get), ...state,
lastUpdateTime: 0,
},
(set, get) => {
return {
...methods(set, get as any),
lastUpdateTime: 0, markUpdate() {
markUpdate() { set({ lastUpdateTime: Date.now() } as Partial<
set({ lastUpdateTime: Date.now() } as Partial< T & M & MakeUpdater<T>
T & M & MakeUpdater<T> >);
>); },
update(updater) {
const state = deepClone(get());
updater(state);
set({
...state,
lastUpdateTime: Date.now(),
});
},
} as M & MakeUpdater<T>;
}, },
update(updater) { ),
const state = deepClone(get()); persistOptions as any,
updater(state); ),
get().markUpdate();
set(state);
},
};
}, persistOptions),
); );
} }