fix(ai): filtered empty user messages across all providers

This commit is contained in:
can1357
2026-01-21 17:23:52 +01:00
parent 59ab91c939
commit 97b07e573a
6 changed files with 81 additions and 23 deletions
+8
View File
@@ -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
+33 -16
View File
@@ -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)
+10 -4
View File
@@ -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(),
+10 -1
View File
@@ -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",
+10 -1
View File
@@ -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",