diff --git a/README.md b/README.md index 3973c84bfde..a7c862b4099 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

ChatGPT Next Web

-English / [简体中文](./README_CN.md) / [日本語](./README_JA.md) +English / [简体中文](./README_CN.md) One-Click to get well-designed cross-platform ChatGPT web UI. @@ -62,6 +62,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI. - 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/). - 🚀 v2.7 let's share conversations as image, or share to ShareGPT! - 🚀 v2.8 now we have a client that runs across all platforms! +- 🚀 v2.9.11 you can use azure endpoint now. ## 主要功能 @@ -93,6 +94,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI. - 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com - 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。 - 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。 +- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。 ## Get Started @@ -153,14 +155,14 @@ After adding or modifying this environment variable, please redeploy the project > [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量) -### `OPENAI_API_KEY` (required) - -Your openai api key. - ### `CODE` (optional) Access password, separated by comma. +### `OPENAI_API_KEY` (required) + +Your openai api key. + ### `BASE_URL` (optional) > Default: `https://api.openai.com` @@ -173,6 +175,20 @@ Override openai api request base url. Specify OpenAI organization ID. +### `AZURE_URL` (optional) + +> Example: https://{azure-resource-url}/openai/deployments/{deploy-name} + +Azure deploy url. + +### `AZURE_API_KEY` (optional) + +Azure Api Key. + +### `AZURE_API_VERSION` (optional) + +Azure Api Version, find it at [Azure Documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions). + ### `HIDE_USER_API_KEY` (optional) > Default: Empty diff --git a/README_CN.md b/README_CN.md index 9e4d1b64bf4..41c507b50ce 100644 --- a/README_CN.md +++ b/README_CN.md @@ -90,6 +90,20 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 指定 OpenAI 中的组织 ID。 +### `AZURE_URL` (可选) + +> 形如:https://{azure-resource-url}/openai/deployments/{deploy-name} + +Azure 部署地址。 + +### `AZURE_API_KEY` (可选) + +Azure 密钥。 + +### `AZURE_API_VERSION` (可选) + +Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。 + ### `HIDE_USER_API_KEY` (可选) 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 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 a0938701f27..7fbece3e454 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"; @@ -128,22 +128,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 82120bcd035..6fddd8eafd4 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) { @@ -267,14 +285,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 95097b462e2..7962d46bee4 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -7,6 +7,7 @@ import { useAccessStore } from "../store"; import Locale from "../locales"; import BotIcon from "../icons/bot.svg"; +import { useEffect } from "react"; import { getClientConfig } from "../config/client"; export function AuthPage() { @@ -14,13 +15,20 @@ export function AuthPage() { const accessStore = useAccessStore(); const goHome = () => navigate(Path.Home); + const goChat = () => navigate(Path.Chat); const resetAccessCode = () => { - accessStore.update((access) => { - access.token = ""; + accessStore.update((access) => { + access.openaiApiKey = ""; access.accessCode = ""; }); }; // Reset access code to empty string - const goPrivacy = () => navigate(Path.PrivacyPage); + + useEffect(() => { + if (getClientConfig()?.isApp) { + navigate(Path.Settings); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return (
@@ -31,56 +39,39 @@ export function AuthPage() {
{Locale.Auth.Title}
{Locale.Auth.Tips}
- {!getClientConfig()?.isApp && ( // Conditionally render the input access code based on whether it's an app - <> - { - accessStore.update( - (access) => (access.accessCode = e.currentTarget.value), - ); - }} - /> -
{Locale.Auth.SubTips}
- { - accessStore.update( - (access) => (access.token = e.currentTarget.value), - ); - }} - /> - - )} - - {getClientConfig()?.isApp && ( // Conditionally render the input access token based on whether it's an app + { + accessStore.update( + (access) => (access.accessCode = e.currentTarget.value), + ); + }} + /> + {!accessStore.hideUserApiKey ? ( <>
{Locale.Auth.SubTips}
{ accessStore.update( - (access) => (access.token = e.currentTarget.value), + (access) => (access.openaiApiKey = e.currentTarget.value), ); }} /> - )} + ) : null}
{ 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 a023f5e51a5..367784c5acf 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"; @@ -685,8 +688,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, @@ -982,16 +993,16 @@ export function Settings() { - - {showAccessCode ? ( + + {showAccessCode && ( { accessStore.update( (access) => (access.accessCode = e.currentTarget.value), @@ -999,44 +1010,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 5760a875379..0dde3f4308f 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -4,22 +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; - MODEL_LIST?: string; - PROXY_URL?: string; - OPENAI_ORG_ID?: string; + OPENAI_ORG_ID?: string; // openai only + VERCEL?: string; + BUILD_MODE?: "standalone" | "export"; + BUILD_APP?: string; // is building desktop app VERCEL_ANALYTICS?: string; // vercel web analytics 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_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 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; } } } @@ -44,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) { @@ -54,16 +60,26 @@ 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, isVercelWebAnalytics: !!process.env.VERCEL_ANALYTICS, + 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 d23c2fa491f..d9b7b7abf96 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -25,10 +25,12 @@ export enum Path { export enum ApiPath { Cors = "/api/cors", + OpenAI = "/api/openai", } export enum SlotID { AppBody = "app-body", + CustomModel = "custom-model", } // This will automatically generate JSON files without the need to include the ".json" extension. export enum FileName { @@ -62,6 +64,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", // text moderation @@ -77,6 +84,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/ar.ts b/app/locales/ar.ts index a5ac797db63..d463497ffa1 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) { @@ -184,15 +180,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد Check: "التحقق", NoAccess: `أدخل مفتاح الجلسة في مفتاح واجهة برمجة التطبيقات بدءًا من البادئة "sess-" للتحقق من الرصيد.`, }, - 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 515ddf601fd..745f2d66781 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) { @@ -216,15 +212,7 @@ const bn: PartialLocaleType = { Check: "চেক", NoAccess: `ব্যালেন্স চেক করতে, API কি-তে "sess-" উপসর্গ দিয়ে সেশন কি প্রবেশ করান।`, }, - AccessCode: { - Title: "অ্যাক্সেস কোড", - SubTitle: "অ্যাক্সেস নিয়ন্ত্রণ সক্রিয়", - Placeholder: "অ্যাক্সেস কোড প্রয়োজন", - }, - Endpoint: { - Title: "ইনটারপয়েন্ট", - SubTitle: "কাস্টম এন্ডপয়েন্টটি হতে হবে http(s):// দিয়ে শুরু হতে হবে", - }, + Model: "মডেল", Temperature: { Title: "তাপমাত্রা", diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 6d9e26011c0..7378f05fbcf 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -352,10 +352,56 @@ const cn = { 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/cs.ts b/app/locales/cs.ts index 37d6584fdfa..dd4d8ebcd00 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) { @@ -141,11 +137,7 @@ const cs: PartialLocaleType = { Check: "Zkontrolovat", NoAccess: `Zadejte klíč relace ve vstupním klíči API s předponou "sess-" pro kontrolu zůstatku.`, }, - 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 a6176a3889d..bb93fc179b7 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) { @@ -142,11 +137,6 @@ const de: PartialLocaleType = { Check: "Erneut prüfen", NoAccess: `Geben Sie den Sitzungsschlüssel in den API-Schlüssel ein, der mit dem Präfix "sess-" beginnt, um den Saldo zu überprüfen.`, }, - AccessCode: { - Title: "Zugangscode", - SubTitle: "Zugangskontrolle aktiviert", - Placeholder: "Zugangscode erforderlich", - }, Model: "Modell", Temperature: { Title: "Temperature", //Temperatur diff --git a/app/locales/en.ts b/app/locales/en.ts index 4de9db7053d..8714c5bcb79 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -336,6 +336,7 @@ const en: LocaleType = { SubTitle: "Use your key to ignore access code limit", Placeholder: "OpenAI API Key", }, + Usage: { Title: "Account Balance", SubTitle(used: any, total: any) { @@ -357,10 +358,55 @@ const en: LocaleType = { 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/locales/es.ts b/app/locales/es.ts index 0028400ca4f..4f8d7c6e750 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) { @@ -141,11 +137,7 @@ const es: PartialLocaleType = { Check: "Comprobar de nuevo", NoAccess: `Ingresa la clave de sesión en la clave API comenzando con el prefijo "sess-" para verificar 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 5a8233af4b9..a50cc4e37cd 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) { @@ -190,11 +186,7 @@ const fr: PartialLocaleType = { Check: "Vérifier", NoAccess: `Entrez la clé de session dans la clé API en commençant par le préfixe "sess-" 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/it.ts b/app/locales/it.ts index f534d3acc21..865f042e057 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) { @@ -142,11 +137,7 @@ const it: PartialLocaleType = { Check: "Controlla ancora", NoAccess: `Inserisci la chiave di sessione nella chiave API che inizia con il prefisso "sess-" per verificare 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 497b0cd633b..121ef7fc57c 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -147,11 +147,7 @@ const jp: PartialLocaleType = { SubTitle: "圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。", }, - Token: { - Title: "APIキー", - SubTitle: "自分のキーを使用してパスワードアクセス制限を迂回する", - Placeholder: "OpenAI APIキー", - }, + Usage: { Title: "残高照会", SubTitle(used: any, total: any) { @@ -164,11 +160,7 @@ const jp: PartialLocaleType = { Check: "再確認", NoAccess: `残高を確認するには、APIキーの先頭に「sess-」を付けたセッションキーを入力してください。`, }, - AccessCode: { - Title: "アクセスパスワード", - SubTitle: "暗号化アクセスが有効になっています", - Placeholder: "アクセスパスワードを入力してください", - }, + Model: "モデル (model)", Temperature: { Title: "ランダム性 (temperature)", diff --git a/app/locales/ko.ts b/app/locales/ko.ts index 3ae328356f2..9e814d031f1 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) { @@ -141,11 +137,7 @@ const ko: PartialLocaleType = { Check: "확인", NoAccess: `잔액을 확인하려면, API 키에 "sess-" 접두사로 시작하는 세션 키를 입력하세요.`, }, - AccessCode: { - Title: "액세스 코드", - SubTitle: "액세스 제어가 활성화됨", - Placeholder: "액세스 코드 입력", - }, + Model: "모델", Temperature: { Title: "온도 (temperature)", diff --git a/app/locales/no.ts b/app/locales/no.ts index 31a71e009bf..a6ecd31ae52 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) { @@ -124,11 +119,7 @@ const no: PartialLocaleType = { Check: "Sjekk", NoAccess: `Skriv inn øktnøkkelen i API-nøkkelen som starter med prefikset "sess-" for å sjekke saldoen.`, }, - 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 1643d258914..53a404f5f97 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) { @@ -142,11 +138,7 @@ const ru: PartialLocaleType = { Check: "Проверить", NoAccess: `Введите ключ сеанса в ключ API, начинающийся с префикса "sess-", чтобы проверить баланс.`, }, - AccessCode: { - Title: "Код доступа", - SubTitle: "Контроль доступа включен", - Placeholder: "Требуется код доступа", - }, + Model: "Модель", Temperature: { Title: "Температура", diff --git a/app/locales/tr.ts b/app/locales/tr.ts index ebc9d0f3129..7438429c107 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) { @@ -141,11 +137,7 @@ const tr: PartialLocaleType = { Check: "Tekrar Kontrol Et", NoAccess: `Bakiyeyi kontrol etmek için, API Anahtarında "sess-" önekiyle başlayan Oturum 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 f9e43d68e78..311469e2dc4 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) { @@ -137,11 +133,7 @@ const tw: PartialLocaleType = { Check: "重新檢查", NoAccess: `輸入以"sess-"為前綴的API金鑰中的會話金鑰以檢查餘額。`, }, - AccessCode: { - Title: "授權碼", - SubTitle: "目前是未授權存取狀態", - Placeholder: "請輸入授權碼", - }, + Model: "模型 (model)", Temperature: { Title: "隨機性 (temperature)", diff --git a/app/locales/vi.ts b/app/locales/vi.ts index b15770f64c9..8bd6d7e5e01 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) { @@ -140,11 +136,7 @@ const vi: PartialLocaleType = { Check: "Kiểm tra", NoAccess: `Nhập Khóa Phiên vào Khóa API bắt đầu bằng tiền tố "sess-" để kiểm tra số dư.`, }, - 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)", 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/store/sync.ts b/app/store/sync.ts index 235dced63ca..ce78a37ab44 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -181,7 +181,7 @@ export const useSyncStore = createPersistStore( if (overwriteAccessControl !== false) { // default is false ref #DEFAULT_SYNC_STATE const accessControl = localState['access-control']; - accessControl.token + accessControl.openaiApiKey accessControl.accessCode accessControl.needCode accessControl.hideUserApiKey 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, + ), ); }