mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-03-07 12:44:26 +08:00
Compare commits
35 Commits
v2.15.5
...
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 |
@@ -66,4 +66,9 @@ ANTHROPIC_API_VERSION=
|
||||
ANTHROPIC_URL=
|
||||
|
||||
### (optional)
|
||||
WHITE_WEBDAV_ENDPOINTS=
|
||||
WHITE_WEBDAV_ENDPOINTS=
|
||||
|
||||
### bedrock (optional)
|
||||
AWS_REGION=
|
||||
AWS_ACCESS_KEY=
|
||||
AWS_SECRET_KEY=
|
||||
@@ -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
|
||||
[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)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ApiPath } from "@/app/constant";
|
||||
import { NextRequest } from "next/server";
|
||||
import { handle as openaiHandler } from "../../openai";
|
||||
import { handle as bedrockHandler } from "../../bedrock";
|
||||
import { handle as azureHandler } from "../../azure";
|
||||
import { handle as googleHandler } from "../../google";
|
||||
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 stabilityHandler } from "../../stability";
|
||||
import { handle as iflytekHandler } from "../../iflytek";
|
||||
import { handle as xaiHandler } from "../../xai";
|
||||
import { handle as proxyHandler } from "../../proxy";
|
||||
|
||||
async function handle(
|
||||
@@ -19,12 +21,15 @@ async function handle(
|
||||
const apiPath = `/api/${params.provider}`;
|
||||
console.log(`[${params.provider} Route] params `, params);
|
||||
switch (apiPath) {
|
||||
case ApiPath.Bedrock:
|
||||
return bedrockHandler(req, { params });
|
||||
case ApiPath.Azure:
|
||||
return azureHandler(req, { params });
|
||||
case ApiPath.Google:
|
||||
return googleHandler(req, { params });
|
||||
case ApiPath.Anthropic:
|
||||
return anthropicHandler(req, { params });
|
||||
|
||||
case ApiPath.Baidu:
|
||||
return baiduHandler(req, { params });
|
||||
case ApiPath.ByteDance:
|
||||
@@ -38,6 +43,8 @@ async function handle(
|
||||
return stabilityHandler(req, { params });
|
||||
case ApiPath.Iflytek:
|
||||
return iflytekHandler(req, { params });
|
||||
case ApiPath.XAI:
|
||||
return xaiHandler(req, { params });
|
||||
case ApiPath.OpenAI:
|
||||
return openaiHandler(req, { params });
|
||||
default:
|
||||
|
||||
@@ -52,6 +52,28 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
||||
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 (!apiKey) {
|
||||
@@ -92,6 +114,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
||||
systemApiKey =
|
||||
serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
|
||||
break;
|
||||
case ModelProvider.XAI:
|
||||
systemApiKey = serverConfig.xaiApiKey;
|
||||
break;
|
||||
case ModelProvider.GPT:
|
||||
default:
|
||||
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,
|
||||
} from "../store";
|
||||
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
|
||||
import { BedrockApi } from "./platforms/bedrock";
|
||||
import { GeminiProApi } from "./platforms/google";
|
||||
import { ClaudeApi } from "./platforms/anthropic";
|
||||
import { ErnieApi } from "./platforms/baidu";
|
||||
@@ -20,6 +21,7 @@ import { QwenApi } from "./platforms/alibaba";
|
||||
import { HunyuanApi } from "./platforms/tencent";
|
||||
import { MoonshotApi } from "./platforms/moonshot";
|
||||
import { SparkApi } from "./platforms/iflytek";
|
||||
import { XAIApi } from "./platforms/xai";
|
||||
|
||||
export const ROLES = ["system", "user", "assistant"] as const;
|
||||
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 interface MultimodalContent {
|
||||
type: "text" | "image_url";
|
||||
type: "text" | "image_url" | "document";
|
||||
text?: string;
|
||||
image_url?: {
|
||||
url: string;
|
||||
};
|
||||
document?: {
|
||||
format: string;
|
||||
name: string;
|
||||
source: {
|
||||
bytes: string;
|
||||
media_type?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface RequestMessage {
|
||||
@@ -128,6 +138,9 @@ export class ClientApi {
|
||||
|
||||
constructor(provider: ModelProvider = ModelProvider.GPT) {
|
||||
switch (provider) {
|
||||
case ModelProvider.Bedrock:
|
||||
this.llm = new BedrockApi();
|
||||
break;
|
||||
case ModelProvider.GeminiPro:
|
||||
this.llm = new GeminiProApi();
|
||||
break;
|
||||
@@ -152,6 +165,9 @@ export class ClientApi {
|
||||
case ModelProvider.Iflytek:
|
||||
this.llm = new SparkApi();
|
||||
break;
|
||||
case ModelProvider.XAI:
|
||||
this.llm = new XAIApi();
|
||||
break;
|
||||
default:
|
||||
this.llm = new ChatGPTApi();
|
||||
}
|
||||
@@ -231,6 +247,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
|
||||
function getConfig() {
|
||||
const modelConfig = chatStore.currentSession().mask.modelConfig;
|
||||
const isBedrock = modelConfig.providerName === ServiceProvider.Bedrock;
|
||||
const isGoogle = modelConfig.providerName === ServiceProvider.Google;
|
||||
const isAzure = modelConfig.providerName === ServiceProvider.Azure;
|
||||
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
|
||||
@@ -239,9 +256,12 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
|
||||
const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
|
||||
const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
|
||||
const isXAI = modelConfig.providerName === ServiceProvider.XAI;
|
||||
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
||||
const apiKey = isGoogle
|
||||
? accessStore.googleApiKey
|
||||
: isBedrock
|
||||
? accessStore.awsAccessKey // Use AWS access key for Bedrock
|
||||
: isAzure
|
||||
? accessStore.azureApiKey
|
||||
: isAnthropic
|
||||
@@ -252,12 +272,15 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
? accessStore.alibabaApiKey
|
||||
: isMoonshot
|
||||
? accessStore.moonshotApiKey
|
||||
: isXAI
|
||||
? accessStore.xaiApiKey
|
||||
: isIflytek
|
||||
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
|
||||
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
|
||||
: ""
|
||||
: accessStore.openaiApiKey;
|
||||
return {
|
||||
isBedrock,
|
||||
isGoogle,
|
||||
isAzure,
|
||||
isAnthropic,
|
||||
@@ -266,6 +289,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
isAlibaba,
|
||||
isMoonshot,
|
||||
isIflytek,
|
||||
isXAI,
|
||||
apiKey,
|
||||
isEnabledAccessControl,
|
||||
};
|
||||
@@ -278,10 +302,13 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
? "x-api-key"
|
||||
: isGoogle
|
||||
? "x-goog-api-key"
|
||||
: isBedrock
|
||||
? "x-api-key"
|
||||
: "Authorization";
|
||||
}
|
||||
|
||||
const {
|
||||
isBedrock,
|
||||
isGoogle,
|
||||
isAzure,
|
||||
isAnthropic,
|
||||
@@ -294,17 +321,30 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
|
||||
const authHeader = getAuthHeader();
|
||||
|
||||
const bearerToken = getBearerToken(
|
||||
apiKey,
|
||||
isAzure || isAnthropic || isGoogle,
|
||||
);
|
||||
if (isBedrock) {
|
||||
// 简单加密 AWS credentials
|
||||
const encrypt = (str: string) =>
|
||||
Buffer.from(str.split("").reverse().join("")).toString("base64");
|
||||
|
||||
if (bearerToken) {
|
||||
headers[authHeader] = bearerToken;
|
||||
} else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
|
||||
headers["Authorization"] = getBearerToken(
|
||||
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||
headers["X-Region"] = encrypt(accessStore.awsRegion);
|
||||
headers["X-Access-Key"] = encrypt(accessStore.awsAccessKey);
|
||||
headers["X-Secret-Key"] = encrypt(accessStore.awsSecretKey);
|
||||
if (accessStore.awsSessionToken) {
|
||||
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;
|
||||
@@ -312,6 +352,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
|
||||
export function getClientApi(provider: ServiceProvider): ClientApi {
|
||||
switch (provider) {
|
||||
case ServiceProvider.Bedrock:
|
||||
return new ClientApi(ModelProvider.Bedrock);
|
||||
case ServiceProvider.Google:
|
||||
return new ClientApi(ModelProvider.GeminiPro);
|
||||
case ServiceProvider.Anthropic:
|
||||
@@ -328,6 +370,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
|
||||
return new ClientApi(ModelProvider.Moonshot);
|
||||
case ServiceProvider.Iflytek:
|
||||
return new ClientApi(ModelProvider.Iflytek);
|
||||
case ServiceProvider.XAI:
|
||||
return new ClientApi(ModelProvider.XAI);
|
||||
default:
|
||||
return new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
|
||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export type MultiBlockContent = {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,7 +192,10 @@ export class GeminiProApi implements LLMApi {
|
||||
requestPayload,
|
||||
getHeaders(),
|
||||
// @ts-ignore
|
||||
[{ functionDeclarations: tools.map((tool) => tool.function) }],
|
||||
tools.length > 0
|
||||
? // @ts-ignore
|
||||
[{ functionDeclarations: tools.map((tool) => tool.function) }]
|
||||
: [],
|
||||
funcs,
|
||||
controller,
|
||||
// parseSSE
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export class MoonshotApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
isVisionModel,
|
||||
isDalle3 as _isDalle3,
|
||||
} from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,17 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--delay: 0.5s;
|
||||
width: var(--full-width);
|
||||
@@ -393,8 +404,8 @@
|
||||
|
||||
button {
|
||||
padding: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Specific styles for iOS devices */
|
||||
@media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
ByteDance,
|
||||
Alibaba,
|
||||
Moonshot,
|
||||
XAI,
|
||||
Google,
|
||||
GoogleSafetySettingsThreshold,
|
||||
OPENAI_BASE_URL,
|
||||
@@ -962,7 +963,75 @@ export function Settings() {
|
||||
</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 ===
|
||||
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 ===
|
||||
ServiceProvider.Stability && (
|
||||
<>
|
||||
@@ -1642,6 +1750,7 @@ export function Settings() {
|
||||
</ListItem>
|
||||
|
||||
{openAIConfigComponent}
|
||||
{bedrockConfigComponent}
|
||||
{azureConfigComponent}
|
||||
{googleConfigComponent}
|
||||
{anthropicConfigComponent}
|
||||
@@ -1652,6 +1761,7 @@ export function Settings() {
|
||||
{moonshotConfigComponent}
|
||||
{stabilityConfigComponent}
|
||||
{lflytekConfigComponent}
|
||||
{XAIConfigComponent}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -12,6 +12,11 @@ declare global {
|
||||
BASE_URL?: string;
|
||||
OPENAI_ORG_ID?: string; // openai only
|
||||
|
||||
// bedrock only
|
||||
BEDROCK_REGION?: string;
|
||||
BEDROCK_API_KEY?: string;
|
||||
BEDROCK_API_SECRET?: string;
|
||||
|
||||
VERCEL?: string;
|
||||
BUILD_MODE?: "standalone" | "export";
|
||||
BUILD_APP?: string; // is building desktop app
|
||||
@@ -71,6 +76,10 @@ declare global {
|
||||
IFLYTEK_API_KEY?: string;
|
||||
IFLYTEK_API_SECRET?: string;
|
||||
|
||||
// xai only
|
||||
XAI_URL?: string;
|
||||
XAI_API_KEY?: string;
|
||||
|
||||
// custom template for preprocessing user input
|
||||
DEFAULT_INPUT_TEMPLATE?: string;
|
||||
}
|
||||
@@ -135,7 +144,7 @@ export const getServerSideConfig = () => {
|
||||
}
|
||||
|
||||
const isStability = !!process.env.STABILITY_API_KEY;
|
||||
|
||||
const isBedrock = !!process.env.BEDROCK_API_KEY;
|
||||
const isAzure = !!process.env.AZURE_URL;
|
||||
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
||||
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
||||
@@ -146,6 +155,7 @@ export const getServerSideConfig = () => {
|
||||
const isAlibaba = !!process.env.ALIBABA_API_KEY;
|
||||
const isMoonshot = !!process.env.MOONSHOT_API_KEY;
|
||||
const isIflytek = !!process.env.IFLYTEK_API_KEY;
|
||||
const isXAI = !!process.env.XAI_API_KEY;
|
||||
// const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
||||
// const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||
// const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
||||
@@ -163,6 +173,11 @@ export const getServerSideConfig = () => {
|
||||
apiKey: getApiKey(process.env.OPENAI_API_KEY),
|
||||
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,
|
||||
stabilityUrl: process.env.STABILITY_URL,
|
||||
stabilityApiKey: getApiKey(process.env.STABILITY_API_KEY),
|
||||
@@ -208,6 +223,10 @@ export const getServerSideConfig = () => {
|
||||
iflytekApiKey: process.env.IFLYTEK_API_KEY,
|
||||
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,
|
||||
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
|
||||
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 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 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 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 UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
|
||||
|
||||
@@ -47,6 +51,7 @@ export enum Path {
|
||||
|
||||
export enum ApiPath {
|
||||
Cors = "",
|
||||
Bedrock = "/api/bedrock",
|
||||
Azure = "/api/azure",
|
||||
OpenAI = "/api/openai",
|
||||
Anthropic = "/api/anthropic",
|
||||
@@ -59,6 +64,7 @@ export enum ApiPath {
|
||||
Iflytek = "/api/iflytek",
|
||||
Stability = "/api/stability",
|
||||
Artifacts = "/api/artifacts",
|
||||
XAI = "/api/xai",
|
||||
}
|
||||
|
||||
export enum SlotID {
|
||||
@@ -111,6 +117,8 @@ export enum ServiceProvider {
|
||||
Moonshot = "Moonshot",
|
||||
Stability = "Stability",
|
||||
Iflytek = "Iflytek",
|
||||
XAI = "XAI",
|
||||
Bedrock = "Bedrock",
|
||||
}
|
||||
|
||||
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
||||
@@ -124,6 +132,7 @@ export enum GoogleSafetySettingsThreshold {
|
||||
|
||||
export enum ModelProvider {
|
||||
Stability = "Stability",
|
||||
Bedrock = "Bedrock",
|
||||
GPT = "GPT",
|
||||
GeminiPro = "GeminiPro",
|
||||
Claude = "Claude",
|
||||
@@ -133,6 +142,7 @@ export enum ModelProvider {
|
||||
Hunyuan = "Hunyuan",
|
||||
Moonshot = "Moonshot",
|
||||
Iflytek = "Iflytek",
|
||||
XAI = "XAI",
|
||||
}
|
||||
|
||||
export const Stability = {
|
||||
@@ -215,6 +225,15 @@ export const Iflytek = {
|
||||
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_SYSTEM_TEMPLATE = `
|
||||
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
|
||||
@@ -294,6 +313,22 @@ const openaiModels = [
|
||||
"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 = [
|
||||
"gemini-1.0-pro",
|
||||
"gemini-1.5-pro-latest",
|
||||
@@ -309,6 +344,9 @@ const anthropicModels = [
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-5-sonnet-latest",
|
||||
"claude-3-opus-latest",
|
||||
];
|
||||
|
||||
const baiduModels = [
|
||||
@@ -364,6 +402,8 @@ const iflytekModels = [
|
||||
"4.0Ultra",
|
||||
];
|
||||
|
||||
const xAIModes = ["grok-beta"];
|
||||
|
||||
let seq = 1000; // 内置的模型序号生成器从1000开始
|
||||
export const DEFAULT_MODELS = [
|
||||
...openaiModels.map((name) => ({
|
||||
@@ -476,6 +516,28 @@ export const DEFAULT_MODELS = [
|
||||
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;
|
||||
|
||||
export const CHAT_PAGE_SIZE = 15;
|
||||
|
||||
@@ -81,6 +81,7 @@ const cn = {
|
||||
Clear: "清除聊天",
|
||||
Settings: "对话设置",
|
||||
UploadImage: "上传图片",
|
||||
UploadDocument: "上传文档",
|
||||
},
|
||||
Rename: "重命名对话",
|
||||
Typing: "正在输入…",
|
||||
@@ -342,6 +343,32 @@ const cn = {
|
||||
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: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
@@ -462,6 +489,17 @@ const cn = {
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
},
|
||||
XAI: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
SubTitle: "使用自定义XAI API Key",
|
||||
Placeholder: "XAI API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
},
|
||||
Stability: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
|
||||
@@ -82,6 +82,7 @@ const en: LocaleType = {
|
||||
Clear: "Clear Context",
|
||||
Settings: "Settings",
|
||||
UploadImage: "Upload Images",
|
||||
UploadDocument: "Upload Documents",
|
||||
},
|
||||
Rename: "Rename Chat",
|
||||
Typing: "Typing…",
|
||||
@@ -346,6 +347,32 @@ const en: LocaleType = {
|
||||
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: {
|
||||
ApiKey: {
|
||||
Title: "Azure Api Key",
|
||||
@@ -446,6 +473,17 @@ const en: LocaleType = {
|
||||
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: {
|
||||
ApiKey: {
|
||||
Title: "Stability API Key",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
StoreKey,
|
||||
ApiPath,
|
||||
OPENAI_BASE_URL,
|
||||
BEDROCK_BASE_URL,
|
||||
ANTHROPIC_BASE_URL,
|
||||
GEMINI_BASE_URL,
|
||||
BAIDU_BASE_URL,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
MOONSHOT_BASE_URL,
|
||||
STABILITY_BASE_URL,
|
||||
IFLYTEK_BASE_URL,
|
||||
XAI_BASE_URL,
|
||||
} from "../constant";
|
||||
import { getHeaders } from "../client/api";
|
||||
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 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;
|
||||
|
||||
@@ -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_XAI_URL = isApp ? XAI_BASE_URL : ApiPath.XAI;
|
||||
|
||||
const DEFAULT_ACCESS_STATE = {
|
||||
accessCode: "",
|
||||
useCustomConfig: false,
|
||||
@@ -54,6 +59,13 @@ const DEFAULT_ACCESS_STATE = {
|
||||
openaiUrl: DEFAULT_OPENAI_URL,
|
||||
openaiApiKey: "",
|
||||
|
||||
// bedrock
|
||||
awsRegion: "",
|
||||
awsAccessKey: "",
|
||||
awsSecretKey: "",
|
||||
awsSessionToken: "",
|
||||
awsCognitoUser: false,
|
||||
|
||||
// azure
|
||||
azureUrl: "",
|
||||
azureApiKey: "",
|
||||
@@ -101,6 +113,10 @@ const DEFAULT_ACCESS_STATE = {
|
||||
iflytekApiKey: "",
|
||||
iflytekApiSecret: "",
|
||||
|
||||
// xai
|
||||
xaiUrl: DEFAULT_XAI_URL,
|
||||
xaiApiKey: "",
|
||||
|
||||
// server config
|
||||
needCode: true,
|
||||
hideUserApiKey: false,
|
||||
@@ -134,6 +150,10 @@ export const useAccessStore = createPersistStore(
|
||||
return ensure(get(), ["openaiApiKey"]);
|
||||
},
|
||||
|
||||
isValidBedrock() {
|
||||
return ensure(get(), ["awsAccessKey", "awsSecretKey", "awsRegion"]);
|
||||
},
|
||||
|
||||
isValidAzure() {
|
||||
return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]);
|
||||
},
|
||||
@@ -169,12 +189,17 @@ export const useAccessStore = createPersistStore(
|
||||
return ensure(get(), ["iflytekApiKey"]);
|
||||
},
|
||||
|
||||
isValidXAI() {
|
||||
return ensure(get(), ["xaiApiKey"]);
|
||||
},
|
||||
|
||||
isAuthorized() {
|
||||
this.fetch();
|
||||
|
||||
// has token or has code or disabled access control
|
||||
return (
|
||||
this.isValidOpenAI() ||
|
||||
this.isValidBedrock() ||
|
||||
this.isValidAzure() ||
|
||||
this.isValidGoogle() ||
|
||||
this.isValidAnthropic() ||
|
||||
@@ -184,6 +209,7 @@ export const useAccessStore = createPersistStore(
|
||||
this.isValidTencent() ||
|
||||
this.isValidMoonshot() ||
|
||||
this.isValidIflytek() ||
|
||||
this.isValidXAI() ||
|
||||
!this.enabledAccessControl() ||
|
||||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||
);
|
||||
|
||||
@@ -285,6 +285,9 @@ export function showPlugins(provider: ServiceProvider, model: string) {
|
||||
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
|
||||
return true;
|
||||
}
|
||||
if (provider == ServiceProvider.Bedrock && !model.includes("claude-2")) {
|
||||
return true;
|
||||
}
|
||||
if (provider == ServiceProvider.Google && !model.includes("vision")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@ export function fetch(url: string, options?: RequestInit): Promise<any> {
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("stream error", e);
|
||||
throw e;
|
||||
// throw e;
|
||||
return new Response("", { status: 599 });
|
||||
});
|
||||
}
|
||||
return window.fetch(url, options);
|
||||
|
||||
@@ -1,2 +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",
|
||||
destination: "https://sharegpt.com/api/conversations",
|
||||
},
|
||||
{
|
||||
source: "/api/proxy/alibaba/:path*",
|
||||
destination: "https://dashscope.aliyuncs.com/api/:path*",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
return {
|
||||
beforeFiles: ret,
|
||||
};
|
||||
|
||||
10
package.json
10
package.json
@@ -33,8 +33,8 @@
|
||||
"html-to-image": "^1.11.11",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mermaid": "^10.6.1",
|
||||
"markdown-to-txt": "^2.0.1",
|
||||
"mermaid": "^10.6.1",
|
||||
"nanoid": "^5.0.3",
|
||||
"next": "^14.1.1",
|
||||
"node-fetch": "^3.3.1",
|
||||
@@ -51,14 +51,16 @@
|
||||
"sass": "^1.59.2",
|
||||
"spark-md5": "^3.0.2",
|
||||
"use-debounce": "^9.0.4",
|
||||
"zustand": "^4.3.8"
|
||||
"zustand": "^4.3.8",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.679.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/api": "^1.6.0",
|
||||
"@tauri-apps/cli": "1.5.11",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.2",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.11.30",
|
||||
|
||||
@@ -119,11 +119,22 @@ pub async fn stream_fetch(
|
||||
}
|
||||
}
|
||||
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 {
|
||||
request_id,
|
||||
status: 599,
|
||||
status_text: err.source().expect("REASON").to_string(),
|
||||
status_text: "Error".to_string(),
|
||||
headers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "NextChat",
|
||||
"version": "2.15.5"
|
||||
"version": "2.15.6"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
||||
85
yarn.lock
85
yarn.lock
@@ -27,6 +27,15 @@
|
||||
dependencies:
|
||||
"@babel/highlight" "^7.18.6"
|
||||
|
||||
"@babel/code-frame@^7.10.4":
|
||||
version "7.26.0"
|
||||
resolved "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.26.0.tgz#9374b5cd068d128dac0b94ff482594273b1c2815"
|
||||
integrity sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.25.9"
|
||||
js-tokens "^4.0.0"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
"@babel/code-frame@^7.12.13", "@babel/code-frame@^7.24.7":
|
||||
version "7.24.7"
|
||||
resolved "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465"
|
||||
@@ -394,6 +403,11 @@
|
||||
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db"
|
||||
integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
|
||||
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
|
||||
|
||||
"@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.21.0":
|
||||
version "7.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180"
|
||||
@@ -2093,13 +2107,26 @@
|
||||
"@tauri-apps/cli-win32-ia32-msvc" "1.5.11"
|
||||
"@tauri-apps/cli-win32-x64-msvc" "1.5.11"
|
||||
|
||||
"@testing-library/jest-dom@^6.4.8":
|
||||
version "6.4.8"
|
||||
resolved "https://registry.npmmirror.com/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz#9c435742b20c6183d4e7034f2b329d562c079daa"
|
||||
integrity sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==
|
||||
"@testing-library/dom@^10.4.0":
|
||||
version "10.4.0"
|
||||
resolved "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8"
|
||||
integrity sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.10.4"
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@types/aria-query" "^5.0.1"
|
||||
aria-query "5.3.0"
|
||||
chalk "^4.1.0"
|
||||
dom-accessibility-api "^0.5.9"
|
||||
lz-string "^1.5.0"
|
||||
pretty-format "^27.0.2"
|
||||
|
||||
"@testing-library/jest-dom@^6.6.2":
|
||||
version "6.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.6.2.tgz#8186aa9a07263adef9cc5a59a4772db8c31f4a5b"
|
||||
integrity sha512-P6GJD4yqc9jZLbe98j/EkyQDTPgqftohZF5FBkHY5BUERZmcf4HeO2k0XaefEg329ux2p21i1A1DmyQ1kKw2Jw==
|
||||
dependencies:
|
||||
"@adobe/css-tools" "^4.4.0"
|
||||
"@babel/runtime" "^7.9.2"
|
||||
aria-query "^5.0.0"
|
||||
chalk "^3.0.0"
|
||||
css.escape "^1.5.1"
|
||||
@@ -2144,6 +2171,11 @@
|
||||
resolved "https://registry.npmmirror.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
|
||||
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
|
||||
|
||||
"@types/aria-query@^5.0.1":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708"
|
||||
integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==
|
||||
|
||||
"@types/babel__core@^7.1.14":
|
||||
version "7.20.5"
|
||||
resolved "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
|
||||
@@ -2263,10 +2295,10 @@
|
||||
dependencies:
|
||||
"@types/istanbul-lib-report" "*"
|
||||
|
||||
"@types/jest@^29.5.13":
|
||||
version "29.5.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.13.tgz#8bc571659f401e6a719a7bf0dbcb8b78c71a8adc"
|
||||
integrity sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==
|
||||
"@types/jest@^29.5.14":
|
||||
version "29.5.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5"
|
||||
integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==
|
||||
dependencies:
|
||||
expect "^29.0.0"
|
||||
pretty-format "^29.0.0"
|
||||
@@ -2738,20 +2770,13 @@ argparse@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||
|
||||
aria-query@^5.0.0:
|
||||
aria-query@5.3.0, aria-query@^5.0.0, aria-query@^5.1.3:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
|
||||
integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==
|
||||
dependencies:
|
||||
dequal "^2.0.3"
|
||||
|
||||
aria-query@^5.1.3:
|
||||
version "5.1.3"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e"
|
||||
integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==
|
||||
dependencies:
|
||||
deep-equal "^2.0.5"
|
||||
|
||||
array-buffer-byte-length@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead"
|
||||
@@ -3081,7 +3106,7 @@ chalk@^3.0.0:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^4.0.0, chalk@^4.1.2:
|
||||
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
@@ -3877,6 +3902,11 @@ doctrine@^3.0.0:
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
dom-accessibility-api@^0.5.9:
|
||||
version "0.5.16"
|
||||
resolved "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
|
||||
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
|
||||
|
||||
dom-accessibility-api@^0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8"
|
||||
@@ -6052,6 +6082,11 @@ lru-cache@^6.0.0:
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
lz-string@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
|
||||
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
|
||||
|
||||
make-dir@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e"
|
||||
@@ -7018,6 +7053,15 @@ prettier@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.2.tgz#78fcecd6d870551aa5547437cdae39d4701dca5b"
|
||||
integrity sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==
|
||||
|
||||
pretty-format@^27.0.2:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
|
||||
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
ansi-styles "^5.0.0"
|
||||
react-is "^17.0.1"
|
||||
|
||||
pretty-format@^29.0.0, pretty-format@^29.7.0:
|
||||
version "29.7.0"
|
||||
resolved "https://registry.npmmirror.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812"
|
||||
@@ -7109,6 +7153,11 @@ react-is@^16.13.1, react-is@^16.7.0:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-is@^17.0.1:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-is@^18.0.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||
|
||||
Reference in New Issue
Block a user