mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-16 22:13:47 +08:00
fix: Bedrock image processing and Edge browser routing - Fixed image prompts by bypassing cache system, added Bedrock models with vision detection, enhanced image processing for URLs, fixed Edge routing to Bedrock, added error handling and debugging
This commit is contained in:
@@ -76,9 +76,37 @@ export async function handle(
|
||||
"Stream:",
|
||||
body.stream,
|
||||
"Messages count:",
|
||||
body.messages.length,
|
||||
body.messages?.length || 0,
|
||||
);
|
||||
|
||||
// Add detailed logging for debugging
|
||||
if (body.messages && body.messages.length > 0) {
|
||||
body.messages.forEach((msg: any, index: number) => {
|
||||
console.log(`[Bedrock] Message ${index}:`, {
|
||||
role: msg.role,
|
||||
contentType: typeof msg.content,
|
||||
isArray: Array.isArray(msg.content),
|
||||
contentLength: Array.isArray(msg.content)
|
||||
? msg.content.length
|
||||
: typeof msg.content === "string"
|
||||
? msg.content.length
|
||||
: "unknown",
|
||||
});
|
||||
|
||||
if (Array.isArray(msg.content)) {
|
||||
msg.content.forEach((item: any, itemIndex: number) => {
|
||||
console.log(`[Bedrock] Message ${index}, Item ${itemIndex}:`, {
|
||||
type: item.type,
|
||||
hasImageUrl: !!item.image_url?.url,
|
||||
urlPreview: item.image_url?.url
|
||||
? item.image_url.url.substring(0, 50) + "..."
|
||||
: null,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
messages,
|
||||
model,
|
||||
@@ -87,6 +115,27 @@ export async function handle(
|
||||
max_tokens,
|
||||
} = body;
|
||||
|
||||
// --- Input Validation ---
|
||||
if (!model || typeof model !== "string") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "Model parameter is required and must be a string",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!Array.isArray(messages) || messages.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "Messages parameter is required and must be a non-empty array",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// --- Payload formatting for Claude on Bedrock ---
|
||||
const isClaudeModel = model.includes("anthropic.claude");
|
||||
if (!isClaudeModel) {
|
||||
@@ -101,23 +150,298 @@ export async function handle(
|
||||
(msg: any) => msg.role !== "system",
|
||||
);
|
||||
|
||||
// Validate we have non-system messages
|
||||
if (userAssistantMessages.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "At least one user or assistant message is required",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Process messages and handle image fetching
|
||||
const processedMessages = await Promise.all(
|
||||
userAssistantMessages.map(async (msg: any) => {
|
||||
let content;
|
||||
|
||||
if (Array.isArray(msg.content)) {
|
||||
const processedContent = await Promise.all(
|
||||
msg.content.map(async (item: any) => {
|
||||
if (item.type === "image_url") {
|
||||
console.log("[Bedrock] Processing image_url item:", item);
|
||||
// Adapt from OpenAI format to Bedrock's format
|
||||
const url = item.image_url?.url;
|
||||
if (!url) {
|
||||
console.warn(
|
||||
"[Bedrock] Image URL is missing in content item",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if it's a data URL or regular URL
|
||||
const dataUrlMatch = url.match(
|
||||
/^data:(image\/[^;]+);base64,(.+)$/,
|
||||
);
|
||||
if (dataUrlMatch) {
|
||||
// Handle data URL (base64)
|
||||
const mediaType = dataUrlMatch[1];
|
||||
const base64Data = dataUrlMatch[2];
|
||||
|
||||
if (!base64Data) {
|
||||
console.warn("[Bedrock] Empty base64 data in image URL");
|
||||
return null;
|
||||
}
|
||||
|
||||
const bedrockImageItem = {
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: mediaType,
|
||||
data: base64Data,
|
||||
},
|
||||
};
|
||||
|
||||
console.log(
|
||||
"[Bedrock] Successfully converted data URL to Bedrock format:",
|
||||
{
|
||||
mediaType,
|
||||
dataLength: base64Data.length,
|
||||
},
|
||||
);
|
||||
|
||||
return bedrockImageItem;
|
||||
} else if (
|
||||
url.startsWith("http://") ||
|
||||
url.startsWith("https://")
|
||||
) {
|
||||
// Handle HTTP URL - fetch directly and convert to base64
|
||||
console.log(
|
||||
"[Bedrock] HTTP URL detected, fetching directly:",
|
||||
url.substring(0, 50) + "...",
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
console.log(
|
||||
"[Bedrock] Fetch response status:",
|
||||
response.status,
|
||||
response.statusText,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(
|
||||
"[Bedrock] Failed to fetch image:",
|
||||
response.status,
|
||||
response.statusText,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
console.log("[Bedrock] Blob info:", {
|
||||
size: blob.size,
|
||||
type: blob.type,
|
||||
});
|
||||
|
||||
if (blob.size === 0) {
|
||||
console.error(
|
||||
"[Bedrock] Fetched blob is empty - cache endpoint may not be working",
|
||||
);
|
||||
console.log(
|
||||
"[Bedrock] This might be a service worker cache issue - image was uploaded but cache retrieval failed",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
console.log(
|
||||
"[Bedrock] ArrayBuffer size:",
|
||||
arrayBuffer.byteLength,
|
||||
);
|
||||
|
||||
if (arrayBuffer.byteLength === 0) {
|
||||
console.error("[Bedrock] ArrayBuffer is empty");
|
||||
return null;
|
||||
}
|
||||
|
||||
const base64Data =
|
||||
Buffer.from(arrayBuffer).toString("base64");
|
||||
console.log("[Bedrock] Base64 conversion:", {
|
||||
originalSize: arrayBuffer.byteLength,
|
||||
base64Length: base64Data.length,
|
||||
isEmpty: !base64Data || base64Data.length === 0,
|
||||
firstChars: base64Data.substring(0, 20),
|
||||
});
|
||||
|
||||
if (!base64Data || base64Data.length === 0) {
|
||||
console.error(
|
||||
"[Bedrock] Base64 data is empty after conversion",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const mediaType = blob.type || "image/jpeg";
|
||||
|
||||
const bedrockImageItem = {
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: mediaType,
|
||||
data: base64Data,
|
||||
},
|
||||
};
|
||||
|
||||
console.log(
|
||||
"[Bedrock] Successfully converted HTTP URL to Bedrock format:",
|
||||
{
|
||||
url: url.substring(0, 50) + "...",
|
||||
mediaType,
|
||||
dataLength: base64Data.length,
|
||||
hasValidData: !!base64Data && base64Data.length > 0,
|
||||
},
|
||||
);
|
||||
|
||||
return bedrockImageItem;
|
||||
} catch (error) {
|
||||
console.error("[Bedrock] Error fetching image:", error);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"[Bedrock] Invalid URL format:",
|
||||
url.substring(0, 50) + "...",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// Handle text content
|
||||
return item;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Filter out nulls and ensure we have content
|
||||
content = processedContent.filter(Boolean);
|
||||
|
||||
// Additional validation: ensure no image objects have empty data
|
||||
content = content.filter((item: any) => {
|
||||
if (item.type === "image") {
|
||||
const hasValidData =
|
||||
item.source?.data && item.source.data.length > 0;
|
||||
if (!hasValidData) {
|
||||
console.error(
|
||||
"[Bedrock] Filtering out image with empty data:",
|
||||
{
|
||||
hasSource: !!item.source,
|
||||
hasData: !!item.source?.data,
|
||||
dataLength: item.source?.data?.length || 0,
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (content.length === 0) {
|
||||
console.warn(
|
||||
"[Bedrock] All content items were filtered out, adding empty text",
|
||||
);
|
||||
content = [{ type: "text", text: "" }];
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[Bedrock] Processed content for message:",
|
||||
content.length,
|
||||
"items",
|
||||
);
|
||||
} else if (typeof msg.content === "string") {
|
||||
content = [{ type: "text", text: msg.content }];
|
||||
} else {
|
||||
console.warn("[Bedrock] Unknown content type:", typeof msg.content);
|
||||
content = [{ type: "text", text: "" }];
|
||||
}
|
||||
|
||||
return {
|
||||
role: msg.role,
|
||||
content: content,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const payload = {
|
||||
anthropic_version: "bedrock-2023-05-31",
|
||||
max_tokens: max_tokens || 4096,
|
||||
temperature: temperature,
|
||||
messages: userAssistantMessages.map((msg: any) => ({
|
||||
role: msg.role, // 'user' or 'assistant'
|
||||
content:
|
||||
typeof msg.content === "string"
|
||||
? [{ type: "text", text: msg.content }]
|
||||
: msg.content, // Assuming MultimodalContent format is compatible
|
||||
})),
|
||||
max_tokens:
|
||||
typeof max_tokens === "number" && max_tokens > 0 ? max_tokens : 4096,
|
||||
temperature:
|
||||
typeof temperature === "number" && temperature >= 0 && temperature <= 1
|
||||
? temperature
|
||||
: 0.7, // Bedrock Claude accepts 0-1 range
|
||||
messages: processedMessages,
|
||||
...(systemPrompts.length > 0 && {
|
||||
system: systemPrompts.map((msg: any) => msg.content).join("\n"),
|
||||
system: systemPrompts
|
||||
.map((msg: any) => {
|
||||
if (typeof msg.content === "string") {
|
||||
return msg.content;
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
// Handle multimodal system prompts by extracting text
|
||||
return msg.content
|
||||
.filter((item: any) => item.type === "text")
|
||||
.map((item: any) => item.text)
|
||||
.join(" ");
|
||||
}
|
||||
return String(msg.content); // Fallback conversion
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
}),
|
||||
};
|
||||
// --- End Payload Formatting ---
|
||||
|
||||
// Log the final payload structure (without base64 data to avoid huge logs)
|
||||
console.log("[Bedrock] Final payload structure:", {
|
||||
anthropic_version: payload.anthropic_version,
|
||||
max_tokens: payload.max_tokens,
|
||||
temperature: payload.temperature,
|
||||
messageCount: payload.messages.length,
|
||||
messages: payload.messages.map((msg: any, index: number) => ({
|
||||
index,
|
||||
role: msg.role,
|
||||
contentItems: msg.content.map((item: any) => ({
|
||||
type: item.type,
|
||||
hasData: item.type === "image" ? !!item.source?.data : !!item.text,
|
||||
mediaType: item.source?.media_type || null,
|
||||
textLength: item.text?.length || null,
|
||||
dataLength: item.source?.data?.length || null,
|
||||
})),
|
||||
})),
|
||||
hasSystem: !!(payload as any).system,
|
||||
});
|
||||
|
||||
// Final validation: check for any empty images
|
||||
const hasEmptyImages = payload.messages.some((msg: any) =>
|
||||
msg.content.some(
|
||||
(item: any) =>
|
||||
item.type === "image" &&
|
||||
(!item.source?.data || item.source.data.length === 0),
|
||||
),
|
||||
);
|
||||
|
||||
if (hasEmptyImages) {
|
||||
console.error(
|
||||
"[Bedrock] Payload contains empty images, this will cause Bedrock to fail",
|
||||
);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "Image processing failed: empty image data detected",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
const command = new InvokeModelWithResponseStreamCommand({
|
||||
modelId: model,
|
||||
@@ -139,13 +463,23 @@ export async function handle(
|
||||
try {
|
||||
for await (const event of responseBody) {
|
||||
if (event.chunk?.bytes) {
|
||||
const chunkData = JSON.parse(decoder.decode(event.chunk.bytes));
|
||||
let chunkData;
|
||||
try {
|
||||
chunkData = JSON.parse(decoder.decode(event.chunk.bytes));
|
||||
} catch (parseError) {
|
||||
console.error(
|
||||
"[Bedrock] Failed to parse chunk JSON:",
|
||||
parseError,
|
||||
);
|
||||
continue; // Skip malformed chunks
|
||||
}
|
||||
|
||||
let responseText = "";
|
||||
let finishReason: string | null = null;
|
||||
|
||||
if (
|
||||
chunkData.type === "content_block_delta" &&
|
||||
chunkData.delta.type === "text_delta"
|
||||
chunkData.delta?.type === "text_delta"
|
||||
) {
|
||||
responseText = chunkData.delta.text || "";
|
||||
} else if (chunkData.type === "message_stop") {
|
||||
@@ -156,35 +490,68 @@ export async function handle(
|
||||
: "length"; // Example logic
|
||||
}
|
||||
|
||||
// Format as OpenAI SSE chunk
|
||||
const sseData = {
|
||||
id: `chatcmpl-${nanoid()}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content: responseText },
|
||||
finish_reason: finishReason,
|
||||
},
|
||||
],
|
||||
};
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify(sseData)}\n\n`),
|
||||
);
|
||||
// Only send non-empty responses or finish signals
|
||||
if (responseText || finishReason) {
|
||||
// Format as OpenAI SSE chunk
|
||||
const sseData = {
|
||||
id: `chatcmpl-${nanoid()}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content: responseText },
|
||||
finish_reason: finishReason,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify(sseData)}\n\n`),
|
||||
);
|
||||
} catch (enqueueError) {
|
||||
console.error(
|
||||
"[Bedrock] Failed to enqueue data:",
|
||||
enqueueError,
|
||||
);
|
||||
break; // Stop processing if client disconnected
|
||||
}
|
||||
}
|
||||
|
||||
if (finishReason) {
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
||||
try {
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
||||
} catch (enqueueError) {
|
||||
console.error(
|
||||
"[Bedrock] Failed to enqueue [DONE]:",
|
||||
enqueueError,
|
||||
);
|
||||
}
|
||||
break; // Exit loop after stop message
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Bedrock] Streaming error:", error);
|
||||
controller.error(error);
|
||||
try {
|
||||
controller.error(error);
|
||||
} catch (controllerError) {
|
||||
console.error(
|
||||
"[Bedrock] Failed to signal controller error:",
|
||||
controllerError,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
controller.close();
|
||||
try {
|
||||
controller.close();
|
||||
} catch (closeError) {
|
||||
console.error(
|
||||
"[Bedrock] Failed to close controller:",
|
||||
closeError,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -205,7 +572,28 @@ export async function handle(
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const response = await client.send(command);
|
||||
const responseBody = JSON.parse(new TextDecoder().decode(response.body));
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error("Empty response body from Bedrock");
|
||||
}
|
||||
|
||||
let responseBody;
|
||||
try {
|
||||
responseBody = JSON.parse(new TextDecoder().decode(response.body));
|
||||
} catch (parseError) {
|
||||
console.error("[Bedrock] Failed to parse response JSON:", parseError);
|
||||
throw new Error("Invalid JSON response from Bedrock");
|
||||
}
|
||||
|
||||
// Validate response structure
|
||||
if (
|
||||
!responseBody.content ||
|
||||
!Array.isArray(responseBody.content) ||
|
||||
responseBody.content.length === 0
|
||||
) {
|
||||
console.error("[Bedrock] Invalid response structure:", responseBody);
|
||||
throw new Error("Invalid response structure from Bedrock");
|
||||
}
|
||||
|
||||
// Format response to match OpenAI
|
||||
const formattedResponse = {
|
||||
|
||||
Reference in New Issue
Block a user