fix(ai): filtered empty user messages across all providers
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Fixed
|
||||
|
||||
- Fixed API validation errors when sending empty user messages (resume with `.`) across all providers:
|
||||
- Google Cloud Code Assist (google-shared.ts)
|
||||
- OpenAI Responses API (openai-responses.ts)
|
||||
- OpenAI Codex Responses API (openai-codex-responses.ts)
|
||||
- Cursor (cursor.ts)
|
||||
- Amazon Bedrock (amazon-bedrock.ts)
|
||||
|
||||
## [6.9.69] - 2026-01-21
|
||||
### Added
|
||||
|
||||
@@ -342,22 +342,39 @@ function convertMessages(context: Context, model: Model<"bedrock-converse-stream
|
||||
|
||||
switch (m.role) {
|
||||
case "user":
|
||||
result.push({
|
||||
role: ConversationRole.USER,
|
||||
content:
|
||||
typeof m.content === "string"
|
||||
? [{ text: sanitizeSurrogates(m.content) }]
|
||||
: m.content.map((c) => {
|
||||
switch (c.type) {
|
||||
case "text":
|
||||
return { text: sanitizeSurrogates(c.text) };
|
||||
case "image":
|
||||
return { image: createImageBlock(c.mimeType, c.data) };
|
||||
default:
|
||||
throw new Error("Unknown user content type");
|
||||
}
|
||||
}),
|
||||
});
|
||||
if (typeof m.content === "string") {
|
||||
// Skip empty user messages
|
||||
if (!m.content || m.content.trim() === "") continue;
|
||||
result.push({
|
||||
role: ConversationRole.USER,
|
||||
content: [{ text: sanitizeSurrogates(m.content) }],
|
||||
});
|
||||
} else {
|
||||
const contentBlocks = m.content
|
||||
.map((c) => {
|
||||
switch (c.type) {
|
||||
case "text":
|
||||
return { text: sanitizeSurrogates(c.text) };
|
||||
case "image":
|
||||
return { image: createImageBlock(c.mimeType, c.data) };
|
||||
default:
|
||||
throw new Error("Unknown user content type");
|
||||
}
|
||||
})
|
||||
.filter((block) => {
|
||||
// Filter out empty text blocks
|
||||
if ("text" in block && block.text) {
|
||||
return block.text.trim().length > 0;
|
||||
}
|
||||
return true; // Keep non-text blocks (images)
|
||||
});
|
||||
// Skip message if all blocks filtered out
|
||||
if (contentBlocks.length === 0) continue;
|
||||
result.push({
|
||||
role: ConversationRole.USER,
|
||||
content: contentBlocks,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "assistant": {
|
||||
// Skip assistant messages with empty content (e.g., from aborted requests)
|
||||
|
||||
@@ -1838,11 +1838,12 @@ function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[]
|
||||
function extractUserMessageText(msg: Message): string {
|
||||
if (msg.role !== "user") return "";
|
||||
const content = msg.content;
|
||||
if (typeof content === "string") return content;
|
||||
return content
|
||||
if (typeof content === "string") return content.trim();
|
||||
const text = content
|
||||
.filter((c): c is TextContent => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join("\n");
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1891,7 +1892,7 @@ function buildConversationTurns(messages: Message[]): Uint8Array[] {
|
||||
|
||||
// Create and serialize user message
|
||||
const userText = extractUserMessageText(msg);
|
||||
if (!userText) {
|
||||
if (!userText || userText.length === 0) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
@@ -1982,10 +1983,15 @@ function buildGrpcRequest(
|
||||
const userText =
|
||||
lastMessage?.role === "user"
|
||||
? typeof lastMessage.content === "string"
|
||||
? lastMessage.content
|
||||
? lastMessage.content.trim()
|
||||
: extractText(lastMessage.content)
|
||||
: "";
|
||||
|
||||
// Validate that we have non-empty user text for the action
|
||||
if (!userText || userText.trim().length === 0) {
|
||||
throw new Error("Cannot send empty user message to Cursor API");
|
||||
}
|
||||
|
||||
const userMessage = create(UserMessageSchema, {
|
||||
text: userText,
|
||||
messageId: crypto.randomUUID(),
|
||||
|
||||
@@ -79,6 +79,8 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
||||
for (const msg of transformedMessages) {
|
||||
if (msg.role === "user") {
|
||||
if (typeof msg.content === "string") {
|
||||
// Skip empty user messages
|
||||
if (!msg.content || msg.content.trim() === "") continue;
|
||||
contents.push({
|
||||
role: "user",
|
||||
parts: [{ text: sanitizeSurrogates(msg.content) }],
|
||||
@@ -96,7 +98,14 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
||||
};
|
||||
}
|
||||
});
|
||||
const filteredParts = !model.input.includes("image") ? parts.filter((p) => p.text !== undefined) : parts;
|
||||
// Filter out images if model doesn't support them, and empty text blocks
|
||||
let filteredParts = !model.input.includes("image") ? parts.filter((p) => p.text !== undefined) : parts;
|
||||
filteredParts = filteredParts.filter((p) => {
|
||||
if (p.text !== undefined) {
|
||||
return p.text.trim().length > 0;
|
||||
}
|
||||
return true; // Keep non-text parts (images)
|
||||
});
|
||||
if (filteredParts.length === 0) continue;
|
||||
contents.push({
|
||||
role: "user",
|
||||
|
||||
@@ -571,6 +571,8 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
||||
for (const msg of transformedMessages) {
|
||||
if (msg.role === "user") {
|
||||
if (typeof msg.content === "string") {
|
||||
// Skip empty user messages
|
||||
if (!msg.content || msg.content.trim() === "") continue;
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: sanitizeSurrogates(msg.content) }],
|
||||
@@ -589,9 +591,16 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
||||
image_url: `data:${item.mimeType};base64,${item.data}`,
|
||||
} satisfies ResponseInputImage;
|
||||
});
|
||||
const filteredContent = !model.input.includes("image")
|
||||
// Filter out images if model doesn't support them, and empty text blocks
|
||||
let filteredContent = !model.input.includes("image")
|
||||
? content.filter((c) => c.type !== "input_image")
|
||||
: content;
|
||||
filteredContent = filteredContent.filter((c) => {
|
||||
if (c.type === "input_text") {
|
||||
return c.text.trim().length > 0;
|
||||
}
|
||||
return true; // Keep non-text content (images)
|
||||
});
|
||||
if (filteredContent.length === 0) continue;
|
||||
messages.push({
|
||||
role: "user",
|
||||
|
||||
@@ -468,6 +468,8 @@ function convertMessages(
|
||||
for (const msg of transformedMessages) {
|
||||
if (msg.role === "user") {
|
||||
if (typeof msg.content === "string") {
|
||||
// Skip empty user messages
|
||||
if (!msg.content || msg.content.trim() === "") continue;
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: sanitizeSurrogates(msg.content) }],
|
||||
@@ -487,9 +489,16 @@ function convertMessages(
|
||||
} satisfies ResponseInputImage;
|
||||
}
|
||||
});
|
||||
const filteredContent = !model.input.includes("image")
|
||||
// Filter out images if model doesn't support them, and empty text blocks
|
||||
let filteredContent = !model.input.includes("image")
|
||||
? content.filter((c) => c.type !== "input_image")
|
||||
: content;
|
||||
filteredContent = filteredContent.filter((c) => {
|
||||
if (c.type === "input_text") {
|
||||
return c.text.trim().length > 0;
|
||||
}
|
||||
return true; // Keep non-text content (images)
|
||||
});
|
||||
if (filteredContent.length === 0) continue;
|
||||
messages.push({
|
||||
role: "user",
|
||||
|
||||
Reference in New Issue
Block a user