mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-04-23 11:34:26 +08:00
Compare commits
6 Commits
website
...
afbf5eb541
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afbf5eb541 | ||
|
|
0f276f59bb | ||
|
|
fc391168e9 | ||
|
|
dca4a0e48f | ||
|
|
722c28839f | ||
|
|
ff356f0c8c |
@@ -66,4 +66,9 @@ ANTHROPIC_API_VERSION=
|
|||||||
ANTHROPIC_URL=
|
ANTHROPIC_URL=
|
||||||
|
|
||||||
### (optional)
|
### (optional)
|
||||||
WHITE_WEBDAV_ENDPOINTS=
|
WHITE_WEBDAV_ENDPOINTS=
|
||||||
|
|
||||||
|
### bedrock (optional)
|
||||||
|
AWS_REGION=
|
||||||
|
AWS_ACCESS_KEY=
|
||||||
|
AWS_SECRET_KEY=
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ApiPath } from "@/app/constant";
|
import { ApiPath } from "@/app/constant";
|
||||||
import { NextRequest } from "next/server";
|
import { NextRequest } from "next/server";
|
||||||
import { handle as openaiHandler } from "../../openai";
|
import { handle as openaiHandler } from "../../openai";
|
||||||
|
import { handle as bedrockHandler } from "../../bedrock";
|
||||||
import { handle as azureHandler } from "../../azure";
|
import { handle as azureHandler } from "../../azure";
|
||||||
import { handle as googleHandler } from "../../google";
|
import { handle as googleHandler } from "../../google";
|
||||||
import { handle as anthropicHandler } from "../../anthropic";
|
import { handle as anthropicHandler } from "../../anthropic";
|
||||||
@@ -20,12 +21,15 @@ async function handle(
|
|||||||
const apiPath = `/api/${params.provider}`;
|
const apiPath = `/api/${params.provider}`;
|
||||||
console.log(`[${params.provider} Route] params `, params);
|
console.log(`[${params.provider} Route] params `, params);
|
||||||
switch (apiPath) {
|
switch (apiPath) {
|
||||||
|
case ApiPath.Bedrock:
|
||||||
|
return bedrockHandler(req, { params });
|
||||||
case ApiPath.Azure:
|
case ApiPath.Azure:
|
||||||
return azureHandler(req, { params });
|
return azureHandler(req, { params });
|
||||||
case ApiPath.Google:
|
case ApiPath.Google:
|
||||||
return googleHandler(req, { params });
|
return googleHandler(req, { params });
|
||||||
case ApiPath.Anthropic:
|
case ApiPath.Anthropic:
|
||||||
return anthropicHandler(req, { params });
|
return anthropicHandler(req, { params });
|
||||||
|
|
||||||
case ApiPath.Baidu:
|
case ApiPath.Baidu:
|
||||||
return baiduHandler(req, { params });
|
return baiduHandler(req, { params });
|
||||||
case ApiPath.ByteDance:
|
case ApiPath.ByteDance:
|
||||||
|
|||||||
@@ -52,6 +52,28 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
|||||||
msg: "you are not allowed to access with your own api key",
|
msg: "you are not allowed to access with your own api key",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Special handling for Bedrock
|
||||||
|
if (modelProvider === ModelProvider.Bedrock) {
|
||||||
|
const region = serverConfig.awsRegion;
|
||||||
|
const accessKeyId = serverConfig.awsAccessKey;
|
||||||
|
const secretAccessKey = serverConfig.awsSecretKey;
|
||||||
|
|
||||||
|
console.log("[Auth] Bedrock credentials:", {
|
||||||
|
region,
|
||||||
|
accessKeyId: accessKeyId ? "***" : undefined,
|
||||||
|
secretKey: secretAccessKey ? "***" : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if AWS credentials are provided
|
||||||
|
if (!region || !accessKeyId || !secretAccessKey) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
msg: "Missing AWS credentials. Please configure Region, Access Key ID, and Secret Access Key in settings.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: false };
|
||||||
|
}
|
||||||
|
|
||||||
// if user does not provide an api key, inject system api key
|
// if user does not provide an api key, inject system api key
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
useChatStore,
|
useChatStore,
|
||||||
} from "../store";
|
} from "../store";
|
||||||
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
|
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
|
||||||
|
import { BedrockApi } from "./platforms/bedrock";
|
||||||
import { GeminiProApi } from "./platforms/google";
|
import { GeminiProApi } from "./platforms/google";
|
||||||
import { ClaudeApi } from "./platforms/anthropic";
|
import { ClaudeApi } from "./platforms/anthropic";
|
||||||
import { ErnieApi } from "./platforms/baidu";
|
import { ErnieApi } from "./platforms/baidu";
|
||||||
@@ -30,11 +31,19 @@ export const TTSModels = ["tts-1", "tts-1-hd"] as const;
|
|||||||
export type ChatModel = ModelType;
|
export type ChatModel = ModelType;
|
||||||
|
|
||||||
export interface MultimodalContent {
|
export interface MultimodalContent {
|
||||||
type: "text" | "image_url";
|
type: "text" | "image_url" | "document";
|
||||||
text?: string;
|
text?: string;
|
||||||
image_url?: {
|
image_url?: {
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
document?: {
|
||||||
|
format: string;
|
||||||
|
name: string;
|
||||||
|
source: {
|
||||||
|
bytes: string;
|
||||||
|
media_type?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestMessage {
|
export interface RequestMessage {
|
||||||
@@ -129,6 +138,9 @@ export class ClientApi {
|
|||||||
|
|
||||||
constructor(provider: ModelProvider = ModelProvider.GPT) {
|
constructor(provider: ModelProvider = ModelProvider.GPT) {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
|
case ModelProvider.Bedrock:
|
||||||
|
this.llm = new BedrockApi();
|
||||||
|
break;
|
||||||
case ModelProvider.GeminiPro:
|
case ModelProvider.GeminiPro:
|
||||||
this.llm = new GeminiProApi();
|
this.llm = new GeminiProApi();
|
||||||
break;
|
break;
|
||||||
@@ -235,6 +247,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
|
|
||||||
function getConfig() {
|
function getConfig() {
|
||||||
const modelConfig = chatStore.currentSession().mask.modelConfig;
|
const modelConfig = chatStore.currentSession().mask.modelConfig;
|
||||||
|
const isBedrock = modelConfig.providerName === ServiceProvider.Bedrock;
|
||||||
const isGoogle = modelConfig.providerName === ServiceProvider.Google;
|
const isGoogle = modelConfig.providerName === ServiceProvider.Google;
|
||||||
const isAzure = modelConfig.providerName === ServiceProvider.Azure;
|
const isAzure = modelConfig.providerName === ServiceProvider.Azure;
|
||||||
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
|
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
|
||||||
@@ -247,6 +260,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
||||||
const apiKey = isGoogle
|
const apiKey = isGoogle
|
||||||
? accessStore.googleApiKey
|
? accessStore.googleApiKey
|
||||||
|
: isBedrock
|
||||||
|
? accessStore.awsAccessKey // Use AWS access key for Bedrock
|
||||||
: isAzure
|
: isAzure
|
||||||
? accessStore.azureApiKey
|
? accessStore.azureApiKey
|
||||||
: isAnthropic
|
: isAnthropic
|
||||||
@@ -265,6 +280,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
: ""
|
: ""
|
||||||
: accessStore.openaiApiKey;
|
: accessStore.openaiApiKey;
|
||||||
return {
|
return {
|
||||||
|
isBedrock,
|
||||||
isGoogle,
|
isGoogle,
|
||||||
isAzure,
|
isAzure,
|
||||||
isAnthropic,
|
isAnthropic,
|
||||||
@@ -286,10 +302,13 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
? "x-api-key"
|
? "x-api-key"
|
||||||
: isGoogle
|
: isGoogle
|
||||||
? "x-goog-api-key"
|
? "x-goog-api-key"
|
||||||
|
: isBedrock
|
||||||
|
? "x-api-key"
|
||||||
: "Authorization";
|
: "Authorization";
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
isBedrock,
|
||||||
isGoogle,
|
isGoogle,
|
||||||
isAzure,
|
isAzure,
|
||||||
isAnthropic,
|
isAnthropic,
|
||||||
@@ -302,17 +321,30 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
|
|
||||||
const authHeader = getAuthHeader();
|
const authHeader = getAuthHeader();
|
||||||
|
|
||||||
const bearerToken = getBearerToken(
|
if (isBedrock) {
|
||||||
apiKey,
|
// 简单加密 AWS credentials
|
||||||
isAzure || isAnthropic || isGoogle,
|
const encrypt = (str: string) =>
|
||||||
);
|
Buffer.from(str.split("").reverse().join("")).toString("base64");
|
||||||
|
|
||||||
if (bearerToken) {
|
headers["X-Region"] = encrypt(accessStore.awsRegion);
|
||||||
headers[authHeader] = bearerToken;
|
headers["X-Access-Key"] = encrypt(accessStore.awsAccessKey);
|
||||||
} else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
|
headers["X-Secret-Key"] = encrypt(accessStore.awsSecretKey);
|
||||||
headers["Authorization"] = getBearerToken(
|
if (accessStore.awsSessionToken) {
|
||||||
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
headers["X-Session-Token"] = encrypt(accessStore.awsSessionToken);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const bearerToken = getBearerToken(
|
||||||
|
apiKey,
|
||||||
|
isAzure || isAnthropic || isGoogle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (bearerToken) {
|
||||||
|
headers[authHeader] = bearerToken;
|
||||||
|
} else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
|
||||||
|
headers["Authorization"] = getBearerToken(
|
||||||
|
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
@@ -320,6 +352,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
|
|
||||||
export function getClientApi(provider: ServiceProvider): ClientApi {
|
export function getClientApi(provider: ServiceProvider): ClientApi {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
|
case ServiceProvider.Bedrock:
|
||||||
|
return new ClientApi(ModelProvider.Bedrock);
|
||||||
case ServiceProvider.Google:
|
case ServiceProvider.Google:
|
||||||
return new ClientApi(ModelProvider.GeminiPro);
|
return new ClientApi(ModelProvider.GeminiPro);
|
||||||
case ServiceProvider.Anthropic:
|
case ServiceProvider.Anthropic:
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,6 +75,17 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
--delay: 0.5s;
|
--delay: 0.5s;
|
||||||
width: var(--full-width);
|
width: var(--full-width);
|
||||||
@@ -393,8 +404,8 @@
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Specific styles for iOS devices */
|
/* Specific styles for iOS devices */
|
||||||
@media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
|
@media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
|
||||||
|
|||||||
@@ -963,7 +963,75 @@ export function Settings() {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
const bedrockConfigComponent = accessStore.provider ===
|
||||||
|
ServiceProvider.Bedrock && (
|
||||||
|
<>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.Bedrock.Region.Title}
|
||||||
|
subTitle={Locale.Settings.Access.Bedrock.Region.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label={Locale.Settings.Access.Bedrock.Region.Title}
|
||||||
|
type="text"
|
||||||
|
value={accessStore.awsRegion}
|
||||||
|
placeholder="us-west-2"
|
||||||
|
onChange={(e) =>
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.awsRegion = e.currentTarget.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.Bedrock.AccessKey.Title}
|
||||||
|
subTitle={Locale.Settings.Access.Bedrock.AccessKey.SubTitle}
|
||||||
|
>
|
||||||
|
<PasswordInput
|
||||||
|
aria-label={Locale.Settings.Access.Bedrock.AccessKey.Title}
|
||||||
|
value={accessStore.awsAccessKey}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.Bedrock.AccessKey.Placeholder}
|
||||||
|
onChange={(e) => {
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.awsAccessKey = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.Bedrock.SecretKey.Title}
|
||||||
|
subTitle={Locale.Settings.Access.Bedrock.SecretKey.SubTitle}
|
||||||
|
>
|
||||||
|
<PasswordInput
|
||||||
|
aria-label={Locale.Settings.Access.Bedrock.SecretKey.Title}
|
||||||
|
value={accessStore.awsSecretKey}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.Bedrock.SecretKey.Placeholder}
|
||||||
|
onChange={(e) => {
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.awsSecretKey = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.Bedrock.SessionToken.Title}
|
||||||
|
subTitle={Locale.Settings.Access.Bedrock.SessionToken.SubTitle}
|
||||||
|
>
|
||||||
|
<PasswordInput
|
||||||
|
aria-label={Locale.Settings.Access.Bedrock.SessionToken.Title}
|
||||||
|
value={accessStore.awsSessionToken}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.Bedrock.SessionToken.Placeholder}
|
||||||
|
onChange={(e) => {
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.awsSessionToken = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
const baiduConfigComponent = accessStore.provider ===
|
const baiduConfigComponent = accessStore.provider ===
|
||||||
ServiceProvider.Baidu && (
|
ServiceProvider.Baidu && (
|
||||||
<>
|
<>
|
||||||
@@ -1682,6 +1750,7 @@ export function Settings() {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
{openAIConfigComponent}
|
{openAIConfigComponent}
|
||||||
|
{bedrockConfigComponent}
|
||||||
{azureConfigComponent}
|
{azureConfigComponent}
|
||||||
{googleConfigComponent}
|
{googleConfigComponent}
|
||||||
{anthropicConfigComponent}
|
{anthropicConfigComponent}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ declare global {
|
|||||||
BASE_URL?: string;
|
BASE_URL?: string;
|
||||||
OPENAI_ORG_ID?: string; // openai only
|
OPENAI_ORG_ID?: string; // openai only
|
||||||
|
|
||||||
|
// bedrock only
|
||||||
|
BEDROCK_REGION?: string;
|
||||||
|
BEDROCK_API_KEY?: string;
|
||||||
|
BEDROCK_API_SECRET?: string;
|
||||||
|
|
||||||
VERCEL?: string;
|
VERCEL?: string;
|
||||||
BUILD_MODE?: "standalone" | "export";
|
BUILD_MODE?: "standalone" | "export";
|
||||||
BUILD_APP?: string; // is building desktop app
|
BUILD_APP?: string; // is building desktop app
|
||||||
@@ -139,7 +144,7 @@ export const getServerSideConfig = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isStability = !!process.env.STABILITY_API_KEY;
|
const isStability = !!process.env.STABILITY_API_KEY;
|
||||||
|
const isBedrock = !!process.env.BEDROCK_API_KEY;
|
||||||
const isAzure = !!process.env.AZURE_URL;
|
const isAzure = !!process.env.AZURE_URL;
|
||||||
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
||||||
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
||||||
@@ -168,6 +173,11 @@ export const getServerSideConfig = () => {
|
|||||||
apiKey: getApiKey(process.env.OPENAI_API_KEY),
|
apiKey: getApiKey(process.env.OPENAI_API_KEY),
|
||||||
openaiOrgId: process.env.OPENAI_ORG_ID,
|
openaiOrgId: process.env.OPENAI_ORG_ID,
|
||||||
|
|
||||||
|
isBedrock,
|
||||||
|
awsRegion: process.env.AWS_REGION,
|
||||||
|
awsAccessKey: process.env.AWS_ACCESS_KEY,
|
||||||
|
awsSecretKey: process.env.AWS_SECRET_KEY,
|
||||||
|
|
||||||
isStability,
|
isStability,
|
||||||
stabilityUrl: process.env.STABILITY_URL,
|
stabilityUrl: process.env.STABILITY_URL,
|
||||||
stabilityApiKey: getApiKey(process.env.STABILITY_API_KEY),
|
stabilityApiKey: getApiKey(process.env.STABILITY_API_KEY),
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
|||||||
export const STABILITY_BASE_URL = "https://api.stability.ai";
|
export const STABILITY_BASE_URL = "https://api.stability.ai";
|
||||||
|
|
||||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||||
|
export const BEDROCK_BASE_URL =
|
||||||
|
"https://bedrock-runtime.us-west-2.amazonaws.com";
|
||||||
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
||||||
|
|
||||||
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
|
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
|
||||||
@@ -49,6 +51,7 @@ export enum Path {
|
|||||||
|
|
||||||
export enum ApiPath {
|
export enum ApiPath {
|
||||||
Cors = "",
|
Cors = "",
|
||||||
|
Bedrock = "/api/bedrock",
|
||||||
Azure = "/api/azure",
|
Azure = "/api/azure",
|
||||||
OpenAI = "/api/openai",
|
OpenAI = "/api/openai",
|
||||||
Anthropic = "/api/anthropic",
|
Anthropic = "/api/anthropic",
|
||||||
@@ -115,6 +118,7 @@ export enum ServiceProvider {
|
|||||||
Stability = "Stability",
|
Stability = "Stability",
|
||||||
Iflytek = "Iflytek",
|
Iflytek = "Iflytek",
|
||||||
XAI = "XAI",
|
XAI = "XAI",
|
||||||
|
Bedrock = "Bedrock",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
||||||
@@ -128,6 +132,7 @@ export enum GoogleSafetySettingsThreshold {
|
|||||||
|
|
||||||
export enum ModelProvider {
|
export enum ModelProvider {
|
||||||
Stability = "Stability",
|
Stability = "Stability",
|
||||||
|
Bedrock = "Bedrock",
|
||||||
GPT = "GPT",
|
GPT = "GPT",
|
||||||
GeminiPro = "GeminiPro",
|
GeminiPro = "GeminiPro",
|
||||||
Claude = "Claude",
|
Claude = "Claude",
|
||||||
@@ -225,6 +230,10 @@ export const XAI = {
|
|||||||
ChatPath: "v1/chat/completions",
|
ChatPath: "v1/chat/completions",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Bedrock = {
|
||||||
|
ChatPath: "converse",
|
||||||
|
};
|
||||||
|
|
||||||
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
||||||
// export const DEFAULT_SYSTEM_TEMPLATE = `
|
// export const DEFAULT_SYSTEM_TEMPLATE = `
|
||||||
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
|
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
|
||||||
@@ -304,6 +313,22 @@ const openaiModels = [
|
|||||||
"o1-preview",
|
"o1-preview",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const bedrockModels = [
|
||||||
|
// Claude Models
|
||||||
|
"anthropic.claude-3-haiku-20240307-v1:0",
|
||||||
|
"anthropic.claude-3-5-haiku-20241022-v1:0",
|
||||||
|
"anthropic.claude-3-sonnet-20240229-v1:0",
|
||||||
|
"anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||||
|
"anthropic.claude-3-opus-20240229-v1:0",
|
||||||
|
|
||||||
|
// Meta Llama Models
|
||||||
|
"us.meta.llama3-2-11b-instruct-v1:0",
|
||||||
|
"us.meta.llama3-2-90b-instruct-v1:0",
|
||||||
|
//Mistral
|
||||||
|
"mistral.mistral-large-2402-v1:0",
|
||||||
|
"mistral.mistral-large-2407-v1:0",
|
||||||
|
];
|
||||||
|
|
||||||
const googleModels = [
|
const googleModels = [
|
||||||
"gemini-1.0-pro",
|
"gemini-1.0-pro",
|
||||||
"gemini-1.5-pro-latest",
|
"gemini-1.5-pro-latest",
|
||||||
@@ -502,6 +527,17 @@ export const DEFAULT_MODELS = [
|
|||||||
sorted: 11,
|
sorted: 11,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
|
...bedrockModels.map((name) => ({
|
||||||
|
name,
|
||||||
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
|
provider: {
|
||||||
|
id: "bedrock",
|
||||||
|
providerName: "Bedrock",
|
||||||
|
providerType: "bedrock",
|
||||||
|
sorted: 13,
|
||||||
|
},
|
||||||
|
})),
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const CHAT_PAGE_SIZE = 15;
|
export const CHAT_PAGE_SIZE = 15;
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ const cn = {
|
|||||||
Clear: "清除聊天",
|
Clear: "清除聊天",
|
||||||
Settings: "对话设置",
|
Settings: "对话设置",
|
||||||
UploadImage: "上传图片",
|
UploadImage: "上传图片",
|
||||||
|
UploadDocument: "上传文档",
|
||||||
},
|
},
|
||||||
Rename: "重命名对话",
|
Rename: "重命名对话",
|
||||||
Typing: "正在输入…",
|
Typing: "正在输入…",
|
||||||
@@ -342,6 +343,32 @@ const cn = {
|
|||||||
SubTitle: "除默认地址外,必须包含 http(s)://",
|
SubTitle: "除默认地址外,必须包含 http(s)://",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Bedrock: {
|
||||||
|
Region: {
|
||||||
|
Title: "AWS Region",
|
||||||
|
SubTitle: "The AWS region where Bedrock service is located",
|
||||||
|
Placeholder: "us-west-2",
|
||||||
|
},
|
||||||
|
AccessKey: {
|
||||||
|
Title: "AWS Access Key ID",
|
||||||
|
SubTitle: "Your AWS access key ID for Bedrock service",
|
||||||
|
Placeholder: "AKIA...",
|
||||||
|
},
|
||||||
|
SecretKey: {
|
||||||
|
Title: "AWS Secret Access Key",
|
||||||
|
SubTitle: "Your AWS secret access key for Bedrock service",
|
||||||
|
Placeholder: "****",
|
||||||
|
},
|
||||||
|
SessionToken: {
|
||||||
|
Title: "AWS Session Token (Optional)",
|
||||||
|
SubTitle: "Your AWS session token if using temporary credentials",
|
||||||
|
Placeholder: "Optional session token",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "AWS Bedrock Endpoint",
|
||||||
|
SubTitle: "Custom endpoint for AWS Bedrock API. Default: ",
|
||||||
|
},
|
||||||
|
},
|
||||||
Azure: {
|
Azure: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "接口密钥",
|
Title: "接口密钥",
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const en: LocaleType = {
|
|||||||
Clear: "Clear Context",
|
Clear: "Clear Context",
|
||||||
Settings: "Settings",
|
Settings: "Settings",
|
||||||
UploadImage: "Upload Images",
|
UploadImage: "Upload Images",
|
||||||
|
UploadDocument: "Upload Documents",
|
||||||
},
|
},
|
||||||
Rename: "Rename Chat",
|
Rename: "Rename Chat",
|
||||||
Typing: "Typing…",
|
Typing: "Typing…",
|
||||||
@@ -346,6 +347,32 @@ const en: LocaleType = {
|
|||||||
SubTitle: "Must start with http(s):// or use /api/openai as default",
|
SubTitle: "Must start with http(s):// or use /api/openai as default",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Bedrock: {
|
||||||
|
Region: {
|
||||||
|
Title: "AWS Region",
|
||||||
|
SubTitle: "The AWS region where Bedrock service is located",
|
||||||
|
Placeholder: "us-west-2",
|
||||||
|
},
|
||||||
|
AccessKey: {
|
||||||
|
Title: "AWS Access Key ID",
|
||||||
|
SubTitle: "Your AWS access key ID for Bedrock service",
|
||||||
|
Placeholder: "AKIA...",
|
||||||
|
},
|
||||||
|
SecretKey: {
|
||||||
|
Title: "AWS Secret Access Key",
|
||||||
|
SubTitle: "Your AWS secret access key for Bedrock service",
|
||||||
|
Placeholder: "****",
|
||||||
|
},
|
||||||
|
SessionToken: {
|
||||||
|
Title: "AWS Session Token (Optional)",
|
||||||
|
SubTitle: "Your AWS session token if using temporary credentials",
|
||||||
|
Placeholder: "Optional session token",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "AWS Bedrock Endpoint",
|
||||||
|
SubTitle: "Custom endpoint for AWS Bedrock API. Default: ",
|
||||||
|
},
|
||||||
|
},
|
||||||
Azure: {
|
Azure: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "Azure Api Key",
|
Title: "Azure Api Key",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
StoreKey,
|
StoreKey,
|
||||||
ApiPath,
|
ApiPath,
|
||||||
OPENAI_BASE_URL,
|
OPENAI_BASE_URL,
|
||||||
|
BEDROCK_BASE_URL,
|
||||||
ANTHROPIC_BASE_URL,
|
ANTHROPIC_BASE_URL,
|
||||||
GEMINI_BASE_URL,
|
GEMINI_BASE_URL,
|
||||||
BAIDU_BASE_URL,
|
BAIDU_BASE_URL,
|
||||||
@@ -26,6 +27,7 @@ let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
|||||||
const isApp = getClientConfig()?.buildMode === "export";
|
const isApp = getClientConfig()?.buildMode === "export";
|
||||||
|
|
||||||
const DEFAULT_OPENAI_URL = isApp ? OPENAI_BASE_URL : ApiPath.OpenAI;
|
const DEFAULT_OPENAI_URL = isApp ? OPENAI_BASE_URL : ApiPath.OpenAI;
|
||||||
|
const DEFAULT_BEDROCK_URL = isApp ? BEDROCK_BASE_URL : ApiPath.Bedrock;
|
||||||
|
|
||||||
const DEFAULT_GOOGLE_URL = isApp ? GEMINI_BASE_URL : ApiPath.Google;
|
const DEFAULT_GOOGLE_URL = isApp ? GEMINI_BASE_URL : ApiPath.Google;
|
||||||
|
|
||||||
@@ -57,6 +59,13 @@ const DEFAULT_ACCESS_STATE = {
|
|||||||
openaiUrl: DEFAULT_OPENAI_URL,
|
openaiUrl: DEFAULT_OPENAI_URL,
|
||||||
openaiApiKey: "",
|
openaiApiKey: "",
|
||||||
|
|
||||||
|
// bedrock
|
||||||
|
awsRegion: "",
|
||||||
|
awsAccessKey: "",
|
||||||
|
awsSecretKey: "",
|
||||||
|
awsSessionToken: "",
|
||||||
|
awsCognitoUser: false,
|
||||||
|
|
||||||
// azure
|
// azure
|
||||||
azureUrl: "",
|
azureUrl: "",
|
||||||
azureApiKey: "",
|
azureApiKey: "",
|
||||||
@@ -141,6 +150,10 @@ export const useAccessStore = createPersistStore(
|
|||||||
return ensure(get(), ["openaiApiKey"]);
|
return ensure(get(), ["openaiApiKey"]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isValidBedrock() {
|
||||||
|
return ensure(get(), ["awsAccessKey", "awsSecretKey", "awsRegion"]);
|
||||||
|
},
|
||||||
|
|
||||||
isValidAzure() {
|
isValidAzure() {
|
||||||
return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]);
|
return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]);
|
||||||
},
|
},
|
||||||
@@ -186,6 +199,7 @@ export const useAccessStore = createPersistStore(
|
|||||||
// has token or has code or disabled access control
|
// has token or has code or disabled access control
|
||||||
return (
|
return (
|
||||||
this.isValidOpenAI() ||
|
this.isValidOpenAI() ||
|
||||||
|
this.isValidBedrock() ||
|
||||||
this.isValidAzure() ||
|
this.isValidAzure() ||
|
||||||
this.isValidGoogle() ||
|
this.isValidGoogle() ||
|
||||||
this.isValidAnthropic() ||
|
this.isValidAnthropic() ||
|
||||||
|
|||||||
@@ -285,6 +285,9 @@ export function showPlugins(provider: ServiceProvider, model: string) {
|
|||||||
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
|
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (provider == ServiceProvider.Bedrock && !model.includes("claude-2")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (provider == ServiceProvider.Google && !model.includes("vision")) {
|
if (provider == ServiceProvider.Google && !model.includes("vision")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,8 @@
|
|||||||
"sass": "^1.59.2",
|
"sass": "^1.59.2",
|
||||||
"spark-md5": "^3.0.2",
|
"spark-md5": "^3.0.2",
|
||||||
"use-debounce": "^9.0.4",
|
"use-debounce": "^9.0.4",
|
||||||
"zustand": "^4.3.8"
|
"zustand": "^4.3.8",
|
||||||
|
"@aws-sdk/client-bedrock-runtime": "^3.679.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/api": "^1.6.0",
|
"@tauri-apps/api": "^1.6.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user