mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 15:08:14 +00:00
fix(openai-completions): guard string assistant content in transform + tool-history (#93681)
When an assistant message's `content` is a raw string at runtime (JSONL transcript replay passes it through even though the type declares an array), the OpenAI-compatible completions path crashes: - `transformMessages` called `assistantMsg.content.flatMap(...)` -> `TypeError: ... .flatMap is not a function` (first crash, always hit). - Two `hasToolHistory` helpers (`openai-transport-stream.ts` and `openai-completions.ts`) called `content.some(...)` -> `TypeError: ... .some is not a function` (siblings, surface once the flatMap crash is fixed). Normalize a string assistant content to an equivalent single text block before transforming (matching the string->text handling already used in anthropic-payload-policy.ts), and `Array.isArray`-guard both `hasToolHistory` helpers so a string assistant simply does not count toward tool history. Verified end-to-end through the real `buildOpenAICompletionsParams` and `streamOpenAICompletions` entry points: before the fix a string-content assistant followed by a toolResult throws TypeError; after the fix params are produced correctly (string preserved as text, tool history detected). Normal array content is unaffected.
This commit is contained in:
@@ -2524,7 +2524,11 @@ function hasToolHistory(messages: Context["messages"]): boolean {
|
||||
return messages.some(
|
||||
(message) =>
|
||||
message.role === "toolResult" ||
|
||||
(message.role === "assistant" && message.content.some((block) => block.type === "toolCall")),
|
||||
// Assistant content can be a raw string from transcript replay; a string
|
||||
// never carries tool calls, so it should not count toward tool history.
|
||||
(message.role === "assistant" &&
|
||||
Array.isArray(message.content) &&
|
||||
message.content.some((block) => block.type === "toolCall")),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,9 @@ function hasToolHistory(messages: Message[]): boolean {
|
||||
return true;
|
||||
}
|
||||
if (msg.role === "assistant") {
|
||||
if (msg.content.some((block) => block.type === "toolCall")) {
|
||||
// Assistant content can be a raw string from transcript replay; a string
|
||||
// never carries tool calls, so it should not count toward tool history.
|
||||
if (Array.isArray(msg.content) && msg.content.some((block) => block.type === "toolCall")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,15 @@ export function transformMessages<TApi extends Api>(
|
||||
assistantMsg.api === model.api &&
|
||||
assistantMsg.model === model.id);
|
||||
|
||||
const transformedContent = assistantMsg.content.flatMap((block) => {
|
||||
// Assistant content is typed as a block array, but transcript replay can
|
||||
// hand us a raw string (JSONL passthrough). Normalize it to an equivalent
|
||||
// single text block before transforming, matching the string->text block
|
||||
// handling already used in anthropic-payload-policy.ts.
|
||||
const contentBlocks = Array.isArray(assistantMsg.content)
|
||||
? assistantMsg.content
|
||||
: [{ type: "text" as const, text: assistantMsg.content as unknown as string }];
|
||||
|
||||
const transformedContent = contentBlocks.flatMap((block) => {
|
||||
if (block.type === "thinking") {
|
||||
if (modelBoundThinkingReplayMode === "drop") {
|
||||
return [];
|
||||
|
||||
Reference in New Issue
Block a user