mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-03-07 20:54:25 +08:00
Compare commits
36 Commits
feat-bt-do
...
1998cf5ced
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1998cf5ced | ||
|
|
1164e1bdf6 | ||
|
|
d55c752e1e | ||
|
|
e3c18bb123 | ||
|
|
00d6cb27f7 | ||
|
|
f532731e2a | ||
|
|
58837f6dec | ||
|
|
e49fe976d9 | ||
|
|
afbf5eb541 | ||
|
|
14f751965f | ||
|
|
0ec423389f | ||
|
|
820ab54e2d | ||
|
|
0f276f59bb | ||
|
|
a6c1eb27a8 | ||
|
|
0dc4071ccc | ||
|
|
4d3949718a | ||
|
|
aef535f1a7 | ||
|
|
686a80e727 | ||
|
|
e49466fa05 | ||
|
|
4b93370814 | ||
|
|
5733e3c588 | ||
|
|
44fc5b5cbf | ||
|
|
fc391168e9 | ||
|
|
2d3f7c922f | ||
|
|
fe8cca3730 | ||
|
|
adf97c6d8b | ||
|
|
dca4a0e48f | ||
|
|
722c28839f | ||
|
|
7c466c9b9c | ||
|
|
b0d28eb77e | ||
|
|
ff356f0c8c | ||
|
|
801dc412f9 | ||
|
|
064e964d75 | ||
|
|
47fb40d572 | ||
|
|
9e18cc260b | ||
|
|
03268ce4d8 |
@@ -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=
|
||||
10
README.md
10
README.md
@@ -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://img.shields.io/badge/BT_Deploy-Install-20a53a" alt="Open in Gitpod" height="30">](https://www.bt.cn/new/download.html)
|
||||
[<img src="https://vercel.com/button" alt="Deploy on Vercel" 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="BT Deply Install" height="30">](https://www.bt.cn/new/download.html) [<img src="https://svgshare.com/i/1AVg.svg" alt="Deploy to Alibaba Cloud" height="30">](https://computenest.aliyun.com/market/service-f1c9b75e59814dc49d52)
|
||||
|
||||
[<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="60" width="288" >](https://monica.im/?utm=nxcrp)
|
||||
|
||||
@@ -301,6 +301,14 @@ iflytek Api Key.
|
||||
|
||||
iflytek Api Secret.
|
||||
|
||||
### `CHATGLM_API_KEY` (optional)
|
||||
|
||||
ChatGLM Api Key.
|
||||
|
||||
### `CHATGLM_URL` (optional)
|
||||
|
||||
ChatGLM Api Url.
|
||||
|
||||
### `HIDE_USER_API_KEY` (optional)
|
||||
|
||||
> Default: Empty
|
||||
|
||||
@@ -184,6 +184,13 @@ ByteDance Api Url.
|
||||
|
||||
讯飞星火Api Secret.
|
||||
|
||||
### `CHATGLM_API_KEY` (可选)
|
||||
|
||||
ChatGLM Api Key.
|
||||
|
||||
### `CHATGLM_URL` (可选)
|
||||
|
||||
ChatGLM Api Url.
|
||||
|
||||
|
||||
### `HIDE_USER_API_KEY` (可选)
|
||||
|
||||
@@ -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";
|
||||
@@ -21,12 +22,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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
265
app/api/bedrock.ts
Normal file
265
app/api/bedrock.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import { getServerSideConfig } from "../config/server";
|
||||
import { prettyObject } from "../utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
BedrockRuntimeClient,
|
||||
ConverseStreamCommand,
|
||||
ConverseStreamCommandInput,
|
||||
Message,
|
||||
ContentBlock,
|
||||
ConverseStreamOutput,
|
||||
} from "@aws-sdk/client-bedrock-runtime";
|
||||
|
||||
const ALLOWED_PATH = new Set(["converse"]);
|
||||
|
||||
function decrypt(str: string): string {
|
||||
try {
|
||||
return Buffer.from(str, "base64").toString().split("").reverse().join("");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConverseRequest {
|
||||
modelId: string;
|
||||
messages: {
|
||||
role: "user" | "assistant" | "system";
|
||||
content: string | any[];
|
||||
}[];
|
||||
inferenceConfig?: {
|
||||
maxTokens?: number;
|
||||
temperature?: number;
|
||||
topP?: number;
|
||||
stopSequences?: string[];
|
||||
};
|
||||
tools?: {
|
||||
name: string;
|
||||
description?: string;
|
||||
input_schema: any;
|
||||
}[];
|
||||
}
|
||||
|
||||
function supportsToolUse(modelId: string): boolean {
|
||||
// llama和mistral模型不支持工具调用
|
||||
return modelId.toLowerCase().includes("claude-3");
|
||||
}
|
||||
|
||||
function formatRequestBody(
|
||||
request: ConverseRequest,
|
||||
): ConverseStreamCommandInput {
|
||||
const messages: Message[] = request.messages.map((msg) => ({
|
||||
role: msg.role === "system" ? "user" : msg.role,
|
||||
content: Array.isArray(msg.content)
|
||||
? msg.content.map((item) => {
|
||||
if (item.type === "tool_use") {
|
||||
return {
|
||||
toolUse: {
|
||||
toolUseId: item.id,
|
||||
name: item.name,
|
||||
input: item.input || "{}",
|
||||
},
|
||||
} as ContentBlock;
|
||||
}
|
||||
if (item.type === "tool_result") {
|
||||
return {
|
||||
toolResult: {
|
||||
toolUseId: item.tool_use_id,
|
||||
content: [{ text: item.content || ";" }],
|
||||
status: "success",
|
||||
},
|
||||
} as ContentBlock;
|
||||
}
|
||||
if (item.type === "text") {
|
||||
return { text: item.text || ";" } as ContentBlock;
|
||||
}
|
||||
if (item.type === "image") {
|
||||
return {
|
||||
image: {
|
||||
format: item.source.media_type.split("/")[1] as
|
||||
| "png"
|
||||
| "jpeg"
|
||||
| "gif"
|
||||
| "webp",
|
||||
source: {
|
||||
bytes: Uint8Array.from(
|
||||
Buffer.from(item.source.data, "base64"),
|
||||
),
|
||||
},
|
||||
},
|
||||
} as ContentBlock;
|
||||
}
|
||||
return { text: ";" } as ContentBlock;
|
||||
})
|
||||
: [{ text: msg.content || ";" } as ContentBlock],
|
||||
}));
|
||||
|
||||
const input: ConverseStreamCommandInput = {
|
||||
modelId: request.modelId,
|
||||
messages,
|
||||
...(request.inferenceConfig && {
|
||||
inferenceConfig: request.inferenceConfig,
|
||||
}),
|
||||
};
|
||||
|
||||
// 只有在支持工具调用的模型上才添加toolConfig
|
||||
if (request.tools?.length && supportsToolUse(request.modelId)) {
|
||||
input.toolConfig = {
|
||||
tools: request.tools.map((tool) => ({
|
||||
toolSpec: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: {
|
||||
json: tool.input_schema,
|
||||
},
|
||||
},
|
||||
})),
|
||||
toolChoice: { auto: {} },
|
||||
};
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const subpath = params.path.join("/");
|
||||
if (!ALLOWED_PATH.has(subpath)) {
|
||||
return NextResponse.json(
|
||||
{ error: true, msg: "Path not allowed: " + 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: "Missing AWS credentials" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new BedrockRuntimeClient({
|
||||
region,
|
||||
credentials: { accessKeyId, secretAccessKey, sessionToken },
|
||||
});
|
||||
|
||||
const body = (await req.json()) as ConverseRequest;
|
||||
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 as AsyncIterable<ConverseStreamOutput>;
|
||||
for await (const event of responseStream) {
|
||||
if (
|
||||
"contentBlockStart" in event &&
|
||||
event.contentBlockStart?.start?.toolUse &&
|
||||
event.contentBlockStart.contentBlockIndex !== undefined
|
||||
) {
|
||||
controller.enqueue(
|
||||
`data: ${JSON.stringify({
|
||||
type: "content_block",
|
||||
content_block: {
|
||||
type: "tool_use",
|
||||
id: event.contentBlockStart.start.toolUse.toolUseId,
|
||||
name: event.contentBlockStart.start.toolUse.name,
|
||||
},
|
||||
index: event.contentBlockStart.contentBlockIndex,
|
||||
})}\n\n`,
|
||||
);
|
||||
} else if (
|
||||
"contentBlockDelta" in event &&
|
||||
event.contentBlockDelta?.delta &&
|
||||
event.contentBlockDelta.contentBlockIndex !== undefined
|
||||
) {
|
||||
const delta = event.contentBlockDelta.delta;
|
||||
|
||||
if ("text" in delta && delta.text) {
|
||||
controller.enqueue(
|
||||
`data: ${JSON.stringify({
|
||||
type: "content_block_delta",
|
||||
delta: {
|
||||
type: "text_delta",
|
||||
text: delta.text,
|
||||
},
|
||||
index: event.contentBlockDelta.contentBlockIndex,
|
||||
})}\n\n`,
|
||||
);
|
||||
} else if ("toolUse" in delta && delta.toolUse?.input) {
|
||||
controller.enqueue(
|
||||
`data: ${JSON.stringify({
|
||||
type: "content_block_delta",
|
||||
delta: {
|
||||
type: "input_json_delta",
|
||||
partial_json: delta.toolUse.input,
|
||||
},
|
||||
index: event.contentBlockDelta.contentBlockIndex,
|
||||
})}\n\n`,
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
"contentBlockStop" in event &&
|
||||
event.contentBlockStop?.contentBlockIndex !== undefined
|
||||
) {
|
||||
controller.enqueue(
|
||||
`data: ${JSON.stringify({
|
||||
type: "content_block_stop",
|
||||
index: event.contentBlockStop.contentBlockIndex,
|
||||
})}\n\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
console.error("[Bedrock] Stream error:", error);
|
||||
controller.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[Bedrock] Error:", e);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: e instanceof Error ? e.message : "Unknown error",
|
||||
details: prettyObject(e),
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
@@ -31,11 +32,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 {
|
||||
@@ -70,7 +79,7 @@ export interface ChatOptions {
|
||||
config: LLMConfig;
|
||||
|
||||
onUpdate?: (message: string, chunk: string) => void;
|
||||
onFinish: (message: string) => void;
|
||||
onFinish: (message: string, responseRes: Response) => void;
|
||||
onError?: (err: Error) => void;
|
||||
onController?: (controller: AbortController) => void;
|
||||
onBeforeTool?: (tool: ChatMessageTool) => void;
|
||||
@@ -130,6 +139,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;
|
||||
@@ -239,6 +251,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;
|
||||
@@ -252,6 +265,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
||||
const apiKey = isGoogle
|
||||
? accessStore.googleApiKey
|
||||
: isBedrock
|
||||
? accessStore.awsAccessKey // Use AWS access key for Bedrock
|
||||
: isAzure
|
||||
? accessStore.azureApiKey
|
||||
: isAnthropic
|
||||
@@ -272,6 +287,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
: ""
|
||||
: accessStore.openaiApiKey;
|
||||
return {
|
||||
isBedrock,
|
||||
isGoogle,
|
||||
isAzure,
|
||||
isAnthropic,
|
||||
@@ -294,10 +310,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,
|
||||
@@ -310,17 +329,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;
|
||||
@@ -328,6 +360,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:
|
||||
|
||||
@@ -143,6 +143,7 @@ export class QwenApi implements LLMApi {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
let responseRes: Response;
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
@@ -172,7 +173,7 @@ export class QwenApi implements LLMApi {
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
options.onFinish(responseText + remainText, responseRes);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -188,6 +189,7 @@ export class QwenApi implements LLMApi {
|
||||
"[Alibaba] request response content type: ",
|
||||
contentType,
|
||||
);
|
||||
responseRes = res;
|
||||
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
@@ -254,7 +256,7 @@ export class QwenApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
||||
@@ -317,13 +317,14 @@ export class ClaudeApi implements LLMApi {
|
||||
};
|
||||
|
||||
try {
|
||||
controller.signal.onabort = () => options.onFinish("");
|
||||
controller.signal.onabort = () =>
|
||||
options.onFinish("", new Response(null, { status: 400 }));
|
||||
|
||||
const res = await fetch(path, payload);
|
||||
const resJson = await res.json();
|
||||
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
} catch (e) {
|
||||
console.error("failed to chat", e);
|
||||
options.onError?.(e as Error);
|
||||
|
||||
@@ -162,6 +162,7 @@ export class ErnieApi implements LLMApi {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
let responseRes: Response;
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
@@ -191,7 +192,7 @@ export class ErnieApi implements LLMApi {
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
options.onFinish(responseText + remainText, responseRes);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -204,7 +205,7 @@ export class ErnieApi implements LLMApi {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log("[Baidu] request response content type: ", contentType);
|
||||
|
||||
responseRes = res;
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
@@ -267,7 +268,7 @@ export class ErnieApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = resJson?.result;
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
||||
292
app/client/platforms/bedrock.ts
Normal file
292
app/client/platforms/bedrock.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { ApiPath } from "../../constant";
|
||||
import { ChatOptions, getHeaders, LLMApi, 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";
|
||||
import { RequestPayload } from "./openai";
|
||||
|
||||
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 {
|
||||
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
throw new Error("Speech not implemented for Bedrock.");
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
console.log("[Response] claude response: ", res);
|
||||
|
||||
return res?.content?.[0]?.text;
|
||||
}
|
||||
|
||||
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 requestBody = {
|
||||
modelId: options.config.model,
|
||||
messages: prompt,
|
||||
inferenceConfig: {
|
||||
maxTokens: modelConfig.max_tokens,
|
||||
temperature: modelConfig.temperature,
|
||||
topP: modelConfig.top_p,
|
||||
stopSequences: [],
|
||||
},
|
||||
};
|
||||
|
||||
const conversePath = `${ApiPath.Bedrock}/converse`;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
if (shouldStream) {
|
||||
let currentToolUse: ChatMessageTool | null = null;
|
||||
let index = -1;
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||
);
|
||||
return stream(
|
||||
conversePath,
|
||||
requestBody,
|
||||
getHeaders(),
|
||||
// @ts-ignore
|
||||
tools.map((tool) => ({
|
||||
name: tool?.function?.name,
|
||||
description: tool?.function?.description,
|
||||
input_schema: tool?.function?.parameters,
|
||||
})),
|
||||
funcs,
|
||||
controller,
|
||||
// parseSSE
|
||||
(text: string, runTools: ChatMessageTool[]) => {
|
||||
// console.log("parseSSE", text, runTools);
|
||||
let chunkJson:
|
||||
| undefined
|
||||
| {
|
||||
type: "content_block_delta" | "content_block_stop";
|
||||
content_block?: {
|
||||
type: "tool_use";
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
delta?: {
|
||||
type: "text_delta" | "input_json_delta";
|
||||
text?: string;
|
||||
partial_json?: string;
|
||||
};
|
||||
index: number;
|
||||
};
|
||||
chunkJson = JSON.parse(text);
|
||||
|
||||
if (chunkJson?.content_block?.type == "tool_use") {
|
||||
index += 1;
|
||||
const id = chunkJson?.content_block.id;
|
||||
const name = chunkJson?.content_block.name;
|
||||
runTools.push({
|
||||
id,
|
||||
type: "function",
|
||||
function: {
|
||||
name,
|
||||
arguments: "",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (
|
||||
chunkJson?.delta?.type == "input_json_delta" &&
|
||||
chunkJson?.delta?.partial_json
|
||||
) {
|
||||
// @ts-ignore
|
||||
runTools[index]["function"]["arguments"] +=
|
||||
chunkJson?.delta?.partial_json;
|
||||
}
|
||||
return chunkJson?.delta?.text;
|
||||
},
|
||||
// processToolMessage, include tool_calls message and tool call results
|
||||
(
|
||||
requestPayload: RequestPayload,
|
||||
toolCallMessage: any,
|
||||
toolCallResult: any[],
|
||||
) => {
|
||||
// reset index value
|
||||
index = -1;
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.splice(
|
||||
// @ts-ignore
|
||||
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)
|
||||
: {},
|
||||
}),
|
||||
),
|
||||
},
|
||||
// @ts-ignore
|
||||
...toolCallResult.map((result) => ({
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: result.tool_call_id,
|
||||
content: result.content,
|
||||
},
|
||||
],
|
||||
})),
|
||||
);
|
||||
},
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
const payload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
...getHeaders(), // get common headers
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
controller.signal.onabort = () =>
|
||||
options.onFinish("", new Response(null, { status: 400 }));
|
||||
|
||||
const res = await fetch(conversePath, payload);
|
||||
const resJson = await res.json();
|
||||
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message, res);
|
||||
} catch (e) {
|
||||
console.error("failed to chat", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
async usage() {
|
||||
return {
|
||||
used: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
async models() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -130,6 +130,7 @@ export class DoubaoApi implements LLMApi {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
let responseRes: Response;
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
@@ -159,7 +160,7 @@ export class DoubaoApi implements LLMApi {
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
options.onFinish(responseText + remainText, responseRes);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -175,7 +176,7 @@ export class DoubaoApi implements LLMApi {
|
||||
"[ByteDance] request response content type: ",
|
||||
contentType,
|
||||
);
|
||||
|
||||
responseRes = res;
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
@@ -241,7 +242,7 @@ export class DoubaoApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
||||
@@ -177,7 +177,7 @@ export class ChatGLMApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
||||
@@ -274,7 +274,7 @@ export class GeminiProApi implements LLMApi {
|
||||
);
|
||||
}
|
||||
const message = apiClient.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
||||
@@ -117,6 +117,7 @@ export class SparkApi implements LLMApi {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
let responseRes: Response;
|
||||
|
||||
// Animate response text to make it look smooth
|
||||
function animateResponseText() {
|
||||
@@ -143,7 +144,7 @@ export class SparkApi implements LLMApi {
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
options.onFinish(responseText + remainText, responseRes);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -156,7 +157,7 @@ export class SparkApi implements LLMApi {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log("[Spark] request response content type: ", contentType);
|
||||
|
||||
responseRes = res;
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
@@ -231,7 +232,7 @@ export class SparkApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
||||
@@ -180,7 +180,7 @@ export class MoonshotApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
||||
@@ -361,7 +361,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = await this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
||||
@@ -142,6 +142,7 @@ export class HunyuanApi implements LLMApi {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
let responseRes: Response;
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
@@ -171,7 +172,7 @@ export class HunyuanApi implements LLMApi {
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
options.onFinish(responseText + remainText, responseRes);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -187,7 +188,7 @@ export class HunyuanApi implements LLMApi {
|
||||
"[Tencent] request response content type: ",
|
||||
contentType,
|
||||
);
|
||||
|
||||
responseRes = res;
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
@@ -253,7 +254,7 @@ export class HunyuanApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
||||
@@ -173,7 +173,7 @@ export class XAIApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1607,7 +1607,7 @@ function _Chat() {
|
||||
title={Locale.Chat.Actions.RefreshTitle}
|
||||
onClick={() => {
|
||||
showToast(Locale.Chat.Actions.RefreshToast);
|
||||
chatStore.summarizeSession(true);
|
||||
chatStore.summarizeSession(true, session);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -964,7 +964,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 && (
|
||||
<>
|
||||
@@ -1724,6 +1792,7 @@ export function Settings() {
|
||||
</ListItem>
|
||||
|
||||
{openAIConfigComponent}
|
||||
{bedrockConfigComponent}
|
||||
{azureConfigComponent}
|
||||
{googleConfigComponent}
|
||||
{anthropicConfigComponent}
|
||||
|
||||
@@ -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
|
||||
@@ -143,7 +148,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;
|
||||
@@ -173,6 +178,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),
|
||||
|
||||
@@ -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/";
|
||||
@@ -51,6 +53,7 @@ export enum Path {
|
||||
|
||||
export enum ApiPath {
|
||||
Cors = "",
|
||||
Bedrock = "/api/bedrock",
|
||||
Azure = "/api/azure",
|
||||
OpenAI = "/api/openai",
|
||||
Anthropic = "/api/anthropic",
|
||||
@@ -118,6 +121,7 @@ export enum ServiceProvider {
|
||||
Stability = "Stability",
|
||||
Iflytek = "Iflytek",
|
||||
XAI = "XAI",
|
||||
Bedrock = "Bedrock",
|
||||
ChatGLM = "ChatGLM",
|
||||
}
|
||||
|
||||
@@ -132,6 +136,7 @@ export enum GoogleSafetySettingsThreshold {
|
||||
|
||||
export enum ModelProvider {
|
||||
Stability = "Stability",
|
||||
Bedrock = "Bedrock",
|
||||
GPT = "GPT",
|
||||
GeminiPro = "GeminiPro",
|
||||
Claude = "Claude",
|
||||
@@ -235,6 +240,10 @@ export const ChatGLM = {
|
||||
ChatPath: "/api/paas/v4/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}}.
|
||||
@@ -314,6 +323,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",
|
||||
@@ -327,11 +352,12 @@ const anthropicModels = [
|
||||
"claude-2.1",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-opus-latest",
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-5-sonnet-latest",
|
||||
"claude-3-opus-latest",
|
||||
"claude-3-5-haiku-latest",
|
||||
];
|
||||
|
||||
const baiduModels = [
|
||||
@@ -523,6 +549,7 @@ export const DEFAULT_MODELS = [
|
||||
sorted: 11,
|
||||
},
|
||||
})),
|
||||
|
||||
...chatglmModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
@@ -534,6 +561,18 @@ export const DEFAULT_MODELS = [
|
||||
sorted: 12,
|
||||
},
|
||||
})),
|
||||
|
||||
...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: "接口密钥",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
StoreKey,
|
||||
ApiPath,
|
||||
OPENAI_BASE_URL,
|
||||
BEDROCK_BASE_URL,
|
||||
ANTHROPIC_BASE_URL,
|
||||
GEMINI_BASE_URL,
|
||||
BAIDU_BASE_URL,
|
||||
@@ -27,6 +28,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;
|
||||
|
||||
@@ -60,6 +62,13 @@ const DEFAULT_ACCESS_STATE = {
|
||||
openaiUrl: DEFAULT_OPENAI_URL,
|
||||
openaiApiKey: "",
|
||||
|
||||
// bedrock
|
||||
awsRegion: "",
|
||||
awsAccessKey: "",
|
||||
awsSecretKey: "",
|
||||
awsSessionToken: "",
|
||||
awsCognitoUser: false,
|
||||
|
||||
// azure
|
||||
azureUrl: "",
|
||||
azureApiKey: "",
|
||||
@@ -148,6 +157,10 @@ export const useAccessStore = createPersistStore(
|
||||
return ensure(get(), ["openaiApiKey"]);
|
||||
},
|
||||
|
||||
isValidBedrock() {
|
||||
return ensure(get(), ["awsAccessKey", "awsSecretKey", "awsRegion"]);
|
||||
},
|
||||
|
||||
isValidAzure() {
|
||||
return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]);
|
||||
},
|
||||
@@ -197,6 +210,7 @@ export const useAccessStore = createPersistStore(
|
||||
// has token or has code or disabled access control
|
||||
return (
|
||||
this.isValidOpenAI() ||
|
||||
this.isValidBedrock() ||
|
||||
this.isValidAzure() ||
|
||||
this.isValidGoogle() ||
|
||||
this.isValidAnthropic() ||
|
||||
|
||||
@@ -352,13 +352,13 @@ export const useChatStore = createPersistStore(
|
||||
return session;
|
||||
},
|
||||
|
||||
onNewMessage(message: ChatMessage) {
|
||||
get().updateCurrentSession((session) => {
|
||||
onNewMessage(message: ChatMessage, targetSession: ChatSession) {
|
||||
get().updateTargetSession(targetSession, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
session.lastUpdate = Date.now();
|
||||
});
|
||||
get().updateStat(message);
|
||||
get().summarizeSession();
|
||||
get().summarizeSession(false, targetSession);
|
||||
},
|
||||
|
||||
async onUserInput(content: string, attachImages?: string[]) {
|
||||
@@ -428,7 +428,7 @@ export const useChatStore = createPersistStore(
|
||||
botMessage.streaming = false;
|
||||
if (message) {
|
||||
botMessage.content = message;
|
||||
get().onNewMessage(botMessage);
|
||||
get().onNewMessage(botMessage, session);
|
||||
}
|
||||
ChatControllerPool.remove(session.id, botMessage.id);
|
||||
},
|
||||
@@ -598,9 +598,12 @@ export const useChatStore = createPersistStore(
|
||||
});
|
||||
},
|
||||
|
||||
summarizeSession(refreshTitle: boolean = false) {
|
||||
summarizeSession(
|
||||
refreshTitle: boolean = false,
|
||||
targetSession: ChatSession,
|
||||
) {
|
||||
const config = useAppConfig.getState();
|
||||
const session = get().currentSession();
|
||||
const session = targetSession;
|
||||
const modelConfig = session.mask.modelConfig;
|
||||
// skip summarize when using dalle3?
|
||||
if (isDalle3(modelConfig.model)) {
|
||||
@@ -649,13 +652,15 @@ export const useChatStore = createPersistStore(
|
||||
stream: false,
|
||||
providerName,
|
||||
},
|
||||
onFinish(message) {
|
||||
if (!isValidMessage(message)) return;
|
||||
get().updateCurrentSession(
|
||||
(session) =>
|
||||
(session.topic =
|
||||
message.length > 0 ? trimTopic(message) : DEFAULT_TOPIC),
|
||||
);
|
||||
onFinish(message, responseRes) {
|
||||
if (responseRes?.status === 200) {
|
||||
get().updateTargetSession(
|
||||
session,
|
||||
(session) =>
|
||||
(session.topic =
|
||||
message.length > 0 ? trimTopic(message) : DEFAULT_TOPIC),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -669,7 +674,7 @@ export const useChatStore = createPersistStore(
|
||||
|
||||
const historyMsgLength = countMessages(toBeSummarizedMsgs);
|
||||
|
||||
if (historyMsgLength > modelConfig?.max_tokens ?? 4000) {
|
||||
if (historyMsgLength > (modelConfig?.max_tokens || 4000)) {
|
||||
const n = toBeSummarizedMsgs.length;
|
||||
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
|
||||
Math.max(0, n - modelConfig.historyMessageCount),
|
||||
@@ -715,22 +720,20 @@ export const useChatStore = createPersistStore(
|
||||
onUpdate(message) {
|
||||
session.memoryPrompt = message;
|
||||
},
|
||||
onFinish(message) {
|
||||
console.log("[Memory] ", message);
|
||||
get().updateCurrentSession((session) => {
|
||||
session.lastSummarizeIndex = lastSummarizeIndex;
|
||||
session.memoryPrompt = message; // Update the memory prompt for stored it in local storage
|
||||
});
|
||||
onFinish(message, responseRes) {
|
||||
if (responseRes?.status === 200) {
|
||||
console.log("[Memory] ", message);
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.lastSummarizeIndex = lastSummarizeIndex;
|
||||
session.memoryPrompt = message; // Update the memory prompt for stored it in local storage
|
||||
});
|
||||
}
|
||||
},
|
||||
onError(err) {
|
||||
console.error("[Summarize] ", err);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function isValidMessage(message: any): boolean {
|
||||
return typeof message === "string" && !message.startsWith("```json");
|
||||
}
|
||||
},
|
||||
|
||||
updateStat(message: ChatMessage) {
|
||||
@@ -746,7 +749,16 @@ export const useChatStore = createPersistStore(
|
||||
updater(sessions[index]);
|
||||
set(() => ({ sessions }));
|
||||
},
|
||||
|
||||
updateTargetSession(
|
||||
targetSession: ChatSession,
|
||||
updater: (session: ChatSession) => void,
|
||||
) {
|
||||
const sessions = get().sessions;
|
||||
const index = sessions.findIndex((s) => s.id === targetSession.id);
|
||||
if (index < 0) return;
|
||||
updater(sessions[index]);
|
||||
set(() => ({ sessions }));
|
||||
},
|
||||
async clearAllData() {
|
||||
await indexedDBStorage.clear();
|
||||
localStorage.clear();
|
||||
|
||||
@@ -266,7 +266,9 @@ export function isVisionModel(model: string) {
|
||||
model.includes("gpt-4-turbo") && !model.includes("preview");
|
||||
|
||||
return (
|
||||
visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo
|
||||
visionKeywords.some((keyword) => model.includes(keyword)) ||
|
||||
isGpt4Turbo ||
|
||||
isDalle3(model)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -286,6 +288,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-3")) {
|
||||
return true;
|
||||
}
|
||||
if (provider == ServiceProvider.Google && !model.includes("vision")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -174,6 +174,7 @@ export function stream(
|
||||
let finished = false;
|
||||
let running = false;
|
||||
let runTools: any[] = [];
|
||||
let responseRes: Response;
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
@@ -272,7 +273,7 @@ export function stream(
|
||||
}
|
||||
console.debug("[ChatAPI] end");
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
options.onFinish(responseText + remainText, responseRes); // 将res传递给onFinish
|
||||
}
|
||||
};
|
||||
|
||||
@@ -304,6 +305,7 @@ export function stream(
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log("[Request] response content type: ", contentType);
|
||||
responseRes = res;
|
||||
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
|
||||
@@ -19,7 +19,7 @@ type StreamResponse = {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
export function fetch(url: string, options?: RequestInit): Promise<any> {
|
||||
export function fetch(url: string, options?: RequestInit): Promise<Response> {
|
||||
if (window.__TAURI__) {
|
||||
const {
|
||||
signal,
|
||||
|
||||
@@ -51,14 +51,15 @@
|
||||
"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/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.2",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "NextChat",
|
||||
"version": "2.15.6"
|
||||
"version": "2.15.7"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
||||
17
yarn.lock
17
yarn.lock
@@ -1201,14 +1201,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
|
||||
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
|
||||
|
||||
"@babel/runtime@^7.12.1", "@babel/runtime@^7.20.7", "@babel/runtime@^7.23.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
version "7.23.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d"
|
||||
integrity sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.12.5", "@babel/runtime@^7.21.0":
|
||||
"@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
version "7.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb"
|
||||
integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==
|
||||
@@ -2134,10 +2127,10 @@
|
||||
lodash "^4.17.21"
|
||||
redent "^3.0.0"
|
||||
|
||||
"@testing-library/react@^16.0.0":
|
||||
version "16.0.0"
|
||||
resolved "https://registry.npmmirror.com/@testing-library/react/-/react-16.0.0.tgz#0a1e0c7a3de25841c3591b8cb7fb0cf0c0a27321"
|
||||
integrity sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==
|
||||
"@testing-library/react@^16.0.1":
|
||||
version "16.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.0.1.tgz#29c0ee878d672703f5e7579f239005e4e0faa875"
|
||||
integrity sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user