mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-03-07 20:54:25 +08:00
Compare commits
6 Commits
feat/markd
...
afbf5eb541
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afbf5eb541 | ||
|
|
0f276f59bb | ||
|
|
fc391168e9 | ||
|
|
dca4a0e48f | ||
|
|
722c28839f | ||
|
|
ff356f0c8c |
@@ -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=
|
||||
@@ -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";
|
||||
@@ -20,12 +21,15 @@ async function handle(
|
||||
const apiPath = `/api/${params.provider}`;
|
||||
console.log(`[${params.provider} Route] params `, params);
|
||||
switch (apiPath) {
|
||||
case ApiPath.Bedrock:
|
||||
return bedrockHandler(req, { params });
|
||||
case ApiPath.Azure:
|
||||
return azureHandler(req, { params });
|
||||
case ApiPath.Google:
|
||||
return googleHandler(req, { params });
|
||||
case ApiPath.Anthropic:
|
||||
return anthropicHandler(req, { params });
|
||||
|
||||
case ApiPath.Baidu:
|
||||
return baiduHandler(req, { params });
|
||||
case ApiPath.ByteDance:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
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,
|
||||
} 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";
|
||||
@@ -30,11 +31,19 @@ export const TTSModels = ["tts-1", "tts-1-hd"] as const;
|
||||
export type ChatModel = ModelType;
|
||||
|
||||
export interface MultimodalContent {
|
||||
type: "text" | "image_url";
|
||||
type: "text" | "image_url" | "document";
|
||||
text?: string;
|
||||
image_url?: {
|
||||
url: string;
|
||||
};
|
||||
document?: {
|
||||
format: string;
|
||||
name: string;
|
||||
source: {
|
||||
bytes: string;
|
||||
media_type?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface RequestMessage {
|
||||
@@ -129,6 +138,9 @@ export class ClientApi {
|
||||
|
||||
constructor(provider: ModelProvider = ModelProvider.GPT) {
|
||||
switch (provider) {
|
||||
case ModelProvider.Bedrock:
|
||||
this.llm = new BedrockApi();
|
||||
break;
|
||||
case ModelProvider.GeminiPro:
|
||||
this.llm = new GeminiProApi();
|
||||
break;
|
||||
@@ -235,6 +247,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
|
||||
function getConfig() {
|
||||
const modelConfig = chatStore.currentSession().mask.modelConfig;
|
||||
const isBedrock = modelConfig.providerName === ServiceProvider.Bedrock;
|
||||
const isGoogle = modelConfig.providerName === ServiceProvider.Google;
|
||||
const isAzure = modelConfig.providerName === ServiceProvider.Azure;
|
||||
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
|
||||
@@ -247,6 +260,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
|
||||
@@ -265,6 +280,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
: ""
|
||||
: accessStore.openaiApiKey;
|
||||
return {
|
||||
isBedrock,
|
||||
isGoogle,
|
||||
isAzure,
|
||||
isAnthropic,
|
||||
@@ -286,10 +302,13 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
? "x-api-key"
|
||||
: isGoogle
|
||||
? "x-goog-api-key"
|
||||
: isBedrock
|
||||
? "x-api-key"
|
||||
: "Authorization";
|
||||
}
|
||||
|
||||
const {
|
||||
isBedrock,
|
||||
isGoogle,
|
||||
isAzure,
|
||||
isAnthropic,
|
||||
@@ -302,17 +321,30 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
|
||||
const authHeader = getAuthHeader();
|
||||
|
||||
const bearerToken = getBearerToken(
|
||||
apiKey,
|
||||
isAzure || isAnthropic || isGoogle,
|
||||
);
|
||||
if (isBedrock) {
|
||||
// 简单加密 AWS credentials
|
||||
const encrypt = (str: string) =>
|
||||
Buffer.from(str.split("").reverse().join("")).toString("base64");
|
||||
|
||||
if (bearerToken) {
|
||||
headers[authHeader] = bearerToken;
|
||||
} else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
|
||||
headers["Authorization"] = getBearerToken(
|
||||
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||
headers["X-Region"] = encrypt(accessStore.awsRegion);
|
||||
headers["X-Access-Key"] = encrypt(accessStore.awsAccessKey);
|
||||
headers["X-Secret-Key"] = encrypt(accessStore.awsSecretKey);
|
||||
if (accessStore.awsSessionToken) {
|
||||
headers["X-Session-Token"] = encrypt(accessStore.awsSessionToken);
|
||||
}
|
||||
} else {
|
||||
const bearerToken = getBearerToken(
|
||||
apiKey,
|
||||
isAzure || isAnthropic || isGoogle,
|
||||
);
|
||||
|
||||
if (bearerToken) {
|
||||
headers[authHeader] = bearerToken;
|
||||
} else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
|
||||
headers["Authorization"] = getBearerToken(
|
||||
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
@@ -320,6 +352,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
|
||||
export function getClientApi(provider: ServiceProvider): ClientApi {
|
||||
switch (provider) {
|
||||
case ServiceProvider.Bedrock:
|
||||
return new ClientApi(ModelProvider.Bedrock);
|
||||
case ServiceProvider.Google:
|
||||
return new ClientApi(ModelProvider.GeminiPro);
|
||||
case ServiceProvider.Anthropic:
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.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) {
|
||||
|
||||
@@ -963,7 +963,75 @@ export function Settings() {
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
|
||||
const bedrockConfigComponent = accessStore.provider ===
|
||||
ServiceProvider.Bedrock && (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Bedrock.Region.Title}
|
||||
subTitle={Locale.Settings.Access.Bedrock.Region.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Settings.Access.Bedrock.Region.Title}
|
||||
type="text"
|
||||
value={accessStore.awsRegion}
|
||||
placeholder="us-west-2"
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) => (access.awsRegion = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Bedrock.AccessKey.Title}
|
||||
subTitle={Locale.Settings.Access.Bedrock.AccessKey.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
aria-label={Locale.Settings.Access.Bedrock.AccessKey.Title}
|
||||
value={accessStore.awsAccessKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.Bedrock.AccessKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.awsAccessKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Bedrock.SecretKey.Title}
|
||||
subTitle={Locale.Settings.Access.Bedrock.SecretKey.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
aria-label={Locale.Settings.Access.Bedrock.SecretKey.Title}
|
||||
value={accessStore.awsSecretKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.Bedrock.SecretKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.awsSecretKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Bedrock.SessionToken.Title}
|
||||
subTitle={Locale.Settings.Access.Bedrock.SessionToken.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
aria-label={Locale.Settings.Access.Bedrock.SessionToken.Title}
|
||||
value={accessStore.awsSessionToken}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.Bedrock.SessionToken.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.awsSessionToken = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
const baiduConfigComponent = accessStore.provider ===
|
||||
ServiceProvider.Baidu && (
|
||||
<>
|
||||
@@ -1682,6 +1750,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
|
||||
@@ -139,7 +144,7 @@ export const getServerSideConfig = () => {
|
||||
}
|
||||
|
||||
const isStability = !!process.env.STABILITY_API_KEY;
|
||||
|
||||
const isBedrock = !!process.env.BEDROCK_API_KEY;
|
||||
const isAzure = !!process.env.AZURE_URL;
|
||||
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
||||
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
||||
@@ -168,6 +173,11 @@ export const getServerSideConfig = () => {
|
||||
apiKey: getApiKey(process.env.OPENAI_API_KEY),
|
||||
openaiOrgId: process.env.OPENAI_ORG_ID,
|
||||
|
||||
isBedrock,
|
||||
awsRegion: process.env.AWS_REGION,
|
||||
awsAccessKey: process.env.AWS_ACCESS_KEY,
|
||||
awsSecretKey: process.env.AWS_SECRET_KEY,
|
||||
|
||||
isStability,
|
||||
stabilityUrl: process.env.STABILITY_URL,
|
||||
stabilityApiKey: getApiKey(process.env.STABILITY_API_KEY),
|
||||
|
||||
@@ -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/";
|
||||
@@ -49,6 +51,7 @@ export enum Path {
|
||||
|
||||
export enum ApiPath {
|
||||
Cors = "",
|
||||
Bedrock = "/api/bedrock",
|
||||
Azure = "/api/azure",
|
||||
OpenAI = "/api/openai",
|
||||
Anthropic = "/api/anthropic",
|
||||
@@ -115,6 +118,7 @@ export enum ServiceProvider {
|
||||
Stability = "Stability",
|
||||
Iflytek = "Iflytek",
|
||||
XAI = "XAI",
|
||||
Bedrock = "Bedrock",
|
||||
}
|
||||
|
||||
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
||||
@@ -128,6 +132,7 @@ export enum GoogleSafetySettingsThreshold {
|
||||
|
||||
export enum ModelProvider {
|
||||
Stability = "Stability",
|
||||
Bedrock = "Bedrock",
|
||||
GPT = "GPT",
|
||||
GeminiPro = "GeminiPro",
|
||||
Claude = "Claude",
|
||||
@@ -225,6 +230,10 @@ export const XAI = {
|
||||
ChatPath: "v1/chat/completions",
|
||||
};
|
||||
|
||||
export const Bedrock = {
|
||||
ChatPath: "converse",
|
||||
};
|
||||
|
||||
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
||||
// export const DEFAULT_SYSTEM_TEMPLATE = `
|
||||
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
|
||||
@@ -304,6 +313,22 @@ const openaiModels = [
|
||||
"o1-preview",
|
||||
];
|
||||
|
||||
const bedrockModels = [
|
||||
// Claude Models
|
||||
"anthropic.claude-3-haiku-20240307-v1:0",
|
||||
"anthropic.claude-3-5-haiku-20241022-v1:0",
|
||||
"anthropic.claude-3-sonnet-20240229-v1:0",
|
||||
"anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
"anthropic.claude-3-opus-20240229-v1:0",
|
||||
|
||||
// Meta Llama Models
|
||||
"us.meta.llama3-2-11b-instruct-v1:0",
|
||||
"us.meta.llama3-2-90b-instruct-v1:0",
|
||||
//Mistral
|
||||
"mistral.mistral-large-2402-v1:0",
|
||||
"mistral.mistral-large-2407-v1:0",
|
||||
];
|
||||
|
||||
const googleModels = [
|
||||
"gemini-1.0-pro",
|
||||
"gemini-1.5-pro-latest",
|
||||
@@ -502,6 +527,17 @@ export const DEFAULT_MODELS = [
|
||||
sorted: 11,
|
||||
},
|
||||
})),
|
||||
...bedrockModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "bedrock",
|
||||
providerName: "Bedrock",
|
||||
providerType: "bedrock",
|
||||
sorted: 13,
|
||||
},
|
||||
})),
|
||||
] as const;
|
||||
|
||||
export const CHAT_PAGE_SIZE = 15;
|
||||
|
||||
@@ -81,6 +81,7 @@ const cn = {
|
||||
Clear: "清除聊天",
|
||||
Settings: "对话设置",
|
||||
UploadImage: "上传图片",
|
||||
UploadDocument: "上传文档",
|
||||
},
|
||||
Rename: "重命名对话",
|
||||
Typing: "正在输入…",
|
||||
@@ -342,6 +343,32 @@ const cn = {
|
||||
SubTitle: "除默认地址外,必须包含 http(s)://",
|
||||
},
|
||||
},
|
||||
Bedrock: {
|
||||
Region: {
|
||||
Title: "AWS Region",
|
||||
SubTitle: "The AWS region where Bedrock service is located",
|
||||
Placeholder: "us-west-2",
|
||||
},
|
||||
AccessKey: {
|
||||
Title: "AWS Access Key ID",
|
||||
SubTitle: "Your AWS access key ID for Bedrock service",
|
||||
Placeholder: "AKIA...",
|
||||
},
|
||||
SecretKey: {
|
||||
Title: "AWS Secret Access Key",
|
||||
SubTitle: "Your AWS secret access key for Bedrock service",
|
||||
Placeholder: "****",
|
||||
},
|
||||
SessionToken: {
|
||||
Title: "AWS Session Token (Optional)",
|
||||
SubTitle: "Your AWS session token if using temporary credentials",
|
||||
Placeholder: "Optional session token",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "AWS Bedrock Endpoint",
|
||||
SubTitle: "Custom endpoint for AWS Bedrock API. Default: ",
|
||||
},
|
||||
},
|
||||
Azure: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
|
||||
@@ -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,
|
||||
@@ -26,6 +27,7 @@ let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
||||
const isApp = getClientConfig()?.buildMode === "export";
|
||||
|
||||
const DEFAULT_OPENAI_URL = isApp ? OPENAI_BASE_URL : ApiPath.OpenAI;
|
||||
const DEFAULT_BEDROCK_URL = isApp ? BEDROCK_BASE_URL : ApiPath.Bedrock;
|
||||
|
||||
const DEFAULT_GOOGLE_URL = isApp ? GEMINI_BASE_URL : ApiPath.Google;
|
||||
|
||||
@@ -57,6 +59,13 @@ const DEFAULT_ACCESS_STATE = {
|
||||
openaiUrl: DEFAULT_OPENAI_URL,
|
||||
openaiApiKey: "",
|
||||
|
||||
// bedrock
|
||||
awsRegion: "",
|
||||
awsAccessKey: "",
|
||||
awsSecretKey: "",
|
||||
awsSessionToken: "",
|
||||
awsCognitoUser: false,
|
||||
|
||||
// azure
|
||||
azureUrl: "",
|
||||
azureApiKey: "",
|
||||
@@ -141,6 +150,10 @@ export const useAccessStore = createPersistStore(
|
||||
return ensure(get(), ["openaiApiKey"]);
|
||||
},
|
||||
|
||||
isValidBedrock() {
|
||||
return ensure(get(), ["awsAccessKey", "awsSecretKey", "awsRegion"]);
|
||||
},
|
||||
|
||||
isValidAzure() {
|
||||
return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]);
|
||||
},
|
||||
@@ -186,6 +199,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() ||
|
||||
|
||||
@@ -285,6 +285,9 @@ export function showPlugins(provider: ServiceProvider, model: string) {
|
||||
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
|
||||
return true;
|
||||
}
|
||||
if (provider == ServiceProvider.Bedrock && !model.includes("claude-2")) {
|
||||
return true;
|
||||
}
|
||||
if (provider == ServiceProvider.Google && !model.includes("vision")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
"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",
|
||||
|
||||
Reference in New Issue
Block a user