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 [];