diff --git a/app/api/bytedance.ts b/app/api/bytedance.ts index cb65b1061..51b39ceb7 100644 --- a/app/api/bytedance.ts +++ b/app/api/bytedance.ts @@ -8,7 +8,7 @@ import { 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 { isModelNotavailableInServer } from "@/app/utils/model"; const serverConfig = getServerSideConfig(); @@ -88,7 +88,7 @@ async function request(req: NextRequest) { // not undefined and is false if ( - isModelAvailableInServer( + isModelNotavailableInServer( serverConfig.customModels, jsonBody?.model as string, ServiceProvider.ByteDance as string, diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index 11b1c6a2c..a54878c96 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -28,7 +28,11 @@ import { isClaudeThinkingModel, isVisionModel, } from "@/app/utils"; -import { preProcessImageContent, streamWithThink } from "@/app/utils/chat"; +import { + preProcessImageAndWebReferenceContent, + preProcessImageContent, + streamWithThink, +} from "@/app/utils/chat"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { RequestPayload } from "./openai"; import { fetch } from "@/app/utils/stream"; @@ -290,7 +294,7 @@ export class ClaudeApi implements LLMApi { // try get base64image from local cache image_url const messages: ChatOptions["messages"] = []; for (const v of options.messages) { - const content = await preProcessImageContent(v.content); + const content = await preProcessImageAndWebReferenceContent(v); messages.push({ role: v.role, content }); } diff --git a/app/client/platforms/bytedance.ts b/app/client/platforms/bytedance.ts index 3ecf4d3ec..526101f96 100644 --- a/app/client/platforms/bytedance.ts +++ b/app/client/platforms/bytedance.ts @@ -1,11 +1,12 @@ "use client"; +import { ApiPath, ByteDance, BYTEDANCE_BASE_URL } from "@/app/constant"; import { - ApiPath, - ByteDance, - BYTEDANCE_BASE_URL, - REQUEST_TIMEOUT_MS, -} from "@/app/constant"; -import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; + useAccessStore, + useAppConfig, + useChatStore, + ChatMessageTool, + usePluginStore, +} from "@/app/store"; import { AgentChatOptions, @@ -18,16 +19,17 @@ import { SpeechOptions, TranscriptionOptions, } from "../api"; -import Locale from "../../locales"; + import { - EventStreamContentType, - fetchEventSource, -} from "@fortaine/fetch-event-source"; -import { prettyObject } from "@/app/utils/format"; + preProcessImageAndWebReferenceContent, + streamWithThink, +} from "@/app/utils/chat"; import { getClientConfig } from "@/app/config/client"; +import { preProcessImageContent } from "@/app/utils/chat"; import { - getMessageTextContent, getWebReferenceMessageTextContent, + getTimeoutMSByModel, + getMessageTextContent, } from "@/app/utils"; import { fetch } from "@/app/utils/stream"; @@ -40,7 +42,7 @@ export interface OpenAIListModelResponse { }>; } -interface RequestPayload { +interface RequestPayloadForByteDance { messages: { role: "system" | "user" | "assistant"; content: string | MultimodalContent[]; @@ -99,10 +101,14 @@ export class DoubaoApi implements LLMApi { } async chat(options: ChatOptions) { - const messages = options.messages.map((v) => ({ - role: v.role, - content: getWebReferenceMessageTextContent(v), - })); + const messages: ChatOptions["messages"] = []; + for (const v of options.messages) { + const content = + v.role === "assistant" + ? getMessageTextContent(v) + : await preProcessImageAndWebReferenceContent(v); + messages.push({ role: v.role, content }); + } const modelConfig = { ...useAppConfig.getState().modelConfig, @@ -113,7 +119,7 @@ export class DoubaoApi implements LLMApi { }; const shouldStream = !!options.config.stream; - const requestPayload: RequestPayload = { + const requestPayload: RequestPayloadForByteDance = { messages, stream: shouldStream, model: modelConfig.model, @@ -138,119 +144,103 @@ export class DoubaoApi implements LLMApi { // make a fetch request const requestTimeoutId = setTimeout( () => controller.abort(), - REQUEST_TIMEOUT_MS, + getTimeoutMSByModel(options.config.model), ); if (shouldStream) { - let responseText = ""; - let remainText = ""; - let finished = false; - let responseRes: Response; + const tools = [] as any; + const funcs = {}; + // const [tools, funcs] = usePluginStore + // .getState() + // .getAsTools( + // useChatStore.getState().currentSession().mask?.plugin || [], + // ); + return streamWithThink( + 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 | null; + tool_calls: ChatMessageTool[]; + reasoning_content: string | null; + }; + }>; - // 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, responseRes); - } - }; - - controller.signal.onabort = finish; - - fetchEventSource(chatPath, { - fetch: fetch as any, - ...chatPayload, - async onopen(res) { - clearTimeout(requestTimeoutId); - const contentType = res.headers.get("content-type"); - console.log( - "[ByteDance] request response content type: ", - contentType, - ); - responseRes = res; - if (contentType?.startsWith("text/plain")) { - responseText = await res.clone().text(); - return finish(); + if (!choices?.length) return { isThinking: false, content: "" }; + + 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; + } } + const reasoning = choices[0]?.delta?.reasoning_content; + const content = choices[0]?.delta?.content; + // Skip if both content and reasoning_content are empty or null if ( - !res.ok || - !res.headers - .get("content-type") - ?.startsWith(EventStreamContentType) || - res.status !== 200 + (!reasoning || reasoning.length === 0) && + (!content || content.length === 0) ) { - 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(); + return { + isThinking: false, + content: "", + }; } - }, - 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); + + if (reasoning && reasoning.length > 0) { + return { + isThinking: true, + content: reasoning, + }; + } else if (content && content.length > 0) { + return { + isThinking: false, + content: content, + }; } + + return { + isThinking: false, + content: "", + }; }, - onclose() { - finish(); + // processToolMessage, include tool_calls message and tool call results + ( + requestPayload: RequestPayloadForByteDance, + toolCallMessage: any, + toolCallResult: any[], + ) => { + requestPayload?.messages?.splice( + requestPayload?.messages?.length, + 0, + toolCallMessage, + ...toolCallResult, + ); }, - onerror(e) { - options.onError?.(e); - throw e; - }, - openWhenHidden: true, - }); + options, + ); } else { const res = await fetch(chatPath, chatPayload); clearTimeout(requestTimeoutId); diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index ed341a98c..f5b5c3c05 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -17,7 +17,10 @@ import { usePluginStore, ChatMessageTool, } from "@/app/store"; -import { stream } from "@/app/utils/chat"; +import { + preProcessImageAndWebReferenceContent, + stream, +} from "@/app/utils/chat"; import { getClientConfig } from "@/app/config/client"; import { GEMINI_BASE_URL } from "@/app/constant"; @@ -88,7 +91,7 @@ export class GeminiProApi implements LLMApi { // try get base64image from local cache image_url const _messages: ChatOptions["messages"] = []; for (const v of options.messages) { - const content = await preProcessImageContent(v.content); + const content = await preProcessImageAndWebReferenceContent(v); _messages.push({ role: v.role, content }); } const messages = _messages.map((v) => { diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index cec3e383a..234b916ea 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -17,7 +17,11 @@ import { usePluginStore, } from "@/app/store"; import { collectModelsWithDefaultModel } from "@/app/utils/model"; -import { preProcessImageContent, stream } from "@/app/utils/chat"; +import { + preProcessImageAndWebReferenceContent, + preProcessImageContent, + stream, +} from "@/app/utils/chat"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing"; @@ -239,7 +243,7 @@ export class ChatGPTApi implements LLMApi { const messages: ChatOptions["messages"] = []; for (const v of options.messages) { const content = visionModel - ? await preProcessImageContent(v.content) + ? await preProcessImageAndWebReferenceContent(v) : getWebReferenceMessageTextContent(v); if (!(isO1 && v.role === "system")) messages.push({ role: v.role, content }); @@ -429,7 +433,7 @@ export class ChatGPTApi implements LLMApi { const messages: AgentChatOptions["messages"] = []; for (const v of options.messages) { const content = visionModel - ? await preProcessImageContent(v.content) + ? await preProcessImageAndWebReferenceContent(v) : getMessageTextContent(v); messages.push({ role: v.role, content }); } diff --git a/app/constant.ts b/app/constant.ts index 875618f0c..8e2be40d4 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -105,6 +105,7 @@ export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id; export const STORAGE_KEY = "chatgpt-next-web"; export const REQUEST_TIMEOUT_MS = 60000; +export const REQUEST_TIMEOUT_MS_FOR_THINKING = REQUEST_TIMEOUT_MS * 5; export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; @@ -417,6 +418,10 @@ const bytedanceModels = [ "Doubao-pro-4k", "Doubao-pro-32k", "Doubao-pro-128k", + "deepseek-v3-241226", + "deepseek-r1-250120", + "deepseek-r1-distill-qwen-7b-250120", + "deepseek-r1-distill-qwen-32b-250120", ]; const alibabaModes = [ diff --git a/app/utils.ts b/app/utils.ts index a2a695339..76f76bc33 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -2,7 +2,11 @@ import { useEffect, useState } from "react"; import { showToast } from "./components/ui-lib"; import Locale, { getLang } from "./locales"; import { RequestMessage } from "./client/api"; -import { DEFAULT_MODELS } from "./constant"; +import { + DEFAULT_MODELS, + REQUEST_TIMEOUT_MS, + REQUEST_TIMEOUT_MS_FOR_THINKING, +} from "./constant"; import { ServiceProvider } from "./constant"; // import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; import { fetch as tauriStreamFetch } from "./utils/stream"; @@ -340,6 +344,20 @@ export function isDalle3(model: string) { return "dall-e-3" === model; } +export function getTimeoutMSByModel(model: string) { + model = model.toLowerCase(); + if ( + model.startsWith("dall-e") || + model.startsWith("dalle") || + model.startsWith("o1") || + model.startsWith("o3") || + model.includes("deepseek-r") || + model.includes("-thinking") + ) + return REQUEST_TIMEOUT_MS_FOR_THINKING; + return REQUEST_TIMEOUT_MS; +} + export function showPlugins(provider: ServiceProvider, model: string) { if ( provider == ServiceProvider.OpenAI || diff --git a/app/utils/chat.ts b/app/utils/chat.ts index b090a8551..884385e09 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -11,6 +11,7 @@ import { } from "@fortaine/fetch-event-source"; import { prettyObject } from "./format"; import { fetch as tauriFetch } from "./stream"; +import { getWebReferenceMessageTextContent } from "../utils"; export function compressImage(file: Blob, maxSize: number): Promise { return new Promise((resolve, reject) => { @@ -92,6 +93,16 @@ export async function preProcessImageContent( return result; } +export async function preProcessImageAndWebReferenceContent( + message: RequestMessage, +) { + const content = message.content; + if (typeof content === "string") { + return getWebReferenceMessageTextContent(message); + } + return preProcessImageContent(content); +} + const imageCaches: Record = {}; export function cacheImageToBase64Image(imageUrl: string) { if (imageUrl.includes(CACHE_URL_PREFIX)) {