From f3b972e57381afc74d9417f802f8ae3c64536f68 Mon Sep 17 00:00:00 2001 From: josephrocca <1167575+josephrocca@users.noreply.github.com> Date: Mon, 27 May 2024 10:31:29 +0800 Subject: [PATCH 01/67] Fix web url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d496d68ed..e96e2f588 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 [网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) -[web-url]: https://chatgpt.nextweb.fun +[web-url]: https://app.nextchat.dev/ [download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases [Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge [Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows From 163fc9e3a31158b4be8ff25f98a1c905c6eb1afb Mon Sep 17 00:00:00 2001 From: Imamuzzaki Abu Salam Date: Fri, 14 Jun 2024 08:45:06 +0700 Subject: [PATCH 02/67] fix someone forgot to update license year to 2024 --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 542e91f4e..c979e90c0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Zhang Yifei +Copyright (c) 2024 Zhang Yifei Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 9b0a7050556bed8d0289aeb9686299c1608b5f3f Mon Sep 17 00:00:00 2001 From: Imamuzzaki Abu Salam Date: Fri, 14 Jun 2024 09:19:38 +0700 Subject: [PATCH 03/67] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index c979e90c0..047f9431e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Zhang Yifei +Copyright (c) 2023-2024 Zhang Yifei Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 6efe4fb73445ea8acd54cbc750b5ef0321d8a50f Mon Sep 17 00:00:00 2001 From: Imamuzzaki Abu Salam Date: Sun, 16 Jun 2024 10:17:58 +0700 Subject: [PATCH 04/67] chore(app/layout.tsx): fix deprecated viewport nextjs 14 --- app/layout.tsx | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index 5898b21a1..637b4556b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,7 +3,7 @@ import "./styles/globals.scss"; import "./styles/markdown.scss"; import "./styles/highlight.scss"; import { getClientConfig } from "./config/client"; -import { type Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { SpeedInsights } from "@vercel/speed-insights/next"; import { getServerSideConfig } from "./config/server"; import { GoogleTagManager } from "@next/third-parties/google"; @@ -12,21 +12,22 @@ const serverConfig = getServerSideConfig(); export const metadata: Metadata = { title: "NextChat", description: "Your personal ChatGPT Chat Bot.", - viewport: { - width: "device-width", - initialScale: 1, - maximumScale: 1, - }, - themeColor: [ - { media: "(prefers-color-scheme: light)", color: "#fafafa" }, - { media: "(prefers-color-scheme: dark)", color: "#151515" }, - ], appleWebApp: { title: "NextChat", statusBarStyle: "default", }, }; +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 1, + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#fafafa" }, + { media: "(prefers-color-scheme: dark)", color: "#151515" }, + ], +}; + export default function RootLayout({ children, }: { From 4640060891c85b6619cdaf7b7ee4c0cfc4404170 Mon Sep 17 00:00:00 2001 From: hengstchon Date: Fri, 21 Jun 2024 12:05:28 +0200 Subject: [PATCH 05/67] feat: support model: claude-3-5-sonnet-20240620 --- app/constant.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/constant.ts b/app/constant.ts index 411e48150..1ccb1aeb2 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -166,6 +166,7 @@ const anthropicModels = [ "claude-3-sonnet-20240229", "claude-3-opus-20240229", "claude-3-haiku-20240307", + "claude-3-5-sonnet-20240620", ]; export const DEFAULT_MODELS = [ From 9fb8fbcc65c29c74473a13715c05725e2b49065d Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 24 Jun 2024 14:31:50 +0800 Subject: [PATCH 06/67] fix: validate the url to avoid SSRF --- app/api/webdav/[...path]/route.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/api/webdav/[...path]/route.ts b/app/api/webdav/[...path]/route.ts index 816c2046b..01286fc1b 100644 --- a/app/api/webdav/[...path]/route.ts +++ b/app/api/webdav/[...path]/route.ts @@ -9,6 +9,14 @@ const mergedAllowedWebDavEndpoints = [ ...config.allowedWebDevEndpoints, ].filter((domain) => Boolean(domain.trim())); +const normalizeUrl = (url: string) => { + try { + return new URL(url); + } catch (err) { + return null; + } +}; + async function handle( req: NextRequest, { params }: { params: { path: string[] } }, @@ -24,9 +32,15 @@ async function handle( // Validate the endpoint to prevent potential SSRF attacks if ( - !mergedAllowedWebDavEndpoints.some( - (allowedEndpoint) => endpoint?.startsWith(allowedEndpoint), - ) + !endpoint || + !mergedAllowedWebDavEndpoints.some((allowedEndpoint) => { + const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint); + const normalizedEndpoint = normalizeUrl(endpoint as string); + + return normalizedEndpoint && + normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname && + normalizedEndpoint.pathname.startsWith(normalizedAllowedEndpoint.pathname); + }) ) { return NextResponse.json( { From b972a0d0817e612fe2a1cba398c338bcec7573e6 Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 24 Jun 2024 14:45:45 +0800 Subject: [PATCH 07/67] feat: bump version --- src-tauri/tauri.conf.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ee87d8d15..6230ba41f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "NextChat", - "version": "2.12.3" + "version": "2.12.4" }, "tauri": { "allowlist": { @@ -112,4 +112,4 @@ } ] } -} +} \ No newline at end of file From 95e3b156c01e51324c32e7be6d818b5ee3f4025f Mon Sep 17 00:00:00 2001 From: fred-bf <157469842+fred-bf@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:36:14 +0800 Subject: [PATCH 08/67] Update Dockerfile --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 51a810fc5..ae9a17cdd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,7 @@ COPY --from=builder /app/.next/server ./.next/server EXPOSE 3000 CMD if [ -n "$PROXY_URL" ]; then \ - export HOSTNAME="127.0.0.1"; \ + export HOSTNAME="0.0.0.0"; \ protocol=$(echo $PROXY_URL | cut -d: -f1); \ host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \ port=$(echo $PROXY_URL | cut -d: -f3); \ @@ -58,7 +58,7 @@ CMD if [ -n "$PROXY_URL" ]; then \ echo "[ProxyList]" >> $conf; \ echo "$protocol $host $port" >> $conf; \ cat /etc/proxychains.conf; \ - proxychains -f $conf node server.js --host 0.0.0.0; \ + proxychains -f $conf node server.js; \ else \ node server.js; \ fi From d65ddead11ad9e14a6b7eb522c5e2fceb6e5df53 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 1 Jul 2024 09:41:01 +0000 Subject: [PATCH 09/67] fix: anthropic client using common getHeaders --- app/client/api.ts | 7 +++++-- app/client/platforms/anthropic.ts | 6 ++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/client/api.ts b/app/client/api.ts index 7bee546b4..502c74698 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -162,14 +162,17 @@ export function getHeaders() { const modelConfig = useChatStore.getState().currentSession().mask.modelConfig; const isGoogle = modelConfig.model.startsWith("gemini"); const isAzure = accessStore.provider === ServiceProvider.Azure; - const authHeader = isAzure ? "api-key" : "Authorization"; + const isAnthropic = accessStore.provider === ServiceProvider.Anthropic; + const authHeader = isAzure ? "api-key" : isAnthropic ? 'x-api-key' : "Authorization"; const apiKey = isGoogle ? accessStore.googleApiKey : isAzure ? accessStore.azureApiKey + : isAnthropic + ? accessStore.anthropicApiKey : accessStore.openaiApiKey; const clientConfig = getClientConfig(); - const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`; + const makeBearer = (s: string) => `${isAzure || isAnthropic ? "" : "Bearer "}${s.trim()}`; const validString = (x: string) => x && x.length > 0; // when using google api in app, not set auth header diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index e90c8f057..b8eca6946 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -1,5 +1,5 @@ import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant"; -import { ChatOptions, LLMApi, MultimodalContent } from "../api"; +import { ChatOptions, getHeaders, LLMApi, MultimodalContent, } from "../api"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { getClientConfig } from "@/app/config/client"; import { DEFAULT_API_HOST } from "@/app/constant"; @@ -190,9 +190,7 @@ export class ClaudeApi implements LLMApi { body: JSON.stringify(requestBody), signal: controller.signal, headers: { - "Content-Type": "application/json", - Accept: "application/json", - "x-api-key": accessStore.anthropicApiKey, + ...getHeaders(), // get common headers "anthropic-version": accessStore.anthropicApiVersion, Authorization: getAuthKey(accessStore.anthropicApiKey), }, From 37e2517dac850aef0bec0430f02356402b8610d8 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 1 Jul 2024 10:24:19 +0000 Subject: [PATCH 10/67] fix: 1. anthropic client using common getHeaders; 2. always using `Authorization` header send access code --- app/client/api.ts | 3 ++- app/client/platforms/anthropic.ts | 27 ++------------------------- 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/app/client/api.ts b/app/client/api.ts index 502c74698..edee99342 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -184,7 +184,8 @@ export function getHeaders() { accessStore.enabledAccessControl() && validString(accessStore.accessCode) ) { - headers[authHeader] = makeBearer( + // access_code must send with header named `Authorization`, will using in auth middleware. + headers['Authorization'] = makeBearer( ACCESS_CODE_PREFIX + accessStore.accessCode, ); } diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index b8eca6946..18c3decac 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -192,7 +192,8 @@ export class ClaudeApi implements LLMApi { headers: { ...getHeaders(), // get common headers "anthropic-version": accessStore.anthropicApiVersion, - Authorization: getAuthKey(accessStore.anthropicApiKey), + // do not send `anthropicApiKey` in browser!!! + // Authorization: getAuthKey(accessStore.anthropicApiKey), }, }; @@ -387,27 +388,3 @@ function trimEnd(s: string, end = " ") { return s; } - -function bearer(value: string) { - return `Bearer ${value.trim()}`; -} - -function getAuthKey(apiKey = "") { - const accessStore = useAccessStore.getState(); - const isApp = !!getClientConfig()?.isApp; - let authKey = ""; - - if (apiKey) { - // use user's api key first - authKey = bearer(apiKey); - } else if ( - accessStore.enabledAccessControl() && - !isApp && - !!accessStore.accessCode - ) { - // or use access code - authKey = bearer(ACCESS_CODE_PREFIX + accessStore.accessCode); - } - - return authKey; -} From 69974d5651e30c879579e3fe1bd63982548b5dbe Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 1 Jul 2024 13:24:01 +0000 Subject: [PATCH 11/67] gemini using real sse format response #3677 #3688 --- app/api/google/[...path]/route.ts | 4 +- app/client/platforms/google.ts | 151 +++++++++++++++++------------- 2 files changed, 89 insertions(+), 66 deletions(-) diff --git a/app/api/google/[...path]/route.ts b/app/api/google/[...path]/route.ts index ebd192891..81e50538a 100644 --- a/app/api/google/[...path]/route.ts +++ b/app/api/google/[...path]/route.ts @@ -63,7 +63,9 @@ async function handle( ); } - const fetchUrl = `${baseUrl}/${path}?key=${key}`; + const fetchUrl = `${baseUrl}/${path}?key=${key}${ + req?.nextUrl?.searchParams?.get("alt") == "sse" ? "&alt=sse" : "" + }`; const fetchOptions: RequestInit = { headers: { "Content-Type": "application/json", diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index a786f5275..7ecba1de4 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -3,6 +3,12 @@ import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { getClientConfig } from "@/app/config/client"; import { DEFAULT_API_HOST } from "@/app/constant"; +import Locale from "../../locales"; +import { + EventStreamContentType, + fetchEventSource, +} from "@fortaine/fetch-event-source"; +import { prettyObject } from "@/app/utils/format"; import { getMessageTextContent, getMessageImages, @@ -20,7 +26,7 @@ export class GeminiProApi implements LLMApi { ); } async chat(options: ChatOptions): Promise { - // const apiClient = this; + const apiClient = this; let multimodal = false; const messages = options.messages.map((v) => { let parts: any[] = [{ text: getMessageTextContent(v) }]; @@ -120,7 +126,9 @@ export class GeminiProApi implements LLMApi { if (!baseUrl) { baseUrl = isApp - ? DEFAULT_API_HOST + "/api/proxy/google/" + Google.ChatPath(modelConfig.model) + ? DEFAULT_API_HOST + + "/api/proxy/google/" + + Google.ChatPath(modelConfig.model) : this.path(Google.ChatPath(modelConfig.model)); } @@ -139,16 +147,15 @@ export class GeminiProApi implements LLMApi { () => controller.abort(), REQUEST_TIMEOUT_MS, ); - + if (shouldStream) { let responseText = ""; let remainText = ""; let finished = false; - let existingTexts: string[] = []; const finish = () => { finished = true; - options.onFinish(existingTexts.join("")); + options.onFinish(responseText + remainText); }; // animate response to make it looks smooth @@ -173,72 +180,86 @@ export class GeminiProApi implements LLMApi { // start animaion animateResponseText(); - fetch( - baseUrl.replace("generateContent", "streamGenerateContent"), - chatPayload, - ) - .then((response) => { - const reader = response?.body?.getReader(); - const decoder = new TextDecoder(); - let partialData = ""; + controller.signal.onabort = finish; - return reader?.read().then(function processText({ - done, - value, - }): Promise { - if (done) { - if (response.status !== 200) { - try { - let data = JSON.parse(ensureProperEnding(partialData)); - if (data && data[0].error) { - options.onError?.(new Error(data[0].error.message)); - } else { - options.onError?.(new Error("Request failed")); - } - } catch (_) { - options.onError?.(new Error("Request failed")); - } - } + // https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb + const chatPath = + baseUrl.replace("generateContent", "streamGenerateContent") + + (baseUrl.indexOf("?") > -1 ? "&alt=sse" : "?alt=sse"); + console.log("chatPath", chatPath); + fetchEventSource(chatPath, { + ...chatPayload, + async onopen(res) { + clearTimeout(requestTimeoutId); + const contentType = res.headers.get("content-type"); + console.log( + "[Gemini] request response content type: ", + contentType, + ); - console.log("Stream complete"); - // options.onFinish(responseText + remainText); - finished = true; - return Promise.resolve(); - } - - partialData += decoder.decode(value, { stream: true }); + if (contentType?.startsWith("text/plain")) { + responseText = await res.clone().text(); + return finish(); + } + if ( + !res.ok || + !res.headers + .get("content-type") + ?.startsWith(EventStreamContentType) || + res.status !== 200 + ) { + const responseTexts = [responseText]; + let extraInfo = await res.clone().text(); try { - let data = JSON.parse(ensureProperEnding(partialData)); + const resJson = await res.clone().json(); + extraInfo = prettyObject(resJson); + } catch {} - const textArray = data.reduce( - (acc: string[], item: { candidates: any[] }) => { - const texts = item.candidates.map((candidate) => - candidate.content.parts - .map((part: { text: any }) => part.text) - .join(""), - ); - return acc.concat(texts); - }, - [], - ); - - if (textArray.length > existingTexts.length) { - const deltaArray = textArray.slice(existingTexts.length); - existingTexts = textArray; - remainText += deltaArray.join(""); - } - } catch (error) { - // console.log("[Response Animation] error: ", error,partialData); - // skip error message when parsing json + if (res.status === 401) { + responseTexts.push(Locale.Error.Unauthorized); } - return reader.read().then(processText); - }); - }) - .catch((error) => { - console.error("Error:", error); - }); + if (extraInfo) { + responseTexts.push(extraInfo); + } + + responseText = responseTexts.join("\n\n"); + + return finish(); + } + }, + onmessage(msg) { + if (msg.data === "[DONE]" || finished) { + return finish(); + } + const text = msg.data; + try { + const json = JSON.parse(text); + const delta = apiClient.extractMessage(json); + + if (delta) { + remainText += delta; + } + + const blockReason = json?.promptFeedback?.blockReason; + if (blockReason) { + // being blocked + console.log(`[Google] [Safety Ratings] result:`, blockReason); + } + } catch (e) { + console.error("[Request] parse error", text, msg); + } + }, + onclose() { + finish(); + }, + onerror(e) { + options.onError?.(e); + throw e; + }, + openWhenHidden: true, + }); } else { const res = await fetch(baseUrl, chatPayload); clearTimeout(requestTimeoutId); @@ -252,7 +273,7 @@ export class GeminiProApi implements LLMApi { ), ); } - const message = this.extractMessage(resJson); + const message = apiClient.extractMessage(resJson); options.onFinish(message); } } catch (e) { From c4ad66f745a539602910f49156d90be5208986e0 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 1 Jul 2024 13:27:06 +0000 Subject: [PATCH 12/67] remove console.log --- app/client/platforms/google.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index 7ecba1de4..4aac1dbff 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -186,7 +186,6 @@ export class GeminiProApi implements LLMApi { const chatPath = baseUrl.replace("generateContent", "streamGenerateContent") + (baseUrl.indexOf("?") > -1 ? "&alt=sse" : "?alt=sse"); - console.log("chatPath", chatPath); fetchEventSource(chatPath, { ...chatPayload, async onopen(res) { From 88c74ae18d74b79caded849f9a022b6d5a8a101d Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 3 Jul 2024 14:09:49 +0800 Subject: [PATCH 13/67] feat: using fetch to get buildin masks --- app/masks/build.ts | 23 ++++++ app/masks/index.ts | 20 ++++- package.json | 16 ++-- yarn.lock | 191 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 241 insertions(+), 9 deletions(-) create mode 100644 app/masks/build.ts diff --git a/app/masks/build.ts b/app/masks/build.ts new file mode 100644 index 000000000..36ced6adf --- /dev/null +++ b/app/masks/build.ts @@ -0,0 +1,23 @@ +import fs from "fs"; +import path from "path"; +import { CN_MASKS } from "./cn"; +import { TW_MASKS } from "./tw"; +import { EN_MASKS } from "./en"; + +const BUILTIN_MASKS: BuiltinMask[] = { + cn: CN_MASKS, + tw: TW_MASKS, + en: EN_MASKS, +}; + +const dirname = path.dirname(__filename); + +fs.writeFile( + dirname + "/../../public/masks.json", + JSON.stringify(BUILTIN_MASKS, null, 4), + function (error) { + if (error) { + console.error("[Build] failed to build masks", error); + } + }, +); diff --git a/app/masks/index.ts b/app/masks/index.ts index aa4917e3e..92f21c6ae 100644 --- a/app/masks/index.ts +++ b/app/masks/index.ts @@ -22,6 +22,20 @@ export const BUILTIN_MASK_STORE = { }, }; -export const BUILTIN_MASKS: BuiltinMask[] = [...CN_MASKS, ...TW_MASKS, ...EN_MASKS].map( - (m) => BUILTIN_MASK_STORE.add(m), -); +export const BUILTIN_MASKS: BuiltinMask[] = []; + +if (typeof window != "undefined") { + // run in browser skip in next server + fetch("/masks.json") + .then((res) => res.json()) + .catch((error) => { + console.error("[Fetch] failed to fetch masks", error); + return { cn: [], tw: [], en: [] }; + }) + .then((masks) => { + const { cn = [], tw = [], en = [] } = masks; + return [...cn, ...tw, ...en].map((m) => { + BUILTIN_MASKS.push(BUILTIN_MASK_STORE.add(m)); + }); + }); +} diff --git a/package.json b/package.json index 4d06b0b14..ed5edb043 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,16 @@ "private": false, "license": "mit", "scripts": { - "dev": "next dev", - "build": "cross-env BUILD_MODE=standalone next build", + "mask": "npx tsx app/masks/build.ts", + "mask:watch": "npx watch 'yarn mask' app/masks", + "dev": "yarn run mask:watch & next dev", + "build": "yarn mask && cross-env BUILD_MODE=standalone next build", "start": "next start", "lint": "next lint", - "export": "cross-env BUILD_MODE=export BUILD_APP=1 next build", - "export:dev": "cross-env BUILD_MODE=export BUILD_APP=1 next dev", - "app:dev": "yarn tauri dev", - "app:build": "yarn tauri build", + "export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build", + "export:dev": "yarn mask:watch & cross-env BUILD_MODE=export BUILD_APP=1 next dev", + "app:dev": "yarn mask:watch & yarn tauri dev", + "app:build": "yarn mask && yarn tauri build", "prompts": "node ./scripts/fetch-prompts.mjs", "prepare": "husky install", "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev" @@ -59,7 +61,9 @@ "husky": "^8.0.0", "lint-staged": "^13.2.2", "prettier": "^3.0.2", + "tsx": "^4.16.0", "typescript": "5.2.2", + "watch": "^1.0.2", "webpack": "^5.88.1" }, "resolutions": { diff --git a/yarn.lock b/yarn.lock index 72df8cafc..c323a5c38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1092,6 +1092,121 @@ resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A== +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -2981,6 +3096,35 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +esbuild@~0.21.5: + version "0.21.5" + resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -3234,6 +3378,13 @@ events@^3.2.0: resolved "https://registry.npmmirror.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +exec-sh@^0.2.0: + version "0.2.2" + resolved "https://registry.npmmirror.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" + integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw== + dependencies: + merge "^1.2.0" + execa@^7.0.0: version "7.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-7.1.1.tgz#3eb3c83d239488e7b409d48e8813b76bb55c9c43" @@ -3376,6 +3527,11 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -3433,6 +3589,13 @@ get-tsconfig@^4.5.0: resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.5.0.tgz#6d52d1c7b299bd3ee9cd7638561653399ac77b0f" integrity sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ== +get-tsconfig@^4.7.5: + version "4.7.5" + resolved "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.7.5.tgz#5e012498579e9a6947511ed0cd403272c7acbbaf" + integrity sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw== + dependencies: + resolve-pkg-maps "^1.0.0" + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -4387,6 +4550,11 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +merge@^1.2.0: + version "1.2.1" + resolved "https://registry.npmmirror.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" + integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== + mermaid@^10.6.1: version "10.6.1" resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.6.1.tgz#701f4160484137a417770ce757ce1887a98c00fc" @@ -5317,6 +5485,11 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + resolve@^1.14.2, resolve@^1.22.1: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" @@ -5823,6 +5996,16 @@ tslib@^2.6.2: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tsx@^4.16.0: + version "4.16.0" + resolved "https://registry.npmmirror.com/tsx/-/tsx-4.16.0.tgz#913dd96f191b76f07a8744201d8c15d510aa1352" + integrity sha512-MPgN+CuY+4iKxGoJNPv+1pyo5YWZAQ5XfsyobUG+zoKG7IkvCPLZDEyoIb8yLS2FcWci1nlxAqmvPlFWD5AFiQ== + dependencies: + esbuild "~0.21.5" + get-tsconfig "^4.7.5" + optionalDependencies: + fsevents "~2.3.3" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -6043,6 +6226,14 @@ vfile@^5.0.0: unist-util-stringify-position "^3.0.0" vfile-message "^3.0.0" +watch@^1.0.2: + version "1.0.2" + resolved "https://registry.npmmirror.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c" + integrity sha512-1u+Z5n9Jc1E2c7qDO8SinPoZuHj7FgbgU1olSFoyaklduDvvtX7GMMtlE6OC9FTXq4KvNAOfj6Zu4vI1e9bAKA== + dependencies: + exec-sh "^0.2.0" + minimist "^1.2.0" + watchpack@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" From 1609abd16666cb31f0b3f0019450655496273158 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 3 Jul 2024 14:18:22 +0800 Subject: [PATCH 14/67] fix ts --- app/masks/build.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/masks/build.ts b/app/masks/build.ts index 36ced6adf..10c09ad75 100644 --- a/app/masks/build.ts +++ b/app/masks/build.ts @@ -4,7 +4,9 @@ import { CN_MASKS } from "./cn"; import { TW_MASKS } from "./tw"; import { EN_MASKS } from "./en"; -const BUILTIN_MASKS: BuiltinMask[] = { +import { type BuiltinMask } from "./typing"; + +const BUILTIN_MASKS: Record = { cn: CN_MASKS, tw: TW_MASKS, en: EN_MASKS, From 2803a91673483d35eae983650bc5a196e0a5c0fa Mon Sep 17 00:00:00 2001 From: ji-jinlong <61379293+ji-jinlong@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:18:24 +0800 Subject: [PATCH 15/67] =?UTF-8?q?readme=20=E6=B7=BB=E5=8A=A0=20DEFAULT=5FM?= =?UTF-8?q?ODEL=20=E5=8F=82=E6=95=B0=20(#4915)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * README_CN.md 添加 DEFAULT_MODEL 的说明 更改默认模型, 很久之前就有大佬支持了, 但更多人只会看readme, readme没有的就以为不支持(包括我). https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/4545 * Update README.md Change default model, it has been supported by experts long ago, but more people only read the readme. If it's not in the readme, they assume it's not supported (including me). https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/4545 * Update README.md ch to en * en2cn * 保持位置和readme.md一致 --- README.md | 4 ++++ README_CN.md | 15 ++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e96e2f588..c77d2023c 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,10 @@ To control custom models, use `+` to add a custom model, use `-` to hide a model User `-all` to disable all default models, `+all` to enable all default models. +### `DEFAULT_MODEL` (optional) + +Change default model + ### `WHITE_WEBDEV_ENDPOINTS` (optional) You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format: diff --git a/README_CN.md b/README_CN.md index 6811102b6..970ecdef2 100644 --- a/README_CN.md +++ b/README_CN.md @@ -106,23 +106,23 @@ Azure 密钥。 Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。 -### `GOOGLE_API_KEY` (optional) +### `GOOGLE_API_KEY` (可选) Google Gemini Pro 密钥. -### `GOOGLE_URL` (optional) +### `GOOGLE_URL` (可选) Google Gemini Pro Api Url. -### `ANTHROPIC_API_KEY` (optional) +### `ANTHROPIC_API_KEY` (可选) anthropic claude Api Key. -### `ANTHROPIC_API_VERSION` (optional) +### `ANTHROPIC_API_VERSION` (可选) anthropic claude Api version. -### `ANTHROPIC_URL` (optional) +### `ANTHROPIC_URL` (可选) anthropic claude Api Url. @@ -156,7 +156,12 @@ anthropic claude Api Url. 用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。 +### `DEFAULT_MODEL` (可选) + +更改默认模型 + ### `DEFAULT_INPUT_TEMPLATE` (可选) + 自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项 ## 开发 From e7b16bfbc09ff1167292485db6fba7dee372171d Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 4 Jul 2024 15:30:24 +0800 Subject: [PATCH 16/67] add function to check model is available --- app/api/anthropic/[...path]/route.ts | 8 ++------ app/api/common.ts | 30 ++++++++++++---------------- app/utils/model.ts | 6 ++++++ 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/app/api/anthropic/[...path]/route.ts b/app/api/anthropic/[...path]/route.ts index 4264893d9..274d7d7c4 100644 --- a/app/api/anthropic/[...path]/route.ts +++ b/app/api/anthropic/[...path]/route.ts @@ -9,7 +9,7 @@ import { import { prettyObject } from "@/app/utils/format"; import { NextRequest, NextResponse } from "next/server"; import { auth } from "../../auth"; -import { collectModelTable } from "@/app/utils/model"; +import { isModelAvailableInServer } from "@/app/utils/model"; const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]); @@ -136,17 +136,13 @@ async function request(req: NextRequest) { // #1815 try to refuse some request to some models if (serverConfig.customModels && req.body) { try { - const modelTable = collectModelTable( - DEFAULT_MODELS, - serverConfig.customModels, - ); const clonedBody = await req.text(); fetchOptions.body = clonedBody; const jsonBody = JSON.parse(clonedBody) as { model?: string }; // not undefined and is false - if (modelTable[jsonBody?.model ?? ""].available === false) { + if (isModelAvailableInServer(jsonBody?.model ?? "")) { return NextResponse.json( { error: true, diff --git a/app/api/common.ts b/app/api/common.ts index a75f2de5c..3e0156569 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSideConfig } from "../config/server"; import { DEFAULT_MODELS, OPENAI_BASE_URL, GEMINI_BASE_URL } from "../constant"; -import { collectModelTable } from "../utils/model"; +import { isModelAvailableInServer } from "../utils/model"; import { makeAzurePath } from "../azure"; const serverConfig = getServerSideConfig(); @@ -83,17 +83,15 @@ export async function requestOpenai(req: NextRequest) { // #1815 try to refuse gpt4 request if (serverConfig.customModels && req.body) { try { - const modelTable = collectModelTable( - DEFAULT_MODELS, - serverConfig.customModels, - ); const clonedBody = await req.text(); fetchOptions.body = clonedBody; const jsonBody = JSON.parse(clonedBody) as { model?: string }; // not undefined and is false - if (modelTable[jsonBody?.model ?? ""].available === false) { + if ( + isModelAvailableInServer(serverConfig.customModels, jsonBody?.model) + ) { return NextResponse.json( { error: true, @@ -112,16 +110,16 @@ export async function requestOpenai(req: NextRequest) { try { const res = await fetch(fetchUrl, fetchOptions); - // Extract the OpenAI-Organization header from the response - const openaiOrganizationHeader = res.headers.get("OpenAI-Organization"); + // Extract the OpenAI-Organization header from the response + const openaiOrganizationHeader = res.headers.get("OpenAI-Organization"); - // Check if serverConfig.openaiOrgId is defined and not an empty string - if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") { - // If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present - console.log("[Org ID]", openaiOrganizationHeader); - } else { - console.log("[Org ID] is not set up."); - } + // Check if serverConfig.openaiOrgId is defined and not an empty string + if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") { + // If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present + console.log("[Org ID]", openaiOrganizationHeader); + } else { + console.log("[Org ID] is not set up."); + } // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); @@ -129,7 +127,6 @@ export async function requestOpenai(req: NextRequest) { // to disable nginx buffering newHeaders.set("X-Accel-Buffering", "no"); - // Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV) // Also, this is to prevent the header from being sent to the client if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") { @@ -142,7 +139,6 @@ export async function requestOpenai(req: NextRequest) { // The browser will try to decode the response with brotli and fail newHeaders.delete("content-encoding"); - return new Response(res.body, { status: res.status, statusText: res.statusText, diff --git a/app/utils/model.ts b/app/utils/model.ts index 056fff2e9..970c4ea1c 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -1,3 +1,4 @@ +import { DEFAULT_MODELS } from "../constant"; import { LLMModel } from "../client/api"; const customProvider = (modelName: string) => ({ @@ -100,3 +101,8 @@ export function collectModelsWithDefaultModel( const allModels = Object.values(modelTable); return allModels; } + +export function isModelAvailableInServer(customModels, modelName) { + const modelTable = collectModelTable(DEFAULT_MODELS, customModels); + return modelTable[modelName ?? ""].available === false; +} From 14f2a8f3700c920b6f4680b3e547e0f94790326e Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 4 Jul 2024 15:32:08 +0800 Subject: [PATCH 17/67] merge model with modelName and providerName --- app/store/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/store/config.ts b/app/store/config.ts index 94cfcd8ec..85929d900 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -116,12 +116,12 @@ export const useAppConfig = createPersistStore( for (const model of oldModels) { model.available = false; - modelMap[model.name] = model; + modelMap[`${model.name}@${model.provider.name}`] = model; } for (const model of newModels) { model.available = true; - modelMap[model.name] = model; + modelMap[`${model.name}@${model.provider.name}`] = model; } set(() => ({ From b9ffd509925f1475e28ddd4b1a9e688e08fc39c0 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 4 Jul 2024 15:44:36 +0800 Subject: [PATCH 18/67] using @ as fullName in modelTable --- app/utils/model.ts | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/app/utils/model.ts b/app/utils/model.ts index 970c4ea1c..cd3a90611 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -24,7 +24,8 @@ export function collectModelTable( // default models models.forEach((m) => { - modelTable[m.name] = { + // using @ as fullName + modelTable[`${m.name}@${m?.provider?.name}`] = { ...m, displayName: m.name, // 'provider' is copied over if it exists }; @@ -46,12 +47,27 @@ export function collectModelTable( (model) => (model.available = available), ); } else { - modelTable[name] = { - name, - displayName: displayName || name, - available, - provider: modelTable[name]?.provider ?? customProvider(name), // Use optional chaining - }; + // 1. find model by name(), and set available value + let count = 0; + for (const fullName in modelTable) { + if (fullName.includes(name)) { + count += 1; + modelTable[fullName]["available"] = available; + if (displayName) { + modelTable[fullName]["displayName"] = displayName; + } + } + } + // 2. if model not exists, create new model with available value + if (count === 0) { + const provider = customProvider(name); + modelTable[`${name}@${provider.name}`] = { + name, + displayName: displayName || name, + available, + provider, // Use optional chaining + }; + } } }); From 7a5596b9091d056d8bbce9cccf3684c4ee625325 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 4 Jul 2024 15:48:48 +0800 Subject: [PATCH 19/67] hotfix --- app/api/anthropic/[...path]/route.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/api/anthropic/[...path]/route.ts b/app/api/anthropic/[...path]/route.ts index 274d7d7c4..f50730fa9 100644 --- a/app/api/anthropic/[...path]/route.ts +++ b/app/api/anthropic/[...path]/route.ts @@ -142,7 +142,9 @@ async function request(req: NextRequest) { const jsonBody = JSON.parse(clonedBody) as { model?: string }; // not undefined and is false - if (isModelAvailableInServer(jsonBody?.model ?? "")) { + if ( + isModelAvailableInServer(serverConfig.customModels, jsonBody?.model) + ) { return NextResponse.json( { error: true, From aa081834395ac11a7d2f037914f5ffb36e9aee02 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 4 Jul 2024 16:03:35 +0800 Subject: [PATCH 20/67] hotfix --- app/api/anthropic/[...path]/route.ts | 7 ++++++- app/api/common.ts | 18 ++++++++++++++++-- app/store/config.ts | 4 ++-- app/utils/model.ts | 9 +++++++-- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/app/api/anthropic/[...path]/route.ts b/app/api/anthropic/[...path]/route.ts index f50730fa9..495002b8f 100644 --- a/app/api/anthropic/[...path]/route.ts +++ b/app/api/anthropic/[...path]/route.ts @@ -4,6 +4,7 @@ import { Anthropic, ApiPath, DEFAULT_MODELS, + ServiceProvider, ModelProvider, } from "@/app/constant"; import { prettyObject } from "@/app/utils/format"; @@ -143,7 +144,11 @@ async function request(req: NextRequest) { // not undefined and is false if ( - isModelAvailableInServer(serverConfig.customModels, jsonBody?.model) + isModelAvailableInServer( + serverConfig.customModels, + jsonBody?.model, + ServiceProvider.Anthropic, + ) ) { return NextResponse.json( { diff --git a/app/api/common.ts b/app/api/common.ts index 3e0156569..c2dfed4ab 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -1,6 +1,11 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSideConfig } from "../config/server"; -import { DEFAULT_MODELS, OPENAI_BASE_URL, GEMINI_BASE_URL } from "../constant"; +import { + DEFAULT_MODELS, + OPENAI_BASE_URL, + GEMINI_BASE_URL, + ServiceProvider, +} from "../constant"; import { isModelAvailableInServer } from "../utils/model"; import { makeAzurePath } from "../azure"; @@ -90,7 +95,16 @@ export async function requestOpenai(req: NextRequest) { // not undefined and is false if ( - isModelAvailableInServer(serverConfig.customModels, jsonBody?.model) + isModelAvailableInServer( + serverConfig.customModels, + jsonBody?.model, + ServiceProvider.OpenAI, + ) || + isModelAvailableInServer( + serverConfig.customModels, + jsonBody?.model, + ServiceProvider.Azure, + ) ) { return NextResponse.json( { diff --git a/app/store/config.ts b/app/store/config.ts index 85929d900..96d6d9125 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -116,12 +116,12 @@ export const useAppConfig = createPersistStore( for (const model of oldModels) { model.available = false; - modelMap[`${model.name}@${model.provider.name}`] = model; + modelMap[`${model.name}@${model?.provider?.name}`] = model; } for (const model of newModels) { model.available = true; - modelMap[`${model.name}@${model.provider.name}`] = model; + modelMap[`${model.name}@${model?.provider?.name}`] = model; } set(() => ({ diff --git a/app/utils/model.ts b/app/utils/model.ts index cd3a90611..9bd6ea673 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -118,7 +118,12 @@ export function collectModelsWithDefaultModel( return allModels; } -export function isModelAvailableInServer(customModels, modelName) { +export function isModelAvailableInServer( + customModels, + modelName, + providerName, +) { + const fullName = `${modelName}@${providerName}`; const modelTable = collectModelTable(DEFAULT_MODELS, customModels); - return modelTable[modelName ?? ""].available === false; + return modelTable[fullName]?.available === false; } From a68341eae64b4d8bf14afd80299d99928a92364e Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 4 Jul 2024 16:11:37 +0800 Subject: [PATCH 21/67] include providerId in fullName --- app/store/config.ts | 4 ++-- app/utils/model.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/store/config.ts b/app/store/config.ts index 96d6d9125..0e7f43ee6 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -116,12 +116,12 @@ export const useAppConfig = createPersistStore( for (const model of oldModels) { model.available = false; - modelMap[`${model.name}@${model?.provider?.name}`] = model; + modelMap[`${model.name}@${model?.provider?.id}`] = model; } for (const model of newModels) { model.available = true; - modelMap[`${model.name}@${model?.provider?.name}`] = model; + modelMap[`${model.name}@${model?.provider?.id}`] = model; } set(() => ({ diff --git a/app/utils/model.ts b/app/utils/model.ts index 9bd6ea673..741971b00 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -24,8 +24,8 @@ export function collectModelTable( // default models models.forEach((m) => { - // using @ as fullName - modelTable[`${m.name}@${m?.provider?.name}`] = { + // using @ as fullName + modelTable[`${m.name}@${m?.provider?.id}`] = { ...m, displayName: m.name, // 'provider' is copied over if it exists }; @@ -61,7 +61,7 @@ export function collectModelTable( // 2. if model not exists, create new model with available value if (count === 0) { const provider = customProvider(name); - modelTable[`${name}@${provider.name}`] = { + modelTable[`${name}@${provider?.id}`] = { name, displayName: displayName || name, available, From 97aa72ec5bbc0e48587bb621d0f2d247b3f8d76a Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 4 Jul 2024 08:36:25 +0000 Subject: [PATCH 22/67] hotfix ts --- app/api/anthropic/[...path]/route.ts | 4 ++-- app/api/common.ts | 8 ++++---- app/utils/model.ts | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/api/anthropic/[...path]/route.ts b/app/api/anthropic/[...path]/route.ts index 495002b8f..78106efa7 100644 --- a/app/api/anthropic/[...path]/route.ts +++ b/app/api/anthropic/[...path]/route.ts @@ -146,8 +146,8 @@ async function request(req: NextRequest) { if ( isModelAvailableInServer( serverConfig.customModels, - jsonBody?.model, - ServiceProvider.Anthropic, + jsonBody?.model as string, + ServiceProvider.Anthropic as string, ) ) { return NextResponse.json( diff --git a/app/api/common.ts b/app/api/common.ts index c2dfed4ab..1454fde2e 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -97,13 +97,13 @@ export async function requestOpenai(req: NextRequest) { if ( isModelAvailableInServer( serverConfig.customModels, - jsonBody?.model, - ServiceProvider.OpenAI, + jsonBody?.model as string, + ServiceProvider.OpenAI as string, ) || isModelAvailableInServer( serverConfig.customModels, - jsonBody?.model, - ServiceProvider.Azure, + jsonBody?.model as string, + ServiceProvider.Azure as string, ) ) { return NextResponse.json( diff --git a/app/utils/model.ts b/app/utils/model.ts index 741971b00..6b5173a59 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -119,9 +119,9 @@ export function collectModelsWithDefaultModel( } export function isModelAvailableInServer( - customModels, - modelName, - providerName, + customModels: string, + modelName: string, + providerName: string, ) { const fullName = `${modelName}@${providerName}`; const modelTable = collectModelTable(DEFAULT_MODELS, customModels); From 8cb204e22ea3b5c0a29b99b8ec486f9346e798b9 Mon Sep 17 00:00:00 2001 From: Lloyd Zhou Date: Thu, 4 Jul 2024 17:18:42 +0800 Subject: [PATCH 23/67] refactor: get language (#4922) * refactor: get language --- app/locales/index.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/locales/index.ts b/app/locales/index.ts index 6e8088a98..acdb3e878 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -97,7 +97,17 @@ function setItem(key: string, value: string) { function getLanguage() { try { - return navigator.language.toLowerCase(); + const locale = new Intl.Locale(navigator.language).maximize(); + const region = locale?.region?.toLowerCase(); + // 1. check region code in ALL_LANGS + if (AllLangs.includes(region as Lang)) { + return region as Lang; + } + // 2. check language code in ALL_LANGS + if (AllLangs.includes(locale.language as Lang)) { + return locale.language as Lang; + } + return DEFAULT_LANG; } catch { return DEFAULT_LANG; } @@ -110,15 +120,7 @@ export function getLang(): Lang { return savedLang as Lang; } - const lang = getLanguage(); - - for (const option of AllLangs) { - if (lang.includes(option)) { - return option; - } - } - - return DEFAULT_LANG; + return getLanguage(); } export function changeLang(lang: Lang) { From 31d94442644232e272ecca36c8f16e4399997633 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 4 Jul 2024 19:38:26 +0800 Subject: [PATCH 24/67] hotfix --- app/utils/model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/utils/model.ts b/app/utils/model.ts index 6b5173a59..249987726 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -3,7 +3,7 @@ import { LLMModel } from "../client/api"; const customProvider = (modelName: string) => ({ id: modelName, - providerName: "", + providerName: "Custom", providerType: "custom", }); @@ -50,7 +50,7 @@ export function collectModelTable( // 1. find model by name(), and set available value let count = 0; for (const fullName in modelTable) { - if (fullName.includes(name)) { + if (fullName.split("@").shift() == name) { count += 1; modelTable[fullName]["available"] = available; if (displayName) { From 1c20137b0e930c8a7171abaabfa857465db77ad0 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 5 Jul 2024 19:59:45 +0800 Subject: [PATCH 25/67] support azure deployment name --- app/api/auth.ts | 2 +- app/api/common.ts | 19 +++++++-------- app/client/api.ts | 19 ++++++++++----- app/client/platforms/openai.ts | 41 ++++++++++++++++++++++++++++++++- app/components/chat.tsx | 35 ++++++++++++++++++---------- app/components/exporter.tsx | 11 +++++---- app/components/home.tsx | 7 +++--- app/components/model-config.tsx | 19 +++++++-------- app/constant.ts | 12 ++++++++++ app/store/access.ts | 7 +++++- app/store/chat.ts | 10 ++++---- app/store/config.ts | 2 ++ app/utils/checkers.ts | 21 ----------------- app/utils/hooks.ts | 7 +++++- next.config.mjs | 5 ++++ 15 files changed, 143 insertions(+), 74 deletions(-) delete mode 100644 app/utils/checkers.ts diff --git a/app/api/auth.ts b/app/api/auth.ts index b750f2d17..2b4702aed 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -75,7 +75,7 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { break; case ModelProvider.GPT: default: - if (serverConfig.isAzure) { + if (req.nextUrl.pathname.includes("azure/deployments")) { systemApiKey = serverConfig.azureApiKey; } else { systemApiKey = serverConfig.apiKey; diff --git a/app/api/common.ts b/app/api/common.ts index 1454fde2e..17b5f9165 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -14,9 +14,11 @@ const serverConfig = getServerSideConfig(); export async function requestOpenai(req: NextRequest) { const controller = new AbortController(); + const isAzure = req.nextUrl.pathname.includes("azure/deployments"); + var authValue, authHeaderName = ""; - if (serverConfig.isAzure) { + if (isAzure) { authValue = req.headers .get("Authorization") @@ -56,14 +58,13 @@ export async function requestOpenai(req: NextRequest) { 10 * 60 * 1000, ); - 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); + if (isAzure) { + const azureApiVersion = req?.nextUrl?.searchParams?.get("api-version"); + baseUrl = baseUrl.split("/deployments").shift(); + path = `${req.nextUrl.pathname.replaceAll( + "/api/azure/", + "", + )}?api-version=${azureApiVersion}`; } const fetchUrl = `${baseUrl}/${path}`; diff --git a/app/client/api.ts b/app/client/api.ts index edee99342..896880fa3 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -30,6 +30,7 @@ export interface RequestMessage { export interface LLMConfig { model: string; + providerName?: string; temperature?: number; top_p?: number; stream?: boolean; @@ -54,6 +55,7 @@ export interface LLMUsage { export interface LLMModel { name: string; + displayName?: string; available: boolean; provider: LLMModelProvider; } @@ -160,10 +162,14 @@ export function getHeaders() { Accept: "application/json", }; const modelConfig = useChatStore.getState().currentSession().mask.modelConfig; - const isGoogle = modelConfig.model.startsWith("gemini"); - const isAzure = accessStore.provider === ServiceProvider.Azure; - const isAnthropic = accessStore.provider === ServiceProvider.Anthropic; - const authHeader = isAzure ? "api-key" : isAnthropic ? 'x-api-key' : "Authorization"; + const isGoogle = modelConfig.providerName == ServiceProvider.Azure; + const isAzure = modelConfig.providerName === ServiceProvider.Azure; + const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic; + const authHeader = isAzure + ? "api-key" + : isAnthropic + ? "x-api-key" + : "Authorization"; const apiKey = isGoogle ? accessStore.googleApiKey : isAzure @@ -172,7 +178,8 @@ export function getHeaders() { ? accessStore.anthropicApiKey : accessStore.openaiApiKey; const clientConfig = getClientConfig(); - const makeBearer = (s: string) => `${isAzure || isAnthropic ? "" : "Bearer "}${s.trim()}`; + const makeBearer = (s: string) => + `${isAzure || isAnthropic ? "" : "Bearer "}${s.trim()}`; const validString = (x: string) => x && x.length > 0; // when using google api in app, not set auth header @@ -185,7 +192,7 @@ export function getHeaders() { validString(accessStore.accessCode) ) { // access_code must send with header named `Authorization`, will using in auth middleware. - headers['Authorization'] = makeBearer( + headers["Authorization"] = makeBearer( ACCESS_CODE_PREFIX + accessStore.accessCode, ); } diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index f35992630..25097e3ba 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -1,13 +1,16 @@ "use client"; +// azure and openai, using same models. so using same LLMApi. import { ApiPath, DEFAULT_API_HOST, DEFAULT_MODELS, OpenaiPath, + Azure, REQUEST_TIMEOUT_MS, ServiceProvider, } from "@/app/constant"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; +import { collectModelsWithDefaultModel } from "@/app/utils/model"; import { ChatOptions, @@ -97,6 +100,15 @@ export class ChatGPTApi implements LLMApi { return [baseUrl, path].join("/"); } + getBaseUrl(apiPath: string) { + const isApp = !!getClientConfig()?.isApp; + let baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath; + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, baseUrl.length - 1); + } + return baseUrl + "/"; + } + extractMessage(res: any) { return res.choices?.at(0)?.message?.content ?? ""; } @@ -113,6 +125,7 @@ export class ChatGPTApi implements LLMApi { ...useChatStore.getState().currentSession().mask.modelConfig, ...{ model: options.config.model, + providerName: options.config.providerName, }, }; @@ -140,7 +153,33 @@ export class ChatGPTApi implements LLMApi { options.onController?.(controller); try { - const chatPath = this.path(OpenaiPath.ChatPath); + let chatPath = ""; + if (modelConfig.providerName == ServiceProvider.Azure) { + // find model, and get displayName as deployName + const { models: configModels, customModels: configCustomModels } = + useAppConfig.getState(); + const { defaultModel, customModels: accessCustomModels } = + useAccessStore.getState(); + + const models = collectModelsWithDefaultModel( + configModels, + [configCustomModels, accessCustomModels].join(","), + defaultModel, + ); + const model = models.find( + (model) => + model.name == modelConfig.model && + model?.provider.providerName == ServiceProvider.Azure, + ); + chatPath = + this.getBaseUrl(ApiPath.Azure) + + Azure.ChatPath( + model?.displayName ?? model.name, + useAccessStore.getState().azureApiVersion, + ); + } else { + chatPath = this.getBaseUrl(ApiPath.OpenAI) + OpenaiPath.ChatPath; + } const chatPayload = { method: "POST", body: JSON.stringify(requestPayload), diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 061192504..b1bdf757f 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -88,6 +88,7 @@ import { Path, REQUEST_TIMEOUT_MS, UNFINISHED_INPUT, + ServiceProvider, } from "../constant"; import { Avatar } from "./emoji"; import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; @@ -448,6 +449,9 @@ export function ChatActions(props: { // switch model const currentModel = chatStore.currentSession().mask.modelConfig.model; + const currentProviderName = + chatStore.currentSession().mask.modelConfig?.providerName || + ServiceProvider.OpenAI; const allModels = useAllModels(); const models = useMemo(() => { const filteredModels = allModels.filter((m) => m.available); @@ -479,13 +483,13 @@ export function ChatActions(props: { const isUnavaliableModel = !models.some((m) => m.name === currentModel); if (isUnavaliableModel && models.length > 0) { // show next model to default model if exist - let nextModel: ModelType = ( - models.find((model) => model.isDefault) || models[0] - ).name; - chatStore.updateCurrentSession( - (session) => (session.mask.modelConfig.model = nextModel), - ); - showToast(nextModel); + let nextModel = models.find((model) => model.isDefault) || models[0]; + chatStore.updateCurrentSession((session) => { + session.mask.modelConfig.model = nextModel.name; + session.mask.modelConfig.providerName = nextModel?.provider + ?.providerName as ServiceProvider; + }); + showToast(nextModel.name); } }, [chatStore, currentModel, models]); @@ -573,19 +577,26 @@ export function ChatActions(props: { {showModelSelector && ( ({ - title: m.displayName, - value: m.name, + title: `${m.displayName}${ + m?.provider?.providerName + ? "(" + m?.provider?.providerName + ")" + : "" + }`, + value: `${m.name}@${m?.provider?.providerName}`, }))} onClose={() => setShowModelSelector(false)} onSelection={(s) => { if (s.length === 0) return; + const [model, providerName] = s[0].split("@"); chatStore.updateCurrentSession((session) => { - session.mask.modelConfig.model = s[0] as ModelType; + session.mask.modelConfig.model = model as ModelType; + session.mask.modelConfig.providerName = + providerName as ServiceProvider; session.mask.syncGlobalConfig = false; }); - showToast(s[0]); + showToast(model); }} /> )} diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 20e240d93..7281fc2f1 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -36,11 +36,14 @@ import { toBlob, toPng } from "html-to-image"; import { DEFAULT_MASK_AVATAR } from "../store/mask"; import { prettyObject } from "../utils/format"; -import { EXPORT_MESSAGE_CLASS_NAME, ModelProvider } from "../constant"; +import { + EXPORT_MESSAGE_CLASS_NAME, + ModelProvider, + ServiceProvider, +} from "../constant"; import { getClientConfig } from "../config/client"; import { ClientApi } from "../client/api"; import { getMessageTextContent } from "../utils"; -import { identifyDefaultClaudeModel } from "../utils/checkers"; const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , @@ -314,9 +317,9 @@ export function PreviewActions(props: { setShouldExport(false); var api: ClientApi; - if (config.modelConfig.model.startsWith("gemini")) { + if (config.modelConfig.providerName == ServiceProvider.Google) { api = new ClientApi(ModelProvider.GeminiPro); - } else if (identifyDefaultClaudeModel(config.modelConfig.model)) { + } else if (config.modelConfig.providerName == ServiceProvider.Anthropic) { api = new ClientApi(ModelProvider.Claude); } else { api = new ClientApi(ModelProvider.GPT); diff --git a/app/components/home.tsx b/app/components/home.tsx index ffac64fda..addb5e803 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -12,7 +12,7 @@ import LoadingIcon from "../icons/three-dots.svg"; import { getCSSVar, useMobileScreen } from "../utils"; import dynamic from "next/dynamic"; -import { ModelProvider, Path, SlotID } from "../constant"; +import { ServiceProvider, ModelProvider, Path, SlotID } from "../constant"; import { ErrorBoundary } from "./error"; import { getISOLang, getLang } from "../locales"; @@ -29,7 +29,6 @@ import { AuthPage } from "./auth"; import { getClientConfig } from "../config/client"; import { ClientApi } from "../client/api"; import { useAccessStore } from "../store"; -import { identifyDefaultClaudeModel } from "../utils/checkers"; export function Loading(props: { noLogo?: boolean }) { return ( @@ -172,9 +171,9 @@ export function useLoadData() { const config = useAppConfig(); var api: ClientApi; - if (config.modelConfig.model.startsWith("gemini")) { + if (config.modelConfig.providerName == ServiceProvider.Google) { api = new ClientApi(ModelProvider.GeminiPro); - } else if (identifyDefaultClaudeModel(config.modelConfig.model)) { + } else if (config.modelConfig.providerName == ServiceProvider.Anthropic) { api = new ClientApi(ModelProvider.Claude); } else { api = new ClientApi(ModelProvider.GPT); diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index e46a018f4..346fd3a71 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -1,3 +1,4 @@ +import { ServiceProvider } from "@/app/constant"; import { ModalConfigValidator, ModelConfig } from "../store"; import Locale from "../locales"; @@ -10,25 +11,25 @@ export function ModelConfigList(props: { updateConfig: (updater: (config: ModelConfig) => void) => void; }) { const allModels = useAllModels(); + const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`; return ( <> + accessStore.update( + (access) => + (access.baiduUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => + (access.baiduApiKey = e.currentTarget.value), + ); + }} + /> + + + { + accessStore.update( + (access) => + (access.baiduSecretKey = e.currentTarget.value), + ); + }} + /> + + + )} )} diff --git a/app/config/server.ts b/app/config/server.ts index b7c85ce6a..2d09c5479 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -35,6 +35,16 @@ declare global { // google tag manager GTM_ID?: string; + // anthropic only + ANTHROPIC_URL?: string; + ANTHROPIC_API_KEY?: string; + ANTHROPIC_API_VERSION?: string; + + // baidu only + BAIDU_URL?: string; + BAIDU_API_KEY?: string; + BAIDU_SECRET_KEY?: string; + // custom template for preprocessing user input DEFAULT_INPUT_TEMPLATE?: string; } @@ -92,7 +102,7 @@ export const getServerSideConfig = () => { const isAzure = !!process.env.AZURE_URL; const isGoogle = !!process.env.GOOGLE_API_KEY; const isAnthropic = !!process.env.ANTHROPIC_API_KEY; - + const isBaidu = !!process.env.BAIDU_API_KEY; // const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? ""; // const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim()); // const randomIndex = Math.floor(Math.random() * apiKeys.length); @@ -124,6 +134,11 @@ export const getServerSideConfig = () => { anthropicApiVersion: process.env.ANTHROPIC_API_VERSION, anthropicUrl: process.env.ANTHROPIC_URL, + isBaidu, + baiduUrl: process.env.BAIDU_URL, + baiduApiKey: getApiKey(process.env.BAIDU_API_KEY), + baiduSecretKey: process.env.BAIDU_SECRET_KEY, + gtmId: process.env.GTM_ID, needCode: ACCESS_CODES.size > 0, diff --git a/app/constant.ts b/app/constant.ts index d44b5b817..6ffc0e0b3 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -14,6 +14,10 @@ export const ANTHROPIC_BASE_URL = "https://api.anthropic.com"; export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/"; +export const BAIDU_BASE_URL = "https://aip.baidubce.com"; + +export const BAIDU_OATUH_URL = `${BAIDU_BASE_URL}/oauth/2.0/token`; + export enum Path { Home = "/", Chat = "/chat", @@ -28,6 +32,7 @@ export enum ApiPath { Azure = "/api/azure", OpenAI = "/api/openai", Anthropic = "/api/anthropic", + Baidu = "/api/baidu", } export enum SlotID { @@ -71,12 +76,14 @@ export enum ServiceProvider { Azure = "Azure", Google = "Google", Anthropic = "Anthropic", + Baidu = "Baidu", } export enum ModelProvider { GPT = "GPT", GeminiPro = "GeminiPro", Claude = "Claude", + Ernie = "Ernie", } export const Anthropic = { @@ -104,6 +111,12 @@ export const Google = { ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`, }; +export const Baidu = { + ExampleEndpoint: "https://aip.baidubce.com", + ChatPath: (modelName: string) => + `/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${modelName}`, +}; + export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang // export const DEFAULT_SYSTEM_TEMPLATE = ` // You are ChatGPT, a large language model trained by {{ServiceProvider}}. @@ -173,6 +186,16 @@ const anthropicModels = [ "claude-3-5-sonnet-20240620", ]; +const baiduModels = [ + "ernie-4.0-turbo-8k", + "completions_pro=ernie-4.0-8k", + "ernie-4.0-8k-preview", + "completions_adv_pro=ernie-4.0-8k-preview-0518", + "ernie-4.0-8k-latest", + "completions=ernie-3.5-8k", + "ernie-3.5-8k-0205", +]; + export const DEFAULT_MODELS = [ ...openaiModels.map((name) => ({ name, @@ -210,6 +233,15 @@ export const DEFAULT_MODELS = [ providerType: "anthropic", }, })), + ...baiduModels.map((name) => ({ + name, + available: true, + provider: { + id: "baidu", + providerName: "Baidu", + providerType: "baidu", + }, + })), ] as const; export const CHAT_PAGE_SIZE = 15; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 2ff94e32d..a872ee75a 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -347,6 +347,22 @@ const cn = { SubTitle: "选择一个特定的 API 版本", }, }, + Baidu: { + ApiKey: { + Title: "接口密钥", + SubTitle: "使用自定义 Baidu API Key 绕过密码访问限制", + Placeholder: "Baidu API Key", + }, + SecretKey: { + Title: "接口密钥", + SubTitle: "使用自定义 Baidu Secret Key 绕过密码访问限制", + Placeholder: "Baidu Secret Key", + }, + Endpoint: { + Title: "接口地址", + SubTitle: "样例:", + }, + }, CustomModel: { Title: "自定义模型名", SubTitle: "增加自定义模型可选项,使用英文逗号隔开", diff --git a/app/store/access.ts b/app/store/access.ts index 03780779e..7e6d01b34 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -47,6 +47,11 @@ const DEFAULT_ACCESS_STATE = { anthropicApiVersion: "2023-06-01", anthropicUrl: "", + // baidu + baiduUrl: "", + baiduApiKey: "", + baiduSecretKey: "", + // server config needCode: true, hideUserApiKey: false, @@ -83,6 +88,10 @@ export const useAccessStore = createPersistStore( return ensure(get(), ["anthropicApiKey"]); }, + isValidBaidu() { + return ensure(get(), ["baiduApiKey", "baiduSecretKey"]); + }, + isAuthorized() { this.fetch(); @@ -92,6 +101,7 @@ export const useAccessStore = createPersistStore( this.isValidAzure() || this.isValidGoogle() || this.isValidAnthropic() || + this.isValidBaidu() || !this.enabledAccessControl() || (this.enabledAccessControl() && ensure(get(), ["accessCode"])) ); diff --git a/app/store/chat.ts b/app/store/chat.ts index 44d41830a..45ab479d9 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -368,6 +368,8 @@ export const useChatStore = createPersistStore( api = new ClientApi(ModelProvider.GeminiPro); } else if (modelConfig.providerName == ServiceProvider.Anthropic) { api = new ClientApi(ModelProvider.Claude); + } else if (modelConfig.providerName == ServiceProvider.Baidu) { + api = new ClientApi(ModelProvider.Ernie); } else { api = new ClientApi(ModelProvider.GPT); } @@ -552,6 +554,8 @@ export const useChatStore = createPersistStore( api = new ClientApi(ModelProvider.GeminiPro); } else if (modelConfig.providerName == ServiceProvider.Anthropic) { api = new ClientApi(ModelProvider.Claude); + } else if (modelConfig.providerName == ServiceProvider.Baidu) { + api = new ClientApi(ModelProvider.Ernie); } else { api = new ClientApi(ModelProvider.GPT); } diff --git a/app/utils/model.ts b/app/utils/model.ts index 249987726..6a02ed7eb 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -24,10 +24,13 @@ export function collectModelTable( // default models models.forEach((m) => { + // supoort name=displayName eg:completions_pro=ernie-4.0-8k + const [name, displayName] = m.name?.split("="); // using @ as fullName - modelTable[`${m.name}@${m?.provider?.id}`] = { + modelTable[`${name}@${m?.provider?.id}`] = { ...m, - displayName: m.name, // 'provider' is copied over if it exists + name, + displayName: displayName || name, // 'provider' is copied over if it exists }; }); From 9b3b4494ba6ff6a517ca17376d2550b1aa651c00 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Sat, 6 Jul 2024 14:59:37 +0800 Subject: [PATCH 35/67] wip: doubao --- app/api/auth.ts | 3 + app/api/bytedance/[...path]/route.ts | 160 +++++++++++++++++ app/client/api.ts | 5 + app/client/platforms/bytedance.ts | 260 +++++++++++++++++++++++++++ app/components/exporter.tsx | 2 + app/components/home.tsx | 2 + app/config/server.ts | 9 + app/constant.ts | 21 +++ app/store/access.ts | 9 + app/store/chat.ts | 4 + 10 files changed, 475 insertions(+) create mode 100644 app/api/bytedance/[...path]/route.ts create mode 100644 app/client/platforms/bytedance.ts diff --git a/app/api/auth.ts b/app/api/auth.ts index 2b4702aed..9c334f2fe 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -73,6 +73,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { case ModelProvider.Claude: systemApiKey = serverConfig.anthropicApiKey; break; + case ModelProvider.Doubao: + systemApiKey = serverConfig.bytedanceApiKey; + break; case ModelProvider.GPT: default: if (req.nextUrl.pathname.includes("azure/deployments")) { diff --git a/app/api/bytedance/[...path]/route.ts b/app/api/bytedance/[...path]/route.ts new file mode 100644 index 000000000..bffb60f6c --- /dev/null +++ b/app/api/bytedance/[...path]/route.ts @@ -0,0 +1,160 @@ +import { getServerSideConfig } from "@/app/config/server"; +import { + BYTEDANCE_BASE_URL, + ApiPath, + ModelProvider, + ServiceProvider, +} from "@/app/constant"; +import { prettyObject } from "@/app/utils/format"; +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/app/api/auth"; +import { isModelAvailableInServer } from "@/app/utils/model"; + +const serverConfig = getServerSideConfig(); + +async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + console.log("[ByteDance Route] params ", params); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + const authResult = auth(req, ModelProvider.Doubao); + if (authResult.error) { + return NextResponse.json(authResult, { + status: 401, + }); + } + + try { + const response = await request(req); + return response; + } catch (e) { + console.error("[ByteDance] ", e); + return NextResponse.json(prettyObject(e)); + } +} + +export const GET = handle; +export const POST = handle; + +export const runtime = "edge"; +export const preferredRegion = [ + "arn1", + "bom1", + "cdg1", + "cle1", + "cpt1", + "dub1", + "fra1", + "gru1", + "hnd1", + "iad1", + "icn1", + "kix1", + "lhr1", + "pdx1", + "sfo1", + "sin1", + "syd1", +]; + +async function request(req: NextRequest) { + const controller = new AbortController(); + + let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ByteDance, ""); + + let baseUrl = serverConfig.bytedanceUrl || BYTEDANCE_BASE_URL; + + if (!baseUrl.startsWith("http")) { + baseUrl = `https://${baseUrl}`; + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, -1); + } + + console.log("[Proxy] ", path); + console.log("[Base Url]", baseUrl); + + const timeoutId = setTimeout( + () => { + controller.abort(); + }, + 10 * 60 * 1000, + ); + + const fetchUrl = `${baseUrl}${path}`; + + const fetchOptions: RequestInit = { + headers: { + "Content-Type": "application/json", + Authorization: req.headers.get("Authorization") ?? "", + }, + method: req.method, + body: req.body, + redirect: "manual", + // @ts-ignore + duplex: "half", + signal: controller.signal, + }; + + // #1815 try to refuse some request to some models + if (serverConfig.customModels && req.body) { + try { + const clonedBody = await req.text(); + fetchOptions.body = clonedBody; + + const jsonBody = JSON.parse(clonedBody) as { model?: string }; + + // not undefined and is false + if ( + isModelAvailableInServer( + serverConfig.customModels, + jsonBody?.model as string, + ServiceProvider.ByteDance as string, + ) + ) { + return NextResponse.json( + { + error: true, + message: `you are not allowed to use ${jsonBody?.model} model`, + }, + { + status: 403, + }, + ); + } + } catch (e) { + console.error(`[ByteDance] filter`, e); + } + } + console.log("[ByteDance request]", fetchOptions.headers, req.method); + try { + const res = await fetch(fetchUrl, fetchOptions); + + console.log( + "[ByteDance response]", + res.status, + " ", + res.headers, + res.url, + ); + // to prevent browser prompt for credentials + const newHeaders = new Headers(res.headers); + newHeaders.delete("www-authenticate"); + // to disable nginx buffering + newHeaders.set("X-Accel-Buffering", "no"); + + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: newHeaders, + }); + } finally { + clearTimeout(timeoutId); + } +} diff --git a/app/client/api.ts b/app/client/api.ts index 41ccbd8e1..ee43fc7cc 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -9,6 +9,8 @@ import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store"; import { ChatGPTApi } from "./platforms/openai"; import { GeminiProApi } from "./platforms/google"; import { ClaudeApi } from "./platforms/anthropic"; +import { DoubaoApi } from "./platforms/bytedance"; + export const ROLES = ["system", "user", "assistant"] as const; export type MessageRole = (typeof ROLES)[number]; @@ -104,6 +106,9 @@ export class ClientApi { case ModelProvider.Claude: this.llm = new ClaudeApi(); break; + case ModelProvider.Doubao: + this.llm = new DoubaoApi(); + break; default: this.llm = new ChatGPTApi(); } diff --git a/app/client/platforms/bytedance.ts b/app/client/platforms/bytedance.ts new file mode 100644 index 000000000..92c1fd558 --- /dev/null +++ b/app/client/platforms/bytedance.ts @@ -0,0 +1,260 @@ +"use client"; +import { + ApiPath, + ByteDance, + DEFAULT_API_HOST, + REQUEST_TIMEOUT_MS, +} from "@/app/constant"; +import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; + +import { + ChatOptions, + getHeaders, + LLMApi, + LLMModel, + MultimodalContent, +} from "../api"; +import Locale from "../../locales"; +import { + EventStreamContentType, + fetchEventSource, +} from "@fortaine/fetch-event-source"; +import { prettyObject } from "@/app/utils/format"; +import { getClientConfig } from "@/app/config/client"; +import { getMessageTextContent, isVisionModel } from "@/app/utils"; + +export interface OpenAIListModelResponse { + object: string; + data: Array<{ + id: string; + object: string; + root: string; + }>; +} + +interface RequestPayload { + messages: { + role: "system" | "user" | "assistant"; + content: string | MultimodalContent[]; + }[]; + stream?: boolean; + model: string; + temperature: number; + presence_penalty: number; + frequency_penalty: number; + top_p: number; + max_tokens?: number; +} + +export class DoubaoApi implements LLMApi { + path(path: string): string { + const accessStore = useAccessStore.getState(); + + let baseUrl = ""; + + if (accessStore.useCustomConfig) { + baseUrl = accessStore.bytedanceUrl; + } + + if (baseUrl.length === 0) { + const isApp = !!getClientConfig()?.isApp; + baseUrl = isApp + ? DEFAULT_API_HOST + "/api/proxy/bytedance" + : ApiPath.ByteDance; + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, baseUrl.length - 1); + } + if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ByteDance)) { + baseUrl = "https://" + baseUrl; + } + + console.log("[Proxy Endpoint] ", baseUrl, path); + + return [baseUrl, path].join("/"); + } + + extractMessage(res: any) { + return res.choices?.at(0)?.message?.content ?? ""; + } + + async chat(options: ChatOptions) { + const visionModel = isVisionModel(options.config.model); + const messages = options.messages.map((v) => ({ + role: v.role, + content: visionModel ? v.content : getMessageTextContent(v), + })); + + const modelConfig = { + ...useAppConfig.getState().modelConfig, + ...useChatStore.getState().currentSession().mask.modelConfig, + ...{ + model: options.config.model, + }, + }; + + const requestPayload: RequestPayload = { + messages, + stream: options.config.stream, + model: modelConfig.model, + temperature: modelConfig.temperature, + presence_penalty: modelConfig.presence_penalty, + frequency_penalty: modelConfig.frequency_penalty, + top_p: modelConfig.top_p, + }; + + console.log("[Request] ByteDance payload: ", requestPayload); + + const shouldStream = !!options.config.stream; + const controller = new AbortController(); + options.onController?.(controller); + + try { + const chatPath = this.path(ByteDance.ChatPath); + const chatPayload = { + method: "POST", + body: JSON.stringify(requestPayload), + signal: controller.signal, + headers: getHeaders(), + }; + + // make a fetch request + const requestTimeoutId = setTimeout( + () => controller.abort(), + REQUEST_TIMEOUT_MS, + ); + + if (shouldStream) { + let responseText = ""; + let remainText = ""; + let finished = false; + + // animate response to make it looks smooth + function animateResponseText() { + if (finished || controller.signal.aborted) { + responseText += remainText; + console.log("[Response Animation] finished"); + if (responseText?.length === 0) { + options.onError?.(new Error("empty response from server")); + } + return; + } + + if (remainText.length > 0) { + const fetchCount = Math.max(1, Math.round(remainText.length / 60)); + const fetchText = remainText.slice(0, fetchCount); + responseText += fetchText; + remainText = remainText.slice(fetchCount); + options.onUpdate?.(responseText, fetchText); + } + + requestAnimationFrame(animateResponseText); + } + + // start animaion + animateResponseText(); + + const finish = () => { + if (!finished) { + finished = true; + options.onFinish(responseText + remainText); + } + }; + + controller.signal.onabort = finish; + + fetchEventSource(chatPath, { + ...chatPayload, + async onopen(res) { + clearTimeout(requestTimeoutId); + const contentType = res.headers.get("content-type"); + console.log( + "[ByteDance] request response content type: ", + contentType, + ); + + if (contentType?.startsWith("text/plain")) { + responseText = await res.clone().text(); + return finish(); + } + + if ( + !res.ok || + !res.headers + .get("content-type") + ?.startsWith(EventStreamContentType) || + res.status !== 200 + ) { + const responseTexts = [responseText]; + let extraInfo = await res.clone().text(); + try { + const resJson = await res.clone().json(); + extraInfo = prettyObject(resJson); + } catch {} + + if (res.status === 401) { + responseTexts.push(Locale.Error.Unauthorized); + } + + if (extraInfo) { + responseTexts.push(extraInfo); + } + + responseText = responseTexts.join("\n\n"); + + return finish(); + } + }, + onmessage(msg) { + if (msg.data === "[DONE]" || finished) { + return finish(); + } + const text = msg.data; + try { + const json = JSON.parse(text); + const choices = json.choices as Array<{ + delta: { content: string }; + }>; + const delta = choices[0]?.delta?.content; + if (delta) { + remainText += delta; + } + } catch (e) { + console.error("[Request] parse error", text, msg); + } + }, + onclose() { + finish(); + }, + onerror(e) { + options.onError?.(e); + throw e; + }, + openWhenHidden: true, + }); + } else { + const res = await fetch(chatPath, chatPayload); + clearTimeout(requestTimeoutId); + + const resJson = await res.json(); + const message = this.extractMessage(resJson); + options.onFinish(message); + } + } catch (e) { + console.log("[Request] failed to make a chat request", e); + options.onError?.(e as Error); + } + } + async usage() { + return { + used: 0, + total: 0, + }; + } + + async models(): Promise { + return []; + } +} +export { ByteDance }; diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 7281fc2f1..1cc531eb8 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -321,6 +321,8 @@ export function PreviewActions(props: { api = new ClientApi(ModelProvider.GeminiPro); } else if (config.modelConfig.providerName == ServiceProvider.Anthropic) { api = new ClientApi(ModelProvider.Claude); + } else if (config.modelConfig.providerName == ServiceProvider.ByteDance) { + api = new ClientApi(ModelProvider.Doubao); } else { api = new ClientApi(ModelProvider.GPT); } diff --git a/app/components/home.tsx b/app/components/home.tsx index addb5e803..7da20df22 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -175,6 +175,8 @@ export function useLoadData() { api = new ClientApi(ModelProvider.GeminiPro); } else if (config.modelConfig.providerName == ServiceProvider.Anthropic) { api = new ClientApi(ModelProvider.Claude); + } else if (config.modelConfig.providerName == ServiceProvider.ByteDance) { + api = new ClientApi(ModelProvider.Doubao); } else { api = new ClientApi(ModelProvider.GPT); } diff --git a/app/config/server.ts b/app/config/server.ts index b7c85ce6a..d50dbf1a1 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -32,6 +32,10 @@ declare global { GOOGLE_API_KEY?: string; GOOGLE_URL?: string; + // bytedance only + BYTEDANCE_URL?: string; + BYTEDANCE_API_KEY?: string; + // google tag manager GTM_ID?: string; @@ -92,6 +96,7 @@ export const getServerSideConfig = () => { const isAzure = !!process.env.AZURE_URL; const isGoogle = !!process.env.GOOGLE_API_KEY; const isAnthropic = !!process.env.ANTHROPIC_API_KEY; + const isBytedance = !!process.env.BYTEDANCE_API_KEY; // const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? ""; // const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim()); @@ -126,6 +131,10 @@ export const getServerSideConfig = () => { gtmId: process.env.GTM_ID, + isBytedance, + bytedanceApiKey: getApiKey(process.env.BYTEDANCE_API_KEY), + bytedanceUrl: process.env.BYTEDANCE_URL, + needCode: ACCESS_CODES.size > 0, code: process.env.CODE, codes: ACCESS_CODES, diff --git a/app/constant.ts b/app/constant.ts index d44b5b817..1ed292d21 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -14,6 +14,8 @@ export const ANTHROPIC_BASE_URL = "https://api.anthropic.com"; export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/"; +export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com"; + export enum Path { Home = "/", Chat = "/chat", @@ -28,6 +30,7 @@ export enum ApiPath { Azure = "/api/azure", OpenAI = "/api/openai", Anthropic = "/api/anthropic", + ByteDance = "/api/bytedance", } export enum SlotID { @@ -71,12 +74,14 @@ export enum ServiceProvider { Azure = "Azure", Google = "Google", Anthropic = "Anthropic", + ByteDance = "ByteDance", } export enum ModelProvider { GPT = "GPT", GeminiPro = "GeminiPro", Claude = "Claude", + Doubao = "Doubao", } export const Anthropic = { @@ -104,6 +109,11 @@ export const Google = { ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`, }; +export const ByteDance = { + ExampleEndpoint: "https://ark.cn-beijing.volces.com/api/v3/chat/completions", + ChatPath: "/api/v3/chat/completions", +}; + export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang // export const DEFAULT_SYSTEM_TEMPLATE = ` // You are ChatGPT, a large language model trained by {{ServiceProvider}}. @@ -173,6 +183,8 @@ const anthropicModels = [ "claude-3-5-sonnet-20240620", ]; +const bytedanceModels = ["ep-20240520082937-424bw=Doubao-lite-4k"]; + export const DEFAULT_MODELS = [ ...openaiModels.map((name) => ({ name, @@ -210,6 +222,15 @@ export const DEFAULT_MODELS = [ providerType: "anthropic", }, })), + ...bytedanceModels.map((name) => ({ + name, + available: true, + provider: { + id: "bytedance", + providerName: "ByteDance", + providerType: "bytedance", + }, + })), ] as const; export const CHAT_PAGE_SIZE = 15; diff --git a/app/store/access.ts b/app/store/access.ts index 03780779e..b04748b8c 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -47,6 +47,10 @@ const DEFAULT_ACCESS_STATE = { anthropicApiVersion: "2023-06-01", anthropicUrl: "", + // bytedance + bytedanceApiKey: "", + bytedanceUrl: "", + // server config needCode: true, hideUserApiKey: false, @@ -83,6 +87,10 @@ export const useAccessStore = createPersistStore( return ensure(get(), ["anthropicApiKey"]); }, + isValidByteDance() { + return ensure(get(), ["bytedanceApiKey"]); + }, + isAuthorized() { this.fetch(); @@ -92,6 +100,7 @@ export const useAccessStore = createPersistStore( this.isValidAzure() || this.isValidGoogle() || this.isValidAnthropic() || + this.isValidByteDance() || !this.enabledAccessControl() || (this.enabledAccessControl() && ensure(get(), ["accessCode"])) ); diff --git a/app/store/chat.ts b/app/store/chat.ts index 44d41830a..475d436d9 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -368,6 +368,8 @@ export const useChatStore = createPersistStore( api = new ClientApi(ModelProvider.GeminiPro); } else if (modelConfig.providerName == ServiceProvider.Anthropic) { api = new ClientApi(ModelProvider.Claude); + } else if (modelConfig.providerName == ServiceProvider.ByteDance) { + api = new ClientApi(ModelProvider.Doubao); } else { api = new ClientApi(ModelProvider.GPT); } @@ -552,6 +554,8 @@ export const useChatStore = createPersistStore( api = new ClientApi(ModelProvider.GeminiPro); } else if (modelConfig.providerName == ServiceProvider.Anthropic) { api = new ClientApi(ModelProvider.Claude); + } else if (modelConfig.providerName == ServiceProvider.ByteDance) { + api = new ClientApi(ModelProvider.Doubao); } else { api = new ClientApi(ModelProvider.GPT); } From f3e3f083774ab01db558a213a0b180fe995ad2c4 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Sat, 6 Jul 2024 21:25:00 +0800 Subject: [PATCH 36/67] fix: apiClient --- app/client/api.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/client/api.ts b/app/client/api.ts index a3d5a36e0..f650139f9 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -225,6 +225,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi { return new ClientApi(ModelProvider.GeminiPro); case ServiceProvider.Anthropic: return new ClientApi(ModelProvider.Claude); + case ServiceProvider.Baidu: + return new ClientApi(ModelProvider.Ernie); default: return new ClientApi(ModelProvider.GPT); } From 1caa61f4c0e8d35bfff2dd670925f8c1ceb8267a Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Sat, 6 Jul 2024 22:59:20 +0800 Subject: [PATCH 37/67] feat: swap name and displayName for bytedance in custom models --- app/client/api.ts | 2 ++ app/config/server.ts | 6 +++--- app/constant.ts | 9 ++++++++- app/utils/model.ts | 12 ++++++++++-- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/client/api.ts b/app/client/api.ts index d2eeca46a..f2e83c391 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -225,6 +225,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi { return new ClientApi(ModelProvider.GeminiPro); case ServiceProvider.Anthropic: return new ClientApi(ModelProvider.Claude); + case ServiceProvider.ByteDance: + return new ClientApi(ModelProvider.Doubao); default: return new ClientApi(ModelProvider.GPT); } diff --git a/app/config/server.ts b/app/config/server.ts index d50dbf1a1..0f57d2d6d 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -32,13 +32,13 @@ declare global { GOOGLE_API_KEY?: string; GOOGLE_URL?: string; + // google tag manager + GTM_ID?: string; + // bytedance only BYTEDANCE_URL?: string; BYTEDANCE_API_KEY?: string; - // google tag manager - GTM_ID?: string; - // custom template for preprocessing user input DEFAULT_INPUT_TEMPLATE?: string; } diff --git a/app/constant.ts b/app/constant.ts index 1ed292d21..5b52073bb 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -183,7 +183,14 @@ const anthropicModels = [ "claude-3-5-sonnet-20240620", ]; -const bytedanceModels = ["ep-20240520082937-424bw=Doubao-lite-4k"]; +const bytedanceModels = [ + "Doubao-lite-4k", + "Doubao-lite-32k", + "Doubao-lite-128k", + "Doubao-pro-4k", + "Doubao-pro-32k", + "Doubao-pro-128k", +]; export const DEFAULT_MODELS = [ ...openaiModels.map((name) => ({ diff --git a/app/utils/model.ts b/app/utils/model.ts index 249987726..62ecc55b3 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -39,7 +39,7 @@ export function collectModelTable( const available = !m.startsWith("-"); const nameConfig = m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m; - const [name, displayName] = nameConfig.split("="); + let [name, displayName] = nameConfig.split("="); // enable or disable all models if (name === "all") { @@ -50,9 +50,17 @@ export function collectModelTable( // 1. find model by name(), and set available value let count = 0; for (const fullName in modelTable) { - if (fullName.split("@").shift() == name) { + const [modelName, providerName] = fullName.split("@"); + if (modelName === name) { count += 1; modelTable[fullName]["available"] = available; + // swap name and displayName for bytedance + if (providerName === "bytedance") { + const tempName = name; + name = displayName; + displayName = tempName; + modelTable[fullName]["name"] = name; + } if (displayName) { modelTable[fullName]["displayName"] = displayName; } From 9bdd37bb631198f8c75b995b47ba87a1e6639c14 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Sun, 7 Jul 2024 21:59:56 +0800 Subject: [PATCH 38/67] feat: qwen --- app/api/alibaba/[...path]/route.ts | 175 +++++++++++++++++++ app/api/auth.ts | 3 + app/client/api.ts | 7 + app/client/platforms/alibaba.ts | 260 +++++++++++++++++++++++++++++ app/client/platforms/openai.ts | 2 +- app/config/server.ts | 9 + app/constant.ts | 29 ++++ app/store/access.ts | 9 + 8 files changed, 493 insertions(+), 1 deletion(-) create mode 100644 app/api/alibaba/[...path]/route.ts create mode 100644 app/client/platforms/alibaba.ts diff --git a/app/api/alibaba/[...path]/route.ts b/app/api/alibaba/[...path]/route.ts new file mode 100644 index 000000000..e30eacbdb --- /dev/null +++ b/app/api/alibaba/[...path]/route.ts @@ -0,0 +1,175 @@ +import { getServerSideConfig } from "@/app/config/server"; +import { + Alibaba, + ALIBABA_BASE_URL, + ApiPath, + ModelProvider, + ServiceProvider, +} from "@/app/constant"; +import { prettyObject } from "@/app/utils/format"; +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/app/api/auth"; +import { isModelAvailableInServer } from "@/app/utils/model"; +import type { RequestPayload } from "@/app/client/platforms/openai"; + +const serverConfig = getServerSideConfig(); + +async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + console.log("[Alibaba Route] params ", params); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + const authResult = auth(req, ModelProvider.Qwen); + if (authResult.error) { + return NextResponse.json(authResult, { + status: 401, + }); + } + + try { + const response = await request(req); + return response; + } catch (e) { + console.error("[Alibaba] ", e); + return NextResponse.json(prettyObject(e)); + } +} + +export const GET = handle; +export const POST = handle; + +export const runtime = "edge"; +export const preferredRegion = [ + "arn1", + "bom1", + "cdg1", + "cle1", + "cpt1", + "dub1", + "fra1", + "gru1", + "hnd1", + "iad1", + "icn1", + "kix1", + "lhr1", + "pdx1", + "sfo1", + "sin1", + "syd1", +]; + +async function request(req: NextRequest) { + const controller = new AbortController(); + + // alibaba use base url or just remove the path + let path = `${req.nextUrl.pathname}`.replaceAll( + ApiPath.Alibaba + "/" + Alibaba.ChatPath, + "", + ); + + let baseUrl = serverConfig.alibabaUrl || ALIBABA_BASE_URL; + + if (!baseUrl.startsWith("http")) { + baseUrl = `https://${baseUrl}`; + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, -1); + } + + console.log("[Proxy] ", path); + console.log("[Base Url]", baseUrl); + + const timeoutId = setTimeout( + () => { + controller.abort(); + }, + 10 * 60 * 1000, + ); + + const fetchUrl = `${baseUrl}${path}`; + + const clonedBody = await req.text(); + + const { messages, model, stream, top_p, ...rest } = JSON.parse( + clonedBody, + ) as RequestPayload; + + const requestBody = { + model, + input: { + messages, + }, + parameters: { + ...rest, + top_p: top_p === 1 ? 0.99 : top_p, // qwen top_p is should be < 1 + result_format: "message", + incremental_output: true, + }, + }; + + const fetchOptions: RequestInit = { + headers: { + "Content-Type": "application/json", + Authorization: req.headers.get("Authorization") ?? "", + "X-DashScope-SSE": stream ? "enable" : "disable", + }, + method: req.method, + body: JSON.stringify(requestBody), + redirect: "manual", + // @ts-ignore + duplex: "half", + signal: controller.signal, + }; + + // #1815 try to refuse some request to some models + if (serverConfig.customModels && req.body) { + try { + // not undefined and is false + if ( + isModelAvailableInServer( + serverConfig.customModels, + model as string, + ServiceProvider.Alibaba as string, + ) + ) { + return NextResponse.json( + { + error: true, + message: `you are not allowed to use ${model} model`, + }, + { + status: 403, + }, + ); + } + } catch (e) { + console.error(`[Alibaba] filter`, e); + } + } + console.log("[Alibaba request]", fetchOptions.headers, req.method); + try { + const res = await fetch(fetchUrl, fetchOptions); + + console.log("[Alibaba response]", res.status, " ", res.headers, res.url); + // to prevent browser prompt for credentials + const newHeaders = new Headers(res.headers); + newHeaders.delete("www-authenticate"); + // to disable nginx buffering + newHeaders.set("X-Accel-Buffering", "no"); + + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: newHeaders, + }); + } finally { + clearTimeout(timeoutId); + } +} diff --git a/app/api/auth.ts b/app/api/auth.ts index 2b4702aed..b9f23d4c4 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -73,6 +73,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { case ModelProvider.Claude: systemApiKey = serverConfig.anthropicApiKey; break; + case ModelProvider.Qwen: + systemApiKey = serverConfig.alibabaApiKey; + break; case ModelProvider.GPT: default: if (req.nextUrl.pathname.includes("azure/deployments")) { diff --git a/app/client/api.ts b/app/client/api.ts index 528a5598a..3677415ce 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -9,6 +9,8 @@ import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store"; import { ChatGPTApi } from "./platforms/openai"; import { GeminiProApi } from "./platforms/google"; import { ClaudeApi } from "./platforms/anthropic"; +import { QwenApi } from "./platforms/alibaba"; + export const ROLES = ["system", "user", "assistant"] as const; export type MessageRole = (typeof ROLES)[number]; @@ -104,6 +106,9 @@ export class ClientApi { case ModelProvider.Claude: this.llm = new ClaudeApi(); break; + case ModelProvider.Qwen: + this.llm = new QwenApi(); + break; default: this.llm = new ChatGPTApi(); } @@ -220,6 +225,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi { return new ClientApi(ModelProvider.GeminiPro); case ServiceProvider.Anthropic: return new ClientApi(ModelProvider.Claude); + case ServiceProvider.Alibaba: + return new ClientApi(ModelProvider.Qwen); default: return new ClientApi(ModelProvider.GPT); } diff --git a/app/client/platforms/alibaba.ts b/app/client/platforms/alibaba.ts new file mode 100644 index 000000000..eefdb017f --- /dev/null +++ b/app/client/platforms/alibaba.ts @@ -0,0 +1,260 @@ +"use client"; +import { + ApiPath, + Alibaba, + DEFAULT_API_HOST, + REQUEST_TIMEOUT_MS, +} from "@/app/constant"; +import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; + +import { + ChatOptions, + getHeaders, + LLMApi, + LLMModel, + MultimodalContent, +} from "../api"; +import Locale from "../../locales"; +import { + EventStreamContentType, + fetchEventSource, +} from "@fortaine/fetch-event-source"; +import { prettyObject } from "@/app/utils/format"; +import { getClientConfig } from "@/app/config/client"; +import { getMessageTextContent, isVisionModel } from "@/app/utils"; + +export interface OpenAIListModelResponse { + object: string; + data: Array<{ + id: string; + object: string; + root: string; + }>; +} + +interface RequestPayload { + messages: { + role: "system" | "user" | "assistant"; + content: string | MultimodalContent[]; + }[]; + stream?: boolean; + model: string; + temperature: number; + presence_penalty: number; + frequency_penalty: number; + top_p: number; + max_tokens?: number; +} + +export class QwenApi implements LLMApi { + path(path: string): string { + const accessStore = useAccessStore.getState(); + + let baseUrl = ""; + + if (accessStore.useCustomConfig) { + baseUrl = accessStore.alibabaUrl; + } + + if (baseUrl.length === 0) { + const isApp = !!getClientConfig()?.isApp; + baseUrl = isApp + ? DEFAULT_API_HOST + "/api/proxy/alibaba" + : ApiPath.Alibaba; + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, baseUrl.length - 1); + } + if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Alibaba)) { + baseUrl = "https://" + baseUrl; + } + + console.log("[Proxy Endpoint] ", baseUrl, path); + + return [baseUrl, path].join("/"); + } + + extractMessage(res: any) { + return res.choices?.at(0)?.message?.content ?? ""; + } + + async chat(options: ChatOptions) { + const visionModel = isVisionModel(options.config.model); + const messages = options.messages.map((v) => ({ + role: v.role, + content: visionModel ? v.content : getMessageTextContent(v), + })); + + const modelConfig = { + ...useAppConfig.getState().modelConfig, + ...useChatStore.getState().currentSession().mask.modelConfig, + ...{ + model: options.config.model, + }, + }; + + const requestPayload: RequestPayload = { + messages, + stream: options.config.stream, + model: modelConfig.model, + temperature: modelConfig.temperature, + presence_penalty: modelConfig.presence_penalty, + frequency_penalty: modelConfig.frequency_penalty, + top_p: modelConfig.top_p, + }; + + console.log("[Request] Alibaba payload: ", requestPayload); + + const shouldStream = !!options.config.stream; + const controller = new AbortController(); + options.onController?.(controller); + + try { + const chatPath = this.path(Alibaba.ChatPath); + const chatPayload = { + method: "POST", + body: JSON.stringify(requestPayload), + signal: controller.signal, + headers: getHeaders(), + }; + + // make a fetch request + const requestTimeoutId = setTimeout( + () => controller.abort(), + REQUEST_TIMEOUT_MS, + ); + + if (shouldStream) { + let responseText = ""; + let remainText = ""; + let finished = false; + + // animate response to make it looks smooth + function animateResponseText() { + if (finished || controller.signal.aborted) { + responseText += remainText; + console.log("[Response Animation] finished"); + if (responseText?.length === 0) { + options.onError?.(new Error("empty response from server")); + } + return; + } + + if (remainText.length > 0) { + const fetchCount = Math.max(1, Math.round(remainText.length / 60)); + const fetchText = remainText.slice(0, fetchCount); + responseText += fetchText; + remainText = remainText.slice(fetchCount); + options.onUpdate?.(responseText, fetchText); + } + + requestAnimationFrame(animateResponseText); + } + + // start animaion + animateResponseText(); + + const finish = () => { + if (!finished) { + finished = true; + options.onFinish(responseText + remainText); + } + }; + + controller.signal.onabort = finish; + + fetchEventSource(chatPath, { + ...chatPayload, + async onopen(res) { + clearTimeout(requestTimeoutId); + const contentType = res.headers.get("content-type"); + console.log( + "[Alibaba] request response content type: ", + contentType, + ); + + if (contentType?.startsWith("text/plain")) { + responseText = await res.clone().text(); + return finish(); + } + + if ( + !res.ok || + !res.headers + .get("content-type") + ?.startsWith(EventStreamContentType) || + res.status !== 200 + ) { + const responseTexts = [responseText]; + let extraInfo = await res.clone().text(); + try { + const resJson = await res.clone().json(); + extraInfo = prettyObject(resJson); + } catch {} + + if (res.status === 401) { + responseTexts.push(Locale.Error.Unauthorized); + } + + if (extraInfo) { + responseTexts.push(extraInfo); + } + + responseText = responseTexts.join("\n\n"); + + return finish(); + } + }, + onmessage(msg) { + if (msg.data === "[DONE]" || finished) { + return finish(); + } + const text = msg.data; + try { + const json = JSON.parse(text); + const choices = json.output.choices as Array<{ + message: { content: string }; + }>; + const delta = choices[0]?.message?.content; + if (delta) { + remainText += delta; + } + } catch (e) { + console.error("[Request] parse error", text, msg); + } + }, + onclose() { + finish(); + }, + onerror(e) { + options.onError?.(e); + throw e; + }, + openWhenHidden: true, + }); + } else { + const res = await fetch(chatPath, chatPayload); + clearTimeout(requestTimeoutId); + + const resJson = await res.json(); + const message = this.extractMessage(resJson); + options.onFinish(message); + } + } catch (e) { + console.log("[Request] failed to make a chat request", e); + options.onError?.(e as Error); + } + } + async usage() { + return { + used: 0, + total: 0, + }; + } + + async models(): Promise { + return []; + } +} +export { Alibaba }; diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 8615172a3..bba359429 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -42,7 +42,7 @@ export interface OpenAIListModelResponse { }>; } -interface RequestPayload { +export interface RequestPayload { messages: { role: "system" | "user" | "assistant"; content: string | MultimodalContent[]; diff --git a/app/config/server.ts b/app/config/server.ts index b7c85ce6a..62624a8e4 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -35,6 +35,10 @@ declare global { // google tag manager GTM_ID?: string; + // alibaba only + ALIBABA_URL?: string; + ALIBABA_API_KEY?: string; + // custom template for preprocessing user input DEFAULT_INPUT_TEMPLATE?: string; } @@ -92,6 +96,7 @@ export const getServerSideConfig = () => { const isAzure = !!process.env.AZURE_URL; const isGoogle = !!process.env.GOOGLE_API_KEY; const isAnthropic = !!process.env.ANTHROPIC_API_KEY; + const isAlibaba = !!process.env.ALIBABA_API_KEY; // const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? ""; // const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim()); @@ -124,6 +129,10 @@ export const getServerSideConfig = () => { anthropicApiVersion: process.env.ANTHROPIC_API_VERSION, anthropicUrl: process.env.ANTHROPIC_URL, + isAlibaba, + alibabaUrl: process.env.ALIBABA_URL, + alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY), + gtmId: process.env.GTM_ID, needCode: ACCESS_CODES.size > 0, diff --git a/app/constant.ts b/app/constant.ts index d44b5b817..01c212b24 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -14,6 +14,9 @@ export const ANTHROPIC_BASE_URL = "https://api.anthropic.com"; export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/"; +export const ALIBABA_BASE_URL = + "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"; + export enum Path { Home = "/", Chat = "/chat", @@ -28,6 +31,7 @@ export enum ApiPath { Azure = "/api/azure", OpenAI = "/api/openai", Anthropic = "/api/anthropic", + Alibaba = "/api/alibaba", } export enum SlotID { @@ -71,12 +75,14 @@ export enum ServiceProvider { Azure = "Azure", Google = "Google", Anthropic = "Anthropic", + Alibaba = "Alibaba", } export enum ModelProvider { GPT = "GPT", GeminiPro = "GeminiPro", Claude = "Claude", + Qwen = "Qwen", } export const Anthropic = { @@ -104,6 +110,10 @@ export const Google = { ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`, }; +export const Alibaba = { + ChatPath: "chat/completions", +}; + export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang // export const DEFAULT_SYSTEM_TEMPLATE = ` // You are ChatGPT, a large language model trained by {{ServiceProvider}}. @@ -173,6 +183,16 @@ const anthropicModels = [ "claude-3-5-sonnet-20240620", ]; +const alibabaModes = [ + "qwen-turbo", + "qwen-plus", + "qwen-max", + "qwen-max-0428", + "qwen-max-0403", + "qwen-max-0107", + "qwen-max-longcontext", +]; + export const DEFAULT_MODELS = [ ...openaiModels.map((name) => ({ name, @@ -210,6 +230,15 @@ export const DEFAULT_MODELS = [ providerType: "anthropic", }, })), + ...alibabaModes.map((name) => ({ + name, + available: true, + provider: { + id: "alibaba", + providerName: "Alibaba", + providerType: "alibaba", + }, + })), ] as const; export const CHAT_PAGE_SIZE = 15; diff --git a/app/store/access.ts b/app/store/access.ts index 03780779e..5ea459049 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -47,6 +47,10 @@ const DEFAULT_ACCESS_STATE = { anthropicApiVersion: "2023-06-01", anthropicUrl: "", + // alibaba + alibabaUrl: "", + alibabaApiKey: "", + // server config needCode: true, hideUserApiKey: false, @@ -83,6 +87,10 @@ export const useAccessStore = createPersistStore( return ensure(get(), ["anthropicApiKey"]); }, + isValidAlibaba() { + return ensure(get(), ["alibabaApiKey"]); + }, + isAuthorized() { this.fetch(); @@ -92,6 +100,7 @@ export const useAccessStore = createPersistStore( this.isValidAzure() || this.isValidGoogle() || this.isValidAnthropic() || + this.isValidAlibaba() || !this.enabledAccessControl() || (this.enabledAccessControl() && ensure(get(), ["accessCode"])) ); From 71af2628eb8d791070fc2b4818f6f46c9068c962 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 00:32:18 +0800 Subject: [PATCH 39/67] hotfix: old AZURE_URL config error: "DeploymentNotFound". #4945 #4930 --- app/api/common.ts | 25 +++++++++++++++++++++++++ app/utils/model.ts | 10 ++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/api/common.ts b/app/api/common.ts index b2fae6df2..5223646d2 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -66,6 +66,31 @@ export async function requestOpenai(req: NextRequest) { "/api/azure/", "", )}?api-version=${azureApiVersion}`; + + // Forward compatibility: + // if display_name(deployment_name) not set, and '{deploy-id}' in AZURE_URL + // then using default '{deploy-id}' + if (serverConfig.customModels) { + const modelName = path.split("/")[1]; + let realDeployName = ""; + serverConfig.customModels + .split(",") + .filter((v) => !!v && !v.startsWith("-") && v.includes(modelName)) + .forEach((m) => { + const [fullName, displayName] = m.split("="); + const [_, providerName] = fullName.split("@"); + if (providerName === "azure" && !displayName) { + const [_, deployId] = serverConfig.azureUrl.split("deployments/"); + if (deployId) { + realDeployName = deployId; + } + } + }); + if (realDeployName) { + console.log("[Replace with DeployId", realDeployName); + path = path.replaceAll(modelName, realDeployName); + } + } } const fetchUrl = `${baseUrl}/${path}`; diff --git a/app/utils/model.ts b/app/utils/model.ts index 249987726..0b160f101 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -47,10 +47,16 @@ export function collectModelTable( (model) => (model.available = available), ); } else { - // 1. find model by name(), and set available value + // 1. find model by name, and set available value + const [customModelName, customProviderName] = name.split("@"); let count = 0; for (const fullName in modelTable) { - if (fullName.split("@").shift() == name) { + const [modelName, providerName] = fullName.split("@"); + if ( + customModelName == modelName && + (customProviderName === undefined || + customProviderName === providerName) + ) { count += 1; modelTable[fullName]["available"] = available; if (displayName) { From 34ab37f31e1fe968c86a4ddc8421a1bfe6a20a27 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 00:47:35 +0800 Subject: [PATCH 40/67] update CUSTOM_MODELS config for Azure mode. --- README.md | 4 ++++ README_CN.md | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/README.md b/README.md index c77d2023c..2cac1088a 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ Specify OpenAI organization ID. ### `AZURE_URL` (optional) > Example: https://{azure-resource-url}/openai/deployments/{deploy-name} +> if you config deployment name in `CUSTOM_MODELS`, you can remove `{deploy-name}` in `AZURE_URL` Azure deploy url. @@ -245,6 +246,9 @@ To control custom models, use `+` to add a custom model, use `-` to hide a model User `-all` to disable all default models, `+all` to enable all default models. +For Azure: use `modelName@azure=deploymentName` to customize model name and deployment name. +> Example: `+gpt-3.5-turbo@azure=gpt35` will show option `gpt35(Azure)` in model list. + ### `DEFAULT_MODEL` (optional) Change default model diff --git a/README_CN.md b/README_CN.md index 970ecdef2..c6cbf6539 100644 --- a/README_CN.md +++ b/README_CN.md @@ -95,6 +95,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 ### `AZURE_URL` (可选) > 形如:https://{azure-resource-url}/openai/deployments/{deploy-name} +> 如果你已经在`CUSTOM_MODELS`中参考`displayName`的方式配置了{deploy-name},那么可以从`AZURE_URL`中移除`{deploy-name}` Azure 部署地址。 @@ -156,6 +157,10 @@ anthropic claude Api Url. 用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。 +在Azure的模式下,支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name) +> 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项 + + ### `DEFAULT_MODEL` (可选) 更改默认模型 From 6ac9789a1c4065c19cdd1bab7a808fbc54c0b1a2 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 12:16:37 +0800 Subject: [PATCH 41/67] hotfix --- app/store/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/config.ts b/app/store/config.ts index 4b0a34f4f..1eaafe12b 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -49,7 +49,7 @@ export const DEFAULT_CONFIG = { modelConfig: { model: "gpt-3.5-turbo" as ModelType, - providerName: "Openai" as ServiceProvider, + providerName: "OpenAI" as ServiceProvider, temperature: 0.5, top_p: 1, max_tokens: 4000, From f68cd2c5c04a33dda4187ee7db4bbfb4026b9e40 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 12:27:44 +0800 Subject: [PATCH 42/67] review code --- app/client/platforms/baidu.ts | 10 +++++----- app/constant.ts | 23 +++++++++++++++++------ app/utils/model.ts | 6 ++---- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/app/client/platforms/baidu.ts b/app/client/platforms/baidu.ts index e2f6f12dd..4fc3d2f64 100644 --- a/app/client/platforms/baidu.ts +++ b/app/client/platforms/baidu.ts @@ -2,7 +2,7 @@ import { ApiPath, Baidu, - DEFAULT_API_HOST, + BAIDU_BASE_URL, REQUEST_TIMEOUT_MS, } from "@/app/constant"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; @@ -21,7 +21,7 @@ import { } from "@fortaine/fetch-event-source"; import { prettyObject } from "@/app/utils/format"; import { getClientConfig } from "@/app/config/client"; -import { getMessageTextContent, isVisionModel } from "@/app/utils"; +import { getMessageTextContent } from "@/app/utils"; export interface OpenAIListModelResponse { object: string; @@ -58,7 +58,8 @@ export class ErnieApi implements LLMApi { if (baseUrl.length === 0) { const isApp = !!getClientConfig()?.isApp; - baseUrl = isApp ? DEFAULT_API_HOST + "/api/proxy/baidu" : ApiPath.Baidu; + // do not use proxy for baidubce api + baseUrl = isApp ? BAIDU_BASE_URL : ApiPath.Baidu; } if (baseUrl.endsWith("/")) { @@ -78,10 +79,9 @@ export class ErnieApi implements LLMApi { } async chat(options: ChatOptions) { - const visionModel = isVisionModel(options.config.model); const messages = options.messages.map((v) => ({ role: v.role, - content: visionModel ? v.content : getMessageTextContent(v), + content: getMessageTextContent(v), })); const modelConfig = { diff --git a/app/constant.ts b/app/constant.ts index 6ffc0e0b3..0fd4d1c24 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -112,9 +112,20 @@ export const Google = { }; export const Baidu = { - ExampleEndpoint: "https://aip.baidubce.com", - ChatPath: (modelName: string) => - `/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${modelName}`, + ExampleEndpoint: BAIDU_BASE_URL, + ChatPath: (modelName: string) => { + let endpoint = modelName; + if (modelName === "ernie-4.0-8k") { + endpoint = "completions_pro"; + } + if (modelName === "ernie-4.0-8k-preview-0518") { + endpoint = "completions_adv_pro"; + } + if (modelName === "ernie-3.5-8k") { + endpoint = "completions"; + } + return `/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${endpoint}`; + }, }; export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang @@ -188,11 +199,11 @@ const anthropicModels = [ const baiduModels = [ "ernie-4.0-turbo-8k", - "completions_pro=ernie-4.0-8k", + "ernie-4.0-8k", "ernie-4.0-8k-preview", - "completions_adv_pro=ernie-4.0-8k-preview-0518", + "ernie-4.0-8k-preview-0518", "ernie-4.0-8k-latest", - "completions=ernie-3.5-8k", + "ernie-3.5-8k", "ernie-3.5-8k-0205", ]; diff --git a/app/utils/model.ts b/app/utils/model.ts index 6a02ed7eb..7c778888e 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -24,13 +24,11 @@ export function collectModelTable( // default models models.forEach((m) => { - // supoort name=displayName eg:completions_pro=ernie-4.0-8k - const [name, displayName] = m.name?.split("="); // using @ as fullName - modelTable[`${name}@${m?.provider?.id}`] = { + modelTable[`${m.name}@${m?.provider?.id}`] = { ...m, name, - displayName: displayName || name, // 'provider' is copied over if it exists + displayName: m.name, // 'provider' is copied over if it exists }; }); From 011b76e4e720be49db847a12ba02a78961a0159e Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 13:39:39 +0800 Subject: [PATCH 43/67] review code --- app/utils/model.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/utils/model.ts b/app/utils/model.ts index 7c778888e..249987726 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -27,7 +27,6 @@ export function collectModelTable( // using @ as fullName modelTable[`${m.name}@${m?.provider?.id}`] = { ...m, - name, displayName: m.name, // 'provider' is copied over if it exists }; }); From fadd7f6eb4cb9d70fb9758ee52c85aac768dc1be Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 14:50:40 +0800 Subject: [PATCH 44/67] try getAccessToken in app, fixbug to fetch in none stream mode --- app/api/baidu/[...path]/route.ts | 41 +++++++++++++------------------- app/client/platforms/baidu.ts | 37 +++++++++++++++++++++------- app/constant.ts | 2 +- 3 files changed, 47 insertions(+), 33 deletions(-) diff --git a/app/api/baidu/[...path]/route.ts b/app/api/baidu/[...path]/route.ts index 27676d29d..5444ba4fe 100644 --- a/app/api/baidu/[...path]/route.ts +++ b/app/api/baidu/[...path]/route.ts @@ -10,6 +10,7 @@ import { prettyObject } from "@/app/utils/format"; import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/app/api/auth"; import { isModelAvailableInServer } from "@/app/utils/model"; +import { getAccessToken } from "@/app/utils/baidu"; const serverConfig = getServerSideConfig(); @@ -30,6 +31,18 @@ async function handle( }); } + if (!serverConfig.baiduApiKey || !serverConfig.baiduSecretKey) { + return NextResponse.json( + { + error: true, + message: `missing BAIDU_API_KEY or BAIDU_SECRET_KEY in server env vars`, + }, + { + status: 401, + }, + ); + } + try { const response = await request(req); return response; @@ -88,7 +101,10 @@ async function request(req: NextRequest) { 10 * 60 * 1000, ); - const { access_token } = await getAccessToken(); + const { access_token } = await getAccessToken( + serverConfig.baiduApiKey, + serverConfig.baiduSecretKey, + ); const fetchUrl = `${baseUrl}${path}?access_token=${access_token}`; const fetchOptions: RequestInit = { @@ -133,11 +149,9 @@ async function request(req: NextRequest) { console.error(`[Baidu] filter`, e); } } - console.log("[Baidu request]", fetchOptions.headers, req.method); try { const res = await fetch(fetchUrl, fetchOptions); - console.log("[Baidu response]", res.status, " ", res.headers, res.url); // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); newHeaders.delete("www-authenticate"); @@ -153,24 +167,3 @@ async function request(req: NextRequest) { clearTimeout(timeoutId); } } - -/** - * 使用 AK,SK 生成鉴权签名(Access Token) - * @return 鉴权签名信息 - */ -async function getAccessToken(): Promise<{ - access_token: string; - expires_in: number; - error?: number; -}> { - const AK = serverConfig.baiduApiKey; - const SK = serverConfig.baiduSecretKey; - const res = await fetch( - `${BAIDU_OATUH_URL}?grant_type=client_credentials&client_id=${AK}&client_secret=${SK}`, - { - method: "POST", - }, - ); - const resJson = await res.json(); - return resJson; -} diff --git a/app/client/platforms/baidu.ts b/app/client/platforms/baidu.ts index 4fc3d2f64..188b78bf9 100644 --- a/app/client/platforms/baidu.ts +++ b/app/client/platforms/baidu.ts @@ -6,6 +6,7 @@ import { REQUEST_TIMEOUT_MS, } from "@/app/constant"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; +import { getAccessToken } from "@/app/utils/baidu"; import { ChatOptions, @@ -74,16 +75,20 @@ export class ErnieApi implements LLMApi { return [baseUrl, path].join("/"); } - extractMessage(res: any) { - return res.choices?.at(0)?.message?.content ?? ""; - } - async chat(options: ChatOptions) { const messages = options.messages.map((v) => ({ role: v.role, content: getMessageTextContent(v), })); + // "error_code": 336006, "error_msg": "the length of messages must be an odd number", + if (messages.length % 2 === 0) { + messages.unshift({ + role: "user", + content: " ", + }); + } + const modelConfig = { ...useAppConfig.getState().modelConfig, ...useChatStore.getState().currentSession().mask.modelConfig, @@ -92,9 +97,10 @@ export class ErnieApi implements LLMApi { }, }; + const shouldStream = !!options.config.stream; const requestPayload: RequestPayload = { messages, - stream: options.config.stream, + stream: shouldStream, model: modelConfig.model, temperature: modelConfig.temperature, presence_penalty: modelConfig.presence_penalty, @@ -104,12 +110,27 @@ export class ErnieApi implements LLMApi { console.log("[Request] Baidu payload: ", requestPayload); - const shouldStream = !!options.config.stream; const controller = new AbortController(); options.onController?.(controller); try { - const chatPath = this.path(Baidu.ChatPath(modelConfig.model)); + let chatPath = this.path(Baidu.ChatPath(modelConfig.model)); + + // getAccessToken can not run in browser, because cors error + if (!!getClientConfig()?.isApp) { + const accessStore = useAccessStore.getState(); + if (accessStore.useCustomConfig) { + if (accessStore.isValidBaidu()) { + const { access_token } = await getAccessToken( + accessStore.baiduApiKey, + accessStore.baiduSecretKey, + ); + chatPath = `${chatPath}${ + chatPath.includes("?") ? "&" : "?" + }access_token=${access_token}`; + } + } + } const chatPayload = { method: "POST", body: JSON.stringify(requestPayload), @@ -230,7 +251,7 @@ export class ErnieApi implements LLMApi { clearTimeout(requestTimeoutId); const resJson = await res.json(); - const message = this.extractMessage(resJson); + const message = resJson?.result; options.onFinish(message); } } catch (e) { diff --git a/app/constant.ts b/app/constant.ts index 0fd4d1c24..3d48dbb62 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -124,7 +124,7 @@ export const Baidu = { if (modelName === "ernie-3.5-8k") { endpoint = "completions"; } - return `/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${endpoint}`; + return `rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${endpoint}`; }, }; From b14a0f24ae2b5d3dee298f6f573016b2356d7fac Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 14:57:19 +0800 Subject: [PATCH 45/67] update locales --- app/locales/cn.ts | 4 ++-- app/locales/en.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/locales/cn.ts b/app/locales/cn.ts index a872ee75a..d7268807c 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -350,12 +350,12 @@ const cn = { Baidu: { ApiKey: { Title: "接口密钥", - SubTitle: "使用自定义 Baidu API Key 绕过密码访问限制", + SubTitle: "使用自定义 Baidu API Key", Placeholder: "Baidu API Key", }, SecretKey: { Title: "接口密钥", - SubTitle: "使用自定义 Baidu Secret Key 绕过密码访问限制", + SubTitle: "使用自定义 Baidu Secret Key", Placeholder: "Baidu Secret Key", }, Endpoint: { diff --git a/app/locales/en.ts b/app/locales/en.ts index aa153f523..3c0d8851f 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -334,6 +334,22 @@ const en: LocaleType = { SubTitle: "Select and input a specific API version", }, }, + Baidu: { + ApiKey: { + Title: "Baidu API Key", + SubTitle: "Use a custom Baidu API Key", + Placeholder: "Baidu API Key", + }, + SecretKey: { + Title: "Baidu Secret Key", + SubTitle: "Use a custom Baidu Secret Key", + Placeholder: "Baidu Secret Key", + }, + Endpoint: { + Title: "Endpoint Address", + SubTitle: "Example:", + }, + }, CustomModel: { Title: "Custom Models", SubTitle: "Custom model options, seperated by comma", From 230e3823a90df67800f29be43d40e87ab42c1a76 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 15:02:44 +0800 Subject: [PATCH 46/67] update readme --- README.md | 12 ++++++++++++ README_CN.md | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/README.md b/README.md index c77d2023c..feaf197c4 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,18 @@ anthropic claude Api version. anthropic claude Api Url. +### `BAIDU_API_KEY` (optional) + +Baidu Api Key. + +### `BAIDU_SECRET_KEY` (optional) + +Baidu Secret Key. + +### `BAIDU_URL` (optional) + +Baidu Api Url. + ### `HIDE_USER_API_KEY` (optional) > Default: Empty diff --git a/README_CN.md b/README_CN.md index 970ecdef2..827d4850f 100644 --- a/README_CN.md +++ b/README_CN.md @@ -126,6 +126,18 @@ anthropic claude Api version. anthropic claude Api Url. +### `BAIDU_API_KEY` (可选) + +Baidu Api Key. + +### `BAIDU_SECRET_KEY` (可选) + +Baidu Secret Key. + +### `BAIDU_URL` (可选) + +Baidu Api Url. + ### `HIDE_USER_API_KEY` (可选) 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 From 147fc9a35a39187babb2b5aae156d47949547423 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 15:10:23 +0800 Subject: [PATCH 47/67] fix ts type error --- app/api/baidu/[...path]/route.ts | 4 ++-- app/api/common.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/api/baidu/[...path]/route.ts b/app/api/baidu/[...path]/route.ts index 5444ba4fe..94c9963c7 100644 --- a/app/api/baidu/[...path]/route.ts +++ b/app/api/baidu/[...path]/route.ts @@ -102,8 +102,8 @@ async function request(req: NextRequest) { ); const { access_token } = await getAccessToken( - serverConfig.baiduApiKey, - serverConfig.baiduSecretKey, + serverConfig.baiduApiKey as string, + serverConfig.baiduSecretKey as string, ); const fetchUrl = `${baseUrl}${path}?access_token=${access_token}`; diff --git a/app/api/common.ts b/app/api/common.ts index 5223646d2..1ffac7fce 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -70,7 +70,7 @@ export async function requestOpenai(req: NextRequest) { // Forward compatibility: // if display_name(deployment_name) not set, and '{deploy-id}' in AZURE_URL // then using default '{deploy-id}' - if (serverConfig.customModels) { + if (serverConfig.customModels && serverConfig.azureUrl) { const modelName = path.split("/")[1]; let realDeployName = ""; serverConfig.customModels @@ -80,7 +80,9 @@ export async function requestOpenai(req: NextRequest) { const [fullName, displayName] = m.split("="); const [_, providerName] = fullName.split("@"); if (providerName === "azure" && !displayName) { - const [_, deployId] = serverConfig.azureUrl.split("deployments/"); + const [_, deployId] = (serverConfig?.azureUrl ?? "").split( + "deployments/", + ); if (deployId) { realDeployName = deployId; } From f2a35f11140b4ee41828ad9024fee88ceebb24b0 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 16:38:22 +0800 Subject: [PATCH 48/67] add missing file --- app/utils/baidu.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 app/utils/baidu.ts diff --git a/app/utils/baidu.ts b/app/utils/baidu.ts new file mode 100644 index 000000000..ddeb17bd5 --- /dev/null +++ b/app/utils/baidu.ts @@ -0,0 +1,23 @@ +import { BAIDU_OATUH_URL } from "../constant"; +/** + * 使用 AK,SK 生成鉴权签名(Access Token) + * @return 鉴权签名信息 + */ +export async function getAccessToken( + clientId: string, + clientSecret: string, +): Promise<{ + access_token: string; + expires_in: number; + error?: number; +}> { + const res = await fetch( + `${BAIDU_OATUH_URL}?grant_type=client_credentials&client_id=${clientId}&client_secret=${clientSecret}`, + { + method: "POST", + mode: "cors", + }, + ); + const resJson = await res.json(); + return resJson; +} From b3023543d67589c30f1c1ffd8f68fd712bc6c1aa Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 16:55:33 +0800 Subject: [PATCH 49/67] update --- app/utils/model.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/utils/model.ts b/app/utils/model.ts index adfbe287b..a3a014877 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -61,9 +61,7 @@ export function collectModelTable( modelTable[fullName]["available"] = available; // swap name and displayName for bytedance if (providerName === "bytedance") { - const tempName = name; - name = displayName; - displayName = tempName; + [name, displayName] = [displayName, name]; modelTable[fullName]["name"] = name; } if (displayName) { From 9d7e19cebf762ac7cd58e579040bd41c4d2cc15e Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 18:05:23 +0800 Subject: [PATCH 50/67] display doubao model name when select model --- app/api/bytedance/[...path]/route.ts | 9 +-------- app/client/platforms/bytedance.ts | 12 ++++-------- app/components/chat.tsx | 26 +++++++++++++++++++++++--- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/app/api/bytedance/[...path]/route.ts b/app/api/bytedance/[...path]/route.ts index bffb60f6c..336c837f0 100644 --- a/app/api/bytedance/[...path]/route.ts +++ b/app/api/bytedance/[...path]/route.ts @@ -132,17 +132,10 @@ async function request(req: NextRequest) { console.error(`[ByteDance] filter`, e); } } - console.log("[ByteDance request]", fetchOptions.headers, req.method); + try { const res = await fetch(fetchUrl, fetchOptions); - console.log( - "[ByteDance response]", - res.status, - " ", - res.headers, - res.url, - ); // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); newHeaders.delete("www-authenticate"); diff --git a/app/client/platforms/bytedance.ts b/app/client/platforms/bytedance.ts index 92c1fd558..ce401e68d 100644 --- a/app/client/platforms/bytedance.ts +++ b/app/client/platforms/bytedance.ts @@ -2,7 +2,7 @@ import { ApiPath, ByteDance, - DEFAULT_API_HOST, + BYTEDANCE_BASE_URL, REQUEST_TIMEOUT_MS, } from "@/app/constant"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; @@ -58,9 +58,7 @@ export class DoubaoApi implements LLMApi { if (baseUrl.length === 0) { const isApp = !!getClientConfig()?.isApp; - baseUrl = isApp - ? DEFAULT_API_HOST + "/api/proxy/bytedance" - : ApiPath.ByteDance; + baseUrl = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance; } if (baseUrl.endsWith("/")) { @@ -94,9 +92,10 @@ export class DoubaoApi implements LLMApi { }, }; + const shouldStream = !!options.config.stream; const requestPayload: RequestPayload = { messages, - stream: options.config.stream, + stream: shouldStream, model: modelConfig.model, temperature: modelConfig.temperature, presence_penalty: modelConfig.presence_penalty, @@ -104,9 +103,6 @@ export class DoubaoApi implements LLMApi { top_p: modelConfig.top_p, }; - console.log("[Request] ByteDance payload: ", requestPayload); - - const shouldStream = !!options.config.stream; const controller = new AbortController(); options.onController?.(controller); diff --git a/app/components/chat.tsx b/app/components/chat.tsx index b1bdf757f..ace404c10 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -467,6 +467,14 @@ export function ChatActions(props: { return filteredModels; } }, [allModels]); + const currentModelName = useMemo(() => { + const model = models.find( + (m) => + m.name == currentModel && + m?.provider?.providerName == currentProviderName, + ); + return model?.displayName ?? ""; + }, [models, currentModel, currentProviderName]); const [showModelSelector, setShowModelSelector] = useState(false); const [showUploadImage, setShowUploadImage] = useState(false); @@ -489,7 +497,11 @@ export function ChatActions(props: { session.mask.modelConfig.providerName = nextModel?.provider ?.providerName as ServiceProvider; }); - showToast(nextModel.name); + showToast( + nextModel?.provider?.providerName == "ByteDance" + ? nextModel.displayName + : nextModel.name, + ); } }, [chatStore, currentModel, models]); @@ -571,7 +583,7 @@ export function ChatActions(props: { setShowModelSelector(true)} - text={currentModel} + text={currentModelName} icon={} /> @@ -596,7 +608,15 @@ export function ChatActions(props: { providerName as ServiceProvider; session.mask.syncGlobalConfig = false; }); - showToast(model); + if (providerName == "ByteDance") { + const selectedModel = models.find( + (m) => + m.name == model && m?.provider.providerName == providerName, + ); + showToast(selectedModel?.displayName ?? ""); + } else { + showToast(model); + } }} /> )} From 1149d455890bfb73df98026d9fad11ecbfa88e52 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 18:06:59 +0800 Subject: [PATCH 51/67] remove check vision model --- app/client/platforms/bytedance.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/client/platforms/bytedance.ts b/app/client/platforms/bytedance.ts index ce401e68d..7677cafe1 100644 --- a/app/client/platforms/bytedance.ts +++ b/app/client/platforms/bytedance.ts @@ -21,7 +21,7 @@ import { } from "@fortaine/fetch-event-source"; import { prettyObject } from "@/app/utils/format"; import { getClientConfig } from "@/app/config/client"; -import { getMessageTextContent, isVisionModel } from "@/app/utils"; +import { getMessageTextContent } from "@/app/utils"; export interface OpenAIListModelResponse { object: string; @@ -78,10 +78,9 @@ export class DoubaoApi implements LLMApi { } async chat(options: ChatOptions) { - const visionModel = isVisionModel(options.config.model); const messages = options.messages.map((v) => ({ role: v.role, - content: visionModel ? v.content : getMessageTextContent(v), + content: getMessageTextContent(v), })); const modelConfig = { From 9d2a633f5e900c67343797a92de41635cdcbe25d Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 18:15:43 +0800 Subject: [PATCH 52/67] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=85=8D=E7=BD=AE=E8=B1=86=E5=8C=85=E7=9A=84?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +++++++++++ README_CN.md | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/README.md b/README.md index 467bfbbe0..0815b723f 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,14 @@ Baidu Secret Key. Baidu Api Url. +### `BYTEDANCE_API_KEY` (optional) + +ByteDance Api Key. + +### `BYTEDANCE_URL` (optional) + +ByteDance Api Url. + ### `HIDE_USER_API_KEY` (optional) > Default: Empty @@ -261,6 +269,9 @@ User `-all` to disable all default models, `+all` to enable all default models. For Azure: use `modelName@azure=deploymentName` to customize model name and deployment name. > Example: `+gpt-3.5-turbo@azure=gpt35` will show option `gpt35(Azure)` in model list. +For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name. +> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list. + ### `DEFAULT_MODEL` (optional) Change default model diff --git a/README_CN.md b/README_CN.md index e6c4d2011..321efe441 100644 --- a/README_CN.md +++ b/README_CN.md @@ -139,6 +139,14 @@ Baidu Secret Key. Baidu Api Url. +### `BYTEDANCE_API_KEY` (可选) + +ByteDance Api Key. + +### `BYTEDANCE_URL` (可选) + +ByteDance Api Url. + ### `HIDE_USER_API_KEY` (可选) 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 @@ -172,6 +180,9 @@ Baidu Api Url. 在Azure的模式下,支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name) > 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项 +在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name) +> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项 + ### `DEFAULT_MODEL` (可选) From 82be426f78449840158adab56a88aa94dfcfc2c7 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 18:19:34 +0800 Subject: [PATCH 53/67] fix eslint error --- app/components/chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index ace404c10..40e02cb57 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -611,7 +611,7 @@ export function ChatActions(props: { if (providerName == "ByteDance") { const selectedModel = models.find( (m) => - m.name == model && m?.provider.providerName == providerName, + m.name == model && m?.provider?.providerName == providerName, ); showToast(selectedModel?.displayName ?? ""); } else { From bb349a03dac8e006c4d125779c506efa98283286 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 19:21:27 +0800 Subject: [PATCH 54/67] fix get headers for bytedance --- app/client/api.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/app/client/api.ts b/app/client/api.ts index ff81f5372..147b11ad2 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -179,6 +179,8 @@ export function getHeaders() { const isGoogle = modelConfig.providerName == ServiceProvider.Google; const isAzure = modelConfig.providerName === ServiceProvider.Azure; const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic; + const isBaidu = modelConfig.providerName == ServiceProvider.Baidu; + const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance; const isEnabledAccessControl = accessStore.enabledAccessControl(); const apiKey = isGoogle ? accessStore.googleApiKey @@ -186,8 +188,18 @@ export function getHeaders() { ? accessStore.azureApiKey : isAnthropic ? accessStore.anthropicApiKey + : isByteDance + ? accessStore.bytedanceApiKey : accessStore.openaiApiKey; - return { isGoogle, isAzure, isAnthropic, apiKey, isEnabledAccessControl }; + return { + isGoogle, + isAzure, + isAnthropic, + isBaidu, + isByteDance, + apiKey, + isEnabledAccessControl, + }; } function getAuthHeader(): string { @@ -203,10 +215,18 @@ export function getHeaders() { function validString(x: string): boolean { return x?.length > 0; } - const { isGoogle, isAzure, isAnthropic, apiKey, isEnabledAccessControl } = - getConfig(); + const { + isGoogle, + isAzure, + isAnthropic, + isBaidu, + apiKey, + isEnabledAccessControl, + } = getConfig(); // when using google api in app, not set auth header if (isGoogle && clientConfig?.isApp) return headers; + // when using baidu api in app, not set auth header + if (isBaidu && clientConfig?.isApp) return headers; const authHeader = getAuthHeader(); From 3628d68d9a7eaf6fa9e9f210f382cc88b6769bea Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 19:56:52 +0800 Subject: [PATCH 55/67] update --- app/api/alibaba/[...path]/route.ts | 7 +------ app/client/api.ts | 4 ++++ app/client/platforms/alibaba.ts | 13 ++++--------- app/constant.ts | 6 +++--- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/app/api/alibaba/[...path]/route.ts b/app/api/alibaba/[...path]/route.ts index e30eacbdb..b2c42ac78 100644 --- a/app/api/alibaba/[...path]/route.ts +++ b/app/api/alibaba/[...path]/route.ts @@ -68,10 +68,7 @@ async function request(req: NextRequest) { const controller = new AbortController(); // alibaba use base url or just remove the path - let path = `${req.nextUrl.pathname}`.replaceAll( - ApiPath.Alibaba + "/" + Alibaba.ChatPath, - "", - ); + let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Alibaba, ""); let baseUrl = serverConfig.alibabaUrl || ALIBABA_BASE_URL; @@ -153,11 +150,9 @@ async function request(req: NextRequest) { console.error(`[Alibaba] filter`, e); } } - console.log("[Alibaba request]", fetchOptions.headers, req.method); try { const res = await fetch(fetchUrl, fetchOptions); - console.log("[Alibaba response]", res.status, " ", res.headers, res.url); // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); newHeaders.delete("www-authenticate"); diff --git a/app/client/api.ts b/app/client/api.ts index 6f6ff6222..c0c71480c 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -185,6 +185,7 @@ export function getHeaders() { const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic; const isBaidu = modelConfig.providerName == ServiceProvider.Baidu; const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance; + const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba; const isEnabledAccessControl = accessStore.enabledAccessControl(); const apiKey = isGoogle ? accessStore.googleApiKey @@ -194,6 +195,8 @@ export function getHeaders() { ? accessStore.anthropicApiKey : isByteDance ? accessStore.bytedanceApiKey + : isAlibaba + ? accessStore.alibabaApiKey : accessStore.openaiApiKey; return { isGoogle, @@ -201,6 +204,7 @@ export function getHeaders() { isAnthropic, isBaidu, isByteDance, + isAlibaba, apiKey, isEnabledAccessControl, }; diff --git a/app/client/platforms/alibaba.ts b/app/client/platforms/alibaba.ts index eefdb017f..72126d728 100644 --- a/app/client/platforms/alibaba.ts +++ b/app/client/platforms/alibaba.ts @@ -2,7 +2,7 @@ import { ApiPath, Alibaba, - DEFAULT_API_HOST, + ALIBABA_BASE_URL, REQUEST_TIMEOUT_MS, } from "@/app/constant"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; @@ -58,9 +58,7 @@ export class QwenApi implements LLMApi { if (baseUrl.length === 0) { const isApp = !!getClientConfig()?.isApp; - baseUrl = isApp - ? DEFAULT_API_HOST + "/api/proxy/alibaba" - : ApiPath.Alibaba; + baseUrl = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba; } if (baseUrl.endsWith("/")) { @@ -76,14 +74,13 @@ export class QwenApi implements LLMApi { } extractMessage(res: any) { - return res.choices?.at(0)?.message?.content ?? ""; + return res?.output?.choices?.at(0)?.message?.content ?? ""; } async chat(options: ChatOptions) { - const visionModel = isVisionModel(options.config.model); const messages = options.messages.map((v) => ({ role: v.role, - content: visionModel ? v.content : getMessageTextContent(v), + content: getMessageTextContent(v), })); const modelConfig = { @@ -104,8 +101,6 @@ export class QwenApi implements LLMApi { top_p: modelConfig.top_p, }; - console.log("[Request] Alibaba payload: ", requestPayload); - const shouldStream = !!options.config.stream; const controller = new AbortController(); options.onController?.(controller); diff --git a/app/constant.ts b/app/constant.ts index 8b5bd2306..c07adad25 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -19,8 +19,7 @@ export const BAIDU_OATUH_URL = `${BAIDU_BASE_URL}/oauth/2.0/token`; export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com"; -export const ALIBABA_BASE_URL = - "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"; +export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/"; export enum Path { Home = "/", @@ -144,7 +143,8 @@ export const ByteDance = { }; export const Alibaba = { - ChatPath: "chat/completions", + ExampleEndpoint: ALIBABA_BASE_URL, + ChatPath: "v1/services/aigc/text-generation/generation", }; export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang From 7573a19dc91749ee1246e1df33950be87ef74c58 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 20:01:58 +0800 Subject: [PATCH 56/67] add custom settings --- app/components/settings.tsx | 46 +++++++++++++++++++++++++++++++++++++ app/locales/cn.ts | 11 +++++++++ app/locales/en.ts | 11 +++++++++ 3 files changed, 68 insertions(+) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 7db09940d..3d77a2631 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -54,6 +54,7 @@ import { Anthropic, Azure, Baidu, + ByteDance, Google, OPENAI_BASE_URL, Path, @@ -1249,6 +1250,51 @@ export function Settings() { )} + + {accessStore.provider === ServiceProvider.ByteDance && ( + <> + + + accessStore.update( + (access) => + (access.bytedanceUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => + (access.bytedanceApiKey = + e.currentTarget.value), + ); + }} + /> + + + )} )} diff --git a/app/locales/cn.ts b/app/locales/cn.ts index d7268807c..d60526870 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -363,6 +363,17 @@ const cn = { SubTitle: "样例:", }, }, + ByteDance: { + ApiKey: { + Title: "接口密钥", + SubTitle: "使用自定义 ByteDance API Key", + Placeholder: "ByteDance API Key", + }, + Endpoint: { + Title: "接口地址", + SubTitle: "样例:", + }, + }, CustomModel: { Title: "自定义模型名", SubTitle: "增加自定义模型可选项,使用英文逗号隔开", diff --git a/app/locales/en.ts b/app/locales/en.ts index 3c0d8851f..136a5bbac 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -350,6 +350,17 @@ const en: LocaleType = { SubTitle: "Example:", }, }, + ByteDance: { + ApiKey: { + Title: "ByteDance API Key", + SubTitle: "Use a custom ByteDance API Key", + Placeholder: "ByteDance API Key", + }, + Endpoint: { + Title: "Endpoint Address", + SubTitle: "Example:", + }, + }, CustomModel: { Title: "Custom Models", SubTitle: "Custom model options, seperated by comma", From e3b3a4fefa64efe3c0d49faa403709900729dc23 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 20:09:03 +0800 Subject: [PATCH 57/67] add custom settings --- app/components/settings.tsx | 45 +++++++++++++++++++++++++++++++++++++ app/locales/cn.ts | 11 +++++++++ app/locales/en.ts | 11 +++++++++ 3 files changed, 67 insertions(+) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 3d77a2631..ba119d1a0 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -55,6 +55,7 @@ import { Azure, Baidu, ByteDance, + Alibaba, Google, OPENAI_BASE_URL, Path, @@ -1295,6 +1296,50 @@ export function Settings() { )} + + {accessStore.provider === ServiceProvider.Alibaba && ( + <> + + + accessStore.update( + (access) => + (access.alibabaUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => + (access.alibabaApiKey = e.currentTarget.value), + ); + }} + /> + + + )} )} diff --git a/app/locales/cn.ts b/app/locales/cn.ts index d60526870..728bdbc59 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -374,6 +374,17 @@ const cn = { SubTitle: "样例:", }, }, + Alibaba: { + ApiKey: { + Title: "接口密钥", + SubTitle: "使用自定义阿里云API Key", + Placeholder: "Alibaba Cloud API Key", + }, + Endpoint: { + Title: "接口地址", + SubTitle: "样例:", + }, + }, CustomModel: { Title: "自定义模型名", SubTitle: "增加自定义模型可选项,使用英文逗号隔开", diff --git a/app/locales/en.ts b/app/locales/en.ts index 136a5bbac..f18f5a19e 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -361,6 +361,17 @@ const en: LocaleType = { SubTitle: "Example:", }, }, + Alibaba: { + ApiKey: { + Title: "Alibaba API Key", + SubTitle: "Use a custom Alibaba Cloud API Key", + Placeholder: "Alibaba Cloud API Key", + }, + Endpoint: { + Title: "Endpoint Address", + SubTitle: "Example:", + }, + }, CustomModel: { Title: "Custom Models", SubTitle: "Custom model options, seperated by comma", From 814aaa4a69daec51f05f7308714b417598d69c45 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 20:15:20 +0800 Subject: [PATCH 58/67] update config for alibaba(qwen) --- README.md | 8 ++++++++ README_CN.md | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/README.md b/README.md index 0815b723f..24967c164 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,14 @@ ByteDance Api Key. ByteDance Api Url. +### `ALIBABA_API_KEY` (optional) + +Alibaba Cloud Api Key. + +### `ALIBABA_URL` (optional) + +Alibaba Cloud Api Url. + ### `HIDE_USER_API_KEY` (optional) > Default: Empty diff --git a/README_CN.md b/README_CN.md index 321efe441..5400bb276 100644 --- a/README_CN.md +++ b/README_CN.md @@ -147,6 +147,14 @@ ByteDance Api Key. ByteDance Api Url. +### `ALIBABA_API_KEY` (可选) + +阿里云(千问)Api Key. + +### `ALIBABA_URL` (可选) + +阿里云(千问)Api Url. + ### `HIDE_USER_API_KEY` (可选) 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 From cd4784c54a213fd38f5b4d8c3093814f29b9e7fa Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 21:14:38 +0800 Subject: [PATCH 59/67] update --- app/components/settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 3d77a2631..4d19fa76e 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -1256,7 +1256,7 @@ export function Settings() { From 044c16da4ccb00c28f6de71f68adcb20bde5f3ea Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 9 Jul 2024 21:17:32 +0800 Subject: [PATCH 60/67] update --- app/components/settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index ba119d1a0..1467f706b 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -1302,7 +1302,7 @@ export function Settings() { From 6885812d213ea372d004994d58718da7dd6f0d41 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 10 Jul 2024 18:59:44 +0800 Subject: [PATCH 61/67] hotfix Gemini finish twice. #4955 #4966 --- app/client/platforms/google.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index 4aac1dbff..828b28a0d 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -154,8 +154,10 @@ export class GeminiProApi implements LLMApi { let finished = false; const finish = () => { - finished = true; - options.onFinish(responseText + remainText); + if (!finished) { + finished = true; + options.onFinish(responseText + remainText); + } }; // animate response to make it looks smooth From 32b82b9cb378bfb6a38a87f9ee04071ba936a47c Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 11 Jul 2024 00:48:58 +0800 Subject: [PATCH 62/67] change build messages for qwen in client --- app/api/alibaba/[...path]/route.ts | 35 ++++++++------------------- app/client/platforms/alibaba.ts | 39 ++++++++++++++++++++---------- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/app/api/alibaba/[...path]/route.ts b/app/api/alibaba/[...path]/route.ts index b2c42ac78..a8b2209ce 100644 --- a/app/api/alibaba/[...path]/route.ts +++ b/app/api/alibaba/[...path]/route.ts @@ -91,34 +91,14 @@ async function request(req: NextRequest) { ); const fetchUrl = `${baseUrl}${path}`; - - const clonedBody = await req.text(); - - const { messages, model, stream, top_p, ...rest } = JSON.parse( - clonedBody, - ) as RequestPayload; - - const requestBody = { - model, - input: { - messages, - }, - parameters: { - ...rest, - top_p: top_p === 1 ? 0.99 : top_p, // qwen top_p is should be < 1 - result_format: "message", - incremental_output: true, - }, - }; - const fetchOptions: RequestInit = { headers: { "Content-Type": "application/json", Authorization: req.headers.get("Authorization") ?? "", - "X-DashScope-SSE": stream ? "enable" : "disable", + "X-DashScope-SSE": req.headers.get("X-DashScope-SSE") ?? "disable", }, method: req.method, - body: JSON.stringify(requestBody), + body: req.body, redirect: "manual", // @ts-ignore duplex: "half", @@ -128,18 +108,23 @@ async function request(req: NextRequest) { // #1815 try to refuse some request to some models if (serverConfig.customModels && req.body) { try { + const clonedBody = await req.text(); + fetchOptions.body = clonedBody; + + const jsonBody = JSON.parse(clonedBody) as { model?: string }; + // not undefined and is false if ( isModelAvailableInServer( serverConfig.customModels, - model as string, - ServiceProvider.Alibaba as string, + jsonBody?.model as string, + ServiceProvider.ByteDance as string, ) ) { return NextResponse.json( { error: true, - message: `you are not allowed to use ${model} model`, + message: `you are not allowed to use ${jsonBody?.model} model`, }, { status: 403, diff --git a/app/client/platforms/alibaba.ts b/app/client/platforms/alibaba.ts index 72126d728..9c21a745a 100644 --- a/app/client/platforms/alibaba.ts +++ b/app/client/platforms/alibaba.ts @@ -32,19 +32,25 @@ export interface OpenAIListModelResponse { }>; } -interface RequestPayload { +interface RequestInput { messages: { role: "system" | "user" | "assistant"; content: string | MultimodalContent[]; }[]; - stream?: boolean; - model: string; +} +interface RequestParam { + result_format: string; + incremental_output?: boolean; temperature: number; - presence_penalty: number; - frequency_penalty: number; + repetition_penalty: number; top_p: number; max_tokens?: number; } +interface RequestPayload { + model: string; + input: RequestInput; + parameters: RequestParam; +} export class QwenApi implements LLMApi { path(path: string): string { @@ -91,17 +97,21 @@ export class QwenApi implements LLMApi { }, }; + const shouldStream = !!options.config.stream; const requestPayload: RequestPayload = { - messages, - stream: options.config.stream, model: modelConfig.model, - temperature: modelConfig.temperature, - presence_penalty: modelConfig.presence_penalty, - frequency_penalty: modelConfig.frequency_penalty, - top_p: modelConfig.top_p, + input: { + messages, + }, + parameters: { + result_format: "message", + incremental_output: shouldStream, + temperature: modelConfig.temperature, + // max_tokens: modelConfig.max_tokens, + top_p: modelConfig.top_p === 1 ? 0.99 : modelConfig.top_p, // qwen top_p is should be < 1 + }, }; - const shouldStream = !!options.config.stream; const controller = new AbortController(); options.onController?.(controller); @@ -111,7 +121,10 @@ export class QwenApi implements LLMApi { method: "POST", body: JSON.stringify(requestPayload), signal: controller.signal, - headers: getHeaders(), + headers: { + ...getHeaders(), + "X-DashScope-SSE": shouldStream ? "enable" : "disable", + }, }; // make a fetch request From 2299a4156db2f90112afc34b313d3d6941b20649 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 11 Jul 2024 00:50:58 +0800 Subject: [PATCH 63/67] change build messages for qwen in client --- app/client/platforms/alibaba.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/platforms/alibaba.ts b/app/client/platforms/alibaba.ts index 9c21a745a..723ba774b 100644 --- a/app/client/platforms/alibaba.ts +++ b/app/client/platforms/alibaba.ts @@ -42,7 +42,7 @@ interface RequestParam { result_format: string; incremental_output?: boolean; temperature: number; - repetition_penalty: number; + repetition_penalty?: number; top_p: number; max_tokens?: number; } From fec36eb298b7e729ecaa2883f93fdb9e83a62087 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 11 Jul 2024 10:22:30 +0800 Subject: [PATCH 64/67] hotfix --- app/api/alibaba/[...path]/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/alibaba/[...path]/route.ts b/app/api/alibaba/[...path]/route.ts index a8b2209ce..c97ce5934 100644 --- a/app/api/alibaba/[...path]/route.ts +++ b/app/api/alibaba/[...path]/route.ts @@ -118,7 +118,7 @@ async function request(req: NextRequest) { isModelAvailableInServer( serverConfig.customModels, jsonBody?.model as string, - ServiceProvider.ByteDance as string, + ServiceProvider.Alibaba as string, ) ) { return NextResponse.json( From 5e7254e8dc434fb57d5190e46cd7c97d81b3318e Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 11 Jul 2024 14:46:12 +0800 Subject: [PATCH 65/67] hotfix: doubao display name --- app/utils/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/model.ts b/app/utils/model.ts index a3a014877..8f6a1a6c7 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -61,7 +61,7 @@ export function collectModelTable( modelTable[fullName]["available"] = available; // swap name and displayName for bytedance if (providerName === "bytedance") { - [name, displayName] = [displayName, name]; + [name, displayName] = [displayName, modelName]; modelTable[fullName]["name"] = name; } if (displayName) { From 17cc9284a0d90c65f3a349d2e3b08d30c24fc82b Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 11 Jul 2024 15:35:36 +0800 Subject: [PATCH 66/67] add config in readme --- README.md | 8 ++++++++ README_CN.md | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/README.md b/README.md index 24967c164..ff9d003bd 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,14 @@ You can use this option if you want to increase the number of webdav service add Customize the default template used to initialize the User Input Preprocessing configuration item in Settings. +### `STABILITY_API_KEY` (optional) + +Stability API key. + +### `STABILITY_URL` (optional) + +Customize Stability API url. + ## Requirements NodeJS >= 18, Docker >= 20 diff --git a/README_CN.md b/README_CN.md index 5400bb276..42121ef15 100644 --- a/README_CN.md +++ b/README_CN.md @@ -200,6 +200,15 @@ ByteDance Api Url. 自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项 +### `STABILITY_API_KEY` (optional) + +Stability API密钥 + +### `STABILITY_URL` (optional) + +自定义的Stability API请求地址 + + ## 开发 点击下方按钮,开始二次开发: From 01ea690421489975d099429ad4bab17356109d25 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 11 Jul 2024 18:35:44 +0800 Subject: [PATCH 67/67] remove code --- app/components/sidebar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 3fc3ef81c..dbe2f2d1a 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -31,7 +31,6 @@ import { Link, useLocation, useNavigate } from "react-router-dom"; import { isIOS, useMobileScreen } from "../utils"; import dynamic from "next/dynamic"; import { Selector, showConfirm, showToast } from "./ui-lib"; -import de from "@/app/locales/de"; const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, { loading: () => null,