sanitizeMessage drops OpenAI reasoning ids but keeps provider-executed web_search parts; the next Responses request fails with "Item 'ws_…' of type 'web_search_call' was provided without its required 'reasoning' item" · Issue #2639 · cloudflare/agents · GitHub
Skip to content

sanitizeMessage drops OpenAI reasoning ids but keeps provider-executed web_search parts; the next Responses request fails with "Item 'ws_…' of type 'web_search_call' was provided without its required 'reasoning' item" #2639

Open

Description

@TheDominator54

Versions: agents 0.28.1, @cloudflare/think 0.20.2, ai 7.0.145 and @ai-sdk/openai 4.0.97. Still reproduces on agents 0.29.0, ai 7.0.147 and @ai-sdk/openai 4.0.98. main sessions/sanitize.ts is unchanged.

What happens

A reasoning model (here gpt-6-luna, through the Responses API, store at its default) uses openai.tools.webSearch(). The assistant message streams a reasoning part (providerMetadata.openai.itemId: "rs_…") and then a provider-executed tool-web_search part whose toolCallId is the ws_… item id. Every Sessions write runs sanitizeMessage(), which:

  • strips openai.itemId and reasoningEncryptedContent from every part, then
  • drops a reasoning part that is left with no text and no metadata.

When the reasoning has summary text, the part survives but without an itemId. @ai-sdk/openai then skips it ("Non-OpenAI reasoning parts are not supported"). Either way the next request has no reasoning item. The web_search result is still sent as {type: "item_reference", id: <toolCallId>}, because the provider uses toolCallId when itemId is missing. OpenAI rejects a web_search_call reference without the reasoning item that preceded it, so every later turn in that conversation fails.

Reproduction (Node, no network: the request body is captured by a custom fetch)

import { sanitizeMessage } from "agents/chat";
import { convertToModelMessages, streamText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";

const assistant = { id: "a1", role: "assistant", parts: [
  { type: "step-start" },
  { type: "reasoning", text: "", state: "done", providerMetadata: { openai: { itemId: "rs_123", reasoningEncryptedContent: null } } },
  { type: "tool-web_search", toolCallId: "ws_456", state: "output-available", input: {},
    output: { action: { type: "search", query: "…" }, sources: [] }, providerExecuted: true },
  { type: "text", text: "…", state: "done", providerMetadata: { openai: { itemId: "msg_789" } } }
]};
// build [user, assistant (raw or sanitizeMessage(assistant)), user], convertToModelMessages, streamText with
// createOpenAI({ fetch: capture }) and tools { web_search: openai.tools.webSearch({}) }; print body.input
request input, unsanitized history:  [user] [item_reference rs_123] [item_reference ws_456] [item_reference msg_789] [user]
request input, as Think persists it: [user] [item_reference ws_456] [assistant] [user]

The second request references ws_456 without rs_123. This happens both with empty reasoning text and with summary text.

Seen live: on gpt-6-luna, every turn after some web_search turns failed with Item 'ws_…' of type 'web_search_call' was provided without its required 'reasoning' item. The stored assistant message had a tool-web_search part and no reasoning part. /new, or undoing that turn, recovered.

Proposed fix (any one of these)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions