Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions core/llm/countTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "../index.js";
import { autodetectTemplateType } from "./autodetect.js";
import {
addSpaceToAnyEmptyMessages,
stripEmptyContentParts,
chatMessageIsEmpty,
isUserOrToolMsg,
messageHasToolCallId,
Expand Down Expand Up @@ -446,7 +446,7 @@ function compileChatMessages({
// Remove any empty messages or non-user/tool trailing messages
msgsCopy = msgsCopy.filter((msg) => !chatMessageIsEmpty(msg));

msgsCopy = addSpaceToAnyEmptyMessages(msgsCopy);
msgsCopy = stripEmptyContentParts(msgsCopy);

// Extract the tool sequence from the end of the message array
const toolSequence = extractToolSequence(msgsCopy);
Expand Down
8 changes: 6 additions & 2 deletions core/llm/llms/Bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,11 +538,15 @@ class Bedrock extends BaseLLM {
): ContentBlock[] {
const blocks: ContentBlock[] = [];
if (typeof content === "string") {
blocks.push({ text: content });
if (content.trim()) {
blocks.push({ text: content });
}
} else {
for (const part of content) {
if (part.type === "text") {
blocks.push({ text: part.text });
if (part.text?.trim()) {
blocks.push({ text: part.text });
}
} else if (part.type === "imageUrl" && part.imageUrl) {
const parsed = parseDataUrl(part.imageUrl.url);
if (parsed) {
Expand Down
47 changes: 26 additions & 21 deletions core/llm/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,36 @@ export function messageHasToolCalls(msg: ChatMessage): boolean {
return msg.role === "assistant" && !!msg.toolCalls;
}

export function messageIsEmpty(message: ChatMessage): boolean {
if (typeof message.content === "string") {
return message.content.trim() === "";
function contentIsEmpty(content: ChatMessage["content"]): boolean {
if (typeof content === "string") {
return content.trim() === "";
}
if (Array.isArray(message.content)) {
return message.content.every(
(item) => item.type === "text" && item.text?.trim() === "",
if (Array.isArray(content)) {
return content.every(
(item) => item.type === "text" && (!item.text || item.text.trim() === ""),
);
}
return false;
return !content;
}

export function messageIsEmpty(message: ChatMessage): boolean {
return contentIsEmpty(message.content);
}

// some providers don't support empty messages
export function addSpaceToAnyEmptyMessages(
messages: ChatMessage[],
): ChatMessage[] {
/** Strip empty text parts from array content. */
export function stripEmptyContentParts(messages: ChatMessage[]): ChatMessage[] {
return messages.map((message) => {
if (messageIsEmpty(message)) {
message.content = " ";
if (Array.isArray(message.content)) {
message.content = message.content.filter(
(part) =>
part.type !== "text" ||
(part.text !== null &&
part.text !== undefined &&
part.text.trim() !== ""),
);
if (message.content.length === 0) {
message.content = "";
}
}
return message;
});
Expand Down Expand Up @@ -57,15 +68,9 @@ export function chatMessageIsEmpty(message: ChatMessage): boolean {
switch (message.role) {
case "system":
case "user":
return (
typeof message.content === "string" && message.content.trim() === ""
);
return contentIsEmpty(message.content);
case "assistant":
return (
typeof message.content === "string" &&
message.content.trim() === "" &&
!message.toolCalls
);
return contentIsEmpty(message.content) && !message.toolCalls;
case "thinking":
case "tool":
return false;
Expand Down
67 changes: 44 additions & 23 deletions core/llm/openaiTypeConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ function appendReasoningFieldsIfSupported(
}
}

function toAssistantContent(
content: ChatMessage["content"],
): string | TextMessagePart[] {
if (typeof content === "string") {
return content || " "; // LM Studio (and other providers) don't accept empty content
}
const parts = content
.filter((p) => p.type === "text" && p.text?.trim())
.map((p) => p as TextMessagePart);
return parts.length > 0 ? parts : " ";
}

export function toChatMessage(
message: ChatMessage,
options: CompletionOptions,
Expand Down Expand Up @@ -145,12 +157,7 @@ export function toChatMessage(
}[];
} = {
role: "assistant",
content:
typeof message.content === "string"
? message.content || " " // LM Studio (and other providers) don't accept empty content
: message.content
.filter((part) => part.type === "text")
.map((part) => part as TextMessagePart),
content: toAssistantContent(message.content),
};

// Add tool calls if present
Expand Down Expand Up @@ -178,31 +185,45 @@ export function toChatMessage(
if (typeof message.content === "string") {
return {
role: "user",
content: message.content ?? " ", // LM Studio (and other providers) don't accept empty content
content: message.content || " ",
};
}

// If no multi-media is in the message, just send as text
// for compatibility with OpenAI-"compatible" servers
// that don't support multi-media format
const hasNonText = message.content.some((item) => item.type !== "text");
if (hasNonText) {
// Filter out empty text parts when there's non-text content (images)
const filteredContent = message.content
.filter(
(part) =>
part.type !== "text" || (part.text && part.text.trim() !== ""),
)
.map((part) => {
if (part.type === "imageUrl") {
return {
type: "image_url" as const,
image_url: {
url: part.imageUrl.url,
detail: "auto" as const,
},
};
}
return part;
});
return {
role: "user",
content: filteredContent,
};
}

return {
role: "user",
content: message.content.some((item) => item.type !== "text")
? message.content.map((part) => {
if (part.type === "imageUrl") {
return {
type: "image_url" as const,
image_url: {
url: part.imageUrl.url,
detail: "auto" as const,
},
};
}
return part;
})
: message.content
.map((item) => (item as TextMessagePart).text)
.join("") || " ",
content:
message.content
.map((item) => (item as TextMessagePart).text)
.join("") || "...",
};
}
}
Expand Down
8 changes: 7 additions & 1 deletion packages/openai-adapters/src/apis/Bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,15 @@ export class BedrockApi implements BaseLlmApi {
const content = message.content;
if (content) {
if (typeof content === "string") {
currentBlocks.push({ text: content });
if (content.trim()) {
currentBlocks.push({ text: content });
}
} else {
content.forEach((part) => {
// Skip empty text parts
if (part.type === "text" && !part.text?.trim()) {
return;
}
currentBlocks.push(this._oaiPartToBedrockPart(part));
});
}
Expand Down
Loading