mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-04-24 20:14:25 +08:00
Compare commits
77 Commits
v2.15.4
...
afbf5eb541
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afbf5eb541 | ||
|
|
0f276f59bb | ||
|
|
fc391168e9 | ||
|
|
dca4a0e48f | ||
|
|
722c28839f | ||
|
|
0581e37236 | ||
|
|
44383a8b33 | ||
|
|
a0fa4d7e72 | ||
|
|
d0bd1bf8fd | ||
|
|
86ffa1e643 | ||
|
|
736cbdbdd1 | ||
|
|
ff356f0c8c | ||
|
|
613d67eada | ||
|
|
89cea18955 | ||
|
|
56bc77d20b | ||
|
|
6d93d37963 | ||
|
|
24df85cf9d | ||
|
|
a4d7a2c6e3 | ||
|
|
49d42bb45d | ||
|
|
4f49626303 | ||
|
|
45db20c1c3 | ||
|
|
82994843f5 | ||
|
|
1110a087a0 | ||
|
|
f0b3e10a6c | ||
|
|
f89872b833 | ||
|
|
90ced92876 | ||
|
|
2c74559010 | ||
|
|
e3ca7e8b44 | ||
|
|
4745706c42 | ||
|
|
c7c2c0211a | ||
|
|
65bb962fc0 | ||
|
|
e791cd441d | ||
|
|
8455fefc8a | ||
|
|
06f897f32f | ||
|
|
deb1e76c41 | ||
|
|
463fa743e9 | ||
|
|
cda4494cec | ||
|
|
87d85c10c3 | ||
|
|
22f83c9e11 | ||
|
|
7f454cbcec | ||
|
|
426269d795 | ||
|
|
370f143157 | ||
|
|
103106bb93 | ||
|
|
2419083adf | ||
|
|
c25903bfb4 | ||
|
|
e34c266438 | ||
|
|
8c39a687b5 | ||
|
|
592f62005b | ||
|
|
12e7caa209 | ||
|
|
b016771555 | ||
|
|
a84383f919 | ||
|
|
7f68fb1ff2 | ||
|
|
8d2003fe68 | ||
|
|
9961b513cc | ||
|
|
819238acaf | ||
|
|
ad49916b1c | ||
|
|
d18bd8a48a | ||
|
|
4a1319f2c0 | ||
|
|
8fd843d228 | ||
|
|
6792d6e475 | ||
|
|
c139038e01 | ||
|
|
4a7fd3a380 | ||
|
|
c98dc31cdf | ||
|
|
bd43af3a8d | ||
|
|
be98aa2078 | ||
|
|
a0d4a04192 | ||
|
|
bd9de4dc4d | ||
|
|
2eebfcf6fe | ||
|
|
c5074f0aa4 | ||
|
|
ba58018a15 | ||
|
|
4ae34ea3ee | ||
|
|
7d55a6d0e4 | ||
|
|
acf9fa36f9 | ||
|
|
461154bb03 | ||
|
|
450766a44b | ||
|
|
1287e39cc6 | ||
|
|
1ef2aa35e9 |
@@ -66,4 +66,9 @@ ANTHROPIC_API_VERSION=
|
|||||||
ANTHROPIC_URL=
|
ANTHROPIC_URL=
|
||||||
|
|
||||||
### (optional)
|
### (optional)
|
||||||
WHITE_WEBDAV_ENDPOINTS=
|
WHITE_WEBDAV_ENDPOINTS=
|
||||||
|
|
||||||
|
### bedrock (optional)
|
||||||
|
AWS_REGION=
|
||||||
|
AWS_ACCESS_KEY=
|
||||||
|
AWS_SECRET_KEY=
|
||||||
4
.github/workflows/deploy_preview.yml
vendored
4
.github/workflows/deploy_preview.yml
vendored
@@ -3,9 +3,7 @@ name: VercelPreviewDeployment
|
|||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types:
|
types:
|
||||||
- opened
|
- review_requested
|
||||||
- synchronize
|
|
||||||
- reopened
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
|
VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
|
||||||
|
|||||||
39
.github/workflows/test.yml
vendored
Normal file
39
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Run Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- "!*"
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- review_requested
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Cache node_modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node_modules-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Run Jest tests
|
||||||
|
run: yarn test:ci
|
||||||
@@ -31,7 +31,7 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
|||||||
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
|
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
|
||||||
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
|
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
|
||||||
|
|
||||||
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) [<img src="https://img.shields.io/badge/BT_Deploy-Install-20a53a" alt="Open in Gitpod" height="30">](https://www.bt.cn/new/download.html)
|
||||||
|
|
||||||
[<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="60" width="288" >](https://monica.im/?utm=nxcrp)
|
[<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="60" width="288" >](https://monica.im/?utm=nxcrp)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ApiPath } from "@/app/constant";
|
import { ApiPath } from "@/app/constant";
|
||||||
import { NextRequest } from "next/server";
|
import { NextRequest } from "next/server";
|
||||||
import { handle as openaiHandler } from "../../openai";
|
import { handle as openaiHandler } from "../../openai";
|
||||||
|
import { handle as bedrockHandler } from "../../bedrock";
|
||||||
import { handle as azureHandler } from "../../azure";
|
import { handle as azureHandler } from "../../azure";
|
||||||
import { handle as googleHandler } from "../../google";
|
import { handle as googleHandler } from "../../google";
|
||||||
import { handle as anthropicHandler } from "../../anthropic";
|
import { handle as anthropicHandler } from "../../anthropic";
|
||||||
@@ -10,6 +11,7 @@ import { handle as alibabaHandler } from "../../alibaba";
|
|||||||
import { handle as moonshotHandler } from "../../moonshot";
|
import { handle as moonshotHandler } from "../../moonshot";
|
||||||
import { handle as stabilityHandler } from "../../stability";
|
import { handle as stabilityHandler } from "../../stability";
|
||||||
import { handle as iflytekHandler } from "../../iflytek";
|
import { handle as iflytekHandler } from "../../iflytek";
|
||||||
|
import { handle as xaiHandler } from "../../xai";
|
||||||
import { handle as proxyHandler } from "../../proxy";
|
import { handle as proxyHandler } from "../../proxy";
|
||||||
|
|
||||||
async function handle(
|
async function handle(
|
||||||
@@ -19,12 +21,15 @@ async function handle(
|
|||||||
const apiPath = `/api/${params.provider}`;
|
const apiPath = `/api/${params.provider}`;
|
||||||
console.log(`[${params.provider} Route] params `, params);
|
console.log(`[${params.provider} Route] params `, params);
|
||||||
switch (apiPath) {
|
switch (apiPath) {
|
||||||
|
case ApiPath.Bedrock:
|
||||||
|
return bedrockHandler(req, { params });
|
||||||
case ApiPath.Azure:
|
case ApiPath.Azure:
|
||||||
return azureHandler(req, { params });
|
return azureHandler(req, { params });
|
||||||
case ApiPath.Google:
|
case ApiPath.Google:
|
||||||
return googleHandler(req, { params });
|
return googleHandler(req, { params });
|
||||||
case ApiPath.Anthropic:
|
case ApiPath.Anthropic:
|
||||||
return anthropicHandler(req, { params });
|
return anthropicHandler(req, { params });
|
||||||
|
|
||||||
case ApiPath.Baidu:
|
case ApiPath.Baidu:
|
||||||
return baiduHandler(req, { params });
|
return baiduHandler(req, { params });
|
||||||
case ApiPath.ByteDance:
|
case ApiPath.ByteDance:
|
||||||
@@ -38,6 +43,8 @@ async function handle(
|
|||||||
return stabilityHandler(req, { params });
|
return stabilityHandler(req, { params });
|
||||||
case ApiPath.Iflytek:
|
case ApiPath.Iflytek:
|
||||||
return iflytekHandler(req, { params });
|
return iflytekHandler(req, { params });
|
||||||
|
case ApiPath.XAI:
|
||||||
|
return xaiHandler(req, { params });
|
||||||
case ApiPath.OpenAI:
|
case ApiPath.OpenAI:
|
||||||
return openaiHandler(req, { params });
|
return openaiHandler(req, { params });
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -52,6 +52,28 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
|||||||
msg: "you are not allowed to access with your own api key",
|
msg: "you are not allowed to access with your own api key",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Special handling for Bedrock
|
||||||
|
if (modelProvider === ModelProvider.Bedrock) {
|
||||||
|
const region = serverConfig.awsRegion;
|
||||||
|
const accessKeyId = serverConfig.awsAccessKey;
|
||||||
|
const secretAccessKey = serverConfig.awsSecretKey;
|
||||||
|
|
||||||
|
console.log("[Auth] Bedrock credentials:", {
|
||||||
|
region,
|
||||||
|
accessKeyId: accessKeyId ? "***" : undefined,
|
||||||
|
secretKey: secretAccessKey ? "***" : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if AWS credentials are provided
|
||||||
|
if (!region || !accessKeyId || !secretAccessKey) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
msg: "Missing AWS credentials. Please configure Region, Access Key ID, and Secret Access Key in settings.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: false };
|
||||||
|
}
|
||||||
|
|
||||||
// if user does not provide an api key, inject system api key
|
// if user does not provide an api key, inject system api key
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
@@ -92,6 +114,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
|||||||
systemApiKey =
|
systemApiKey =
|
||||||
serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
|
serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
|
||||||
break;
|
break;
|
||||||
|
case ModelProvider.XAI:
|
||||||
|
systemApiKey = serverConfig.xaiApiKey;
|
||||||
|
break;
|
||||||
case ModelProvider.GPT:
|
case ModelProvider.GPT:
|
||||||
default:
|
default:
|
||||||
if (req.nextUrl.pathname.includes("azure/deployments")) {
|
if (req.nextUrl.pathname.includes("azure/deployments")) {
|
||||||
|
|||||||
562
app/api/bedrock.ts
Normal file
562
app/api/bedrock.ts
Normal file
@@ -0,0 +1,562 @@
|
|||||||
|
import { getServerSideConfig } from "../config/server";
|
||||||
|
import { prettyObject } from "../utils/format";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import {
|
||||||
|
BedrockRuntimeClient,
|
||||||
|
ConverseStreamCommand,
|
||||||
|
ConverseStreamCommandInput,
|
||||||
|
ConverseStreamOutput,
|
||||||
|
ModelStreamErrorException,
|
||||||
|
type Message,
|
||||||
|
type ContentBlock,
|
||||||
|
type SystemContentBlock,
|
||||||
|
type Tool,
|
||||||
|
type ToolChoice,
|
||||||
|
type ToolResultContentBlock,
|
||||||
|
} from "@aws-sdk/client-bedrock-runtime";
|
||||||
|
|
||||||
|
// 解密函数
|
||||||
|
function decrypt(str: string): string {
|
||||||
|
try {
|
||||||
|
return Buffer.from(str, "base64").toString().split("").reverse().join("");
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants and Types
|
||||||
|
const ALLOWED_PATH = new Set(["converse"]);
|
||||||
|
|
||||||
|
export interface ConverseRequest {
|
||||||
|
modelId: string;
|
||||||
|
messages: {
|
||||||
|
role: "user" | "assistant" | "system";
|
||||||
|
content: string | ContentItem[];
|
||||||
|
}[];
|
||||||
|
inferenceConfig?: {
|
||||||
|
maxTokens?: number;
|
||||||
|
temperature?: number;
|
||||||
|
topP?: number;
|
||||||
|
stopSequences?: string[];
|
||||||
|
};
|
||||||
|
toolConfig?: {
|
||||||
|
tools: Tool[];
|
||||||
|
toolChoice?: ToolChoice;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContentItem {
|
||||||
|
type: "text" | "image_url" | "document" | "tool_use" | "tool_result";
|
||||||
|
text?: string;
|
||||||
|
image_url?: {
|
||||||
|
url: string; // base64 data URL
|
||||||
|
};
|
||||||
|
document?: {
|
||||||
|
format: DocumentFormat;
|
||||||
|
name: string;
|
||||||
|
source: {
|
||||||
|
bytes: string; // base64
|
||||||
|
};
|
||||||
|
};
|
||||||
|
tool_use?: {
|
||||||
|
tool_use_id: string;
|
||||||
|
name: string;
|
||||||
|
input: any;
|
||||||
|
};
|
||||||
|
tool_result?: {
|
||||||
|
tool_use_id: string;
|
||||||
|
content: ToolResultItem[];
|
||||||
|
status: "success" | "error";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolResultItem {
|
||||||
|
type: "text" | "image" | "document" | "json";
|
||||||
|
text?: string;
|
||||||
|
image?: {
|
||||||
|
format: "png" | "jpeg" | "gif" | "webp";
|
||||||
|
source: {
|
||||||
|
bytes: string; // base64
|
||||||
|
};
|
||||||
|
};
|
||||||
|
document?: {
|
||||||
|
format: DocumentFormat;
|
||||||
|
name: string;
|
||||||
|
source: {
|
||||||
|
bytes: string; // base64
|
||||||
|
};
|
||||||
|
};
|
||||||
|
json?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentFormat =
|
||||||
|
| "pdf"
|
||||||
|
| "csv"
|
||||||
|
| "doc"
|
||||||
|
| "docx"
|
||||||
|
| "xls"
|
||||||
|
| "xlsx"
|
||||||
|
| "html"
|
||||||
|
| "txt"
|
||||||
|
| "md";
|
||||||
|
|
||||||
|
function validateImageSize(base64Data: string): boolean {
|
||||||
|
const sizeInBytes = (base64Data.length * 3) / 4;
|
||||||
|
const maxSize = 3.75 * 1024 * 1024;
|
||||||
|
if (sizeInBytes > maxSize) {
|
||||||
|
throw new Error("Image size exceeds 3.75 MB limit");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content Processing Functions
|
||||||
|
function convertContentToAWSBlock(item: ContentItem): ContentBlock | null {
|
||||||
|
if (item.type === "text" && item.text) {
|
||||||
|
return { text: item.text };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === "image_url" && item.image_url?.url) {
|
||||||
|
const base64Match = item.image_url.url.match(
|
||||||
|
/^data:image\/([a-zA-Z]*);base64,([^"]*)/,
|
||||||
|
);
|
||||||
|
if (base64Match) {
|
||||||
|
const format = base64Match[1].toLowerCase();
|
||||||
|
if (["png", "jpeg", "gif", "webp"].includes(format)) {
|
||||||
|
validateImageSize(base64Match[2]);
|
||||||
|
return {
|
||||||
|
image: {
|
||||||
|
format: format as "png" | "jpeg" | "gif" | "webp",
|
||||||
|
source: {
|
||||||
|
bytes: Uint8Array.from(Buffer.from(base64Match[2], "base64")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === "tool_use" && item.tool_use) {
|
||||||
|
return {
|
||||||
|
toolUse: {
|
||||||
|
toolUseId: item.tool_use.tool_use_id,
|
||||||
|
name: item.tool_use.name,
|
||||||
|
input: item.tool_use.input,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === "tool_result" && item.tool_result) {
|
||||||
|
const toolResultContent = item.tool_result.content
|
||||||
|
.map((resultItem) => {
|
||||||
|
if (resultItem.type === "text" && resultItem.text) {
|
||||||
|
return { text: resultItem.text } as ToolResultContentBlock;
|
||||||
|
}
|
||||||
|
if (resultItem.type === "image" && resultItem.image) {
|
||||||
|
return {
|
||||||
|
image: {
|
||||||
|
format: resultItem.image.format,
|
||||||
|
source: {
|
||||||
|
bytes: Uint8Array.from(
|
||||||
|
Buffer.from(resultItem.image.source.bytes, "base64"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ToolResultContentBlock;
|
||||||
|
}
|
||||||
|
if (resultItem.type === "document" && resultItem.document) {
|
||||||
|
return {
|
||||||
|
document: {
|
||||||
|
format: resultItem.document.format,
|
||||||
|
name: resultItem.document.name,
|
||||||
|
source: {
|
||||||
|
bytes: Uint8Array.from(
|
||||||
|
Buffer.from(resultItem.document.source.bytes, "base64"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ToolResultContentBlock;
|
||||||
|
}
|
||||||
|
if (resultItem.type === "json" && resultItem.json) {
|
||||||
|
return { json: resultItem.json } as ToolResultContentBlock;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((content): content is ToolResultContentBlock => content !== null);
|
||||||
|
|
||||||
|
if (toolResultContent.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
toolUseId: item.tool_result.tool_use_id,
|
||||||
|
content: toolResultContent,
|
||||||
|
status: item.tool_result.status,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertContentToAWS(content: string | ContentItem[]): ContentBlock[] {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return [{ text: content }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks = content
|
||||||
|
.map(convertContentToAWSBlock)
|
||||||
|
.filter((block): block is ContentBlock => block !== null);
|
||||||
|
|
||||||
|
return blocks.length > 0 ? blocks : [{ text: "" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMessages(messages: ConverseRequest["messages"]): {
|
||||||
|
messages: Message[];
|
||||||
|
systemPrompt?: SystemContentBlock[];
|
||||||
|
} {
|
||||||
|
const systemMessages = messages.filter((msg) => msg.role === "system");
|
||||||
|
const nonSystemMessages = messages.filter((msg) => msg.role !== "system");
|
||||||
|
|
||||||
|
const systemPrompt =
|
||||||
|
systemMessages.length > 0
|
||||||
|
? systemMessages.map((msg) => {
|
||||||
|
if (typeof msg.content === "string") {
|
||||||
|
return { text: msg.content } as SystemContentBlock;
|
||||||
|
}
|
||||||
|
const blocks = convertContentToAWS(msg.content);
|
||||||
|
return blocks[0] as SystemContentBlock;
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const formattedMessages = nonSystemMessages.reduce(
|
||||||
|
(acc: Message[], curr, idx) => {
|
||||||
|
if (idx > 0 && curr.role === nonSystemMessages[idx - 1].role) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = convertContentToAWS(curr.content);
|
||||||
|
if (content.length > 0) {
|
||||||
|
acc.push({
|
||||||
|
role: curr.role as "user" | "assistant",
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (formattedMessages.length === 0 || formattedMessages[0].role !== "user") {
|
||||||
|
formattedMessages.unshift({
|
||||||
|
role: "user",
|
||||||
|
content: [{ text: "Hello" }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formattedMessages[formattedMessages.length - 1].role !== "user") {
|
||||||
|
formattedMessages.push({
|
||||||
|
role: "user",
|
||||||
|
content: [{ text: "Continue" }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { messages: formattedMessages, systemPrompt };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRequestBody(
|
||||||
|
request: ConverseRequest,
|
||||||
|
): ConverseStreamCommandInput {
|
||||||
|
const { messages, systemPrompt } = formatMessages(request.messages);
|
||||||
|
const input: ConverseStreamCommandInput = {
|
||||||
|
modelId: request.modelId,
|
||||||
|
messages,
|
||||||
|
...(systemPrompt && { system: systemPrompt }),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.inferenceConfig) {
|
||||||
|
input.inferenceConfig = {
|
||||||
|
maxTokens: request.inferenceConfig.maxTokens,
|
||||||
|
temperature: request.inferenceConfig.temperature,
|
||||||
|
topP: request.inferenceConfig.topP,
|
||||||
|
stopSequences: request.inferenceConfig.stopSequences,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.toolConfig) {
|
||||||
|
input.toolConfig = {
|
||||||
|
tools: request.toolConfig.tools,
|
||||||
|
toolChoice: request.toolConfig.toolChoice,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const logInput = {
|
||||||
|
...input,
|
||||||
|
messages: messages.map((msg) => ({
|
||||||
|
role: msg.role,
|
||||||
|
content: msg.content?.map((content) => {
|
||||||
|
if ("image" in content && content.image) {
|
||||||
|
return {
|
||||||
|
image: {
|
||||||
|
format: content.image.format,
|
||||||
|
source: { bytes: "[BINARY]" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if ("document" in content && content.document) {
|
||||||
|
return {
|
||||||
|
document: { ...content.document, source: { bytes: "[BINARY]" } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[Bedrock] Formatted request:",
|
||||||
|
JSON.stringify(logInput, null, 2),
|
||||||
|
);
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main Request Handler
|
||||||
|
export async function handle(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } },
|
||||||
|
) {
|
||||||
|
console.log("[Bedrock Route] params ", params);
|
||||||
|
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const subpath = params.path.join("/");
|
||||||
|
|
||||||
|
if (!ALLOWED_PATH.has(subpath)) {
|
||||||
|
console.log("[Bedrock Route] forbidden path ", subpath);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
msg: "you are not allowed to request " + subpath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
|
// 首先尝试使用环境变量中的凭证
|
||||||
|
let region = serverConfig.awsRegion;
|
||||||
|
let accessKeyId = serverConfig.awsAccessKey;
|
||||||
|
let secretAccessKey = serverConfig.awsSecretKey;
|
||||||
|
let sessionToken = undefined;
|
||||||
|
|
||||||
|
// 如果环境变量中没有配置,则尝试使用前端传来的加密凭证
|
||||||
|
if (!region || !accessKeyId || !secretAccessKey) {
|
||||||
|
// 解密前端传来的凭证
|
||||||
|
region = decrypt(req.headers.get("X-Region") ?? "");
|
||||||
|
accessKeyId = decrypt(req.headers.get("X-Access-Key") ?? "");
|
||||||
|
secretAccessKey = decrypt(req.headers.get("X-Secret-Key") ?? "");
|
||||||
|
sessionToken = req.headers.get("X-Session-Token")
|
||||||
|
? decrypt(req.headers.get("X-Session-Token") ?? "")
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!region || !accessKeyId || !secretAccessKey) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
msg: "AWS credentials not found in environment variables or request headers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new BedrockRuntimeClient({
|
||||||
|
region,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId,
|
||||||
|
secretAccessKey,
|
||||||
|
sessionToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await handleConverseRequest(req, client);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Bedrock] ", e);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: e instanceof Error ? e.message : "Unknown error",
|
||||||
|
details: prettyObject(e),
|
||||||
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConverseRequest(
|
||||||
|
req: NextRequest,
|
||||||
|
client: BedrockRuntimeClient,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const body = (await req.json()) as ConverseRequest;
|
||||||
|
const { modelId } = body;
|
||||||
|
|
||||||
|
console.log("[Bedrock] Invoking model:", modelId);
|
||||||
|
|
||||||
|
const command = new ConverseStreamCommand(formatRequestBody(body));
|
||||||
|
const response = await client.send(command);
|
||||||
|
|
||||||
|
if (!response.stream) {
|
||||||
|
throw new Error("No stream in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
try {
|
||||||
|
const responseStream = response.stream;
|
||||||
|
if (!responseStream) {
|
||||||
|
throw new Error("No stream in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const event of responseStream) {
|
||||||
|
const output = event as ConverseStreamOutput;
|
||||||
|
|
||||||
|
if ("messageStart" in output && output.messageStart?.role) {
|
||||||
|
controller.enqueue(
|
||||||
|
`data: ${JSON.stringify({
|
||||||
|
stream: {
|
||||||
|
messageStart: { role: output.messageStart.role },
|
||||||
|
},
|
||||||
|
})}\n\n`,
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
"contentBlockStart" in output &&
|
||||||
|
output.contentBlockStart
|
||||||
|
) {
|
||||||
|
controller.enqueue(
|
||||||
|
`data: ${JSON.stringify({
|
||||||
|
stream: {
|
||||||
|
contentBlockStart: {
|
||||||
|
contentBlockIndex:
|
||||||
|
output.contentBlockStart.contentBlockIndex,
|
||||||
|
start: output.contentBlockStart.start,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}\n\n`,
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
"contentBlockDelta" in output &&
|
||||||
|
output.contentBlockDelta?.delta
|
||||||
|
) {
|
||||||
|
if ("text" in output.contentBlockDelta.delta) {
|
||||||
|
controller.enqueue(
|
||||||
|
`data: ${JSON.stringify({
|
||||||
|
stream: {
|
||||||
|
contentBlockDelta: {
|
||||||
|
delta: { text: output.contentBlockDelta.delta.text },
|
||||||
|
contentBlockIndex:
|
||||||
|
output.contentBlockDelta.contentBlockIndex,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}\n\n`,
|
||||||
|
);
|
||||||
|
} else if ("toolUse" in output.contentBlockDelta.delta) {
|
||||||
|
controller.enqueue(
|
||||||
|
`data: ${JSON.stringify({
|
||||||
|
stream: {
|
||||||
|
contentBlockDelta: {
|
||||||
|
delta: {
|
||||||
|
toolUse: {
|
||||||
|
input:
|
||||||
|
output.contentBlockDelta.delta.toolUse?.input,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentBlockIndex:
|
||||||
|
output.contentBlockDelta.contentBlockIndex,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}\n\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
"contentBlockStop" in output &&
|
||||||
|
output.contentBlockStop
|
||||||
|
) {
|
||||||
|
controller.enqueue(
|
||||||
|
`data: ${JSON.stringify({
|
||||||
|
stream: {
|
||||||
|
contentBlockStop: {
|
||||||
|
contentBlockIndex:
|
||||||
|
output.contentBlockStop.contentBlockIndex,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}\n\n`,
|
||||||
|
);
|
||||||
|
} else if ("messageStop" in output && output.messageStop) {
|
||||||
|
controller.enqueue(
|
||||||
|
`data: ${JSON.stringify({
|
||||||
|
stream: {
|
||||||
|
messageStop: {
|
||||||
|
stopReason: output.messageStop.stopReason,
|
||||||
|
additionalModelResponseFields:
|
||||||
|
output.messageStop.additionalModelResponseFields,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}\n\n`,
|
||||||
|
);
|
||||||
|
} else if ("metadata" in output && output.metadata) {
|
||||||
|
controller.enqueue(
|
||||||
|
`data: ${JSON.stringify({
|
||||||
|
stream: {
|
||||||
|
metadata: {
|
||||||
|
usage: output.metadata.usage,
|
||||||
|
metrics: output.metadata.metrics,
|
||||||
|
trace: output.metadata.trace,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}\n\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
controller.close();
|
||||||
|
} catch (error) {
|
||||||
|
const errorResponse = {
|
||||||
|
stream: {
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? error.constructor.name
|
||||||
|
: "UnknownError",
|
||||||
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
...(error instanceof ModelStreamErrorException && {
|
||||||
|
originalStatusCode: error.originalStatusCode,
|
||||||
|
originalMessage: error.originalMessage,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
controller.enqueue(`data: ${JSON.stringify(errorResponse)}\n\n`);
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Bedrock] Request error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
128
app/api/xai.ts
Normal file
128
app/api/xai.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
import {
|
||||||
|
XAI_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";
|
||||||
|
|
||||||
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
|
export async function handle(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } },
|
||||||
|
) {
|
||||||
|
console.log("[XAI Route] params ", params);
|
||||||
|
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResult = auth(req, ModelProvider.XAI);
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request(req);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[XAI] ", 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.XAI, "");
|
||||||
|
|
||||||
|
let baseUrl = serverConfig.xaiUrl || XAI_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 (
|
||||||
|
isModelAvailableInServer(
|
||||||
|
serverConfig.customModels,
|
||||||
|
jsonBody?.model as string,
|
||||||
|
ServiceProvider.XAI as string,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[XAI] 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
useChatStore,
|
useChatStore,
|
||||||
} from "../store";
|
} from "../store";
|
||||||
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
|
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
|
||||||
|
import { BedrockApi } from "./platforms/bedrock";
|
||||||
import { GeminiProApi } from "./platforms/google";
|
import { GeminiProApi } from "./platforms/google";
|
||||||
import { ClaudeApi } from "./platforms/anthropic";
|
import { ClaudeApi } from "./platforms/anthropic";
|
||||||
import { ErnieApi } from "./platforms/baidu";
|
import { ErnieApi } from "./platforms/baidu";
|
||||||
@@ -20,6 +21,7 @@ import { QwenApi } from "./platforms/alibaba";
|
|||||||
import { HunyuanApi } from "./platforms/tencent";
|
import { HunyuanApi } from "./platforms/tencent";
|
||||||
import { MoonshotApi } from "./platforms/moonshot";
|
import { MoonshotApi } from "./platforms/moonshot";
|
||||||
import { SparkApi } from "./platforms/iflytek";
|
import { SparkApi } from "./platforms/iflytek";
|
||||||
|
import { XAIApi } from "./platforms/xai";
|
||||||
|
|
||||||
export const ROLES = ["system", "user", "assistant"] as const;
|
export const ROLES = ["system", "user", "assistant"] as const;
|
||||||
export type MessageRole = (typeof ROLES)[number];
|
export type MessageRole = (typeof ROLES)[number];
|
||||||
@@ -29,11 +31,19 @@ export const TTSModels = ["tts-1", "tts-1-hd"] as const;
|
|||||||
export type ChatModel = ModelType;
|
export type ChatModel = ModelType;
|
||||||
|
|
||||||
export interface MultimodalContent {
|
export interface MultimodalContent {
|
||||||
type: "text" | "image_url";
|
type: "text" | "image_url" | "document";
|
||||||
text?: string;
|
text?: string;
|
||||||
image_url?: {
|
image_url?: {
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
document?: {
|
||||||
|
format: string;
|
||||||
|
name: string;
|
||||||
|
source: {
|
||||||
|
bytes: string;
|
||||||
|
media_type?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestMessage {
|
export interface RequestMessage {
|
||||||
@@ -128,6 +138,9 @@ export class ClientApi {
|
|||||||
|
|
||||||
constructor(provider: ModelProvider = ModelProvider.GPT) {
|
constructor(provider: ModelProvider = ModelProvider.GPT) {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
|
case ModelProvider.Bedrock:
|
||||||
|
this.llm = new BedrockApi();
|
||||||
|
break;
|
||||||
case ModelProvider.GeminiPro:
|
case ModelProvider.GeminiPro:
|
||||||
this.llm = new GeminiProApi();
|
this.llm = new GeminiProApi();
|
||||||
break;
|
break;
|
||||||
@@ -152,6 +165,9 @@ export class ClientApi {
|
|||||||
case ModelProvider.Iflytek:
|
case ModelProvider.Iflytek:
|
||||||
this.llm = new SparkApi();
|
this.llm = new SparkApi();
|
||||||
break;
|
break;
|
||||||
|
case ModelProvider.XAI:
|
||||||
|
this.llm = new XAIApi();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this.llm = new ChatGPTApi();
|
this.llm = new ChatGPTApi();
|
||||||
}
|
}
|
||||||
@@ -231,6 +247,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
|
|
||||||
function getConfig() {
|
function getConfig() {
|
||||||
const modelConfig = chatStore.currentSession().mask.modelConfig;
|
const modelConfig = chatStore.currentSession().mask.modelConfig;
|
||||||
|
const isBedrock = modelConfig.providerName === ServiceProvider.Bedrock;
|
||||||
const isGoogle = modelConfig.providerName === ServiceProvider.Google;
|
const isGoogle = modelConfig.providerName === ServiceProvider.Google;
|
||||||
const isAzure = modelConfig.providerName === ServiceProvider.Azure;
|
const isAzure = modelConfig.providerName === ServiceProvider.Azure;
|
||||||
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
|
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
|
||||||
@@ -239,9 +256,12 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
|
const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
|
||||||
const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
|
const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
|
||||||
const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
|
const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
|
||||||
|
const isXAI = modelConfig.providerName === ServiceProvider.XAI;
|
||||||
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
||||||
const apiKey = isGoogle
|
const apiKey = isGoogle
|
||||||
? accessStore.googleApiKey
|
? accessStore.googleApiKey
|
||||||
|
: isBedrock
|
||||||
|
? accessStore.awsAccessKey // Use AWS access key for Bedrock
|
||||||
: isAzure
|
: isAzure
|
||||||
? accessStore.azureApiKey
|
? accessStore.azureApiKey
|
||||||
: isAnthropic
|
: isAnthropic
|
||||||
@@ -252,12 +272,15 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
? accessStore.alibabaApiKey
|
? accessStore.alibabaApiKey
|
||||||
: isMoonshot
|
: isMoonshot
|
||||||
? accessStore.moonshotApiKey
|
? accessStore.moonshotApiKey
|
||||||
|
: isXAI
|
||||||
|
? accessStore.xaiApiKey
|
||||||
: isIflytek
|
: isIflytek
|
||||||
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
|
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
|
||||||
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
|
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
|
||||||
: ""
|
: ""
|
||||||
: accessStore.openaiApiKey;
|
: accessStore.openaiApiKey;
|
||||||
return {
|
return {
|
||||||
|
isBedrock,
|
||||||
isGoogle,
|
isGoogle,
|
||||||
isAzure,
|
isAzure,
|
||||||
isAnthropic,
|
isAnthropic,
|
||||||
@@ -266,6 +289,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
isAlibaba,
|
isAlibaba,
|
||||||
isMoonshot,
|
isMoonshot,
|
||||||
isIflytek,
|
isIflytek,
|
||||||
|
isXAI,
|
||||||
apiKey,
|
apiKey,
|
||||||
isEnabledAccessControl,
|
isEnabledAccessControl,
|
||||||
};
|
};
|
||||||
@@ -278,10 +302,13 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
? "x-api-key"
|
? "x-api-key"
|
||||||
: isGoogle
|
: isGoogle
|
||||||
? "x-goog-api-key"
|
? "x-goog-api-key"
|
||||||
|
: isBedrock
|
||||||
|
? "x-api-key"
|
||||||
: "Authorization";
|
: "Authorization";
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
isBedrock,
|
||||||
isGoogle,
|
isGoogle,
|
||||||
isAzure,
|
isAzure,
|
||||||
isAnthropic,
|
isAnthropic,
|
||||||
@@ -294,17 +321,30 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
|
|
||||||
const authHeader = getAuthHeader();
|
const authHeader = getAuthHeader();
|
||||||
|
|
||||||
const bearerToken = getBearerToken(
|
if (isBedrock) {
|
||||||
apiKey,
|
// 简单加密 AWS credentials
|
||||||
isAzure || isAnthropic || isGoogle,
|
const encrypt = (str: string) =>
|
||||||
);
|
Buffer.from(str.split("").reverse().join("")).toString("base64");
|
||||||
|
|
||||||
if (bearerToken) {
|
headers["X-Region"] = encrypt(accessStore.awsRegion);
|
||||||
headers[authHeader] = bearerToken;
|
headers["X-Access-Key"] = encrypt(accessStore.awsAccessKey);
|
||||||
} else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
|
headers["X-Secret-Key"] = encrypt(accessStore.awsSecretKey);
|
||||||
headers["Authorization"] = getBearerToken(
|
if (accessStore.awsSessionToken) {
|
||||||
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
headers["X-Session-Token"] = encrypt(accessStore.awsSessionToken);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const bearerToken = getBearerToken(
|
||||||
|
apiKey,
|
||||||
|
isAzure || isAnthropic || isGoogle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (bearerToken) {
|
||||||
|
headers[authHeader] = bearerToken;
|
||||||
|
} else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
|
||||||
|
headers["Authorization"] = getBearerToken(
|
||||||
|
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
@@ -312,6 +352,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
|
|
||||||
export function getClientApi(provider: ServiceProvider): ClientApi {
|
export function getClientApi(provider: ServiceProvider): ClientApi {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
|
case ServiceProvider.Bedrock:
|
||||||
|
return new ClientApi(ModelProvider.Bedrock);
|
||||||
case ServiceProvider.Google:
|
case ServiceProvider.Google:
|
||||||
return new ClientApi(ModelProvider.GeminiPro);
|
return new ClientApi(ModelProvider.GeminiPro);
|
||||||
case ServiceProvider.Anthropic:
|
case ServiceProvider.Anthropic:
|
||||||
@@ -328,6 +370,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
|
|||||||
return new ClientApi(ModelProvider.Moonshot);
|
return new ClientApi(ModelProvider.Moonshot);
|
||||||
case ServiceProvider.Iflytek:
|
case ServiceProvider.Iflytek:
|
||||||
return new ClientApi(ModelProvider.Iflytek);
|
return new ClientApi(ModelProvider.Iflytek);
|
||||||
|
case ServiceProvider.XAI:
|
||||||
|
return new ClientApi(ModelProvider.XAI);
|
||||||
default:
|
default:
|
||||||
return new ClientApi(ModelProvider.GPT);
|
return new ClientApi(ModelProvider.GPT);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
|||||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
|
import { preProcessImageContent, stream } from "@/app/utils/chat";
|
||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||||
import { RequestPayload } from "./openai";
|
import { RequestPayload } from "./openai";
|
||||||
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
export type MultiBlockContent = {
|
export type MultiBlockContent = {
|
||||||
type: "image" | "text";
|
type: "image" | "text";
|
||||||
|
|||||||
317
app/client/platforms/bedrock.ts
Normal file
317
app/client/platforms/bedrock.ts
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import { ApiPath } from "../../constant";
|
||||||
|
import {
|
||||||
|
ChatOptions,
|
||||||
|
getHeaders,
|
||||||
|
LLMApi,
|
||||||
|
LLMModel,
|
||||||
|
LLMUsage,
|
||||||
|
SpeechOptions,
|
||||||
|
} from "../api";
|
||||||
|
import {
|
||||||
|
useAppConfig,
|
||||||
|
usePluginStore,
|
||||||
|
useChatStore,
|
||||||
|
ChatMessageTool,
|
||||||
|
} from "../../store";
|
||||||
|
import { getMessageTextContent, isVisionModel } from "../../utils";
|
||||||
|
import { fetch } from "../../utils/stream";
|
||||||
|
import { preProcessImageContent, stream } from "../../utils/chat";
|
||||||
|
|
||||||
|
export type MultiBlockContent = {
|
||||||
|
type: "image" | "text";
|
||||||
|
source?: {
|
||||||
|
type: string;
|
||||||
|
media_type: string;
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnthropicMessage = {
|
||||||
|
role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper];
|
||||||
|
content: string | MultiBlockContent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClaudeMapper = {
|
||||||
|
assistant: "assistant",
|
||||||
|
user: "user",
|
||||||
|
system: "user",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export class BedrockApi implements LLMApi {
|
||||||
|
usage(): Promise<LLMUsage> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
models(): Promise<LLMModel[]> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||||
|
throw new Error("Speech not implemented for Bedrock.");
|
||||||
|
}
|
||||||
|
|
||||||
|
extractMessage(res: any) {
|
||||||
|
console.log("[Response] bedrock response: ", res);
|
||||||
|
if (Array.isArray(res?.content)) {
|
||||||
|
return res.content;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(options: ChatOptions): Promise<void> {
|
||||||
|
const visionModel = isVisionModel(options.config.model);
|
||||||
|
const shouldStream = !!options.config.stream;
|
||||||
|
const modelConfig = {
|
||||||
|
...useAppConfig.getState().modelConfig,
|
||||||
|
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||||
|
...{
|
||||||
|
model: options.config.model,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// try get base64image from local cache image_url
|
||||||
|
const messages: ChatOptions["messages"] = [];
|
||||||
|
for (const v of options.messages) {
|
||||||
|
const content = await preProcessImageContent(v.content);
|
||||||
|
messages.push({ role: v.role, content });
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = ["system", "user"];
|
||||||
|
|
||||||
|
// roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages
|
||||||
|
for (let i = 0; i < messages.length - 1; i++) {
|
||||||
|
const message = messages[i];
|
||||||
|
const nextMessage = messages[i + 1];
|
||||||
|
|
||||||
|
if (keys.includes(message.role) && keys.includes(nextMessage.role)) {
|
||||||
|
messages[i] = [
|
||||||
|
message,
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: ";",
|
||||||
|
},
|
||||||
|
] as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = messages
|
||||||
|
.flat()
|
||||||
|
.filter((v) => {
|
||||||
|
if (!v.content) return false;
|
||||||
|
if (typeof v.content === "string" && !v.content.trim()) return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((v) => {
|
||||||
|
const { role, content } = v;
|
||||||
|
const insideRole = ClaudeMapper[role] ?? "user";
|
||||||
|
|
||||||
|
if (!visionModel || typeof content === "string") {
|
||||||
|
return {
|
||||||
|
role: insideRole,
|
||||||
|
content: getMessageTextContent(v),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
role: insideRole,
|
||||||
|
content: content
|
||||||
|
.filter((v) => v.image_url || v.text)
|
||||||
|
.map(({ type, text, image_url }) => {
|
||||||
|
if (type === "text") {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
text: text!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { url = "" } = image_url || {};
|
||||||
|
const colonIndex = url.indexOf(":");
|
||||||
|
const semicolonIndex = url.indexOf(";");
|
||||||
|
const comma = url.indexOf(",");
|
||||||
|
|
||||||
|
const mimeType = url.slice(colonIndex + 1, semicolonIndex);
|
||||||
|
const encodeType = url.slice(semicolonIndex + 1, comma);
|
||||||
|
const data = url.slice(comma + 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "image" as const,
|
||||||
|
source: {
|
||||||
|
type: encodeType,
|
||||||
|
media_type: mimeType,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prompt[0]?.role === "assistant") {
|
||||||
|
prompt.unshift({
|
||||||
|
role: "user",
|
||||||
|
content: ";",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [tools, funcs] = usePluginStore
|
||||||
|
.getState()
|
||||||
|
.getAsTools(useChatStore.getState().currentSession().mask?.plugin || []);
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
modelId: options.config.model,
|
||||||
|
messages: messages.filter((msg) => msg.content.length > 0),
|
||||||
|
inferenceConfig: {
|
||||||
|
maxTokens: modelConfig.max_tokens,
|
||||||
|
temperature: modelConfig.temperature,
|
||||||
|
topP: modelConfig.top_p,
|
||||||
|
stopSequences: [],
|
||||||
|
},
|
||||||
|
toolConfig:
|
||||||
|
Array.isArray(tools) && tools.length > 0
|
||||||
|
? {
|
||||||
|
tools: tools.map((tool: any) => ({
|
||||||
|
toolSpec: {
|
||||||
|
name: tool?.function?.name,
|
||||||
|
description: tool?.function?.description,
|
||||||
|
inputSchema: {
|
||||||
|
json: tool?.function?.parameters,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
toolChoice: { auto: {} },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const conversePath = `${ApiPath.Bedrock}/converse`;
|
||||||
|
const controller = new AbortController();
|
||||||
|
options.onController?.(controller);
|
||||||
|
|
||||||
|
if (shouldStream) {
|
||||||
|
let currentToolUse: ChatMessageTool | null = null;
|
||||||
|
return stream(
|
||||||
|
conversePath,
|
||||||
|
requestBody,
|
||||||
|
getHeaders(),
|
||||||
|
Array.isArray(tools)
|
||||||
|
? tools.map((tool: any) => ({
|
||||||
|
name: tool?.function?.name,
|
||||||
|
description: tool?.function?.description,
|
||||||
|
input_schema: tool?.function?.parameters,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
funcs,
|
||||||
|
controller,
|
||||||
|
// parseSSE
|
||||||
|
(text: string, runTools: ChatMessageTool[]) => {
|
||||||
|
const parsed = JSON.parse(text);
|
||||||
|
const event = parsed.stream;
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
console.warn("[Bedrock] Unexpected event format:", parsed);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.messageStart) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.contentBlockStart?.start?.toolUse) {
|
||||||
|
const { toolUseId, name } = event.contentBlockStart.start.toolUse;
|
||||||
|
currentToolUse = {
|
||||||
|
id: toolUseId,
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name,
|
||||||
|
arguments: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
runTools.push(currentToolUse);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.contentBlockDelta?.delta?.text) {
|
||||||
|
return event.contentBlockDelta.delta.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.contentBlockDelta?.delta?.toolUse?.input &&
|
||||||
|
currentToolUse?.function
|
||||||
|
) {
|
||||||
|
currentToolUse.function.arguments +=
|
||||||
|
event.contentBlockDelta.delta.toolUse.input;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.internalServerException ||
|
||||||
|
event.modelStreamErrorException ||
|
||||||
|
event.validationException ||
|
||||||
|
event.throttlingException ||
|
||||||
|
event.serviceUnavailableException
|
||||||
|
) {
|
||||||
|
const errorMessage =
|
||||||
|
event.internalServerException?.message ||
|
||||||
|
event.modelStreamErrorException?.message ||
|
||||||
|
event.validationException?.message ||
|
||||||
|
event.throttlingException?.message ||
|
||||||
|
event.serviceUnavailableException?.message ||
|
||||||
|
"Unknown error";
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
// processToolMessage
|
||||||
|
(requestPayload: any, toolCallMessage: any, toolCallResult: any[]) => {
|
||||||
|
currentToolUse = null;
|
||||||
|
requestPayload?.messages?.splice(
|
||||||
|
requestPayload?.messages?.length,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: toolCallMessage.tool_calls.map(
|
||||||
|
(tool: ChatMessageTool) => ({
|
||||||
|
type: "tool_use",
|
||||||
|
id: tool.id,
|
||||||
|
name: tool?.function?.name,
|
||||||
|
input: tool?.function?.arguments
|
||||||
|
? JSON.parse(tool?.function?.arguments)
|
||||||
|
: {},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
...toolCallResult.map((result) => ({
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool_result",
|
||||||
|
tool_use_id: result.tool_call_id,
|
||||||
|
content: result.content,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const response = await fetch(conversePath, {
|
||||||
|
method: "POST",
|
||||||
|
headers: getHeaders(),
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(`Bedrock API error: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseBody = await response.json();
|
||||||
|
const content = this.extractMessage(responseBody);
|
||||||
|
options.onFinish(content);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("[Bedrock] Chat error:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,21 +7,25 @@ import {
|
|||||||
LLMUsage,
|
LLMUsage,
|
||||||
SpeechOptions,
|
SpeechOptions,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
import {
|
||||||
|
useAccessStore,
|
||||||
|
useAppConfig,
|
||||||
|
useChatStore,
|
||||||
|
usePluginStore,
|
||||||
|
ChatMessageTool,
|
||||||
|
} from "@/app/store";
|
||||||
|
import { stream } from "@/app/utils/chat";
|
||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
import { GEMINI_BASE_URL } from "@/app/constant";
|
import { GEMINI_BASE_URL } from "@/app/constant";
|
||||||
import Locale from "../../locales";
|
|
||||||
import {
|
|
||||||
EventStreamContentType,
|
|
||||||
fetchEventSource,
|
|
||||||
} from "@fortaine/fetch-event-source";
|
|
||||||
import { prettyObject } from "@/app/utils/format";
|
|
||||||
import {
|
import {
|
||||||
getMessageTextContent,
|
getMessageTextContent,
|
||||||
getMessageImages,
|
getMessageImages,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
} from "@/app/utils";
|
} from "@/app/utils";
|
||||||
import { preProcessImageContent } from "@/app/utils/chat";
|
import { preProcessImageContent } from "@/app/utils/chat";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { RequestPayload } from "./openai";
|
||||||
import { fetch } from "@/app/utils/stream";
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
export class GeminiProApi implements LLMApi {
|
export class GeminiProApi implements LLMApi {
|
||||||
@@ -178,115 +182,84 @@ export class GeminiProApi implements LLMApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (shouldStream) {
|
if (shouldStream) {
|
||||||
let responseText = "";
|
const [tools, funcs] = usePluginStore
|
||||||
let remainText = "";
|
.getState()
|
||||||
let finished = false;
|
.getAsTools(
|
||||||
|
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||||
|
);
|
||||||
|
return stream(
|
||||||
|
chatPath,
|
||||||
|
requestPayload,
|
||||||
|
getHeaders(),
|
||||||
|
// @ts-ignore
|
||||||
|
tools.length > 0
|
||||||
|
? // @ts-ignore
|
||||||
|
[{ functionDeclarations: tools.map((tool) => tool.function) }]
|
||||||
|
: [],
|
||||||
|
funcs,
|
||||||
|
controller,
|
||||||
|
// parseSSE
|
||||||
|
(text: string, runTools: ChatMessageTool[]) => {
|
||||||
|
// console.log("parseSSE", text, runTools);
|
||||||
|
const chunkJson = JSON.parse(text);
|
||||||
|
|
||||||
const finish = () => {
|
const functionCall = chunkJson?.candidates
|
||||||
if (!finished) {
|
?.at(0)
|
||||||
finished = true;
|
?.content.parts.at(0)?.functionCall;
|
||||||
options.onFinish(responseText + remainText);
|
if (functionCall) {
|
||||||
}
|
const { name, args } = functionCall;
|
||||||
};
|
runTools.push({
|
||||||
|
id: nanoid(),
|
||||||
// animate response to make it looks smooth
|
type: "function",
|
||||||
function animateResponseText() {
|
function: {
|
||||||
if (finished || controller.signal.aborted) {
|
name,
|
||||||
responseText += remainText;
|
arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
|
||||||
finish();
|
},
|
||||||
return;
|
});
|
||||||
}
|
}
|
||||||
|
return chunkJson?.candidates?.at(0)?.content.parts.at(0)?.text;
|
||||||
if (remainText.length > 0) {
|
},
|
||||||
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
// processToolMessage, include tool_calls message and tool call results
|
||||||
const fetchText = remainText.slice(0, fetchCount);
|
(
|
||||||
responseText += fetchText;
|
requestPayload: RequestPayload,
|
||||||
remainText = remainText.slice(fetchCount);
|
toolCallMessage: any,
|
||||||
options.onUpdate?.(responseText, fetchText);
|
toolCallResult: any[],
|
||||||
}
|
) => {
|
||||||
|
// @ts-ignore
|
||||||
requestAnimationFrame(animateResponseText);
|
requestPayload?.contents?.splice(
|
||||||
}
|
// @ts-ignore
|
||||||
|
requestPayload?.contents?.length,
|
||||||
// start animaion
|
0,
|
||||||
animateResponseText();
|
{
|
||||||
|
role: "model",
|
||||||
controller.signal.onabort = finish;
|
parts: toolCallMessage.tool_calls.map(
|
||||||
|
(tool: ChatMessageTool) => ({
|
||||||
fetchEventSource(chatPath, {
|
functionCall: {
|
||||||
fetch: fetch as any,
|
name: tool?.function?.name,
|
||||||
...chatPayload,
|
args: JSON.parse(tool?.function?.arguments as string),
|
||||||
async onopen(res) {
|
},
|
||||||
clearTimeout(requestTimeoutId);
|
}),
|
||||||
const contentType = res.headers.get("content-type");
|
),
|
||||||
console.log(
|
},
|
||||||
"[Gemini] request response content type: ",
|
// @ts-ignore
|
||||||
contentType,
|
...toolCallResult.map((result) => ({
|
||||||
|
role: "function",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
functionResponse: {
|
||||||
|
name: result.name,
|
||||||
|
response: {
|
||||||
|
name: result.name,
|
||||||
|
content: result.content, // TODO just text content...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (contentType?.startsWith("text/plain")) {
|
|
||||||
responseText = await res.clone().text();
|
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!res.ok ||
|
|
||||||
!res.headers
|
|
||||||
.get("content-type")
|
|
||||||
?.startsWith(EventStreamContentType) ||
|
|
||||||
res.status !== 200
|
|
||||||
) {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onmessage(msg) {
|
options,
|
||||||
if (msg.data === "[DONE]" || finished) {
|
);
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
const text = msg.data;
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(text);
|
|
||||||
const delta = apiClient.extractMessage(json);
|
|
||||||
|
|
||||||
if (delta) {
|
|
||||||
remainText += delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockReason = json?.promptFeedback?.blockReason;
|
|
||||||
if (blockReason) {
|
|
||||||
// being blocked
|
|
||||||
console.log(`[Google] [Safety Ratings] result:`, blockReason);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[Request] parse error", text, msg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onclose() {
|
|
||||||
finish();
|
|
||||||
},
|
|
||||||
onerror(e) {
|
|
||||||
options.onError?.(e);
|
|
||||||
throw e;
|
|
||||||
},
|
|
||||||
openWhenHidden: true,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch(chatPath, chatPayload);
|
const res = await fetch(chatPath, chatPayload);
|
||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
import { getMessageTextContent } from "@/app/utils";
|
import { getMessageTextContent } from "@/app/utils";
|
||||||
import { RequestPayload } from "./openai";
|
import { RequestPayload } from "./openai";
|
||||||
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
export class MoonshotApi implements LLMApi {
|
export class MoonshotApi implements LLMApi {
|
||||||
private disableListModels = true;
|
private disableListModels = true;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
isVisionModel,
|
isVisionModel,
|
||||||
isDalle3 as _isDalle3,
|
isDalle3 as _isDalle3,
|
||||||
} from "@/app/utils";
|
} from "@/app/utils";
|
||||||
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
export interface OpenAIListModelResponse {
|
export interface OpenAIListModelResponse {
|
||||||
object: string;
|
object: string;
|
||||||
@@ -352,7 +353,7 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
// make a fetch request
|
// make a fetch request
|
||||||
const requestTimeoutId = setTimeout(
|
const requestTimeoutId = setTimeout(
|
||||||
() => controller.abort(),
|
() => controller.abort(),
|
||||||
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||||
);
|
);
|
||||||
|
|
||||||
const res = await fetch(chatPath, chatPayload);
|
const res = await fetch(chatPath, chatPayload);
|
||||||
|
|||||||
193
app/client/platforms/xai.ts
Normal file
193
app/client/platforms/xai.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
"use client";
|
||||||
|
// azure and openai, using same models. so using same LLMApi.
|
||||||
|
import { ApiPath, XAI_BASE_URL, XAI, 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 XAIApi implements LLMApi {
|
||||||
|
private disableListModels = true;
|
||||||
|
|
||||||
|
path(path: string): string {
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
|
||||||
|
let baseUrl = "";
|
||||||
|
|
||||||
|
if (accessStore.useCustomConfig) {
|
||||||
|
baseUrl = accessStore.xaiUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.length === 0) {
|
||||||
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
|
const apiPath = ApiPath.XAI;
|
||||||
|
baseUrl = isApp ? XAI_BASE_URL : apiPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||||
|
}
|
||||||
|
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.XAI)) {
|
||||||
|
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<ArrayBuffer> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("[Request] xai payload: ", requestPayload);
|
||||||
|
|
||||||
|
const shouldStream = !!options.config.stream;
|
||||||
|
const controller = new AbortController();
|
||||||
|
options.onController?.(controller);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chatPath = this.path(XAI.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);
|
||||||
|
}
|
||||||
|
} 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<LLMModel[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import Logo from "../icons/logo.svg";
|
|||||||
import { useMobileScreen } from "@/app/utils";
|
import { useMobileScreen } from "@/app/utils";
|
||||||
import BotIcon from "../icons/bot.svg";
|
import BotIcon from "../icons/bot.svg";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
|
import { PasswordInput } from "./ui-lib";
|
||||||
import LeftIcon from "@/app/icons/left.svg";
|
import LeftIcon from "@/app/icons/left.svg";
|
||||||
import { safeLocalStorage } from "@/app/utils";
|
import { safeLocalStorage } from "@/app/utils";
|
||||||
import {
|
import {
|
||||||
@@ -60,36 +61,43 @@ export function AuthPage() {
|
|||||||
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
|
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
|
||||||
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
|
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
|
||||||
|
|
||||||
<input
|
<PasswordInput
|
||||||
className={styles["auth-input"]}
|
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||||
type="password"
|
aria={Locale.Settings.ShowPassword}
|
||||||
placeholder={Locale.Auth.Input}
|
aria-label={Locale.Auth.Input}
|
||||||
value={accessStore.accessCode}
|
value={accessStore.accessCode}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Auth.Input}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
accessStore.update(
|
accessStore.update(
|
||||||
(access) => (access.accessCode = e.currentTarget.value),
|
(access) => (access.accessCode = e.currentTarget.value),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!accessStore.hideUserApiKey ? (
|
{!accessStore.hideUserApiKey ? (
|
||||||
<>
|
<>
|
||||||
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
|
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
|
||||||
<input
|
<PasswordInput
|
||||||
className={styles["auth-input"]}
|
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||||
type="password"
|
aria={Locale.Settings.ShowPassword}
|
||||||
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
aria-label={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||||
value={accessStore.openaiApiKey}
|
value={accessStore.openaiApiKey}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
accessStore.update(
|
accessStore.update(
|
||||||
(access) => (access.openaiApiKey = e.currentTarget.value),
|
(access) => (access.openaiApiKey = e.currentTarget.value),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
<PasswordInput
|
||||||
className={styles["auth-input-second"]}
|
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||||
type="password"
|
aria={Locale.Settings.ShowPassword}
|
||||||
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
aria-label={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||||
value={accessStore.googleApiKey}
|
value={accessStore.googleApiKey}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
accessStore.update(
|
accessStore.update(
|
||||||
(access) => (access.googleApiKey = e.currentTarget.value),
|
(access) => (access.googleApiKey = e.currentTarget.value),
|
||||||
|
|||||||
@@ -75,6 +75,17 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
--delay: 0.5s;
|
--delay: 0.5s;
|
||||||
width: var(--full-width);
|
width: var(--full-width);
|
||||||
@@ -393,8 +404,8 @@
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Specific styles for iOS devices */
|
/* Specific styles for iOS devices */
|
||||||
@media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
|
@media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
|
||||||
|
|||||||
@@ -115,11 +115,14 @@ import { getClientConfig } from "../config/client";
|
|||||||
import { useAllModels } from "../utils/hooks";
|
import { useAllModels } from "../utils/hooks";
|
||||||
import { MultimodalContent } from "../client/api";
|
import { MultimodalContent } from "../client/api";
|
||||||
|
|
||||||
const localStorage = safeLocalStorage();
|
|
||||||
import { ClientApi } from "../client/api";
|
import { ClientApi } from "../client/api";
|
||||||
import { createTTSPlayer } from "../utils/audio";
|
import { createTTSPlayer } from "../utils/audio";
|
||||||
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
|
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
|
||||||
|
|
||||||
|
import { isEmpty } from "lodash-es";
|
||||||
|
|
||||||
|
const localStorage = safeLocalStorage();
|
||||||
|
|
||||||
const ttsPlayer = createTTSPlayer();
|
const ttsPlayer = createTTSPlayer();
|
||||||
|
|
||||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||||
@@ -1015,7 +1018,7 @@ function _Chat() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const doSubmit = (userInput: string) => {
|
const doSubmit = (userInput: string) => {
|
||||||
if (userInput.trim() === "") return;
|
if (userInput.trim() === "" && isEmpty(attachImages)) return;
|
||||||
const matchCommand = chatCommands.match(userInput);
|
const matchCommand = chatCommands.match(userInput);
|
||||||
if (matchCommand.matched) {
|
if (matchCommand.matched) {
|
||||||
setUserInput("");
|
setUserInput("");
|
||||||
|
|||||||
@@ -140,6 +140,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
&-narrow {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-logo {
|
.sidebar-logo {
|
||||||
|
|||||||
@@ -169,6 +169,12 @@ export function PreCode(props: { children: any }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CustomCode(props: { children: any; className?: string }) {
|
function CustomCode(props: { children: any; className?: string }) {
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const session = chatStore.currentSession();
|
||||||
|
const config = useAppConfig();
|
||||||
|
const enableCodeFold =
|
||||||
|
session.mask?.enableCodeFold !== false && config.enableCodeFold;
|
||||||
|
|
||||||
const ref = useRef<HTMLPreElement>(null);
|
const ref = useRef<HTMLPreElement>(null);
|
||||||
const [collapsed, setCollapsed] = useState(true);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
const [showToggle, setShowToggle] = useState(false);
|
const [showToggle, setShowToggle] = useState(false);
|
||||||
@@ -184,25 +190,30 @@ function CustomCode(props: { children: any; className?: string }) {
|
|||||||
const toggleCollapsed = () => {
|
const toggleCollapsed = () => {
|
||||||
setCollapsed((collapsed) => !collapsed);
|
setCollapsed((collapsed) => !collapsed);
|
||||||
};
|
};
|
||||||
|
const renderShowMoreButton = () => {
|
||||||
|
if (showToggle && enableCodeFold && collapsed) {
|
||||||
|
return (
|
||||||
|
<div className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}>
|
||||||
|
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<code
|
<code
|
||||||
className={props?.className}
|
className={props?.className}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={{
|
style={{
|
||||||
maxHeight: collapsed ? "400px" : "none",
|
maxHeight: enableCodeFold && collapsed ? "400px" : "none",
|
||||||
overflowY: "hidden",
|
overflowY: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</code>
|
</code>
|
||||||
{showToggle && collapsed && (
|
|
||||||
<div
|
{renderShowMoreButton()}
|
||||||
className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}
|
|
||||||
>
|
|
||||||
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,6 +183,23 @@ export function MaskConfig(props: {
|
|||||||
></input>
|
></input>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
|
{globalConfig.enableCodeFold && (
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Mask.Config.CodeFold.Title}
|
||||||
|
subTitle={Locale.Mask.Config.CodeFold.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label={Locale.Mask.Config.CodeFold.Title}
|
||||||
|
type="checkbox"
|
||||||
|
checked={props.mask.enableCodeFold !== false}
|
||||||
|
onChange={(e) => {
|
||||||
|
props.updateMask((mask) => {
|
||||||
|
mask.enableCodeFold = e.currentTarget.checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
|
||||||
{!props.shouldSyncFromGlobal ? (
|
{!props.shouldSyncFromGlobal ? (
|
||||||
<ListItem
|
<ListItem
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import Locale, {
|
|||||||
changeLang,
|
changeLang,
|
||||||
getLang,
|
getLang,
|
||||||
} from "../locales";
|
} from "../locales";
|
||||||
import { copyToClipboard } from "../utils";
|
import { copyToClipboard, clientUpdate, semverCompare } from "../utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
Anthropic,
|
Anthropic,
|
||||||
@@ -59,6 +59,7 @@ import {
|
|||||||
ByteDance,
|
ByteDance,
|
||||||
Alibaba,
|
Alibaba,
|
||||||
Moonshot,
|
Moonshot,
|
||||||
|
XAI,
|
||||||
Google,
|
Google,
|
||||||
GoogleSafetySettingsThreshold,
|
GoogleSafetySettingsThreshold,
|
||||||
OPENAI_BASE_URL,
|
OPENAI_BASE_URL,
|
||||||
@@ -585,7 +586,7 @@ export function Settings() {
|
|||||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||||
const currentVersion = updateStore.formatVersion(updateStore.version);
|
const currentVersion = updateStore.formatVersion(updateStore.version);
|
||||||
const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
|
const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
|
||||||
const hasNewVersion = currentVersion !== remoteId;
|
const hasNewVersion = semverCompare(currentVersion, remoteId) === -1;
|
||||||
const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
|
const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
|
||||||
|
|
||||||
function checkUpdate(force = false) {
|
function checkUpdate(force = false) {
|
||||||
@@ -962,7 +963,75 @@ export function Settings() {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
const bedrockConfigComponent = accessStore.provider ===
|
||||||
|
ServiceProvider.Bedrock && (
|
||||||
|
<>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.Bedrock.Region.Title}
|
||||||
|
subTitle={Locale.Settings.Access.Bedrock.Region.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label={Locale.Settings.Access.Bedrock.Region.Title}
|
||||||
|
type="text"
|
||||||
|
value={accessStore.awsRegion}
|
||||||
|
placeholder="us-west-2"
|
||||||
|
onChange={(e) =>
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.awsRegion = e.currentTarget.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.Bedrock.AccessKey.Title}
|
||||||
|
subTitle={Locale.Settings.Access.Bedrock.AccessKey.SubTitle}
|
||||||
|
>
|
||||||
|
<PasswordInput
|
||||||
|
aria-label={Locale.Settings.Access.Bedrock.AccessKey.Title}
|
||||||
|
value={accessStore.awsAccessKey}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.Bedrock.AccessKey.Placeholder}
|
||||||
|
onChange={(e) => {
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.awsAccessKey = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.Bedrock.SecretKey.Title}
|
||||||
|
subTitle={Locale.Settings.Access.Bedrock.SecretKey.SubTitle}
|
||||||
|
>
|
||||||
|
<PasswordInput
|
||||||
|
aria-label={Locale.Settings.Access.Bedrock.SecretKey.Title}
|
||||||
|
value={accessStore.awsSecretKey}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.Bedrock.SecretKey.Placeholder}
|
||||||
|
onChange={(e) => {
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.awsSecretKey = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.Bedrock.SessionToken.Title}
|
||||||
|
subTitle={Locale.Settings.Access.Bedrock.SessionToken.SubTitle}
|
||||||
|
>
|
||||||
|
<PasswordInput
|
||||||
|
aria-label={Locale.Settings.Access.Bedrock.SessionToken.Title}
|
||||||
|
value={accessStore.awsSessionToken}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.Bedrock.SessionToken.Placeholder}
|
||||||
|
onChange={(e) => {
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.awsSessionToken = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
const baiduConfigComponent = accessStore.provider ===
|
const baiduConfigComponent = accessStore.provider ===
|
||||||
ServiceProvider.Baidu && (
|
ServiceProvider.Baidu && (
|
||||||
<>
|
<>
|
||||||
@@ -1194,6 +1263,45 @@ export function Settings() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const XAIConfigComponent = accessStore.provider === ServiceProvider.XAI && (
|
||||||
|
<>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.XAI.Endpoint.Title}
|
||||||
|
subTitle={
|
||||||
|
Locale.Settings.Access.XAI.Endpoint.SubTitle + XAI.ExampleEndpoint
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label={Locale.Settings.Access.XAI.Endpoint.Title}
|
||||||
|
type="text"
|
||||||
|
value={accessStore.xaiUrl}
|
||||||
|
placeholder={XAI.ExampleEndpoint}
|
||||||
|
onChange={(e) =>
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.xaiUrl = e.currentTarget.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.XAI.ApiKey.Title}
|
||||||
|
subTitle={Locale.Settings.Access.XAI.ApiKey.SubTitle}
|
||||||
|
>
|
||||||
|
<PasswordInput
|
||||||
|
aria-label={Locale.Settings.Access.XAI.ApiKey.Title}
|
||||||
|
value={accessStore.xaiApiKey}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.XAI.ApiKey.Placeholder}
|
||||||
|
onChange={(e) => {
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.xaiApiKey = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const stabilityConfigComponent = accessStore.provider ===
|
const stabilityConfigComponent = accessStore.provider ===
|
||||||
ServiceProvider.Stability && (
|
ServiceProvider.Stability && (
|
||||||
<>
|
<>
|
||||||
@@ -1357,9 +1465,17 @@ export function Settings() {
|
|||||||
{checkingUpdate ? (
|
{checkingUpdate ? (
|
||||||
<LoadingIcon />
|
<LoadingIcon />
|
||||||
) : hasNewVersion ? (
|
) : hasNewVersion ? (
|
||||||
<Link href={updateUrl} target="_blank" className="link">
|
clientConfig?.isApp ? (
|
||||||
{Locale.Settings.Update.GoToUpdate}
|
<IconButton
|
||||||
</Link>
|
icon={<ResetIcon></ResetIcon>}
|
||||||
|
text={Locale.Settings.Update.GoToUpdate}
|
||||||
|
onClick={() => clientUpdate()}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Link href={updateUrl} target="_blank" className="link">
|
||||||
|
{Locale.Settings.Update.GoToUpdate}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<ResetIcon></ResetIcon>}
|
icon={<ResetIcon></ResetIcon>}
|
||||||
@@ -1509,6 +1625,22 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Mask.Config.CodeFold.Title}
|
||||||
|
subTitle={Locale.Mask.Config.CodeFold.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label={Locale.Mask.Config.CodeFold.Title}
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.enableCodeFold}
|
||||||
|
data-testid="enable-code-fold-checkbox"
|
||||||
|
onChange={(e) =>
|
||||||
|
updateConfig(
|
||||||
|
(config) => (config.enableCodeFold = e.currentTarget.checked),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<SyncItems />
|
<SyncItems />
|
||||||
@@ -1618,6 +1750,7 @@ export function Settings() {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
{openAIConfigComponent}
|
{openAIConfigComponent}
|
||||||
|
{bedrockConfigComponent}
|
||||||
{azureConfigComponent}
|
{azureConfigComponent}
|
||||||
{googleConfigComponent}
|
{googleConfigComponent}
|
||||||
{anthropicConfigComponent}
|
{anthropicConfigComponent}
|
||||||
@@ -1628,6 +1761,7 @@ export function Settings() {
|
|||||||
{moonshotConfigComponent}
|
{moonshotConfigComponent}
|
||||||
{stabilityConfigComponent}
|
{stabilityConfigComponent}
|
||||||
{lflytekConfigComponent}
|
{lflytekConfigComponent}
|
||||||
|
{XAIConfigComponent}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -165,11 +165,17 @@ export function SideBarHeader(props: {
|
|||||||
subTitle?: string | React.ReactNode;
|
subTitle?: string | React.ReactNode;
|
||||||
logo?: React.ReactNode;
|
logo?: React.ReactNode;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
shouldNarrow?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { title, subTitle, logo, children } = props;
|
const { title, subTitle, logo, children, shouldNarrow } = props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
<div
|
||||||
|
className={`${styles["sidebar-header"]} ${
|
||||||
|
shouldNarrow ? styles["sidebar-header-narrow"] : ""
|
||||||
|
}`}
|
||||||
|
data-tauri-drag-region
|
||||||
|
>
|
||||||
<div className={styles["sidebar-title-container"]}>
|
<div className={styles["sidebar-title-container"]}>
|
||||||
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
||||||
{title}
|
{title}
|
||||||
@@ -227,6 +233,7 @@ export function SideBar(props: { className?: string }) {
|
|||||||
title="NextChat"
|
title="NextChat"
|
||||||
subTitle="Build your own AI assistant."
|
subTitle="Build your own AI assistant."
|
||||||
logo={<ChatGptIcon />}
|
logo={<ChatGptIcon />}
|
||||||
|
shouldNarrow={shouldNarrow}
|
||||||
>
|
>
|
||||||
<div className={styles["sidebar-header-bar"]}>
|
<div className={styles["sidebar-header-bar"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ declare global {
|
|||||||
BASE_URL?: string;
|
BASE_URL?: string;
|
||||||
OPENAI_ORG_ID?: string; // openai only
|
OPENAI_ORG_ID?: string; // openai only
|
||||||
|
|
||||||
|
// bedrock only
|
||||||
|
BEDROCK_REGION?: string;
|
||||||
|
BEDROCK_API_KEY?: string;
|
||||||
|
BEDROCK_API_SECRET?: string;
|
||||||
|
|
||||||
VERCEL?: string;
|
VERCEL?: string;
|
||||||
BUILD_MODE?: "standalone" | "export";
|
BUILD_MODE?: "standalone" | "export";
|
||||||
BUILD_APP?: string; // is building desktop app
|
BUILD_APP?: string; // is building desktop app
|
||||||
@@ -71,6 +76,10 @@ declare global {
|
|||||||
IFLYTEK_API_KEY?: string;
|
IFLYTEK_API_KEY?: string;
|
||||||
IFLYTEK_API_SECRET?: string;
|
IFLYTEK_API_SECRET?: string;
|
||||||
|
|
||||||
|
// xai only
|
||||||
|
XAI_URL?: string;
|
||||||
|
XAI_API_KEY?: string;
|
||||||
|
|
||||||
// custom template for preprocessing user input
|
// custom template for preprocessing user input
|
||||||
DEFAULT_INPUT_TEMPLATE?: string;
|
DEFAULT_INPUT_TEMPLATE?: string;
|
||||||
}
|
}
|
||||||
@@ -135,7 +144,7 @@ export const getServerSideConfig = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isStability = !!process.env.STABILITY_API_KEY;
|
const isStability = !!process.env.STABILITY_API_KEY;
|
||||||
|
const isBedrock = !!process.env.BEDROCK_API_KEY;
|
||||||
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;
|
||||||
@@ -146,6 +155,7 @@ export const getServerSideConfig = () => {
|
|||||||
const isAlibaba = !!process.env.ALIBABA_API_KEY;
|
const isAlibaba = !!process.env.ALIBABA_API_KEY;
|
||||||
const isMoonshot = !!process.env.MOONSHOT_API_KEY;
|
const isMoonshot = !!process.env.MOONSHOT_API_KEY;
|
||||||
const isIflytek = !!process.env.IFLYTEK_API_KEY;
|
const isIflytek = !!process.env.IFLYTEK_API_KEY;
|
||||||
|
const isXAI = !!process.env.XAI_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);
|
||||||
@@ -163,6 +173,11 @@ export const getServerSideConfig = () => {
|
|||||||
apiKey: getApiKey(process.env.OPENAI_API_KEY),
|
apiKey: getApiKey(process.env.OPENAI_API_KEY),
|
||||||
openaiOrgId: process.env.OPENAI_ORG_ID,
|
openaiOrgId: process.env.OPENAI_ORG_ID,
|
||||||
|
|
||||||
|
isBedrock,
|
||||||
|
awsRegion: process.env.AWS_REGION,
|
||||||
|
awsAccessKey: process.env.AWS_ACCESS_KEY,
|
||||||
|
awsSecretKey: process.env.AWS_SECRET_KEY,
|
||||||
|
|
||||||
isStability,
|
isStability,
|
||||||
stabilityUrl: process.env.STABILITY_URL,
|
stabilityUrl: process.env.STABILITY_URL,
|
||||||
stabilityApiKey: getApiKey(process.env.STABILITY_API_KEY),
|
stabilityApiKey: getApiKey(process.env.STABILITY_API_KEY),
|
||||||
@@ -208,6 +223,10 @@ export const getServerSideConfig = () => {
|
|||||||
iflytekApiKey: process.env.IFLYTEK_API_KEY,
|
iflytekApiKey: process.env.IFLYTEK_API_KEY,
|
||||||
iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
|
iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
|
||||||
|
|
||||||
|
isXAI,
|
||||||
|
xaiUrl: process.env.XAI_URL,
|
||||||
|
xaiApiKey: getApiKey(process.env.XAI_API_KEY),
|
||||||
|
|
||||||
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||||
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
|
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
|
||||||
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
|
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
|||||||
export const STABILITY_BASE_URL = "https://api.stability.ai";
|
export const STABILITY_BASE_URL = "https://api.stability.ai";
|
||||||
|
|
||||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||||
|
export const BEDROCK_BASE_URL =
|
||||||
|
"https://bedrock-runtime.us-west-2.amazonaws.com";
|
||||||
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
||||||
|
|
||||||
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
|
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
|
||||||
@@ -28,6 +30,8 @@ export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
|
|||||||
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
|
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
|
||||||
export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
|
export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
|
||||||
|
|
||||||
|
export const XAI_BASE_URL = "https://api.x.ai";
|
||||||
|
|
||||||
export const CACHE_URL_PREFIX = "/api/cache";
|
export const CACHE_URL_PREFIX = "/api/cache";
|
||||||
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
|
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
|
||||||
|
|
||||||
@@ -47,6 +51,7 @@ export enum Path {
|
|||||||
|
|
||||||
export enum ApiPath {
|
export enum ApiPath {
|
||||||
Cors = "",
|
Cors = "",
|
||||||
|
Bedrock = "/api/bedrock",
|
||||||
Azure = "/api/azure",
|
Azure = "/api/azure",
|
||||||
OpenAI = "/api/openai",
|
OpenAI = "/api/openai",
|
||||||
Anthropic = "/api/anthropic",
|
Anthropic = "/api/anthropic",
|
||||||
@@ -59,6 +64,7 @@ export enum ApiPath {
|
|||||||
Iflytek = "/api/iflytek",
|
Iflytek = "/api/iflytek",
|
||||||
Stability = "/api/stability",
|
Stability = "/api/stability",
|
||||||
Artifacts = "/api/artifacts",
|
Artifacts = "/api/artifacts",
|
||||||
|
XAI = "/api/xai",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SlotID {
|
export enum SlotID {
|
||||||
@@ -111,6 +117,8 @@ export enum ServiceProvider {
|
|||||||
Moonshot = "Moonshot",
|
Moonshot = "Moonshot",
|
||||||
Stability = "Stability",
|
Stability = "Stability",
|
||||||
Iflytek = "Iflytek",
|
Iflytek = "Iflytek",
|
||||||
|
XAI = "XAI",
|
||||||
|
Bedrock = "Bedrock",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
||||||
@@ -124,6 +132,7 @@ export enum GoogleSafetySettingsThreshold {
|
|||||||
|
|
||||||
export enum ModelProvider {
|
export enum ModelProvider {
|
||||||
Stability = "Stability",
|
Stability = "Stability",
|
||||||
|
Bedrock = "Bedrock",
|
||||||
GPT = "GPT",
|
GPT = "GPT",
|
||||||
GeminiPro = "GeminiPro",
|
GeminiPro = "GeminiPro",
|
||||||
Claude = "Claude",
|
Claude = "Claude",
|
||||||
@@ -133,6 +142,7 @@ export enum ModelProvider {
|
|||||||
Hunyuan = "Hunyuan",
|
Hunyuan = "Hunyuan",
|
||||||
Moonshot = "Moonshot",
|
Moonshot = "Moonshot",
|
||||||
Iflytek = "Iflytek",
|
Iflytek = "Iflytek",
|
||||||
|
XAI = "XAI",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Stability = {
|
export const Stability = {
|
||||||
@@ -215,6 +225,15 @@ export const Iflytek = {
|
|||||||
ChatPath: "v1/chat/completions",
|
ChatPath: "v1/chat/completions",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const XAI = {
|
||||||
|
ExampleEndpoint: XAI_BASE_URL,
|
||||||
|
ChatPath: "v1/chat/completions",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Bedrock = {
|
||||||
|
ChatPath: "converse",
|
||||||
|
};
|
||||||
|
|
||||||
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
||||||
// export const DEFAULT_SYSTEM_TEMPLATE = `
|
// export const DEFAULT_SYSTEM_TEMPLATE = `
|
||||||
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
|
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
|
||||||
@@ -294,6 +313,22 @@ const openaiModels = [
|
|||||||
"o1-preview",
|
"o1-preview",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const bedrockModels = [
|
||||||
|
// Claude Models
|
||||||
|
"anthropic.claude-3-haiku-20240307-v1:0",
|
||||||
|
"anthropic.claude-3-5-haiku-20241022-v1:0",
|
||||||
|
"anthropic.claude-3-sonnet-20240229-v1:0",
|
||||||
|
"anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||||
|
"anthropic.claude-3-opus-20240229-v1:0",
|
||||||
|
|
||||||
|
// Meta Llama Models
|
||||||
|
"us.meta.llama3-2-11b-instruct-v1:0",
|
||||||
|
"us.meta.llama3-2-90b-instruct-v1:0",
|
||||||
|
//Mistral
|
||||||
|
"mistral.mistral-large-2402-v1:0",
|
||||||
|
"mistral.mistral-large-2407-v1:0",
|
||||||
|
];
|
||||||
|
|
||||||
const googleModels = [
|
const googleModels = [
|
||||||
"gemini-1.0-pro",
|
"gemini-1.0-pro",
|
||||||
"gemini-1.5-pro-latest",
|
"gemini-1.5-pro-latest",
|
||||||
@@ -309,6 +344,9 @@ const anthropicModels = [
|
|||||||
"claude-3-opus-20240229",
|
"claude-3-opus-20240229",
|
||||||
"claude-3-haiku-20240307",
|
"claude-3-haiku-20240307",
|
||||||
"claude-3-5-sonnet-20240620",
|
"claude-3-5-sonnet-20240620",
|
||||||
|
"claude-3-5-sonnet-20241022",
|
||||||
|
"claude-3-5-sonnet-latest",
|
||||||
|
"claude-3-opus-latest",
|
||||||
];
|
];
|
||||||
|
|
||||||
const baiduModels = [
|
const baiduModels = [
|
||||||
@@ -364,6 +402,8 @@ const iflytekModels = [
|
|||||||
"4.0Ultra",
|
"4.0Ultra",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const xAIModes = ["grok-beta"];
|
||||||
|
|
||||||
let seq = 1000; // 内置的模型序号生成器从1000开始
|
let seq = 1000; // 内置的模型序号生成器从1000开始
|
||||||
export const DEFAULT_MODELS = [
|
export const DEFAULT_MODELS = [
|
||||||
...openaiModels.map((name) => ({
|
...openaiModels.map((name) => ({
|
||||||
@@ -476,6 +516,28 @@ export const DEFAULT_MODELS = [
|
|||||||
sorted: 10,
|
sorted: 10,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
|
...xAIModes.map((name) => ({
|
||||||
|
name,
|
||||||
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
|
provider: {
|
||||||
|
id: "xai",
|
||||||
|
providerName: "XAI",
|
||||||
|
providerType: "xai",
|
||||||
|
sorted: 11,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...bedrockModels.map((name) => ({
|
||||||
|
name,
|
||||||
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
|
provider: {
|
||||||
|
id: "bedrock",
|
||||||
|
providerName: "Bedrock",
|
||||||
|
providerType: "bedrock",
|
||||||
|
sorted: 13,
|
||||||
|
},
|
||||||
|
})),
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const CHAT_PAGE_SIZE = 15;
|
export const CHAT_PAGE_SIZE = 15;
|
||||||
|
|||||||
7
app/global.d.ts
vendored
7
app/global.d.ts
vendored
@@ -26,6 +26,13 @@ declare interface Window {
|
|||||||
isPermissionGranted(): Promise<boolean>;
|
isPermissionGranted(): Promise<boolean>;
|
||||||
sendNotification(options: string | Options): void;
|
sendNotification(options: string | Options): void;
|
||||||
};
|
};
|
||||||
|
updater: {
|
||||||
|
checkUpdate(): Promise<UpdateResult>;
|
||||||
|
installUpdate(): Promise<void>;
|
||||||
|
onUpdaterEvent(
|
||||||
|
handler: (status: UpdateStatusResult) => void,
|
||||||
|
): Promise<UnlistenFn>;
|
||||||
|
};
|
||||||
http: {
|
http: {
|
||||||
fetch<T>(
|
fetch<T>(
|
||||||
url: string,
|
url: string,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ const cn = {
|
|||||||
Clear: "清除聊天",
|
Clear: "清除聊天",
|
||||||
Settings: "对话设置",
|
Settings: "对话设置",
|
||||||
UploadImage: "上传图片",
|
UploadImage: "上传图片",
|
||||||
|
UploadDocument: "上传文档",
|
||||||
},
|
},
|
||||||
Rename: "重命名对话",
|
Rename: "重命名对话",
|
||||||
Typing: "正在输入…",
|
Typing: "正在输入…",
|
||||||
@@ -205,6 +206,8 @@ const cn = {
|
|||||||
IsChecking: "正在检查更新...",
|
IsChecking: "正在检查更新...",
|
||||||
FoundUpdate: (x: string) => `发现新版本:${x}`,
|
FoundUpdate: (x: string) => `发现新版本:${x}`,
|
||||||
GoToUpdate: "前往更新",
|
GoToUpdate: "前往更新",
|
||||||
|
Success: "更新成功!",
|
||||||
|
Failed: "更新失败",
|
||||||
},
|
},
|
||||||
SendKey: "发送键",
|
SendKey: "发送键",
|
||||||
Theme: "主题",
|
Theme: "主题",
|
||||||
@@ -340,6 +343,32 @@ const cn = {
|
|||||||
SubTitle: "除默认地址外,必须包含 http(s)://",
|
SubTitle: "除默认地址外,必须包含 http(s)://",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Bedrock: {
|
||||||
|
Region: {
|
||||||
|
Title: "AWS Region",
|
||||||
|
SubTitle: "The AWS region where Bedrock service is located",
|
||||||
|
Placeholder: "us-west-2",
|
||||||
|
},
|
||||||
|
AccessKey: {
|
||||||
|
Title: "AWS Access Key ID",
|
||||||
|
SubTitle: "Your AWS access key ID for Bedrock service",
|
||||||
|
Placeholder: "AKIA...",
|
||||||
|
},
|
||||||
|
SecretKey: {
|
||||||
|
Title: "AWS Secret Access Key",
|
||||||
|
SubTitle: "Your AWS secret access key for Bedrock service",
|
||||||
|
Placeholder: "****",
|
||||||
|
},
|
||||||
|
SessionToken: {
|
||||||
|
Title: "AWS Session Token (Optional)",
|
||||||
|
SubTitle: "Your AWS session token if using temporary credentials",
|
||||||
|
Placeholder: "Optional session token",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "AWS Bedrock Endpoint",
|
||||||
|
SubTitle: "Custom endpoint for AWS Bedrock API. Default: ",
|
||||||
|
},
|
||||||
|
},
|
||||||
Azure: {
|
Azure: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "接口密钥",
|
Title: "接口密钥",
|
||||||
@@ -460,6 +489,17 @@ const cn = {
|
|||||||
SubTitle: "样例:",
|
SubTitle: "样例:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
XAI: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "接口密钥",
|
||||||
|
SubTitle: "使用自定义XAI API Key",
|
||||||
|
Placeholder: "XAI API Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "接口地址",
|
||||||
|
SubTitle: "样例:",
|
||||||
|
},
|
||||||
|
},
|
||||||
Stability: {
|
Stability: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "接口密钥",
|
Title: "接口密钥",
|
||||||
@@ -495,8 +535,8 @@ const cn = {
|
|||||||
|
|
||||||
Model: "模型 (model)",
|
Model: "模型 (model)",
|
||||||
CompressModel: {
|
CompressModel: {
|
||||||
Title: "压缩模型",
|
Title: "对话摘要模型",
|
||||||
SubTitle: "用于压缩历史记录的模型",
|
SubTitle: "用于压缩历史记录、生成对话标题的模型",
|
||||||
},
|
},
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "随机性 (temperature)",
|
Title: "随机性 (temperature)",
|
||||||
@@ -665,6 +705,10 @@ const cn = {
|
|||||||
Title: "启用Artifacts",
|
Title: "启用Artifacts",
|
||||||
SubTitle: "启用之后可以直接渲染HTML页面",
|
SubTitle: "启用之后可以直接渲染HTML页面",
|
||||||
},
|
},
|
||||||
|
CodeFold: {
|
||||||
|
Title: "启用代码折叠",
|
||||||
|
SubTitle: "启用之后可以自动折叠/展开过长的代码块",
|
||||||
|
},
|
||||||
Share: {
|
Share: {
|
||||||
Title: "分享此面具",
|
Title: "分享此面具",
|
||||||
SubTitle: "生成此面具的直达链接",
|
SubTitle: "生成此面具的直达链接",
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const en: LocaleType = {
|
|||||||
Clear: "Clear Context",
|
Clear: "Clear Context",
|
||||||
Settings: "Settings",
|
Settings: "Settings",
|
||||||
UploadImage: "Upload Images",
|
UploadImage: "Upload Images",
|
||||||
|
UploadDocument: "Upload Documents",
|
||||||
},
|
},
|
||||||
Rename: "Rename Chat",
|
Rename: "Rename Chat",
|
||||||
Typing: "Typing…",
|
Typing: "Typing…",
|
||||||
@@ -207,6 +208,8 @@ const en: LocaleType = {
|
|||||||
IsChecking: "Checking update...",
|
IsChecking: "Checking update...",
|
||||||
FoundUpdate: (x: string) => `Found new version: ${x}`,
|
FoundUpdate: (x: string) => `Found new version: ${x}`,
|
||||||
GoToUpdate: "Update",
|
GoToUpdate: "Update",
|
||||||
|
Success: "Update Successful.",
|
||||||
|
Failed: "Update Failed.",
|
||||||
},
|
},
|
||||||
SendKey: "Send Key",
|
SendKey: "Send Key",
|
||||||
Theme: "Theme",
|
Theme: "Theme",
|
||||||
@@ -344,6 +347,32 @@ const en: LocaleType = {
|
|||||||
SubTitle: "Must start with http(s):// or use /api/openai as default",
|
SubTitle: "Must start with http(s):// or use /api/openai as default",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Bedrock: {
|
||||||
|
Region: {
|
||||||
|
Title: "AWS Region",
|
||||||
|
SubTitle: "The AWS region where Bedrock service is located",
|
||||||
|
Placeholder: "us-west-2",
|
||||||
|
},
|
||||||
|
AccessKey: {
|
||||||
|
Title: "AWS Access Key ID",
|
||||||
|
SubTitle: "Your AWS access key ID for Bedrock service",
|
||||||
|
Placeholder: "AKIA...",
|
||||||
|
},
|
||||||
|
SecretKey: {
|
||||||
|
Title: "AWS Secret Access Key",
|
||||||
|
SubTitle: "Your AWS secret access key for Bedrock service",
|
||||||
|
Placeholder: "****",
|
||||||
|
},
|
||||||
|
SessionToken: {
|
||||||
|
Title: "AWS Session Token (Optional)",
|
||||||
|
SubTitle: "Your AWS session token if using temporary credentials",
|
||||||
|
Placeholder: "Optional session token",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "AWS Bedrock Endpoint",
|
||||||
|
SubTitle: "Custom endpoint for AWS Bedrock API. Default: ",
|
||||||
|
},
|
||||||
|
},
|
||||||
Azure: {
|
Azure: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "Azure Api Key",
|
Title: "Azure Api Key",
|
||||||
@@ -444,6 +473,17 @@ const en: LocaleType = {
|
|||||||
SubTitle: "Example: ",
|
SubTitle: "Example: ",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
XAI: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "XAI API Key",
|
||||||
|
SubTitle: "Use a custom XAI API Key",
|
||||||
|
Placeholder: "XAI API Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint Address",
|
||||||
|
SubTitle: "Example: ",
|
||||||
|
},
|
||||||
|
},
|
||||||
Stability: {
|
Stability: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "Stability API Key",
|
Title: "Stability API Key",
|
||||||
@@ -500,8 +540,8 @@ const en: LocaleType = {
|
|||||||
|
|
||||||
Model: "Model",
|
Model: "Model",
|
||||||
CompressModel: {
|
CompressModel: {
|
||||||
Title: "Compression Model",
|
Title: "Summary Model",
|
||||||
SubTitle: "Model used to compress history",
|
SubTitle: "Model used to compress history and generate title",
|
||||||
},
|
},
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Temperature",
|
Title: "Temperature",
|
||||||
@@ -675,6 +715,11 @@ const en: LocaleType = {
|
|||||||
Title: "Enable Artifacts",
|
Title: "Enable Artifacts",
|
||||||
SubTitle: "Can render HTML page when enable artifacts.",
|
SubTitle: "Can render HTML page when enable artifacts.",
|
||||||
},
|
},
|
||||||
|
CodeFold: {
|
||||||
|
Title: "Enable CodeFold",
|
||||||
|
SubTitle:
|
||||||
|
"Automatically collapse/expand overly long code blocks when CodeFold is enabled",
|
||||||
|
},
|
||||||
Share: {
|
Share: {
|
||||||
Title: "Share This Mask",
|
Title: "Share This Mask",
|
||||||
SubTitle: "Generate a link to this mask",
|
SubTitle: "Generate a link to this mask",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
StoreKey,
|
StoreKey,
|
||||||
ApiPath,
|
ApiPath,
|
||||||
OPENAI_BASE_URL,
|
OPENAI_BASE_URL,
|
||||||
|
BEDROCK_BASE_URL,
|
||||||
ANTHROPIC_BASE_URL,
|
ANTHROPIC_BASE_URL,
|
||||||
GEMINI_BASE_URL,
|
GEMINI_BASE_URL,
|
||||||
BAIDU_BASE_URL,
|
BAIDU_BASE_URL,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
MOONSHOT_BASE_URL,
|
MOONSHOT_BASE_URL,
|
||||||
STABILITY_BASE_URL,
|
STABILITY_BASE_URL,
|
||||||
IFLYTEK_BASE_URL,
|
IFLYTEK_BASE_URL,
|
||||||
|
XAI_BASE_URL,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import { getHeaders } from "../client/api";
|
import { getHeaders } from "../client/api";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
@@ -25,6 +27,7 @@ let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
|||||||
const isApp = getClientConfig()?.buildMode === "export";
|
const isApp = getClientConfig()?.buildMode === "export";
|
||||||
|
|
||||||
const DEFAULT_OPENAI_URL = isApp ? OPENAI_BASE_URL : ApiPath.OpenAI;
|
const DEFAULT_OPENAI_URL = isApp ? OPENAI_BASE_URL : ApiPath.OpenAI;
|
||||||
|
const DEFAULT_BEDROCK_URL = isApp ? BEDROCK_BASE_URL : ApiPath.Bedrock;
|
||||||
|
|
||||||
const DEFAULT_GOOGLE_URL = isApp ? GEMINI_BASE_URL : ApiPath.Google;
|
const DEFAULT_GOOGLE_URL = isApp ? GEMINI_BASE_URL : ApiPath.Google;
|
||||||
|
|
||||||
@@ -44,6 +47,8 @@ const DEFAULT_STABILITY_URL = isApp ? STABILITY_BASE_URL : ApiPath.Stability;
|
|||||||
|
|
||||||
const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek;
|
const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek;
|
||||||
|
|
||||||
|
const DEFAULT_XAI_URL = isApp ? XAI_BASE_URL : ApiPath.XAI;
|
||||||
|
|
||||||
const DEFAULT_ACCESS_STATE = {
|
const DEFAULT_ACCESS_STATE = {
|
||||||
accessCode: "",
|
accessCode: "",
|
||||||
useCustomConfig: false,
|
useCustomConfig: false,
|
||||||
@@ -54,6 +59,13 @@ const DEFAULT_ACCESS_STATE = {
|
|||||||
openaiUrl: DEFAULT_OPENAI_URL,
|
openaiUrl: DEFAULT_OPENAI_URL,
|
||||||
openaiApiKey: "",
|
openaiApiKey: "",
|
||||||
|
|
||||||
|
// bedrock
|
||||||
|
awsRegion: "",
|
||||||
|
awsAccessKey: "",
|
||||||
|
awsSecretKey: "",
|
||||||
|
awsSessionToken: "",
|
||||||
|
awsCognitoUser: false,
|
||||||
|
|
||||||
// azure
|
// azure
|
||||||
azureUrl: "",
|
azureUrl: "",
|
||||||
azureApiKey: "",
|
azureApiKey: "",
|
||||||
@@ -101,6 +113,10 @@ const DEFAULT_ACCESS_STATE = {
|
|||||||
iflytekApiKey: "",
|
iflytekApiKey: "",
|
||||||
iflytekApiSecret: "",
|
iflytekApiSecret: "",
|
||||||
|
|
||||||
|
// xai
|
||||||
|
xaiUrl: DEFAULT_XAI_URL,
|
||||||
|
xaiApiKey: "",
|
||||||
|
|
||||||
// server config
|
// server config
|
||||||
needCode: true,
|
needCode: true,
|
||||||
hideUserApiKey: false,
|
hideUserApiKey: false,
|
||||||
@@ -134,6 +150,10 @@ export const useAccessStore = createPersistStore(
|
|||||||
return ensure(get(), ["openaiApiKey"]);
|
return ensure(get(), ["openaiApiKey"]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isValidBedrock() {
|
||||||
|
return ensure(get(), ["awsAccessKey", "awsSecretKey", "awsRegion"]);
|
||||||
|
},
|
||||||
|
|
||||||
isValidAzure() {
|
isValidAzure() {
|
||||||
return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]);
|
return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]);
|
||||||
},
|
},
|
||||||
@@ -169,12 +189,17 @@ export const useAccessStore = createPersistStore(
|
|||||||
return ensure(get(), ["iflytekApiKey"]);
|
return ensure(get(), ["iflytekApiKey"]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isValidXAI() {
|
||||||
|
return ensure(get(), ["xaiApiKey"]);
|
||||||
|
},
|
||||||
|
|
||||||
isAuthorized() {
|
isAuthorized() {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
|
|
||||||
// has token or has code or disabled access control
|
// has token or has code or disabled access control
|
||||||
return (
|
return (
|
||||||
this.isValidOpenAI() ||
|
this.isValidOpenAI() ||
|
||||||
|
this.isValidBedrock() ||
|
||||||
this.isValidAzure() ||
|
this.isValidAzure() ||
|
||||||
this.isValidGoogle() ||
|
this.isValidGoogle() ||
|
||||||
this.isValidAnthropic() ||
|
this.isValidAnthropic() ||
|
||||||
@@ -184,6 +209,7 @@ export const useAccessStore = createPersistStore(
|
|||||||
this.isValidTencent() ||
|
this.isValidTencent() ||
|
||||||
this.isValidMoonshot() ||
|
this.isValidMoonshot() ||
|
||||||
this.isValidIflytek() ||
|
this.isValidIflytek() ||
|
||||||
|
this.isValidXAI() ||
|
||||||
!this.enabledAccessControl() ||
|
!this.enabledAccessControl() ||
|
||||||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -372,22 +372,16 @@ export const useChatStore = createPersistStore(
|
|||||||
|
|
||||||
if (attachImages && attachImages.length > 0) {
|
if (attachImages && attachImages.length > 0) {
|
||||||
mContent = [
|
mContent = [
|
||||||
{
|
...(userContent
|
||||||
type: "text",
|
? [{ type: "text" as const, text: userContent }]
|
||||||
text: userContent,
|
: []),
|
||||||
},
|
...attachImages.map((url) => ({
|
||||||
|
type: "image_url" as const,
|
||||||
|
image_url: { url },
|
||||||
|
})),
|
||||||
];
|
];
|
||||||
mContent = mContent.concat(
|
|
||||||
attachImages.map((url) => {
|
|
||||||
return {
|
|
||||||
type: "image_url",
|
|
||||||
image_url: {
|
|
||||||
url: url,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let userMessage: ChatMessage = createMessage({
|
let userMessage: ChatMessage = createMessage({
|
||||||
role: "user",
|
role: "user",
|
||||||
content: mContent,
|
content: mContent,
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export const DEFAULT_CONFIG = {
|
|||||||
|
|
||||||
enableArtifacts: true, // show artifacts config
|
enableArtifacts: true, // show artifacts config
|
||||||
|
|
||||||
|
enableCodeFold: true, // code fold config
|
||||||
|
|
||||||
disablePromptHint: false,
|
disablePromptHint: false,
|
||||||
|
|
||||||
dontShowMaskSplashScreen: false, // dont show splash screen when create chat
|
dontShowMaskSplashScreen: false, // dont show splash screen when create chat
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type Mask = {
|
|||||||
builtin: boolean;
|
builtin: boolean;
|
||||||
plugin?: string[];
|
plugin?: string[];
|
||||||
enableArtifacts?: boolean;
|
enableArtifacts?: boolean;
|
||||||
|
enableCodeFold?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_MASK_STATE = {
|
export const DEFAULT_MASK_STATE = {
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export const usePromptStore = createPersistStore(
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROMPT_URL = "./prompts.json";
|
const PROMPT_URL = "./prompts.json";
|
||||||
|
|
||||||
type PromptList = Array<[string, string]>;
|
type PromptList = Array<[string, string]>;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from "../constant";
|
} from "../constant";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
|
import { clientUpdate } from "../utils";
|
||||||
import ChatGptIcon from "../icons/chatgpt.png";
|
import ChatGptIcon from "../icons/chatgpt.png";
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
import { ClientApi } from "../client/api";
|
import { ClientApi } from "../client/api";
|
||||||
@@ -119,6 +120,7 @@ export const useUpdateStore = createPersistStore(
|
|||||||
icon: `${ChatGptIcon.src}`,
|
icon: `${ChatGptIcon.src}`,
|
||||||
sound: "Default",
|
sound: "Default",
|
||||||
});
|
});
|
||||||
|
clientUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
40
app/utils.ts
40
app/utils.ts
@@ -285,6 +285,12 @@ export function showPlugins(provider: ServiceProvider, model: string) {
|
|||||||
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
|
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (provider == ServiceProvider.Bedrock && !model.includes("claude-2")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (provider == ServiceProvider.Google && !model.includes("vision")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,3 +389,37 @@ export function getOperationId(operation: {
|
|||||||
`${operation.method.toUpperCase()}${operation.path.replaceAll("/", "_")}`
|
`${operation.method.toUpperCase()}${operation.path.replaceAll("/", "_")}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clientUpdate() {
|
||||||
|
// this a wild for updating client app
|
||||||
|
return window.__TAURI__?.updater
|
||||||
|
.checkUpdate()
|
||||||
|
.then((updateResult) => {
|
||||||
|
if (updateResult.shouldUpdate) {
|
||||||
|
window.__TAURI__?.updater
|
||||||
|
.installUpdate()
|
||||||
|
.then((result) => {
|
||||||
|
showToast(Locale.Settings.Update.Success);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("[Install Update Error]", e);
|
||||||
|
showToast(Locale.Settings.Update.Failed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("[Check Update Error]", e);
|
||||||
|
showToast(Locale.Settings.Update.Failed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb
|
||||||
|
export function semverCompare(a: string, b: string) {
|
||||||
|
if (a.startsWith(b + "-")) return -1;
|
||||||
|
if (b.startsWith(a + "-")) return 1;
|
||||||
|
return a.localeCompare(b, undefined, {
|
||||||
|
numeric: true,
|
||||||
|
sensitivity: "case",
|
||||||
|
caseFirst: "upper",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export function stream(
|
|||||||
return e.toString();
|
return e.toString();
|
||||||
})
|
})
|
||||||
.then((content) => ({
|
.then((content) => ({
|
||||||
|
name: tool.function.name,
|
||||||
role: "tool",
|
role: "tool",
|
||||||
content,
|
content,
|
||||||
tool_call_id: tool.id,
|
tool_call_id: tool.id,
|
||||||
|
|||||||
@@ -100,7 +100,8 @@ export function fetch(url: string, options?: RequestInit): Promise<any> {
|
|||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error("stream error", e);
|
console.error("stream error", e);
|
||||||
throw e;
|
// throw e;
|
||||||
|
return new Response("", { status: 599 });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return window.fetch(url, options);
|
return window.fetch(url, options);
|
||||||
|
|||||||
21
jest.config.ts
Normal file
21
jest.config.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Config } from "jest";
|
||||||
|
import nextJest from "next/jest.js";
|
||||||
|
|
||||||
|
const createJestConfig = nextJest({
|
||||||
|
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||||
|
dir: "./",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add any custom config to be passed to Jest
|
||||||
|
const config: Config = {
|
||||||
|
coverageProvider: "v8",
|
||||||
|
testEnvironment: "jsdom",
|
||||||
|
testMatch: ["**/*.test.js", "**/*.test.ts", "**/*.test.jsx", "**/*.test.tsx"],
|
||||||
|
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
|
||||||
|
moduleNameMapper: {
|
||||||
|
"^@/(.*)$": "<rootDir>/$1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||||
|
export default createJestConfig(config);
|
||||||
24
jest.setup.ts
Normal file
24
jest.setup.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
|
||||||
|
global.fetch = jest.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: () => Promise.resolve({}),
|
||||||
|
headers: new Headers(),
|
||||||
|
redirected: false,
|
||||||
|
statusText: "OK",
|
||||||
|
type: "basic",
|
||||||
|
url: "",
|
||||||
|
clone: function () {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
body: null,
|
||||||
|
bodyUsed: false,
|
||||||
|
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
|
||||||
|
blob: () => Promise.resolve(new Blob()),
|
||||||
|
formData: () => Promise.resolve(new FormData()),
|
||||||
|
text: () => Promise.resolve(""),
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -94,8 +94,12 @@ if (mode !== "export") {
|
|||||||
source: "/sharegpt",
|
source: "/sharegpt",
|
||||||
destination: "https://sharegpt.com/api/conversations",
|
destination: "https://sharegpt.com/api/conversations",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/api/proxy/alibaba/:path*",
|
||||||
|
destination: "https://dashscope.aliyuncs.com/api/:path*",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
beforeFiles: ret,
|
beforeFiles: ret,
|
||||||
};
|
};
|
||||||
|
|||||||
18
package.json
18
package.json
@@ -15,7 +15,9 @@
|
|||||||
"app:build": "yarn mask && yarn tauri build",
|
"app:build": "yarn mask && yarn tauri build",
|
||||||
"prompts": "node ./scripts/fetch-prompts.mjs",
|
"prompts": "node ./scripts/fetch-prompts.mjs",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
|
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
|
||||||
|
"test": "jest --watch",
|
||||||
|
"test:ci": "jest --ci"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortaine/fetch-event-source": "^3.0.6",
|
"@fortaine/fetch-event-source": "^3.0.6",
|
||||||
@@ -31,8 +33,8 @@
|
|||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mermaid": "^10.6.1",
|
|
||||||
"markdown-to-txt": "^2.0.1",
|
"markdown-to-txt": "^2.0.1",
|
||||||
|
"mermaid": "^10.6.1",
|
||||||
"nanoid": "^5.0.3",
|
"nanoid": "^5.0.3",
|
||||||
"next": "^14.1.1",
|
"next": "^14.1.1",
|
||||||
"node-fetch": "^3.3.1",
|
"node-fetch": "^3.3.1",
|
||||||
@@ -49,11 +51,16 @@
|
|||||||
"sass": "^1.59.2",
|
"sass": "^1.59.2",
|
||||||
"spark-md5": "^3.0.2",
|
"spark-md5": "^3.0.2",
|
||||||
"use-debounce": "^9.0.4",
|
"use-debounce": "^9.0.4",
|
||||||
"zustand": "^4.3.8"
|
"zustand": "^4.3.8",
|
||||||
|
"@aws-sdk/client-bedrock-runtime": "^3.679.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/api": "^1.6.0",
|
"@tauri-apps/api": "^1.6.0",
|
||||||
"@tauri-apps/cli": "1.5.11",
|
"@tauri-apps/cli": "1.5.11",
|
||||||
|
"@testing-library/dom": "^10.4.0",
|
||||||
|
"@testing-library/jest-dom": "^6.6.2",
|
||||||
|
"@testing-library/react": "^16.0.0",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
@@ -69,8 +76,11 @@
|
|||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unused-imports": "^3.2.0",
|
"eslint-plugin-unused-imports": "^3.2.0",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"lint-staged": "^13.2.2",
|
"lint-staged": "^13.2.2",
|
||||||
"prettier": "^3.0.2",
|
"prettier": "^3.0.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.16.0",
|
"tsx": "^4.16.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"watch": "^1.0.2",
|
"watch": "^1.0.2",
|
||||||
@@ -80,4 +90,4 @@
|
|||||||
"lint-staged/yaml": "^2.2.2"
|
"lint-staged/yaml": "^2.2.2"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.19"
|
"packageManager": "yarn@1.22.19"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,11 +119,22 @@ pub async fn stream_fetch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("Error response: {:?}", err.source().expect("REASON").to_string());
|
let error: String = err.source()
|
||||||
|
.map(|e| e.to_string())
|
||||||
|
.unwrap_or_else(|| "Unknown error occurred".to_string());
|
||||||
|
println!("Error response: {:?}", error);
|
||||||
|
tauri::async_runtime::spawn( async move {
|
||||||
|
if let Err(e) = window.emit(event_name, ChunkPayload{ request_id, chunk: error.into() }) {
|
||||||
|
println!("Failed to emit chunk payload: {:?}", e);
|
||||||
|
}
|
||||||
|
if let Err(e) = window.emit(event_name, EndPayload{ request_id, status: 0 }) {
|
||||||
|
println!("Failed to emit end payload: {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
StreamResponse {
|
StreamResponse {
|
||||||
request_id,
|
request_id,
|
||||||
status: 599,
|
status: 599,
|
||||||
status_text: err.source().expect("REASON").to_string(),
|
status_text: "Error".to_string(),
|
||||||
headers: HashMap::new(),
|
headers: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "NextChat",
|
"productName": "NextChat",
|
||||||
"version": "2.15.4"
|
"version": "2.15.6"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/releases/latest/download/latest.json"
|
"https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/releases/latest/download/latest.json"
|
||||||
],
|
],
|
||||||
"dialog": false,
|
"dialog": true,
|
||||||
"windows": {
|
"windows": {
|
||||||
"installMode": "passive"
|
"installMode": "passive"
|
||||||
},
|
},
|
||||||
|
|||||||
9
test/sum-module.test.ts
Normal file
9
test/sum-module.test.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
function sum(a: number, b: number) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("sum module", () => {
|
||||||
|
test("adds 1 + 2 to equal 3", () => {
|
||||||
|
expect(sum(1, 2)).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user