From b7ffca031ebda555c373783820056541307ceba0 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 10 Nov 2023 02:43:30 +0800 Subject: [PATCH 1/2] feat: close #935 add azure support --- app/api/auth.ts | 18 ++- app/api/common.ts | 31 +++-- app/azure.ts | 9 ++ app/client/api.ts | 16 ++- app/client/platforms/openai.ts | 48 ++++++-- app/components/auth.tsx | 6 +- app/components/chat.tsx | 4 +- app/components/settings.tsx | 187 ++++++++++++++++++++++++------ app/components/ui-lib.module.scss | 2 +- app/components/ui-lib.tsx | 14 +-- app/config/server.ts | 35 ++++-- app/constant.ts | 11 ++ app/locales/cn.ts | 66 ++++++++--- app/locales/en.ts | 66 ++++++++--- app/store/access.ts | 57 +++++++-- app/utils/clone.ts | 7 ++ app/utils/store.ts | 51 ++++---- 17 files changed, 478 insertions(+), 150 deletions(-) create mode 100644 app/azure.ts diff --git a/app/api/auth.ts b/app/api/auth.ts index e0453b2b47f..c1f6e7fdec2 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -28,7 +28,7 @@ export function auth(req: NextRequest) { const authToken = req.headers.get("Authorization") ?? ""; // 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(); @@ -39,7 +39,7 @@ export function auth(req: NextRequest) { console.log("[User IP] ", getIP(req)); console.log("[Time] ", new Date().toLocaleString()); - if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) { + if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) { return { error: true, 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 (!token) { - const apiKey = serverConfig.apiKey; - if (apiKey) { + if (!apiKey) { + const serverApiKey = serverConfig.isAzure + ? serverConfig.azureApiKey + : serverConfig.apiKey; + + if (serverApiKey) { console.log("[Auth] use system api key"); - req.headers.set("Authorization", `Bearer ${apiKey}`); + req.headers.set( + "Authorization", + `${serverConfig.isAzure ? "" : "Bearer "}${serverApiKey}`, + ); } else { console.log("[Auth] admin did not provide an api key"); } diff --git a/app/api/common.ts b/app/api/common.ts index a1decd42f5b..fc877b02db2 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -1,19 +1,24 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSideConfig } from "../config/server"; 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(); export async function requestOpenai(req: NextRequest) { const controller = new AbortController(); + 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/", "", ); - let baseUrl = serverConfig.baseUrl ?? OPENAI_BASE_URL; + let baseUrl = + serverConfig.azureUrl ?? serverConfig.baseUrl ?? OPENAI_BASE_URL; if (!baseUrl.startsWith("http")) { baseUrl = `https://${baseUrl}`; @@ -23,7 +28,7 @@ export async function requestOpenai(req: NextRequest) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Proxy] ", openaiPath); + console.log("[Proxy] ", path); console.log("[Base Url]", baseUrl); console.log("[Org ID]", serverConfig.openaiOrgId); @@ -34,14 +39,24 @@ export async function requestOpenai(req: NextRequest) { 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 = { headers: { "Content-Type": "application/json", "Cache-Control": "no-store", - Authorization: authValue, - ...(process.env.OPENAI_ORG_ID && { - "OpenAI-Organization": process.env.OPENAI_ORG_ID, + [authHeaderName]: authValue, + ...(serverConfig.openaiOrgId && { + "OpenAI-Organization": serverConfig.openaiOrgId, }), }, method: req.method, diff --git a/app/azure.ts b/app/azure.ts new file mode 100644 index 00000000000..48406c55ba5 --- /dev/null +++ b/app/azure.ts @@ -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; +} diff --git a/app/client/api.ts b/app/client/api.ts index b04dd88b88c..eedd2c9ab48 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -1,5 +1,5 @@ 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 { ChatGPTApi } from "./platforms/openai"; @@ -127,22 +127,26 @@ export const api = new ClientApi(); export function getHeaders() { const accessStore = useAccessStore.getState(); - let headers: Record = { + const headers: Record = { "Content-Type": "application/json", "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; // use user's api key first - if (validString(accessStore.token)) { - headers.Authorization = makeBearer(accessStore.token); + if (validString(apiKey)) { + headers[authHeader] = makeBearer(apiKey); } else if ( accessStore.enabledAccessControl() && validString(accessStore.accessCode) ) { - headers.Authorization = makeBearer( + headers[authHeader] = makeBearer( ACCESS_CODE_PREFIX + accessStore.accessCode, ); } diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 4a5ddce7de6..930d606900a 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -1,8 +1,10 @@ import { + ApiPath, DEFAULT_API_HOST, DEFAULT_MODELS, OpenaiPath, REQUEST_TIMEOUT_MS, + ServiceProvider, } from "@/app/constant"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; @@ -14,6 +16,7 @@ import { } from "@fortaine/fetch-event-source"; import { prettyObject } from "@/app/utils/format"; import { getClientConfig } from "@/app/config/client"; +import { makeAzurePath } from "@/app/azure"; export interface OpenAIListModelResponse { object: string; @@ -28,20 +31,35 @@ export class ChatGPTApi implements LLMApi { private disableListModels = true; path(path: string): string { - let openaiUrl = useAccessStore.getState().openaiUrl; - const apiPath = "/api/openai"; + const accessStore = useAccessStore.getState(); - 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; - 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 (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.OpenAI)) { + baseUrl = "https://" + baseUrl; } - if (!openaiUrl.startsWith("http") && !openaiUrl.startsWith(apiPath)) { - openaiUrl = "https://" + openaiUrl; + + if (isAzure) { + path = makeAzurePath(path, accessStore.azureApiVersion); } - return [openaiUrl, path].join("/"); + + return [baseUrl, path].join("/"); } extractMessage(res: any) { @@ -156,14 +174,20 @@ export class ChatGPTApi implements LLMApi { } const text = msg.data; try { - const json = JSON.parse(text); - const delta = json.choices[0].delta.content; + const json = JSON.parse(text) as { + choices: Array<{ + delta: { + content: string; + }; + }>; + }; + const delta = json.choices[0]?.delta?.content; if (delta) { responseText += delta; options.onUpdate?.(responseText, delta); } } catch (e) { - console.error("[Request] parse error", text, msg); + console.error("[Request] parse error", text); } }, onclose() { diff --git a/app/components/auth.tsx b/app/components/auth.tsx index 577d7754240..3e1548a1325 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -18,7 +18,7 @@ export function AuthPage() { const goChat = () => navigate(Path.Chat); const resetAccessCode = () => { accessStore.update((access) => { - access.token = ""; + access.openaiApiKey = ""; access.accessCode = ""; }); }; // Reset access code to empty string @@ -57,10 +57,10 @@ export function AuthPage() { className={styles["auth-input"]} type="password" placeholder={Locale.Settings.Token.Placeholder} - value={accessStore.token} + value={accessStore.openaiApiKey} onChange={(e) => { accessStore.update( - (access) => (access.token = e.currentTarget.value), + (access) => (access.openaiApiKey = e.currentTarget.value), ); }} /> diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 9afb49f7a66..c27c3eee464 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -998,7 +998,9 @@ function _Chat() { ).then((res) => { if (!res) return; if (payload.key) { - accessStore.update((access) => (access.token = payload.key!)); + accessStore.update( + (access) => (access.openaiApiKey = payload.key!), + ); } if (payload.url) { accessStore.update((access) => (access.openaiUrl = payload.url!)); diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 572c0743a11..178fcec57e9 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -51,10 +51,13 @@ import Locale, { import { copyToClipboard } from "../utils"; import Link from "next/link"; import { + Azure, OPENAI_BASE_URL, Path, RELEASE_URL, STORAGE_KEY, + ServiceProvider, + SlotID, UPDATE_URL, } from "../constant"; import { Prompt, SearchService, usePromptStore } from "../store/prompt"; @@ -580,8 +583,16 @@ export function Settings() { const accessStore = useAccessStore(); const shouldHideBalanceQuery = useMemo(() => { const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL); - return accessStore.hideBalanceQuery || isOpenAiUrl; - }, [accessStore.hideBalanceQuery, accessStore.openaiUrl]); + return ( + accessStore.hideBalanceQuery || + isOpenAiUrl || + accessStore.provider === ServiceProvider.Azure + ); + }, [ + accessStore.hideBalanceQuery, + accessStore.openaiUrl, + accessStore.provider, + ]); const usage = { used: updateStore.used, @@ -877,16 +888,16 @@ export function Settings() { - - {showAccessCode ? ( + + {showAccessCode && ( { accessStore.update( (access) => (access.accessCode = e.currentTarget.value), @@ -894,44 +905,152 @@ export function Settings() { }} /> - ) : ( - <> )} - {!accessStore.hideUserApiKey ? ( + {!accessStore.hideUserApiKey && ( <> accessStore.update( - (access) => (access.openaiUrl = e.currentTarget.value), + (access) => + (access.useCustomConfig = e.currentTarget.checked), ) } > - - { - accessStore.update( - (access) => (access.token = e.currentTarget.value), - ); - }} - /> - + {accessStore.useCustomConfig && ( + <> + + + + + {accessStore.provider === "OpenAI" ? ( + <> + + + accessStore.update( + (access) => + (access.openaiUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => + (access.openaiApiKey = e.currentTarget.value), + ); + }} + /> + + + ) : ( + <> + + + accessStore.update( + (access) => + (access.azureUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => + (access.azureApiKey = e.currentTarget.value), + ); + }} + /> + + + + accessStore.update( + (access) => + (access.azureApiVersion = + e.currentTarget.value), + ) + } + > + + + )} + + )} - ) : null} + )} {!shouldHideBalanceQuery ? ( - | JSX.Element - | null - | undefined; -}) { - return
{props.children}
; +export function List(props: { children: React.ReactNode; id?: string }) { + return ( +
+ {props.children} +
+ ); } export function Loading() { diff --git a/app/config/server.ts b/app/config/server.ts index 007c3973863..2f2e7d7fd8a 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -4,19 +4,28 @@ import { DEFAULT_MODELS } from "../constant"; declare global { namespace NodeJS { interface ProcessEnv { + PROXY_URL?: string; // docker only + OPENAI_API_KEY?: string; CODE?: string; + BASE_URL?: string; - PROXY_URL?: string; - OPENAI_ORG_ID?: string; + OPENAI_ORG_ID?: string; // openai only + 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_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 DISABLE_FAST_LINK?: string; // disallow parse settings from url or not 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 ?? ""; if (disableGPT4) { @@ -51,15 +60,25 @@ export const getServerSideConfig = () => { .join(","); } + const isAzure = !!process.env.AZURE_URL; + return { + baseUrl: process.env.BASE_URL, 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, codes: ACCESS_CODES, - needCode: ACCESS_CODES.size > 0, - baseUrl: process.env.BASE_URL, + proxyUrl: process.env.PROXY_URL, - openaiOrgId: process.env.OPENAI_ORG_ID, isVercel: !!process.env.VERCEL, + hideUserApiKey: !!process.env.HIDE_USER_API_KEY, disableGPT4, hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY, diff --git a/app/constant.ts b/app/constant.ts index a97b8782292..fbc0c72e378 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -23,10 +23,12 @@ export enum Path { export enum ApiPath { Cors = "/api/cors", + OpenAI = "/api/openai", } export enum SlotID { AppBody = "app-body", + CustomModel = "custom-model", } export enum FileName { @@ -60,6 +62,11 @@ export const REQUEST_TIMEOUT_MS = 60000; export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; +export enum ServiceProvider { + OpenAI = "OpenAI", + Azure = "Azure", +} + export const OpenaiPath = { ChatPath: "v1/chat/completions", UsagePath: "dashboard/billing/usage", @@ -67,6 +74,10 @@ export const OpenaiPath = { 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_SYSTEM_TEMPLATE = ` You are ChatGPT, a large language model trained by OpenAI. diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 4cd963fb8e2..e721adef7ae 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -258,11 +258,6 @@ const cn = { Title: "历史消息长度压缩阈值", SubTitle: "当未压缩的历史消息超过该值时,将进行压缩", }, - Token: { - Title: "API Key", - SubTitle: "使用自己的 Key 可绕过密码访问限制", - Placeholder: "OpenAI API Key", - }, Usage: { Title: "余额查询", @@ -273,19 +268,56 @@ const cn = { Check: "重新检查", NoAccess: "输入 API Key 或访问密码查看余额", }, - AccessCode: { - Title: "访问密码", - SubTitle: "管理员已开启加密访问", - Placeholder: "请输入访问密码", - }, - Endpoint: { - Title: "接口地址", - SubTitle: "除默认地址外,必须包含 http(s)://", - }, - CustomModel: { - Title: "自定义模型名", - SubTitle: "增加自定义模型可选项,使用英文逗号隔开", + + Access: { + AccessCode: { + Title: "访问密码", + SubTitle: "管理员已开启加密访问", + Placeholder: "请输入访问密码", + }, + CustomEndpoint: { + Title: "自定义接口", + SubTitle: "是否使用自定义 Azure 或 OpenAI 服务", + }, + 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)", Temperature: { Title: "随机性 (temperature)", diff --git a/app/locales/en.ts b/app/locales/en.ts index 928c4b72d4e..c6e61ecab04 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -262,11 +262,7 @@ const en: LocaleType = { SubTitle: "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: { Title: "Account Balance", SubTitle(used: any, total: any) { @@ -276,19 +272,55 @@ const en: LocaleType = { Check: "Check", NoAccess: "Enter API Key to check balance", }, - AccessCode: { - Title: "Access Code", - SubTitle: "Access control enabled", - Placeholder: "Need Access Code", - }, - Endpoint: { - Title: "Endpoint", - SubTitle: "Custom endpoint must start with http(s)://", - }, - CustomModel: { - Title: "Custom Models", - SubTitle: "Add extra model options, separate by comma", + Access: { + AccessCode: { + Title: "Access Code", + SubTitle: "Access control Enabled", + Placeholder: "Enter Code", + }, + CustomEndpoint: { + Title: "Custom Endpoint", + SubTitle: "Use custom Azure or OpenAI service", + }, + Provider: { + 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", Temperature: { Title: "Temperature", diff --git a/app/store/access.ts b/app/store/access.ts index f87e44a2ac4..2abe1e3cc9f 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -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 { getClientConfig } from "../config/client"; import { createPersistStore } from "../utils/store"; +import { ensure } from "../utils/clone"; let fetchState = 0; // 0 not fetch, 1 fetching, 2 done const DEFAULT_OPENAI_URL = - getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/"; -console.log("[API] default openai url", DEFAULT_OPENAI_URL); + getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : ApiPath.OpenAI; const DEFAULT_ACCESS_STATE = { - token: "", accessCode: "", + useCustomConfig: false, + + provider: ServiceProvider.OpenAI, + + // openai + openaiUrl: DEFAULT_OPENAI_URL, + openaiApiKey: "", + + // azure + azureUrl: "", + azureApiKey: "", + azureApiVersion: "2023-08-01-preview", + + // server config needCode: true, hideUserApiKey: false, hideBalanceQuery: false, disableGPT4: false, disableFastLink: false, customModels: "", - - openaiUrl: DEFAULT_OPENAI_URL, }; export const useAccessStore = createPersistStore( @@ -31,12 +47,24 @@ export const useAccessStore = createPersistStore( return get().needCode; }, + + isValidOpenAI() { + return ensure(get(), ["openaiUrl", "openaiApiKey"]); + }, + + isValidAzure() { + return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]); + }, + isAuthorized() { this.fetch(); // has token or has code or disabled access control return ( - !!get().token || !!get().accessCode || !this.enabledAccessControl() + this.isValidOpenAI() || + this.isValidAzure() || + !this.enabledAccessControl() || + (this.enabledAccessControl() && ensure(get(), ["accessCode"])) ); }, fetch() { @@ -64,6 +92,19 @@ export const useAccessStore = createPersistStore( }), { 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; + }, }, ); diff --git a/app/utils/clone.ts b/app/utils/clone.ts index 2958b6b9c35..c42288f7789 100644 --- a/app/utils/clone.ts +++ b/app/utils/clone.ts @@ -1,3 +1,10 @@ export function deepClone(obj: T) { return JSON.parse(JSON.stringify(obj)); } + +export function ensure( + obj: T, + keys: Array<[keyof T][number]>, +) { + return keys.every((k) => obj[k] !== undefined && obj[k] !== null); +} diff --git a/app/utils/store.ts b/app/utils/store.ts index cd151dc4925..684a1911279 100644 --- a/app/utils/store.ts +++ b/app/utils/store.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import { persist } from "zustand/middleware"; +import { combine, persist } from "zustand/middleware"; import { Updater } from "../typing"; import { deepClone } from "./clone"; @@ -23,33 +23,42 @@ type SetStoreState = ( replace?: boolean | undefined, ) => void; -export function createPersistStore( - defaultState: T, +export function createPersistStore( + state: T, methods: ( set: SetStoreState>, get: () => T & MakeUpdater, ) => M, persistOptions: SecondParam>>, ) { - return create>()( - persist((set, get) => { - return { - ...defaultState, - ...methods(set as any, get), - - lastUpdateTime: 0, - markUpdate() { - set({ lastUpdateTime: Date.now() } as Partial< - T & M & MakeUpdater - >); + return create( + persist( + combine( + { + ...state, + lastUpdateTime: 0, }, - update(updater) { - const state = deepClone(get()); - updater(state); - get().markUpdate(); - set(state); + (set, get) => { + return { + ...methods(set, get as any), + + markUpdate() { + set({ lastUpdateTime: Date.now() } as Partial< + T & M & MakeUpdater + >); + }, + update(updater) { + const state = deepClone(get()); + updater(state); + set({ + ...state, + lastUpdateTime: Date.now(), + }); + }, + } as M & MakeUpdater; }, - }; - }, persistOptions), + ), + persistOptions as any, + ), ); } From c9dd953817798d785abef6c92f6c9d047c5d16ca Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 10 Nov 2023 02:50:50 +0800 Subject: [PATCH 2/2] fixup --- app/components/auth.tsx | 2 +- app/locales/ar.ts | 16 ++-------------- app/locales/bn.ts | 16 ++-------------- app/locales/cs.ts | 12 ++---------- app/locales/de.ts | 12 +----------- app/locales/es.ts | 12 ++---------- app/locales/fr.ts | 12 ++---------- app/locales/id.ts | 21 +++++---------------- app/locales/it.ts | 13 ++----------- app/locales/jp.ts | 15 ++++----------- app/locales/ko.ts | 12 ++---------- app/locales/no.ts | 13 ++----------- app/locales/ru.ts | 12 ++---------- app/locales/tr.ts | 12 ++---------- app/locales/tw.ts | 12 ++---------- app/locales/vi.ts | 12 ++---------- 16 files changed, 35 insertions(+), 169 deletions(-) diff --git a/app/components/auth.tsx b/app/components/auth.tsx index 3e1548a1325..7962d46bee4 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -56,7 +56,7 @@ export function AuthPage() { { accessStore.update( diff --git a/app/locales/ar.ts b/app/locales/ar.ts index d5844acd695..b58c3a2e8e5 100644 --- a/app/locales/ar.ts +++ b/app/locales/ar.ts @@ -167,11 +167,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد Title: "حد الضغط للتاريخ", SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد", }, - Token: { - Title: "مفتاح API", - SubTitle: "استخدم مفتاحك لتجاوز حد رمز الوصول", - Placeholder: "مفتاح OpenAI API", - }, + Usage: { Title: "رصيد الحساب", SubTitle(used: any, total: any) { @@ -181,15 +177,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد Check: "التحقق", NoAccess: "أدخل مفتاح API للتحقق من الرصيد", }, - AccessCode: { - Title: "رمز الوصول", - SubTitle: "تم تمكين التحكم في الوصول", - Placeholder: "رمز الوصول المطلوب", - }, - Endpoint: { - Title: "نقطة النهاية", - SubTitle: "يجب أن تبدأ نقطة النهاية المخصصة بـ http(s)://", - }, + Model: "النموذج", Temperature: { Title: "الحرارة", diff --git a/app/locales/bn.ts b/app/locales/bn.ts index 2db132cecc2..6dfb0da9bc4 100644 --- a/app/locales/bn.ts +++ b/app/locales/bn.ts @@ -199,11 +199,7 @@ const bn: PartialLocaleType = { SubTitle: "নকুল বার্তা দৈর্ঘ্য সীমা অতিক্রান্ত হলে ঐ বার্তাটি সঙ্কুচিত হবে", }, - Token: { - Title: "অ্যাপি কী", - SubTitle: "অ্যাক্সেস কোড সীমা উপেক্ষা করতে আপনার কীটি ব্যবহার করুন", - Placeholder: "OpenAI API কী", - }, + Usage: { Title: "একাউন্ট ব্যালেন্স", SubTitle(used: any, total: any) { @@ -213,15 +209,7 @@ const bn: PartialLocaleType = { Check: "চেক", NoAccess: "ব্যালেন্স চেক করতে অ্যাপি কী ইনপুট করুন", }, - AccessCode: { - Title: "অ্যাক্সেস কোড", - SubTitle: "অ্যাক্সেস নিয়ন্ত্রণ সক্রিয়", - Placeholder: "অ্যাক্সেস কোড প্রয়োজন", - }, - Endpoint: { - Title: "ইনটারপয়েন্ট", - SubTitle: "কাস্টম এন্ডপয়েন্টটি হতে হবে http(s):// দিয়ে শুরু হতে হবে", - }, + Model: "মডেল", Temperature: { Title: "তাপমাত্রা", diff --git a/app/locales/cs.ts b/app/locales/cs.ts index 57aa803e42b..c1a84430fb3 100644 --- a/app/locales/cs.ts +++ b/app/locales/cs.ts @@ -124,11 +124,7 @@ const cs: PartialLocaleType = { SubTitle: "Komprese proběhne, pokud délka nekomprimovaných zpráv přesáhne tuto hodnotu", }, - Token: { - Title: "API klíč", - SubTitle: "Použitím klíče ignorujete omezení přístupového kódu", - Placeholder: "Klíč API OpenAI", - }, + Usage: { Title: "Stav účtu", SubTitle(used: any, total: any) { @@ -138,11 +134,7 @@ const cs: PartialLocaleType = { Check: "Zkontrolovat", NoAccess: "Pro kontrolu zůstatku zadejte klíč API", }, - AccessCode: { - Title: "Přístupový kód", - SubTitle: "Kontrola přístupu povolena", - Placeholder: "Potřebujete přístupový kód", - }, + Model: "Model", Temperature: { Title: "Teplota", diff --git a/app/locales/de.ts b/app/locales/de.ts index e0bdc52b749..2fe871bc9f0 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -124,12 +124,7 @@ const de: PartialLocaleType = { SubTitle: "Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet", }, - Token: { - Title: "API-Schlüssel", - SubTitle: - "Verwenden Sie Ihren Schlüssel, um das Zugangscode-Limit zu ignorieren", - Placeholder: "OpenAI API-Schlüssel", - }, + Usage: { Title: "Kontostand", SubTitle(used: any, total: any) { @@ -139,11 +134,6 @@ const de: PartialLocaleType = { Check: "Erneut prüfen", NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen", }, - AccessCode: { - Title: "Zugangscode", - SubTitle: "Zugangskontrolle aktiviert", - Placeholder: "Zugangscode erforderlich", - }, Model: "Modell", Temperature: { Title: "Temperature", //Temperatur diff --git a/app/locales/es.ts b/app/locales/es.ts index a6ae154f44f..7d742d536e5 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -124,11 +124,7 @@ const es: PartialLocaleType = { SubTitle: "Se comprimirán los mensajes si la longitud de los mensajes no comprimidos supera el valor", }, - Token: { - Title: "Clave de API", - SubTitle: "Utiliza tu clave para ignorar el límite de código de acceso", - Placeholder: "Clave de la API de OpenAI", - }, + Usage: { Title: "Saldo de la cuenta", SubTitle(used: any, total: any) { @@ -138,11 +134,7 @@ const es: PartialLocaleType = { Check: "Comprobar de nuevo", NoAccess: "Introduzca la clave API para comprobar el saldo", }, - AccessCode: { - Title: "Código de acceso", - SubTitle: "Control de acceso habilitado", - Placeholder: "Necesita código de acceso", - }, + Model: "Modelo", Temperature: { Title: "Temperatura", diff --git a/app/locales/fr.ts b/app/locales/fr.ts index f5200f2719c..944754d62a7 100644 --- a/app/locales/fr.ts +++ b/app/locales/fr.ts @@ -173,11 +173,7 @@ const fr: PartialLocaleType = { SubTitle: "Comprimera si la longueur des messages non compressés dépasse cette valeur", }, - Token: { - Title: "Clé API", - SubTitle: "Utilisez votre clé pour ignorer la limite du code d'accès", - Placeholder: "Clé OpenAI API", - }, + Usage: { Title: "Solde du compte", SubTitle(used: any, total: any) { @@ -187,11 +183,7 @@ const fr: PartialLocaleType = { Check: "Vérifier", NoAccess: "Entrez la clé API pour vérifier le solde", }, - AccessCode: { - Title: "Code d'accès", - SubTitle: "Contrôle d'accès activé", - Placeholder: "Code d'accès requis", - }, + Model: "Modèle", Temperature: { Title: "Température", diff --git a/app/locales/id.ts b/app/locales/id.ts index b5e4a70b751..4da55948efc 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -4,8 +4,9 @@ import { PartialLocaleType } from "./index"; const id: PartialLocaleType = { WIP: "Coming Soon...", Error: { - Unauthorized: "Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).", - }, + Unauthorized: + "Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).", + }, Auth: { Title: "Diperlukan Kode Akses", Tips: "Masukkan kode akses di bawah", @@ -237,11 +238,7 @@ const id: PartialLocaleType = { SubTitle: "Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi", }, - Token: { - Title: "Kunci API", - SubTitle: "Gunakan kunci Anda untuk melewati batas kode akses", - Placeholder: "Kunci API OpenAI", - }, + Usage: { Title: "Saldo Akun", SubTitle(used: any, total: any) { @@ -251,15 +248,7 @@ const id: PartialLocaleType = { Check: "Periksa", NoAccess: "Masukkan kunci API untuk memeriksa saldo", }, - AccessCode: { - Title: "Kode Akses", - SubTitle: "Kontrol akses diaktifkan", - Placeholder: "Diperlukan kode akses", - }, - Endpoint: { - Title: "Endpoint", - SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom", - }, + Model: "Model", Temperature: { Title: "Suhu", diff --git a/app/locales/it.ts b/app/locales/it.ts index bf20747b108..7f0a95846c2 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -124,12 +124,7 @@ const it: PartialLocaleType = { SubTitle: "Comprimerà se la lunghezza dei messaggi non compressi supera il valore", }, - Token: { - Title: "API Key", - SubTitle: - "Utilizzare la chiave per ignorare il limite del codice di accesso", - Placeholder: "OpenAI API Key", - }, + Usage: { Title: "Bilancio Account", SubTitle(used: any, total: any) { @@ -139,11 +134,7 @@ const it: PartialLocaleType = { Check: "Controlla ancora", NoAccess: "Inserire la chiave API per controllare il saldo", }, - AccessCode: { - Title: "Codice d'accesso", - SubTitle: "Controllo d'accesso abilitato", - Placeholder: "Inserisci il codice d'accesso", - }, + Model: "Modello GPT", Temperature: { Title: "Temperature", diff --git a/app/locales/jp.ts b/app/locales/jp.ts index b63e8ba3a56..e0ea07c755b 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -20,7 +20,8 @@ const jp: PartialLocaleType = { Stop: "停止", Retry: "リトライ", Pin: "ピン", - PinToastContent: "コンテキストプロンプトに1つのメッセージをピン留めしました", + PinToastContent: + "コンテキストプロンプトに1つのメッセージをピン留めしました", PinToastAction: "表示", Delete: "削除", Edit: "編集", @@ -146,11 +147,7 @@ const jp: PartialLocaleType = { SubTitle: "圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。", }, - Token: { - Title: "APIキー", - SubTitle: "自分のキーを使用してパスワードアクセス制限を迂回する", - Placeholder: "OpenAI APIキー", - }, + Usage: { Title: "残高照会", SubTitle(used: any, total: any) { @@ -160,11 +157,7 @@ const jp: PartialLocaleType = { Check: "再確認", NoAccess: "APIキーまたはアクセスパスワードを入力して残高を表示", }, - AccessCode: { - Title: "アクセスパスワード", - SubTitle: "暗号化アクセスが有効になっています", - Placeholder: "アクセスパスワードを入力してください", - }, + Model: "モデル (model)", Temperature: { Title: "ランダム性 (temperature)", diff --git a/app/locales/ko.ts b/app/locales/ko.ts index 717ce30b2f8..844459fc4ea 100644 --- a/app/locales/ko.ts +++ b/app/locales/ko.ts @@ -124,11 +124,7 @@ const ko: PartialLocaleType = { Title: "기록 압축 임계값", SubTitle: "미압축 메시지 길이가 임계값을 초과하면 압축됨", }, - Token: { - Title: "API 키", - SubTitle: "액세스 코드 제한을 무시하기 위해 키 사용", - Placeholder: "OpenAI API 키", - }, + Usage: { Title: "계정 잔액", SubTitle(used: any, total: any) { @@ -138,11 +134,7 @@ const ko: PartialLocaleType = { Check: "확인", NoAccess: "잔액 확인을 위해 API 키를 입력하세요.", }, - AccessCode: { - Title: "액세스 코드", - SubTitle: "액세스 제어가 활성화됨", - Placeholder: "액세스 코드 입력", - }, + Model: "모델", Temperature: { Title: "온도 (temperature)", diff --git a/app/locales/no.ts b/app/locales/no.ts index 43c92916f3e..3a0e61107a4 100644 --- a/app/locales/no.ts +++ b/app/locales/no.ts @@ -106,12 +106,7 @@ const no: PartialLocaleType = { SubTitle: "Komprimer dersom ikke-komprimert lengde på meldinger overskrider denne verdien", }, - Token: { - Title: "API Key", - SubTitle: - "Bruk din egen API-nøkkel for å ignorere tilgangskoden begrensning", - Placeholder: "OpenAI API-nøkkel", - }, + Usage: { Title: "Saldo for konto", SubTitle(used: any, total: any) { @@ -121,11 +116,7 @@ const no: PartialLocaleType = { Check: "Sjekk", NoAccess: "Skriv inn API-nøkkelen for å sjekke saldo", }, - AccessCode: { - Title: "Tilgangskode", - SubTitle: "Tilgangskontroll på", - Placeholder: "Trenger tilgangskode", - }, + Model: "Model", Temperature: { Title: "Temperatur", diff --git a/app/locales/ru.ts b/app/locales/ru.ts index bf98b4eb865..d12cf3e4258 100644 --- a/app/locales/ru.ts +++ b/app/locales/ru.ts @@ -125,11 +125,7 @@ const ru: PartialLocaleType = { SubTitle: "Будет сжимать, если длина несжатых сообщений превышает указанное значение", }, - Token: { - Title: "API ключ", - SubTitle: "Используйте свой ключ, чтобы игнорировать лимит доступа", - Placeholder: "API ключ OpenAI", - }, + Usage: { Title: "Баланс аккаунта", SubTitle(used: any, total: any) { @@ -139,11 +135,7 @@ const ru: PartialLocaleType = { Check: "Проверить", NoAccess: "Введите API ключ, чтобы проверить баланс", }, - AccessCode: { - Title: "Код доступа", - SubTitle: "Контроль доступа включен", - Placeholder: "Требуется код доступа", - }, + Model: "Модель", Temperature: { Title: "Температура", diff --git a/app/locales/tr.ts b/app/locales/tr.ts index 06996d83dac..524c1b2c546 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -124,11 +124,7 @@ const tr: PartialLocaleType = { SubTitle: "Sıkıştırılmamış mesajların uzunluğu bu değeri aşarsa sıkıştırılır", }, - Token: { - Title: "API Anahtarı", - SubTitle: "Erişim kodu sınırını yoksaymak için anahtarınızı kullanın", - Placeholder: "OpenAI API Anahtarı", - }, + Usage: { Title: "Hesap Bakiyesi", SubTitle(used: any, total: any) { @@ -138,11 +134,7 @@ const tr: PartialLocaleType = { Check: "Tekrar Kontrol Et", NoAccess: "Bakiyeyi kontrol etmek için API anahtarını girin", }, - AccessCode: { - Title: "Erişim Kodu", - SubTitle: "Erişim kontrolü etkinleştirme", - Placeholder: "Erişim Kodu Gerekiyor", - }, + Model: "Model", Temperature: { Title: "Gerçeklik", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index e9f38d097e1..af47e30ff71 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -120,11 +120,7 @@ const tw: PartialLocaleType = { Title: "歷史訊息長度壓縮閾值", SubTitle: "當未壓縮的歷史訊息超過該值時,將進行壓縮", }, - Token: { - Title: "API Key", - SubTitle: "使用自己的 Key 可規避授權存取限制", - Placeholder: "OpenAI API Key", - }, + Usage: { Title: "帳戶餘額", SubTitle(used: any, total: any) { @@ -134,11 +130,7 @@ const tw: PartialLocaleType = { Check: "重新檢查", NoAccess: "輸入 API Key 檢視餘額", }, - AccessCode: { - Title: "授權碼", - SubTitle: "目前是未授權存取狀態", - Placeholder: "請輸入授權碼", - }, + Model: "模型 (model)", Temperature: { Title: "隨機性 (temperature)", diff --git a/app/locales/vi.ts b/app/locales/vi.ts index 8f53a3dc1ee..3d95b566497 100644 --- a/app/locales/vi.ts +++ b/app/locales/vi.ts @@ -123,11 +123,7 @@ const vi: PartialLocaleType = { Title: "Ngưỡng nén lịch sử tin nhắn", SubTitle: "Thực hiện nén nếu số lượng tin nhắn chưa nén vượt quá ngưỡng", }, - Token: { - Title: "API Key", - SubTitle: "Sử dụng khóa của bạn để bỏ qua giới hạn mã truy cập", - Placeholder: "OpenAI API Key", - }, + Usage: { Title: "Hạn mức tài khoản", SubTitle(used: any, total: any) { @@ -137,11 +133,7 @@ const vi: PartialLocaleType = { Check: "Kiểm tra", NoAccess: "Nhập API Key để kiểm tra hạn mức", }, - AccessCode: { - Title: "Mã truy cập", - SubTitle: "Đã bật kiểm soát truy cập", - Placeholder: "Nhập mã truy cập", - }, + Model: "Mô hình", Temperature: { Title: "Tính ngẫu nhiên (temperature)",