Compare commits

...

8 Commits

Author SHA1 Message Date
Fred
27ac18d9d7 feat: init support for deepseek 2024-05-15 14:47:43 +08:00
DeanYao
3513c6801e Merge pull request #4626 from ChatGPTNextWeb/chore-fix
feat: googleApiKey & anthropicApiKey support setting multi-key
2024-05-07 15:06:02 +08:00
Dean-YZG
864529cbf6 feat: googleApiKey & anthropicApiKey support setting multi-key 2024-05-06 21:14:53 +08:00
DeanYao
58c0d3e12d Merge pull request #4625 from ChatGPTNextWeb/chore-fix
feat: fix 1)the property named 'role' of the first message must be 'u…
2024-05-06 20:48:29 +08:00
Dean-YZG
a1493bfb4e feat: bugfix 2024-05-06 20:46:53 +08:00
butterfly
b3e856df1d feat: fix 1)the property named 'role' of the first message must be 'user' 2)if default summarize model 'gpt-3.5-turbo' is blocked, use currentModel instead 3)if apiurl&apikey set by location, useCustomConfig would be opened 2024-05-06 19:26:39 +08:00
fred-bf
52312dbd23 Merge pull request #4595 from ChatGPTNextWeb/feat/bump-version
feat: bump version code
2024-04-30 13:28:30 +08:00
Fred
b2e8a1eaa2 feat: bump version code 2024-04-30 13:27:07 +08:00
14 changed files with 116 additions and 52 deletions

View File

@@ -54,10 +54,11 @@ ANTHROPIC_API_KEY=
### anthropic claude Api version. (optional) ### anthropic claude Api version. (optional)
ANTHROPIC_API_VERSION= ANTHROPIC_API_VERSION=
# deepseek api key (optional)
DEEPSEEK_API_KEY=
### anthropic claude Api url (optional) ### anthropic claude Api url (optional)
ANTHROPIC_URL= ANTHROPIC_URL=
### (optional) ### (optional)
WHITE_WEBDEV_ENDPOINTS= WEBDEV_ENDPOINTS_WHITELIST=

View File

@@ -212,6 +212,10 @@ anthropic claude Api version.
anthropic claude Api Url. anthropic claude Api Url.
### `DEEPSEEK_API_KEY` (optional)
deepseek Api Key.
### `HIDE_USER_API_KEY` (optional) ### `HIDE_USER_API_KEY` (optional)
> Default: Empty > Default: Empty
@@ -245,11 +249,12 @@ 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. User `-all` to disable all default models, `+all` to enable all default models.
### `WHITE_WEBDEV_ENDPOINTS` (可选) ### `WEBDEV_ENDPOINTS_WHITELIST` (可选)
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 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
- Each address must be a complete endpoint
> `https://xxxx/yyy` - Each address must be a complete endpoint
> `https://xxxx/yyy`
- Multiple addresses are connected by ', ' - Multiple addresses are connected by ', '
## Requirements ## Requirements

View File

@@ -126,6 +126,10 @@ anthropic claude Api version.
anthropic claude Api Url. anthropic claude Api Url.
### `DEEPSEEK_API_KEY` (optional)
deepseek Api Key.
### `HIDE_USER_API_KEY` (可选) ### `HIDE_USER_API_KEY` (可选)
如果你不想让用户自行填入 API Key将此环境变量设置为 1 即可。 如果你不想让用户自行填入 API Key将此环境变量设置为 1 即可。
@@ -142,11 +146,12 @@ anthropic claude Api Url.
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。 如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
### `WHITE_WEBDEV_ENDPOINTS` (可选) ### `WEBDEV_ENDPOINTS_WHITELIST` (可选)
如果你想增加允许访问的webdav服务地址可以使用该选项格式要求 如果你想增加允许访问的webdav服务地址可以使用该选项格式要求
- 每一个地址必须是一个完整的 endpoint - 每一个地址必须是一个完整的 endpoint
> `https://xxxx/xxx` > `https://xxxx/xxx`
- 多个地址以`,`相连 - 多个地址以`,`相连
### `CUSTOM_MODELS` (可选) ### `CUSTOM_MODELS` (可选)

View File

