From e77fa3aeba08bbbf534d550ecda895a4649a9657 Mon Sep 17 00:00:00 2001 From: Alix-007 Date: Wed, 17 Jun 2026 01:32:46 +0800 Subject: [PATCH] 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. --- src/agents/openai-transport-stream.ts | 6 +++++- src/llm/providers/openai-completions.ts | 4 +++- src/llm/providers/transform-messages.ts | 10 +++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/agents/openai-transport-stream.ts b/src/agents/openai-transport-stream.ts index 1fe8f3d672e..2f39d08bb38 100644 --- a/src/agents/openai-transport-stream.ts +++ b/src/agents/openai-transport-stream.ts @@ -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")), ); } diff --git a/src/llm/providers/openai-completions.ts b/src/llm/providers/openai-completions.ts index 3c41fe20803..0fa4d9b3ea0 100644 --- a/src/llm/providers/openai-completions.ts +++ b/src/llm/providers/openai-completions.ts @@ -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; } } diff --git a/src/llm/providers/transform-messages.ts b/src/llm/providers/transform-messages.ts index 68ff48c86d5..25a3b94ab51 100644 --- a/src/llm/providers/transform-messages.ts +++ b/src/llm/providers/transform-messages.ts @@ -117,7 +117,15 @@ export function transformMessages( 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 [];