From 8498cadae8f394c680be6addf35a489e75d33954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E8=B6=85?= Date: Fri, 2 Aug 2024 21:05:21 +0800 Subject: [PATCH 01/11] fix: Fixed an issue where the sample of the reply content was displayed out of order --- app/components/chat.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index bb4b611ad79..5b4adca862a 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1470,6 +1470,7 @@ function _Chat() { )}
Date: Sat, 3 Aug 2024 12:40:33 +0800 Subject: [PATCH 02/11] fix: Fixed the issue that WebDAV synchronization could not check the status and failed during the first backup --- app/api/webdav/[...path]/route.ts | 17 +++++++++++------ app/utils/cloud/webdav.ts | 14 +++++++++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/api/webdav/[...path]/route.ts b/app/api/webdav/[...path]/route.ts index 1f58a884fe3..9f96cbfcf74 100644 --- a/app/api/webdav/[...path]/route.ts +++ b/app/api/webdav/[...path]/route.ts @@ -29,6 +29,7 @@ async function handle( const requestUrl = new URL(req.url); let endpoint = requestUrl.searchParams.get("endpoint"); + let proxy_method = requestUrl.searchParams.get("proxy_method") || req.method; // Validate the endpoint to prevent potential SSRF attacks if ( @@ -65,7 +66,11 @@ async function handle( const targetPath = `${endpoint}${endpointPath}`; // only allow MKCOL, GET, PUT - if (req.method !== "MKCOL" && req.method !== "GET" && req.method !== "PUT") { + if ( + proxy_method !== "MKCOL" && + proxy_method !== "GET" && + proxy_method !== "PUT" + ) { return NextResponse.json( { error: true, @@ -78,7 +83,7 @@ async function handle( } // for MKCOL request, only allow request ${folder} - if (req.method === "MKCOL" && !targetPath.endsWith(folder)) { + if (proxy_method === "MKCOL" && !targetPath.endsWith(folder)) { return NextResponse.json( { error: true, @@ -91,7 +96,7 @@ async function handle( } // for GET request, only allow request ending with fileName - if (req.method === "GET" && !targetPath.endsWith(fileName)) { + if (proxy_method === "GET" && !targetPath.endsWith(fileName)) { return NextResponse.json( { error: true, @@ -104,7 +109,7 @@ async function handle( } // for PUT request, only allow request ending with fileName - if (req.method === "PUT" && !targetPath.endsWith(fileName)) { + if (proxy_method === "PUT" && !targetPath.endsWith(fileName)) { return NextResponse.json( { error: true, @@ -118,7 +123,7 @@ async function handle( const targetUrl = targetPath; - const method = req.method; + const method = proxy_method || req.method; const shouldNotHaveBody = ["get", "head"].includes( method?.toLowerCase() ?? "", ); @@ -143,7 +148,7 @@ async function handle( "[Any Proxy]", targetUrl, { - method: req.method, + method: method, }, { status: fetchResult?.status, diff --git a/app/utils/cloud/webdav.ts b/app/utils/cloud/webdav.ts index 0ca781b7584..aa42649ca11 100644 --- a/app/utils/cloud/webdav.ts +++ b/app/utils/cloud/webdav.ts @@ -14,8 +14,8 @@ export function createWebDavClient(store: SyncStore) { return { async check() { try { - const res = await fetch(this.path(folder, proxyUrl), { - method: "MKCOL", + const res = await fetch(this.path(folder, proxyUrl, "MKCOL"), { + method: "GET", headers: this.headers(), }); const success = [201, 200, 404, 405, 301, 302, 307, 308].includes( @@ -42,6 +42,10 @@ export function createWebDavClient(store: SyncStore) { console.log("[WebDav] get key = ", key, res.status, res.statusText); + if (404 == res.status) { + return ""; + } + return await res.text(); }, @@ -62,7 +66,7 @@ export function createWebDavClient(store: SyncStore) { authorization: `Basic ${auth}`, }; }, - path(path: string, proxyUrl: string = "") { + path(path: string, proxyUrl: string = "", proxyMethod: string = "") { if (path.startsWith("/")) { path = path.slice(1); } @@ -78,9 +82,13 @@ export function createWebDavClient(store: SyncStore) { let u = new URL(proxyUrl + pathPrefix + path); // add query params u.searchParams.append("endpoint", config.endpoint); + proxyMethod && u.searchParams.append("proxy_method", proxyMethod); url = u.toString(); } catch (e) { url = pathPrefix + path + "?endpoint=" + config.endpoint; + if (proxyMethod) { + url += "&proxy_method=" + proxyMethod; + } } return url; From d0e296adf82ffd169554ef8ab97f1005dabd0bbb Mon Sep 17 00:00:00 2001 From: HyiKi Date: Mon, 5 Aug 2024 15:41:13 +0800 Subject: [PATCH 03/11] fix: baidu error_code 336006 --- app/client/platforms/baidu.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/client/platforms/baidu.ts b/app/client/platforms/baidu.ts index 188b78bf963..29c020df416 100644 --- a/app/client/platforms/baidu.ts +++ b/app/client/platforms/baidu.ts @@ -77,7 +77,8 @@ export class ErnieApi implements LLMApi { async chat(options: ChatOptions) { const messages = options.messages.map((v) => ({ - role: v.role, + // "error_code": 336006, "error_msg": "the role of message with odd index in the messages must be assistant", + role: v.role === "system" ? "assistant" : v.role, content: getMessageTextContent(v), })); From 9ab45c396919d37221521a737f41b5591c52c856 Mon Sep 17 00:00:00 2001 From: HyiKi Date: Mon, 5 Aug 2024 20:50:36 +0800 Subject: [PATCH 04/11] fix: baidu error_code 336006 change the summary role from system to user --- app/client/platforms/baidu.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/client/platforms/baidu.ts b/app/client/platforms/baidu.ts index 29c020df416..3be147f4985 100644 --- a/app/client/platforms/baidu.ts +++ b/app/client/platforms/baidu.ts @@ -77,17 +77,24 @@ export class ErnieApi implements LLMApi { async chat(options: ChatOptions) { const messages = options.messages.map((v) => ({ - // "error_code": 336006, "error_msg": "the role of message with odd index in the messages must be assistant", - role: v.role === "system" ? "assistant" : v.role, + // "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function", + role: v.role === "system" ? "user" : 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: " ", - }); + if (messages.at(0)?.role === "user") { + messages.splice(1, 0, { + role: "assistant", + content: " ", + }); + } else { + messages.unshift({ + role: "user", + content: " ", + }); + } } const modelConfig = { From d7e2ee63d87bb713231e11d3ff2dabb3b1904e0c Mon Sep 17 00:00:00 2001 From: HyiKi Date: Tue, 6 Aug 2024 10:45:25 +0800 Subject: [PATCH 05/11] fix: tencent InvalidParameter error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix "Messages 中 system 角色必须位于列表的最开始" --- app/client/platforms/tencent.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/client/platforms/tencent.ts b/app/client/platforms/tencent.ts index e9e49d3f0b0..579008a9b9d 100644 --- a/app/client/platforms/tencent.ts +++ b/app/client/platforms/tencent.ts @@ -91,8 +91,9 @@ export class HunyuanApi implements LLMApi { async chat(options: ChatOptions) { const visionModel = isVisionModel(options.config.model); - const messages = options.messages.map((v) => ({ - role: v.role, + const messages = options.messages.map((v, index) => ({ + // "Messages 中 system 角色必须位于列表的最开始" + role: index !== 0 && v.role === "system" ? "user" : v.role, content: visionModel ? v.content : getMessageTextContent(v), })); From 3da717d9fcb43134336d0105b8e794699edbf559 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Tue, 6 Aug 2024 11:20:03 +0800 Subject: [PATCH 06/11] fix: azure summary --- app/store/chat.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/store/chat.ts b/app/store/chat.ts index 7b47f3ec629..653926d1b02 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -547,7 +547,8 @@ export const useChatStore = createPersistStore( return; } - const api: ClientApi = getClientApi(modelConfig.providerName); + const providerName = modelConfig.providerName; + const api: ClientApi = getClientApi(providerName); // remove error messages if any const messages = session.messages; @@ -570,6 +571,7 @@ export const useChatStore = createPersistStore( config: { model: getSummarizeModel(session.mask.modelConfig.model), stream: false, + providerName, }, onFinish(message) { get().updateCurrentSession( From 54fdf40f5a60dbf9b4161094a559b0efe7270af8 Mon Sep 17 00:00:00 2001 From: HyiKi Date: Mon, 5 Aug 2024 15:41:13 +0800 Subject: [PATCH 07/11] fix: baidu error_code 336006 --- app/client/platforms/baidu.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/client/platforms/baidu.ts b/app/client/platforms/baidu.ts index 188b78bf963..29c020df416 100644 --- a/app/client/platforms/baidu.ts +++ b/app/client/platforms/baidu.ts @@ -77,7 +77,8 @@ export class ErnieApi implements LLMApi { async chat(options: ChatOptions) { const messages = options.messages.map((v) => ({ - role: v.role, + // "error_code": 336006, "error_msg": "the role of message with odd index in the messages must be assistant", + role: v.role === "system" ? "assistant" : v.role, content: getMessageTextContent(v), })); From b667eff6bdbafe0e131b077e03f863317cf5ef45 Mon Sep 17 00:00:00 2001 From: HyiKi Date: Mon, 5 Aug 2024 20:50:36 +0800 Subject: [PATCH 08/11] fix: baidu error_code 336006 change the summary role from system to user --- app/client/platforms/baidu.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/client/platforms/baidu.ts b/app/client/platforms/baidu.ts index 29c020df416..3be147f4985 100644 --- a/app/client/platforms/baidu.ts +++ b/app/client/platforms/baidu.ts @@ -77,17 +77,24 @@ export class ErnieApi implements LLMApi { async chat(options: ChatOptions) { const messages = options.messages.map((v) => ({ - // "error_code": 336006, "error_msg": "the role of message with odd index in the messages must be assistant", - role: v.role === "system" ? "assistant" : v.role, + // "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function", + role: v.role === "system" ? "user" : 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: " ", - }); + if (messages.at(0)?.role === "user") { + messages.splice(1, 0, { + role: "assistant", + content: " ", + }); + } else { + messages.unshift({ + role: "user", + content: " ", + }); + } } const modelConfig = { From f900283b0975de9d88270244e3131d6d0f188eeb Mon Sep 17 00:00:00 2001 From: HyiKi Date: Tue, 6 Aug 2024 10:45:25 +0800 Subject: [PATCH 09/11] fix: tencent InvalidParameter error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix "Messages 中 system 角色必须位于列表的最开始" --- app/client/platforms/tencent.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/client/platforms/tencent.ts b/app/client/platforms/tencent.ts index e9e49d3f0b0..579008a9b9d 100644 --- a/app/client/platforms/tencent.ts +++ b/app/client/platforms/tencent.ts @@ -91,8 +91,9 @@ export class HunyuanApi implements LLMApi { async chat(options: ChatOptions) { const visionModel = isVisionModel(options.config.model); - const messages = options.messages.map((v) => ({ - role: v.role, + const messages = options.messages.map((v, index) => ({ + // "Messages 中 system 角色必须位于列表的最开始" + role: index !== 0 && v.role === "system" ? "user" : v.role, content: visionModel ? v.content : getMessageTextContent(v), })); From b2c1644d69929ce4073d458b0eb4cf7d416e22ed Mon Sep 17 00:00:00 2001 From: webws <1067533975@qq.com> Date: Sat, 3 Aug 2024 17:20:26 +0800 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20add=20support=20for=20iFLYTEK=20S?= =?UTF-8?q?park=20API=20(=E6=8E=A5=E5=85=A5=E8=AE=AF=E9=A3=9E=E6=98=9F?= =?UTF-8?q?=E7=81=AB=E6=A8=A1=E5=9E=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 ++ README_CN.md | 14 ++ app/api/[provider]/[...path]/route.ts | 4 +- app/api/auth.ts | 4 + app/api/iflytek.ts | 131 ++++++++++++++ app/client/api.ts | 12 ++ app/client/platforms/iflytek.ts | 240 ++++++++++++++++++++++++++ app/components/settings.tsx | 56 ++++++ app/config/server.ts | 11 ++ app/constant.ts | 28 +++ app/locales/cn.ts | 16 ++ app/locales/en.ts | 16 ++ app/store/access.ts | 13 ++ 13 files changed, 556 insertions(+), 1 deletion(-) create mode 100644 app/api/iflytek.ts create mode 100644 app/client/platforms/iflytek.ts diff --git a/README.md b/README.md index 56e7f9435dd..c9f195771cf 100644 --- a/README.md +++ b/README.md @@ -280,6 +280,18 @@ Alibaba Cloud Api Key. Alibaba Cloud Api Url. +### `IFLYTEK_URL` (Optional) + +iflytek Api Url. + +### `IFLYTEK_API_KEY` (Optional) + +iflytek Api Key. + +### `IFLYTEK_API_SECRET` (Optional) + +iflytek Api Secret. + ### `HIDE_USER_API_KEY` (optional) > Default: Empty diff --git a/README_CN.md b/README_CN.md index 8c464dc098c..beed396c5aa 100644 --- a/README_CN.md +++ b/README_CN.md @@ -172,6 +172,20 @@ ByteDance Api Url. 阿里云(千问)Api Url. +### `IFLYTEK_URL` (可选) + +讯飞星火Api Url. + +### `IFLYTEK_API_KEY` (可选) + +讯飞星火Api Key. + +### `IFLYTEK_API_SECRET` (可选) + +讯飞星火Api Secret. + + + ### `HIDE_USER_API_KEY` (可选) 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 diff --git a/app/api/[provider]/[...path]/route.ts b/app/api/[provider]/[...path]/route.ts index 6d028ac364d..06e3e51603c 100644 --- a/app/api/[provider]/[...path]/route.ts +++ b/app/api/[provider]/[...path]/route.ts @@ -9,7 +9,7 @@ import { handle as bytedanceHandler } from "../../bytedance"; import { handle as alibabaHandler } from "../../alibaba"; import { handle as moonshotHandler } from "../../moonshot"; import { handle as stabilityHandler } from "../../stability"; - +import { handle as iflytekHandler } from "../../iflytek"; async function handle( req: NextRequest, { params }: { params: { provider: string; path: string[] } }, @@ -34,6 +34,8 @@ async function handle( return moonshotHandler(req, { params }); case ApiPath.Stability: return stabilityHandler(req, { params }); + case ApiPath.Iflytek: + return iflytekHandler(req, { params }); default: return openaiHandler(req, { params }); } diff --git a/app/api/auth.ts b/app/api/auth.ts index ff52dcd6ee3..95965ceec2d 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -88,6 +88,10 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { case ModelProvider.Moonshot: systemApiKey = serverConfig.moonshotApiKey; break; + case ModelProvider.Iflytek: + systemApiKey = + serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret; + break; case ModelProvider.GPT: default: if (req.nextUrl.pathname.includes("azure/deployments")) { diff --git a/app/api/iflytek.ts b/app/api/iflytek.ts new file mode 100644 index 00000000000..eabdd9f4ce6 --- /dev/null +++ b/app/api/iflytek.ts @@ -0,0 +1,131 @@ +import { getServerSideConfig } from "@/app/config/server"; +import { + Iflytek, + IFLYTEK_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"; +// iflytek + +const serverConfig = getServerSideConfig(); + +export async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + console.log("[Iflytek Route] params ", params); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + const authResult = auth(req, ModelProvider.Iflytek); + if (authResult.error) { + return NextResponse.json(authResult, { + status: 401, + }); + } + + try { + const response = await request(req); + return response; + } catch (e) { + console.error("[Iflytek] ", e); + return NextResponse.json(prettyObject(e)); + } +} + +async function request(req: NextRequest) { + const controller = new AbortController(); + + // iflytek use base url or just remove the path + let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Iflytek, ""); + + let baseUrl = serverConfig.iflytekUrl || IFLYTEK_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, + }; + + // 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.Iflytek as string, + ) + ) { + return NextResponse.json( + { + error: true, + message: `you are not allowed to use ${jsonBody?.model} model`, + }, + { + status: 403, + }, + ); + } + } catch (e) { + console.error(`[Iflytek] filter`, e); + } + } + try { + const res = await fetch(fetchUrl, fetchOptions); + + // 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 dba97fff09f..98202c4db76 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -14,6 +14,7 @@ import { DoubaoApi } from "./platforms/bytedance"; import { QwenApi } from "./platforms/alibaba"; import { HunyuanApi } from "./platforms/tencent"; import { MoonshotApi } from "./platforms/moonshot"; +import { SparkApi } from "./platforms/iflytek"; export const ROLES = ["system", "user", "assistant"] as const; export type MessageRole = (typeof ROLES)[number]; @@ -128,6 +129,9 @@ export class ClientApi { case ModelProvider.Moonshot: this.llm = new MoonshotApi(); break; + case ModelProvider.Iflytek: + this.llm = new SparkApi(); + break; default: this.llm = new ChatGPTApi(); } @@ -211,6 +215,7 @@ export function getHeaders() { const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance; const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba; const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot; + const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek; const isEnabledAccessControl = accessStore.enabledAccessControl(); const apiKey = isGoogle ? accessStore.googleApiKey @@ -224,6 +229,10 @@ export function getHeaders() { ? accessStore.alibabaApiKey : isMoonshot ? accessStore.moonshotApiKey + : isIflytek + ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret + ? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret + : "" : accessStore.openaiApiKey; return { isGoogle, @@ -233,6 +242,7 @@ export function getHeaders() { isByteDance, isAlibaba, isMoonshot, + isIflytek, apiKey, isEnabledAccessControl, }; @@ -286,6 +296,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi { return new ClientApi(ModelProvider.Hunyuan); case ServiceProvider.Moonshot: return new ClientApi(ModelProvider.Moonshot); + case ServiceProvider.Iflytek: + return new ClientApi(ModelProvider.Iflytek); default: return new ClientApi(ModelProvider.GPT); } diff --git a/app/client/platforms/iflytek.ts b/app/client/platforms/iflytek.ts new file mode 100644 index 00000000000..73cea5ba0e7 --- /dev/null +++ b/app/client/platforms/iflytek.ts @@ -0,0 +1,240 @@ +"use client"; +import { + ApiPath, + DEFAULT_API_HOST, + Iflytek, + REQUEST_TIMEOUT_MS, +} from "@/app/constant"; +import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; + +import { ChatOptions, getHeaders, LLMApi, LLMModel } 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 } from "@/app/utils"; + +import { OpenAIListModelResponse, RequestPayload } from "./openai"; + +export class SparkApi implements LLMApi { + private disableListModels = true; + + path(path: string): string { + const accessStore = useAccessStore.getState(); + + let baseUrl = ""; + + if (accessStore.useCustomConfig) { + baseUrl = accessStore.iflytekUrl; + } + + if (baseUrl.length === 0) { + const isApp = !!getClientConfig()?.isApp; + const apiPath = ApiPath.Iflytek; + baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath; + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, baseUrl.length - 1); + } + if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Iflytek)) { + 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 messages: ChatOptions["messages"] = []; + for (const v of options.messages) { + const content = getMessageTextContent(v); + messages.push({ role: v.role, content }); + } + + const modelConfig = { + ...useAppConfig.getState().modelConfig, + ...useChatStore.getState().currentSession().mask.modelConfig, + ...{ + model: options.config.model, + providerName: options.config.providerName, + }, + }; + + 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, + // max_tokens: Math.max(modelConfig.max_tokens, 1024), + // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. + }; + + console.log("[Request] Spark payload: ", requestPayload); + + const shouldStream = !!options.config.stream; + const controller = new AbortController(); + options.onController?.(controller); + + try { + const chatPath = this.path(Iflytek.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 text to make it look smooth + function animateResponseText() { + if (finished || controller.signal.aborted) { + responseText += remainText; + console.log("[Response Animation] finished"); + 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 animation + 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("[Spark] request response content type: ", contentType); + + if (contentType?.startsWith("text/plain")) { + responseText = await res.clone().text(); + return finish(); + } + + // Handle different error scenarios + if ( + !res.ok || + !res.headers + .get("content-type") + ?.startsWith(EventStreamContentType) || + res.status !== 200 + ) { + let extraInfo = await res.clone().text(); + try { + const resJson = await res.clone().json(); + extraInfo = prettyObject(resJson); + } catch {} + + if (res.status === 401) { + extraInfo = Locale.Error.Unauthorized; + } + + options.onError?.( + new Error( + `Request failed with status ${res.status}: ${extraInfo}`, + ), + ); + 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); + options.onError?.(new Error(`Failed to parse response: ${text}`)); + } + }, + onclose() { + finish(); + }, + onerror(e) { + options.onError?.(e); + throw e; + }, + openWhenHidden: true, + }); + } else { + const res = await fetch(chatPath, chatPayload); + clearTimeout(requestTimeoutId); + + if (!res.ok) { + const errorText = await res.text(); + options.onError?.( + new Error(`Request failed with status ${res.status}: ${errorText}`), + ); + return; + } + + 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 []; + } +} diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 93976ac4551..71fd2d8398e 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -68,6 +68,7 @@ import { SlotID, UPDATE_URL, Stability, + Iflytek, } from "../constant"; import { Prompt, SearchService, usePromptStore } from "../store/prompt"; import { ErrorBoundary } from "./error"; @@ -1172,6 +1173,60 @@ export function Settings() { ); + const lflytekConfigComponent = accessStore.provider === + ServiceProvider.Iflytek && ( + <> + + + accessStore.update( + (access) => (access.iflytekUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => (access.iflytekApiKey = e.currentTarget.value), + ); + }} + /> + + + + { + accessStore.update( + (access) => (access.iflytekApiSecret = e.currentTarget.value), + ); + }} + /> + + + ); return ( @@ -1475,6 +1530,7 @@ export function Settings() { {tencentConfigComponent} {moonshotConfigComponent} {stabilityConfigComponent} + {lflytekConfigComponent} )} diff --git a/app/config/server.ts b/app/config/server.ts index 70c20ce644f..1fba454e8d7 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -66,6 +66,11 @@ declare global { MOONSHOT_URL?: string; MOONSHOT_API_KEY?: string; + // iflytek only + IFLYTEK_URL?: string; + IFLYTEK_API_KEY?: string; + IFLYTEK_API_SECRET?: string; + // custom template for preprocessing user input DEFAULT_INPUT_TEMPLATE?: string; } @@ -131,6 +136,7 @@ export const getServerSideConfig = () => { const isBytedance = !!process.env.BYTEDANCE_API_KEY; const isAlibaba = !!process.env.ALIBABA_API_KEY; const isMoonshot = !!process.env.MOONSHOT_API_KEY; + const isIflytek = !!process.env.IFLYTEK_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); @@ -188,6 +194,11 @@ export const getServerSideConfig = () => { moonshotUrl: process.env.MOONSHOT_URL, moonshotApiKey: getApiKey(process.env.MOONSHOT_API_KEY), + isIflytek, + iflytekUrl: process.env.IFLYTEK_URL, + iflytekApiKey: process.env.IFLYTEK_API_KEY, + iflytekApiSecret: process.env.IFLYTEK_API_SECRET, + cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID, cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID, cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY), diff --git a/app/constant.ts b/app/constant.ts index 212351d5691..aa207718c18 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -26,6 +26,7 @@ export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/"; export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com"; export const MOONSHOT_BASE_URL = "https://api.moonshot.cn"; +export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com"; export const CACHE_URL_PREFIX = "/api/cache"; export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`; @@ -53,6 +54,7 @@ export enum ApiPath { Alibaba = "/api/alibaba", Tencent = "/api/tencent", Moonshot = "/api/moonshot", + Iflytek = "/api/iflytek", Stability = "/api/stability", Artifacts = "/api/artifacts", } @@ -109,6 +111,7 @@ export enum ServiceProvider { Tencent = "Tencent", Moonshot = "Moonshot", Stability = "Stability", + Iflytek = "Iflytek", } // Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings @@ -130,6 +133,7 @@ export enum ModelProvider { Qwen = "Qwen", Hunyuan = "Hunyuan", Moonshot = "Moonshot", + Iflytek = "Iflytek", } export const Stability = { @@ -206,6 +210,11 @@ export const Moonshot = { ChatPath: "v1/chat/completions", }; +export const Iflytek = { + ExampleEndpoint: IFLYTEK_BASE_URL, + ChatPath: "v1/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}}. @@ -325,6 +334,14 @@ const tencentModels = [ const moonshotModes = ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"]; +const iflytekModels = [ + "general", + "generalv3", + "pro-128k", + "generalv3.5", + "4.0Ultra", +]; + let seq = 1000; // 内置的模型序号生成器从1000开始 export const DEFAULT_MODELS = [ ...openaiModels.map((name) => ({ @@ -426,6 +443,17 @@ export const DEFAULT_MODELS = [ sorted: 9, }, })), + ...iflytekModels.map((name) => ({ + name, + available: true, + sorted: seq++, + provider: { + id: "iflytek", + providerName: "Iflytek", + providerType: "iflytek", + sorted: 10, + }, + })), ] as const; export const CHAT_PAGE_SIZE = 15; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 69ada8784a0..4f47403abe2 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -436,6 +436,22 @@ const cn = { SubTitle: "样例:", }, }, + Iflytek: { + ApiKey: { + Title: "ApiKey", + SubTitle: "从讯飞星火控制台获取的 APIKey", + Placeholder: "APIKey", + }, + ApiSecret: { + Title: "ApiSecret", + SubTitle: "从讯飞星火控制台获取的 APISecret", + Placeholder: "APISecret", + }, + Endpoint: { + Title: "接口地址", + SubTitle: "样例:", + }, + }, CustomModel: { Title: "自定义模型名", SubTitle: "增加自定义模型可选项,使用英文逗号隔开", diff --git a/app/locales/en.ts b/app/locales/en.ts index 9a7410cbe0b..ac788032979 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -420,6 +420,22 @@ const en: LocaleType = { SubTitle: "Example: ", }, }, + Iflytek: { + ApiKey: { + Title: "Iflytek API Key", + SubTitle: "Use a Iflytek API Key", + Placeholder: "Iflytek API Key", + }, + ApiSecret: { + Title: "Iflytek API Secret", + SubTitle: "Use a Iflytek API Secret", + Placeholder: "Iflytek API Secret", + }, + Endpoint: { + Title: "Endpoint Address", + SubTitle: "Example: ", + }, + }, CustomModel: { Title: "Custom Models", SubTitle: "Custom model options, seperated by comma", diff --git a/app/store/access.ts b/app/store/access.ts index be06fb67d02..b89b080d80e 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -51,6 +51,10 @@ const DEFAULT_STABILITY_URL = isApp ? DEFAULT_API_HOST + "/api/proxy/stability" : ApiPath.Stability; +const DEFAULT_IFLYTEK_URL = isApp + ? DEFAULT_API_HOST + "/api/proxy/iflytek" + : ApiPath.Iflytek; + const DEFAULT_ACCESS_STATE = { accessCode: "", useCustomConfig: false, @@ -103,6 +107,11 @@ const DEFAULT_ACCESS_STATE = { tencentSecretKey: "", tencentSecretId: "", + // iflytek + iflytekUrl: DEFAULT_IFLYTEK_URL, + iflytekApiKey: "", + iflytekApiSecret: "", + // server config needCode: true, hideUserApiKey: false, @@ -158,6 +167,9 @@ export const useAccessStore = createPersistStore( isValidMoonshot() { return ensure(get(), ["moonshotApiKey"]); }, + isValidIflytek() { + return ensure(get(), ["iflytekApiKey"]); + }, isAuthorized() { this.fetch(); @@ -173,6 +185,7 @@ export const useAccessStore = createPersistStore( this.isValidAlibaba() || this.isValidTencent || this.isValidMoonshot() || + this.isValidIflytek() || !this.enabledAccessControl() || (this.enabledAccessControl() && ensure(get(), ["accessCode"])) ); From 624e4dbaaf6d4b6738541e779737ae946a96bc2a Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 6 Aug 2024 22:41:35 +0800 Subject: [PATCH 11/11] update version --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index feef57d1661..245254effe5 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "NextChat", - "version": "2.14.0" + "version": "2.14.1" }, "tauri": { "allowlist": {