@@ -73,6 +73,10 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
case ModelProvider.Claude: case ModelProvider.Claude:
systemApiKey = serverConfig.anthropicApiKey; systemApiKey = serverConfig.anthropicApiKey;
break; break;
case ModelProvider.Deepseek:
systemApiKey = serverConfig.deepseekApiKey;
break;
case ModelProvider.GPT: case ModelProvider.GPT:
default: default:
if (serverConfig.isAzure) { if (serverConfig.isAzure) {

View File

@@ -87,6 +87,8 @@ export async function requestOpenai(req: NextRequest) {
DEFAULT_MODELS, DEFAULT_MODELS,
serverConfig.customModels, serverConfig.customModels,
); );
// check if deepseek model
const clonedBody = await req.text(); const clonedBody = await req.text();
fetchOptions.body = clonedBody; fetchOptions.body = clonedBody;
@@ -112,16 +114,16 @@ export async function requestOpenai(req: NextRequest) {
try { try {
const res = await fetch(fetchUrl, fetchOptions); const res = await fetch(fetchUrl, fetchOptions);
// Extract the OpenAI-Organization header from the response // Extract the OpenAI-Organization header from the response
const openaiOrganizationHeader = res.headers.get("OpenAI-Organization"); const openaiOrganizationHeader = res.headers.get("OpenAI-Organization");
// Check if serverConfig.openaiOrgId is defined and not an empty string // Check if serverConfig.openaiOrgId is defined and not an empty string
if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") { if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") {
// If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present // If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present
console.log("[Org ID]", openaiOrganizationHeader); console.log("[Org ID]", openaiOrganizationHeader);
} else { } else {
console.log("[Org ID] is not set up."); console.log("[Org ID] is not set up.");
} }
// to prevent browser prompt for credentials // to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers); const newHeaders = new Headers(res.headers);
@@ -129,7 +131,6 @@ export async function requestOpenai(req: NextRequest) {
// to disable nginx buffering // to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no"); 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) // 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 // Also, this is to prevent the header from being sent to the client
if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") { if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") {
@@ -142,7 +143,6 @@ export async function requestOpenai(req: NextRequest) {
// The browser will try to decode the response with brotli and fail // The browser will try to decode the response with brotli and fail
newHeaders.delete("content-encoding"); newHeaders.delete("content-encoding");
return new Response(res.body, { return new Response(res.body, {
status: res.status, status: res.status,
statusText: res.statusText, statusText: res.statusText,

View File

@@ -1,12 +1,12 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { STORAGE_KEY, internalWhiteWebDavEndpoints } from "../../../constant"; import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant";
import { getServerSideConfig } from "@/app/config/server"; import { getServerSideConfig } from "@/app/config/server";
const config = getServerSideConfig(); const config = getServerSideConfig();
const mergedWhiteWebDavEndpoints = [ const mergedAllowedWebDavEndpoints = [
...internalWhiteWebDavEndpoints, ...internalAllowedWebDavEndpoints,
...config.whiteWebDevEndpoints, ...config.allowedWebDevEndpoints,
].filter((domain) => Boolean(domain.trim())); ].filter((domain) => Boolean(domain.trim()));
async function handle( async function handle(
@@ -24,7 +24,9 @@ async function handle(
// Validate the endpoint to prevent potential SSRF attacks // Validate the endpoint to prevent potential SSRF attacks
if ( if (
!mergedWhiteWebDavEndpoints.some((white) => endpoint?.startsWith(white)) !mergedAllowedWebDavEndpoints.some(
(allowedEndpoint) => endpoint?.startsWith(allowedEndpoint),
)
) { ) {
return NextResponse.json( return NextResponse.json(
{ {

View File

@@ -70,7 +70,7 @@ export abstract class LLMApi {
abstract models(): Promise<LLMModel[]>; abstract models(): Promise<LLMModel[]>;
} }
type ProviderName = "openai" | "azure" | "claude" | "palm"; type ProviderName = "openai" | "azure" | "claude" | "palm" | "deepseek";
interface Model { interface Model {
name: string; name: string;
@@ -162,6 +162,7 @@ export function getHeaders() {
const modelConfig = useChatStore.getState().currentSession().mask.modelConfig; const modelConfig = useChatStore.getState().currentSession().mask.modelConfig;
const isGoogle = modelConfig.model.startsWith("gemini"); const isGoogle = modelConfig.model.startsWith("gemini");
const isAzure = accessStore.provider === ServiceProvider.Azure; const isAzure = accessStore.provider === ServiceProvider.Azure;
const isDeepSeek = accessStore.provider === ServiceProvider.DeepSeek;
const authHeader = isAzure ? "api-key" : "Authorization"; const authHeader = isAzure ? "api-key" : "Authorization";
const apiKey = isGoogle const apiKey = isGoogle
? accessStore.googleApiKey ? accessStore.googleApiKey

View File

@@ -161,6 +161,13 @@ export class ClaudeApi implements LLMApi {
}; };
}); });
if (prompt[0]?.role === "assistant") {
prompt.unshift({
role: "user",
content: ";",
});
}
const requestBody: AnthropicChatRequest = { const requestBody: AnthropicChatRequest = {
messages: prompt, messages: prompt,
stream: shouldStream, stream: shouldStream,

View File

@@ -1088,6 +1088,7 @@ function _Chat() {
if (payload.url) { if (payload.url) {
accessStore.update((access) => (access.openaiUrl = payload.url!)); accessStore.update((access) => (access.openaiUrl = payload.url!));
} }
accessStore.update((access) => (access.useCustomConfig = true));
}); });
} }
} catch { } catch {

View File

@@ -51,6 +51,22 @@ const ACCESS_CODES = (function getAccessCodes(): Set<string> {
} }
})(); })();
function getApiKey(keys?: string) {
const apiKeyEnvVar = keys ?? "";
const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
const randomIndex = Math.floor(Math.random() * apiKeys.length);
const apiKey = apiKeys[randomIndex];
if (apiKey) {
console.log(
`[Server Config] using ${randomIndex + 1} of ${
apiKeys.length
} api key - ${apiKey}`,
);
}
return apiKey;
}
export const getServerSideConfig = () => { export const getServerSideConfig = () => {
if (typeof process === "undefined") { if (typeof process === "undefined") {
throw Error( throw Error(
@@ -73,38 +89,41 @@ export const getServerSideConfig = () => {
const isAzure = !!process.env.AZURE_URL; const isAzure = !!process.env.AZURE_URL;
const isGoogle = !!process.env.GOOGLE_API_KEY; const isGoogle = !!process.env.GOOGLE_API_KEY;
const isAnthropic = !!process.env.ANTHROPIC_API_KEY; const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
const isDeepSeek = !!process.env.DEEPSEEK_API_KEY;
const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? ""; // const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim()); // const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
const randomIndex = Math.floor(Math.random() * apiKeys.length); // const randomIndex = Math.floor(Math.random() * apiKeys.length);
const apiKey = apiKeys[randomIndex]; // const apiKey = apiKeys[randomIndex];
console.log( // console.log(
`[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`, // `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
); // );
const whiteWebDevEndpoints = (process.env.WHITE_WEBDEV_ENDPOINTS ?? "").split( const allowedWebDevEndpoints = (
",", process.env.WEBDEV_ENDPOINTS_WHITELIST ?? ""
); ).split(",");
return { return {
baseUrl: process.env.BASE_URL, baseUrl: process.env.BASE_URL,
apiKey, apiKey: getApiKey(process.env.OPENAI_API_KEY),
openaiOrgId: process.env.OPENAI_ORG_ID, openaiOrgId: process.env.OPENAI_ORG_ID,
isAzure, isAzure,
azureUrl: process.env.AZURE_URL, azureUrl: process.env.AZURE_URL,
azureApiKey: process.env.AZURE_API_KEY, azureApiKey: getApiKey(process.env.AZURE_API_KEY),
azureApiVersion: process.env.AZURE_API_VERSION, azureApiVersion: process.env.AZURE_API_VERSION,
isGoogle, isGoogle,
googleApiKey: process.env.GOOGLE_API_KEY, googleApiKey: getApiKey(process.env.GOOGLE_API_KEY),
googleUrl: process.env.GOOGLE_URL, googleUrl: process.env.GOOGLE_URL,
isAnthropic, isAnthropic,
anthropicApiKey: process.env.ANTHROPIC_API_KEY, anthropicApiKey: getApiKey(process.env.ANTHROPIC_API_KEY),
anthropicApiVersion: process.env.ANTHROPIC_API_VERSION, anthropicApiVersion: process.env.ANTHROPIC_API_VERSION,
anthropicUrl: process.env.ANTHROPIC_URL, anthropicUrl: process.env.ANTHROPIC_URL,
deepseekApiKey: getApiKey(process.env.DEEPSEEK_API_KEY),
gtmId: process.env.GTM_ID, gtmId: process.env.GTM_ID,
needCode: ACCESS_CODES.size > 0, needCode: ACCESS_CODES.size > 0,
@@ -120,6 +139,6 @@ export const getServerSideConfig = () => {
disableFastLink: !!process.env.DISABLE_FAST_LINK, disableFastLink: !!process.env.DISABLE_FAST_LINK,
customModels, customModels,
defaultModel, defaultModel,
whiteWebDevEndpoints, allowedWebDevEndpoints,
}; };
}; };

View File

@@ -1,3 +1,5 @@
import { Chat } from "./components/chat";
export const OWNER = "Yidadaa"; export const OWNER = "Yidadaa";
export const REPO = "ChatGPT-Next-Web"; export const REPO = "ChatGPT-Next-Web";
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
@@ -70,12 +72,14 @@ export enum ServiceProvider {
Azure = "Azure", Azure = "Azure",
Google = "Google", Google = "Google",
Anthropic = "Anthropic", Anthropic = "Anthropic",
DeepSeek = "DeepSeek",
} }
export enum ModelProvider { export enum ModelProvider {
GPT = "GPT", GPT = "GPT",
GeminiPro = "GeminiPro", GeminiPro = "GeminiPro",
Claude = "Claude", Claude = "Claude",
Deepseek = "DeepSeek",
} }
export const Anthropic = { export const Anthropic = {
@@ -136,16 +140,11 @@ export const KnowledgeCutOffDate: Record<string, string> = {
const openaiModels = [ const openaiModels = [
"gpt-3.5-turbo", "gpt-3.5-turbo",
"gpt-3.5-turbo-1106",
"gpt-3.5-turbo-0125",
"gpt-4", "gpt-4",
"gpt-4-0613",
"gpt-4-32k", "gpt-4-32k",
"gpt-4-32k-0613",
"gpt-4-turbo", "gpt-4-turbo",
"gpt-4-turbo-preview", "gpt-4-turbo-preview",
"gpt-4-vision-preview", "gpt-4-vision-preview",
"gpt-4-turbo-2024-04-09",
]; ];
const googleModels = [ const googleModels = [
@@ -163,6 +162,8 @@ const anthropicModels = [
"claude-3-haiku-20240307", "claude-3-haiku-20240307",
]; ];
const deepseekModels = ["deepseek-chat"];
export const DEFAULT_MODELS = [ export const DEFAULT_MODELS = [
...openaiModels.map((name) => ({ ...openaiModels.map((name) => ({
name, name,
@@ -191,13 +192,22 @@ export const DEFAULT_MODELS = [
providerType: "anthropic", providerType: "anthropic",
}, },
})), })),
...deepseekModels.map((name) => ({
name,
available: true,
provider: {
id: "deepseek",
providerName: "DeepSeek",
providerType: "deepseek",
},
})),
] as const; ] as const;
export const CHAT_PAGE_SIZE = 15; export const CHAT_PAGE_SIZE = 15;
export const MAX_RENDER_MSG_COUNT = 45; export const MAX_RENDER_MSG_COUNT = 45;
// some famous webdav endpoints // some famous webdav endpoints
export const internalWhiteWebDavEndpoints = [ export const internalAllowedWebDavEndpoints = [
"https://dav.jianguoyun.com/dav/", "https://dav.jianguoyun.com/dav/",
"https://dav.dropdav.com/", "https://dav.dropdav.com/",
"https://dav.box.com/dav", "https://dav.box.com/dav",

View File

@@ -21,6 +21,8 @@ import { estimateTokenLength } from "../utils/token";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store"; import { createPersistStore } from "../utils/store";
import { identifyDefaultClaudeModel } from "../utils/checkers"; import { identifyDefaultClaudeModel } from "../utils/checkers";
import { collectModelsWithDefaultModel } from "../utils/model";
import { useAccessStore } from "./access";
export type ChatMessage = RequestMessage & { export type ChatMessage = RequestMessage & {
date: string; date: string;
@@ -87,9 +89,19 @@ function createEmptySession(): ChatSession {
function getSummarizeModel(currentModel: string) { function getSummarizeModel(currentModel: string) {
// if it is using gpt-* models, force to use 3.5 to summarize // if it is using gpt-* models, force to use 3.5 to summarize
if (currentModel.startsWith("gpt")) { if (currentModel.startsWith("gpt")) {
return SUMMARIZE_MODEL; const configStore = useAppConfig.getState();
const accessStore = useAccessStore.getState();
const allModel = collectModelsWithDefaultModel(
configStore.models,
[configStore.customModels, accessStore.customModels].join(","),
accessStore.defaultModel,
);
const summarizeModel = allModel.find(
(m) => m.name === SUMMARIZE_MODEL && m.available,
);
return summarizeModel?.name ?? currentModel;
} }
if (currentModel.startsWith("gemini-pro")) { if (currentModel.startsWith("gemini")) {
return GEMINI_SUMMARIZE_MODEL; return GEMINI_SUMMARIZE_MODEL;
} }
return currentModel; return currentModel;

View File

@@ -64,13 +64,10 @@ export function collectModelTableWithDefaultModel(
) { ) {
let modelTable = collectModelTable(models, customModels); let modelTable = collectModelTable(models, customModels);
if (defaultModel && defaultModel !== "") { if (defaultModel && defaultModel !== "") {
delete modelTable[defaultModel];
modelTable[defaultModel] = { modelTable[defaultModel] = {
...modelTable[defaultModel],
name: defaultModel, name: defaultModel,
displayName: defaultModel,
available: true, available: true,
provider:
modelTable[defaultModel]?.provider ?? customProvider(defaultModel),
isDefault: true, isDefault: true,
}; };
} }

View File

@@ -9,7 +9,7 @@
}, },
"package": { "package": {
"productName": "NextChat", "productName": "NextChat",
"version": "2.11.3" "version": "2.12.2"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {