diff --git a/app/api/[provider]/[...path]/route.ts b/app/api/[provider]/[...path]/route.ts index 3017fd37180..3b5833d7e99 100644 --- a/app/api/[provider]/[...path]/route.ts +++ b/app/api/[provider]/[...path]/route.ts @@ -10,6 +10,7 @@ import { handle as alibabaHandler } from "../../alibaba"; import { handle as moonshotHandler } from "../../moonshot"; import { handle as stabilityHandler } from "../../stability"; import { handle as iflytekHandler } from "../../iflytek"; +import { handle as deepseekHandler } from "../../deepseek"; import { handle as xaiHandler } from "../../xai"; import { handle as chatglmHandler } from "../../glm"; import { handle as proxyHandler } from "../../proxy"; @@ -40,6 +41,8 @@ async function handle( return stabilityHandler(req, { params }); case ApiPath.Iflytek: return iflytekHandler(req, { params }); + case ApiPath.DeepSeek: + return deepseekHandler(req, { params }); case ApiPath.XAI: return xaiHandler(req, { params }); case ApiPath.ChatGLM: diff --git a/app/api/auth.ts b/app/api/auth.ts index 6703b64bd15..1760c249cc4 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -92,6 +92,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { systemApiKey = serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret; break; + case ModelProvider.DeepSeek: + systemApiKey = serverConfig.deepseekApiKey; + break; case ModelProvider.XAI: systemApiKey = serverConfig.xaiApiKey; break; diff --git a/app/api/common.ts b/app/api/common.ts index 8b75d4aedf6..b7e41fa2647 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -124,7 +124,7 @@ export async function requestOpenai(req: NextRequest) { [ ServiceProvider.OpenAI, ServiceProvider.Azure, - jsonBody?.model as string, // support provider-unspecified model + jsonBody?.model as string, // support provider-unspecified model ], ) ) { diff --git a/app/api/deepseek.ts b/app/api/deepseek.ts new file mode 100644 index 00000000000..06d97a0d606 --- /dev/null +++ b/app/api/deepseek.ts @@ -0,0 +1,128 @@ +import { getServerSideConfig } from "@/app/config/server"; +import { + DEEPSEEK_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 { isModelNotavailableInServer } from "@/app/utils/model"; + +const serverConfig = getServerSideConfig(); + +export async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + console.log("[DeepSeek Route] params ", params); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + const authResult = auth(req, ModelProvider.DeepSeek); + if (authResult.error) { + return NextResponse.json(authResult, { + status: 401, + }); + } + + try { + const response = await request(req); + return response; + } catch (e) { + console.error("[DeepSeek] ", e); + return NextResponse.json(prettyObject(e)); + } +} + +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.DeepSeek, ""); + + let baseUrl = serverConfig.deepseekUrl || DEEPSEEK_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 ( + isModelNotavailableInServer( + serverConfig.customModels, + jsonBody?.model as string, + ServiceProvider.Moonshot as string, + ) + ) { + return NextResponse.json( + { + error: true, + message: `you are not allowed to use ${jsonBody?.model} model`, + }, + { + status: 403, + }, + ); + } + } catch (e) { + console.error(`[DeepSeek] 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 1da81e96448..8f263763ba6 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -20,6 +20,7 @@ import { QwenApi } from "./platforms/alibaba"; import { HunyuanApi } from "./platforms/tencent"; import { MoonshotApi } from "./platforms/moonshot"; import { SparkApi } from "./platforms/iflytek"; +import { DeepSeekApi } from "./platforms/deepseek"; import { XAIApi } from "./platforms/xai"; import { ChatGLMApi } from "./platforms/glm"; @@ -154,6 +155,9 @@ export class ClientApi { case ModelProvider.Iflytek: this.llm = new SparkApi(); break; + case ModelProvider.DeepSeek: + this.llm = new DeepSeekApi(); + break; case ModelProvider.XAI: this.llm = new XAIApi(); break; @@ -247,6 +251,7 @@ export function getHeaders(ignoreHeaders: boolean = false) { const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba; const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot; const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek; + const isDeepSeek = modelConfig.providerName === ServiceProvider.DeepSeek; const isXAI = modelConfig.providerName === ServiceProvider.XAI; const isChatGLM = modelConfig.providerName === ServiceProvider.ChatGLM; const isEnabledAccessControl = accessStore.enabledAccessControl(); @@ -264,6 +269,8 @@ export function getHeaders(ignoreHeaders: boolean = false) { ? accessStore.moonshotApiKey : isXAI ? accessStore.xaiApiKey + : isDeepSeek + ? accessStore.deepseekApiKey : isChatGLM ? accessStore.chatglmApiKey : isIflytek @@ -280,6 +287,7 @@ export function getHeaders(ignoreHeaders: boolean = false) { isAlibaba, isMoonshot, isIflytek, + isDeepSeek, isXAI, isChatGLM, apiKey, @@ -302,6 +310,13 @@ export function getHeaders(ignoreHeaders: boolean = false) { isAzure, isAnthropic, isBaidu, + isByteDance, + isAlibaba, + isMoonshot, + isIflytek, + isDeepSeek, + isXAI, + isChatGLM, apiKey, isEnabledAccessControl, } = getConfig(); @@ -344,6 +359,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi { return new ClientApi(ModelProvider.Moonshot); case ServiceProvider.Iflytek: return new ClientApi(ModelProvider.Iflytek); + case ServiceProvider.DeepSeek: + return new ClientApi(ModelProvider.DeepSeek); case ServiceProvider.XAI: return new ClientApi(ModelProvider.XAI); case ServiceProvider.ChatGLM: diff --git a/app/client/platforms/deepseek.ts b/app/client/platforms/deepseek.ts new file mode 100644 index 00000000000..28f15a43579 --- /dev/null +++ b/app/client/platforms/deepseek.ts @@ -0,0 +1,200 @@ +"use client"; +// azure and openai, using same models. so using same LLMApi. +import { + ApiPath, + DEEPSEEK_BASE_URL, + DeepSeek, + REQUEST_TIMEOUT_MS, +} from "@/app/constant"; +import { + useAccessStore, + useAppConfig, + useChatStore, + ChatMessageTool, + usePluginStore, +} from "@/app/store"; +import { stream } from "@/app/utils/chat"; +import { + ChatOptions, + getHeaders, + LLMApi, + LLMModel, + SpeechOptions, +} from "../api"; +import { getClientConfig } from "@/app/config/client"; +import { getMessageTextContent } from "@/app/utils"; +import { RequestPayload } from "./openai"; +import { fetch } from "@/app/utils/stream"; + +export class DeepSeekApi implements LLMApi { + private disableListModels = true; + + path(path: string): string { + const accessStore = useAccessStore.getState(); + + let baseUrl = ""; + + if (accessStore.useCustomConfig) { + baseUrl = accessStore.moonshotUrl; + } + + if (baseUrl.length === 0) { + const isApp = !!getClientConfig()?.isApp; + const apiPath = ApiPath.DeepSeek; + baseUrl = isApp ? DEEPSEEK_BASE_URL : apiPath; + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, baseUrl.length - 1); + } + if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.DeepSeek)) { + baseUrl = "https://" + baseUrl; + } + + console.log("[Proxy Endpoint] ", baseUrl, path); + + return [baseUrl, path].join("/"); + } + + extractMessage(res: any) { + return res.choices?.at(0)?.message?.content ?? ""; + } + + speech(options: SpeechOptions): Promise { + throw new Error("Method not implemented."); + } + + 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] openai payload: ", requestPayload); + + const shouldStream = !!options.config.stream; + const controller = new AbortController(); + options.onController?.(controller); + + try { + const chatPath = this.path(DeepSeek.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) { + const [tools, funcs] = usePluginStore + .getState() + .getAsTools( + useChatStore.getState().currentSession().mask?.plugin || [], + ); + return stream( + chatPath, + requestPayload, + getHeaders(), + tools as any, + funcs, + controller, + // parseSSE + (text: string, runTools: ChatMessageTool[]) => { + // console.log("parseSSE", text, runTools); + const json = JSON.parse(text); + const choices = json.choices as Array<{ + delta: { + content: string; + tool_calls: ChatMessageTool[]; + }; + }>; + const tool_calls = choices[0]?.delta?.tool_calls; + if (tool_calls?.length > 0) { + const index = tool_calls[0]?.index; + const id = tool_calls[0]?.id; + const args = tool_calls[0]?.function?.arguments; + if (id) { + runTools.push({ + id, + type: tool_calls[0]?.type, + function: { + name: tool_calls[0]?.function?.name as string, + arguments: args, + }, + }); + } else { + // @ts-ignore + runTools[index]["function"]["arguments"] += args; + } + } + return choices[0]?.delta?.content; + }, + // processToolMessage, include tool_calls message and tool call results + ( + requestPayload: RequestPayload, + toolCallMessage: any, + toolCallResult: any[], + ) => { + // @ts-ignore + requestPayload?.messages?.splice( + // @ts-ignore + requestPayload?.messages?.length, + 0, + toolCallMessage, + ...toolCallResult, + ); + }, + options, + ); + } else { + const res = await fetch(chatPath, chatPayload); + clearTimeout(requestTimeoutId); + + const resJson = await res.json(); + const message = this.extractMessage(resJson); + options.onFinish(message, res); + } + } 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/config/server.ts b/app/config/server.ts index bd88082169a..d5ffaab5467 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -72,6 +72,9 @@ declare global { IFLYTEK_API_KEY?: string; IFLYTEK_API_SECRET?: string; + DEEPSEEK_URL?: string; + DEEPSEEK_API_KEY?: string; + // xai only XAI_URL?: string; XAI_API_KEY?: string; @@ -148,6 +151,7 @@ export const getServerSideConfig = () => { const isAlibaba = !!process.env.ALIBABA_API_KEY; const isMoonshot = !!process.env.MOONSHOT_API_KEY; const isIflytek = !!process.env.IFLYTEK_API_KEY; + const isDeepSeek = !!process.env.DEEPSEEK_API_KEY; const isXAI = !!process.env.XAI_API_KEY; const isChatGLM = !!process.env.CHATGLM_API_KEY; // const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? ""; @@ -212,6 +216,10 @@ export const getServerSideConfig = () => { iflytekApiKey: process.env.IFLYTEK_API_KEY, iflytekApiSecret: process.env.IFLYTEK_API_SECRET, + isDeepSeek, + deepseekUrl: process.env.DEEPSEEK_URL, + deepseekApiKey: getApiKey(process.env.DEEPSEEK_API_KEY), + isXAI, xaiUrl: process.env.XAI_URL, xaiApiKey: getApiKey(process.env.XAI_API_KEY), diff --git a/app/constant.ts b/app/constant.ts index dcb68ce43bd..8163f51b46b 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -28,6 +28,8 @@ 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 DEEPSEEK_BASE_URL = "https://api.deepseek.com"; + export const XAI_BASE_URL = "https://api.x.ai"; export const CHATGLM_BASE_URL = "https://open.bigmodel.cn"; @@ -65,6 +67,7 @@ export enum ApiPath { Artifacts = "/api/artifacts", XAI = "/api/xai", ChatGLM = "/api/chatglm", + DeepSeek = "/api/deepseek", } export enum SlotID { @@ -119,6 +122,7 @@ export enum ServiceProvider { Iflytek = "Iflytek", XAI = "XAI", ChatGLM = "ChatGLM", + DeepSeek = "DeepSeek", } // Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings @@ -143,6 +147,7 @@ export enum ModelProvider { Iflytek = "Iflytek", XAI = "XAI", ChatGLM = "ChatGLM", + DeepSeek = "DeepSeek", } export const Stability = { @@ -225,6 +230,11 @@ export const Iflytek = { ChatPath: "v1/chat/completions", }; +export const DeepSeek = { + ExampleEndpoint: DEEPSEEK_BASE_URL, + ChatPath: "chat/completions", +}; + export const XAI = { ExampleEndpoint: XAI_BASE_URL, ChatPath: "v1/chat/completions", @@ -277,6 +287,8 @@ export const KnowledgeCutOffDate: Record = { // it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously. "gemini-pro": "2023-12", "gemini-pro-vision": "2023-12", + "deepseek-chat": "2024-07", + "deepseek-coder": "2024-07", }; export const DEFAULT_TTS_ENGINE = "OpenAI-TTS"; @@ -423,6 +435,8 @@ const iflytekModels = [ "4.0Ultra", ]; +const deepseekModels = ["deepseek-chat", "deepseek-coder"]; + const xAIModes = ["grok-beta"]; const chatglmModels = [ @@ -579,6 +593,17 @@ export const DEFAULT_MODELS = [ sorted: 12, }, })), + ...deepseekModels.map((name) => ({ + name, + available: true, + sorted: seq++, + provider: { + id: "deepseek", + providerName: "DeepSeek", + providerType: "deepseek", + sorted: 13, + }, + })), ] as const; export const CHAT_PAGE_SIZE = 15; diff --git a/app/store/access.ts b/app/store/access.ts index 4796b2fe84e..3c7f84adac0 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -13,6 +13,7 @@ import { MOONSHOT_BASE_URL, STABILITY_BASE_URL, IFLYTEK_BASE_URL, + DEEPSEEK_BASE_URL, XAI_BASE_URL, CHATGLM_BASE_URL, } from "../constant"; @@ -47,6 +48,8 @@ const DEFAULT_STABILITY_URL = isApp ? STABILITY_BASE_URL : ApiPath.Stability; const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek; +const DEFAULT_DEEPSEEK_URL = isApp ? DEEPSEEK_BASE_URL : ApiPath.DeepSeek; + const DEFAULT_XAI_URL = isApp ? XAI_BASE_URL : ApiPath.XAI; const DEFAULT_CHATGLM_URL = isApp ? CHATGLM_BASE_URL : ApiPath.ChatGLM; @@ -108,6 +111,10 @@ const DEFAULT_ACCESS_STATE = { iflytekApiKey: "", iflytekApiSecret: "", + // deepseek + deepseekUrl: DEFAULT_DEEPSEEK_URL, + deepseekApiKey: "", + // xai xaiUrl: DEFAULT_XAI_URL, xaiApiKey: "", @@ -183,6 +190,9 @@ export const useAccessStore = createPersistStore( isValidIflytek() { return ensure(get(), ["iflytekApiKey"]); }, + isValidDeepSeek() { + return ensure(get(), ["deepseekApiKey"]); + }, isValidXAI() { return ensure(get(), ["xaiApiKey"]); @@ -207,6 +217,7 @@ export const useAccessStore = createPersistStore( this.isValidTencent() || this.isValidMoonshot() || this.isValidIflytek() || + this.isValidDeepSeek() || this.isValidXAI() || this.isValidChatGLM() || !this.enabledAccessControl() ||