diff --git a/.env.template b/.env.template index a9eda4d61..5fac3b47b 100644 --- a/.env.template +++ b/.env.template @@ -73,7 +73,6 @@ ANTHROPIC_API_VERSION= ### anthropic claude Api url (optional) ANTHROPIC_URL= - ### (optional) WHITE_WEBDAV_ENDPOINTS= @@ -83,3 +82,8 @@ AWS_REGION= AWS_ACCESS_KEY=AKIA AWS_SECRET_KEY= +### siliconflow Api key (optional) +SILICONFLOW_API_KEY= + +### siliconflow Api url (optional) +SILICONFLOW_URL= diff --git a/LICENSE b/LICENSE index 047f9431e..4864ab00d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023-2024 Zhang Yifei +Copyright (c) 2023-2025 NextChat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/app/client/platforms/bytedance.ts b/app/client/platforms/bytedance.ts index a2f0660d8..c2f128128 100644 --- a/app/client/platforms/bytedance.ts +++ b/app/client/platforms/bytedance.ts @@ -22,7 +22,7 @@ import { } from "@fortaine/fetch-event-source"; import { prettyObject } from "@/app/utils/format"; import { getClientConfig } from "@/app/config/client"; -import { getMessageTextContent } from "@/app/utils"; +import { preProcessImageContent } from "@/app/utils/chat"; import { fetch } from "@/app/utils/stream"; export interface OpenAIListModelResponse { @@ -84,10 +84,11 @@ export class DoubaoApi implements LLMApi { } async chat(options: ChatOptions) { - const messages = options.messages.map((v) => ({ - role: v.role, - content: getMessageTextContent(v), - })); + const messages: ChatOptions["messages"] = []; + for (const v of options.messages) { + const content = await preProcessImageContent(v.content); + messages.push({ role: v.role, content }); + } const modelConfig = { ...useAppConfig.getState().modelConfig, diff --git a/app/client/platforms/deepseek.ts b/app/client/platforms/deepseek.ts index 2bf3b2338..c436ae61d 100644 --- a/app/client/platforms/deepseek.ts +++ b/app/client/platforms/deepseek.ts @@ -5,6 +5,7 @@ import { DEEPSEEK_BASE_URL, DeepSeek, REQUEST_TIMEOUT_MS, + REQUEST_TIMEOUT_MS_FOR_THINKING, } from "@/app/constant"; import { useAccessStore, @@ -117,10 +118,14 @@ export class DeepSeekApi implements LLMApi { // console.log(chatPayload); + const isR1 = + options.config.model.endsWith("-reasoner") || + options.config.model.endsWith("-r1"); + // make a fetch request const requestTimeoutId = setTimeout( () => controller.abort(), - REQUEST_TIMEOUT_MS, + isR1 ? REQUEST_TIMEOUT_MS_FOR_THINKING : REQUEST_TIMEOUT_MS, ); if (shouldStream) { diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index 5ca8e1071..1e593dd42 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -1,4 +1,9 @@ -import { ApiPath, Google, REQUEST_TIMEOUT_MS } from "@/app/constant"; +import { + ApiPath, + Google, + REQUEST_TIMEOUT_MS, + REQUEST_TIMEOUT_MS_FOR_THINKING, +} from "@/app/constant"; import { ChatOptions, getHeaders, @@ -69,9 +74,16 @@ export class GeminiProApi implements LLMApi { .join("\n\n"); }; + let content = ""; + if (Array.isArray(res)) { + res.map((item) => { + content += getTextFromParts(item?.candidates?.at(0)?.content?.parts); + }); + } + return ( getTextFromParts(res?.candidates?.at(0)?.content?.parts) || - getTextFromParts(res?.at(0)?.candidates?.at(0)?.content?.parts) || + content || //getTextFromParts(res?.at(0)?.candidates?.at(0)?.content?.parts) || res?.error?.message || "" ); @@ -190,10 +202,11 @@ export class GeminiProApi implements LLMApi { headers: getHeaders(), }; + const isThinking = options.config.model.includes("-thinking"); // make a fetch request const requestTimeoutId = setTimeout( () => controller.abort(), - REQUEST_TIMEOUT_MS, + isThinking ? REQUEST_TIMEOUT_MS_FOR_THINKING : REQUEST_TIMEOUT_MS, ); if (shouldStream) { diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 467bb82e0..fbe533cad 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -8,6 +8,7 @@ import { Azure, REQUEST_TIMEOUT_MS, ServiceProvider, + REQUEST_TIMEOUT_MS_FOR_THINKING, } from "@/app/constant"; import { ChatMessageTool, @@ -195,7 +196,9 @@ export class ChatGPTApi implements LLMApi { let requestPayload: RequestPayload | DalleRequestPayload; const isDalle3 = _isDalle3(options.config.model); - const isO1OrO3 = options.config.model.startsWith("o1") || options.config.model.startsWith("o3"); + const isO1OrO3 = + options.config.model.startsWith("o1") || + options.config.model.startsWith("o3"); if (isDalle3) { const prompt = getMessageTextContent( options.messages.slice(-1)?.pop() as any, @@ -359,7 +362,9 @@ export class ChatGPTApi implements LLMApi { // make a fetch request const requestTimeoutId = setTimeout( () => controller.abort(), - isDalle3 || isO1OrO3 ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow. + isDalle3 || isO1OrO3 + ? REQUEST_TIMEOUT_MS_FOR_THINKING + : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow. ); const res = await fetch(chatPath, chatPayload); diff --git a/app/client/platforms/siliconflow.ts b/app/client/platforms/siliconflow.ts index fe2f9862b..1ad316a61 100644 --- a/app/client/platforms/siliconflow.ts +++ b/app/client/platforms/siliconflow.ts @@ -4,7 +4,7 @@ import { ApiPath, SILICONFLOW_BASE_URL, SiliconFlow, - REQUEST_TIMEOUT_MS, + REQUEST_TIMEOUT_MS_FOR_THINKING, } from "@/app/constant"; import { useAccessStore, @@ -120,10 +120,10 @@ export class SiliconflowApi implements LLMApi { // console.log(chatPayload); - // make a fetch request + // Use extended timeout for thinking models as they typically require more processing time const requestTimeoutId = setTimeout( () => controller.abort(), - REQUEST_TIMEOUT_MS, + REQUEST_TIMEOUT_MS_FOR_THINKING, ); if (shouldStream) { @@ -174,8 +174,8 @@ export class SiliconflowApi implements LLMApi { // Skip if both content and reasoning_content are empty or null if ( - (!reasoning || reasoning.trim().length === 0) && - (!content || content.trim().length === 0) + (!reasoning || reasoning.length === 0) && + (!content || content.length === 0) ) { return { isThinking: false, @@ -183,12 +183,12 @@ export class SiliconflowApi implements LLMApi { }; } - if (reasoning && reasoning.trim().length > 0) { + if (reasoning && reasoning.length > 0) { return { isThinking: true, content: reasoning, }; - } else if (content && content.trim().length > 0) { + } else if (content && content.length > 0) { return { isThinking: false, content: content, diff --git a/app/client/platforms/xai.ts b/app/client/platforms/xai.ts index 06dbaaa29..8c41c2d98 100644 --- a/app/client/platforms/xai.ts +++ b/app/client/platforms/xai.ts @@ -17,7 +17,7 @@ import { SpeechOptions, } from "../api"; import { getClientConfig } from "@/app/config/client"; -import { getMessageTextContent } from "@/app/utils"; +import { preProcessImageContent } from "@/app/utils/chat"; import { RequestPayload } from "./openai"; import { fetch } from "@/app/utils/stream"; @@ -62,7 +62,7 @@ export class XAIApi implements LLMApi { async chat(options: ChatOptions) { const messages: ChatOptions["messages"] = []; for (const v of options.messages) { - const content = getMessageTextContent(v); + const content = await preProcessImageContent(v.content); messages.push({ role: v.role, content }); } diff --git a/app/components/emoji.tsx b/app/components/emoji.tsx index 54d1c1c99..ecb1c6581 100644 --- a/app/components/emoji.tsx +++ b/app/components/emoji.tsx @@ -6,8 +6,21 @@ import EmojiPicker, { import { ModelType } from "../store"; -import BotIcon from "../icons/bot.svg"; -import BlackBotIcon from "../icons/black-bot.svg"; +import BotIconDefault from "../icons/llm-icons/default.svg"; +import BotIconOpenAI from "../icons/llm-icons/openai.svg"; +import BotIconGemini from "../icons/llm-icons/gemini.svg"; +import BotIconGemma from "../icons/llm-icons/gemma.svg"; +import BotIconClaude from "../icons/llm-icons/claude.svg"; +import BotIconMeta from "../icons/llm-icons/meta.svg"; +import BotIconMistral from "../icons/llm-icons/mistral.svg"; +import BotIconDeepseek from "../icons/llm-icons/deepseek.svg"; +import BotIconMoonshot from "../icons/llm-icons/moonshot.svg"; +import BotIconQwen from "../icons/llm-icons/qwen.svg"; +import BotIconWenxin from "../icons/llm-icons/wenxin.svg"; +import BotIconGrok from "../icons/llm-icons/grok.svg"; +import BotIconHunyuan from "../icons/llm-icons/hunyuan.svg"; +import BotIconDoubao from "../icons/llm-icons/doubao.svg"; +import BotIconChatglm from "../icons/llm-icons/chatglm.svg"; export function getEmojiUrl(unified: string, style: EmojiStyle) { // Whoever owns this Content Delivery Network (CDN), I am using your CDN to serve emojis @@ -33,17 +46,55 @@ export function AvatarPicker(props: { } export function Avatar(props: { model?: ModelType; avatar?: string }) { + let LlmIcon = BotIconDefault; + if (props.model) { + const modelName = props.model.toLowerCase(); + + if ( + modelName.startsWith("gpt") || + modelName.startsWith("chatgpt") || + modelName.startsWith("dall-e") || + modelName.startsWith("dalle") || + modelName.startsWith("o1") || + modelName.startsWith("o3") + ) { + LlmIcon = BotIconOpenAI; + } else if (modelName.startsWith("gemini")) { + LlmIcon = BotIconGemini; + } else if (modelName.startsWith("gemma")) { + LlmIcon = BotIconGemma; + } else if (modelName.startsWith("claude")) { + LlmIcon = BotIconClaude; + } else if (modelName.startsWith("llama")) { + LlmIcon = BotIconMeta; + } else if (modelName.startsWith("mixtral")) { + LlmIcon = BotIconMistral; + } else if (modelName.startsWith("deepseek")) { + LlmIcon = BotIconDeepseek; + } else if (modelName.startsWith("moonshot")) { + LlmIcon = BotIconMoonshot; + } else if (modelName.startsWith("qwen")) { + LlmIcon = BotIconQwen; + } else if (modelName.startsWith("ernie")) { + LlmIcon = BotIconWenxin; + } else if (modelName.startsWith("grok")) { + LlmIcon = BotIconGrok; + } else if (modelName.startsWith("hunyuan")) { + LlmIcon = BotIconHunyuan; + } else if (modelName.startsWith("doubao") || modelName.startsWith("ep-")) { + LlmIcon = BotIconDoubao; + } else if ( + modelName.startsWith("glm") || + modelName.startsWith("cogview-") || + modelName.startsWith("cogvideox-") + ) { + LlmIcon = BotIconChatglm; + } + return (
- {props.model?.startsWith("gpt-4") || - props.model?.startsWith("chatgpt-4o") || - props.model?.startsWith("o1") || - props.model?.startsWith("o3") ? ( - - ) : ( - - )} +
); } diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index 14956103c..e52b888d0 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -24,6 +24,7 @@ import React, { useRef, } from "react"; import { IconButton } from "./button"; +import { Avatar } from "./emoji"; import clsx from "clsx"; export function Popover(props: { @@ -539,6 +540,7 @@ export function Selector(props: { key={i} title={item.title} subTitle={item.subTitle} + icon={} onClick={(e) => { if (item.disable) { e.stopPropagation(); diff --git a/app/constant.ts b/app/constant.ts index 55679ea26..652541df0 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -111,6 +111,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"; @@ -545,10 +546,14 @@ const googleModels = [ "gemini-exp-1114", "gemini-exp-1121", "gemini-exp-1206", + "gemini-2.0-flash", "gemini-2.0-flash-exp", + "gemini-2.0-flash-lite-preview-02-05", "gemini-2.0-flash-thinking-exp", "gemini-2.0-flash-thinking-exp-1219", "gemini-2.0-flash-thinking-exp-01-21", + "gemini-2.0-pro-exp", + "gemini-2.0-pro-exp-02-05", ]; const anthropicModels = [ @@ -621,7 +626,16 @@ const iflytekModels = [ const deepseekModels = ["deepseek-chat", "deepseek-coder", "deepseek-reasoner"]; -const xAIModes = ["grok-beta"]; +const xAIModes = [ + "grok-beta", + "grok-2", + "grok-2-1212", + "grok-2-latest", + "grok-vision-beta", + "grok-2-vision-1212", + "grok-2-vision", + "grok-2-vision-latest", +]; const chatglmModels = [ "glm-4-plus", @@ -656,6 +670,8 @@ const siliconflowModels = [ "deepseek-ai/DeepSeek-V3", "meta-llama/Llama-3.3-70B-Instruct", "THUDM/glm-4-9b-chat", + "Pro/deepseek-ai/DeepSeek-R1", + "Pro/deepseek-ai/DeepSeek-V3", ]; let seq = 1000; // 内置的模型序号生成器从1000开始 diff --git a/app/icons/llm-icons/chatglm.svg b/app/icons/llm-icons/chatglm.svg new file mode 100644 index 000000000..642750f3e --- /dev/null +++ b/app/icons/llm-icons/chatglm.svg @@ -0,0 +1,14 @@ + + ChatGLM + + + + + + + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/claude.svg b/app/icons/llm-icons/claude.svg new file mode 100644 index 000000000..ca8e447bb --- /dev/null +++ b/app/icons/llm-icons/claude.svg @@ -0,0 +1,8 @@ + + Claude + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/deepseek.svg b/app/icons/llm-icons/deepseek.svg new file mode 100644 index 000000000..30440e316 --- /dev/null +++ b/app/icons/llm-icons/deepseek.svg @@ -0,0 +1,8 @@ + + DeepSeek + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/default.svg b/app/icons/llm-icons/default.svg new file mode 100644 index 000000000..2ebff6b3f --- /dev/null +++ b/app/icons/llm-icons/default.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/doubao.svg b/app/icons/llm-icons/doubao.svg new file mode 100644 index 000000000..79b1b822a --- /dev/null +++ b/app/icons/llm-icons/doubao.svg @@ -0,0 +1,14 @@ + + Doubao + + + + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/gemini.svg b/app/icons/llm-icons/gemini.svg new file mode 100644 index 000000000..587669135 --- /dev/null +++ b/app/icons/llm-icons/gemini.svg @@ -0,0 +1,15 @@ + + Gemini + + + + + + + + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/gemma.svg b/app/icons/llm-icons/gemma.svg new file mode 100644 index 000000000..daf1a035c --- /dev/null +++ b/app/icons/llm-icons/gemma.svg @@ -0,0 +1,15 @@ + + Gemma + + + + + + + + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/grok.svg b/app/icons/llm-icons/grok.svg new file mode 100644 index 000000000..8125cd610 --- /dev/null +++ b/app/icons/llm-icons/grok.svg @@ -0,0 +1,8 @@ + + Grok + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/hunyuan.svg b/app/icons/llm-icons/hunyuan.svg new file mode 100644 index 000000000..f67930c98 --- /dev/null +++ b/app/icons/llm-icons/hunyuan.svg @@ -0,0 +1,17 @@ + + Hunyuan + + + + + + + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/meta.svg b/app/icons/llm-icons/meta.svg new file mode 100644 index 000000000..75dc40df7 --- /dev/null +++ b/app/icons/llm-icons/meta.svg @@ -0,0 +1,93 @@ + + Meta + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/mistral.svg b/app/icons/llm-icons/mistral.svg new file mode 100644 index 000000000..e577faca5 --- /dev/null +++ b/app/icons/llm-icons/mistral.svg @@ -0,0 +1,15 @@ + + Mistral + + + + + + + + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/moonshot.svg b/app/icons/llm-icons/moonshot.svg new file mode 100644 index 000000000..5206e0f12 --- /dev/null +++ b/app/icons/llm-icons/moonshot.svg @@ -0,0 +1,8 @@ + + MoonshotAI + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/openai.svg b/app/icons/llm-icons/openai.svg new file mode 100644 index 000000000..564cd5e87 --- /dev/null +++ b/app/icons/llm-icons/openai.svg @@ -0,0 +1,8 @@ + + OpenAI + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/qwen.svg b/app/icons/llm-icons/qwen.svg new file mode 100644 index 000000000..857ce2186 --- /dev/null +++ b/app/icons/llm-icons/qwen.svg @@ -0,0 +1,14 @@ + + Qwen + + + + + + + + + + + \ No newline at end of file diff --git a/app/icons/llm-icons/wenxin.svg b/app/icons/llm-icons/wenxin.svg new file mode 100644 index 000000000..0030b0e01 --- /dev/null +++ b/app/icons/llm-icons/wenxin.svg @@ -0,0 +1,18 @@ + + Wenxin + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/utils/chat.ts b/app/utils/chat.ts index c04d33cbf..b77955e6e 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -576,7 +576,7 @@ export function streamWithThink( try { const chunk = parseSSE(text, runTools); // Skip if content is empty - if (!chunk?.content || chunk.content.trim().length === 0) { + if (!chunk?.content || chunk.content.length === 0) { return; } // Check if thinking mode changed