Skip to content

fix(runtime): make Responses reasoning replay dialect-aware #2513

Closed

Description

@me2seeks

What happens

DeepSeek Responses reasoning now arrives and is stored after #2328, but a later tool-loop request tries to replay that plaintext reasoning through the OpenAI encrypted-reasoning shape.

With store: false, @ai-sdk/openai rejects that shape when it has no reasoningEncryptedContent, removes the reasoning item from the request, and prints this process warning:

AI SDK Warning (openai.responses / deepseek-v4-flash): Reasoning parts without encrypted content are not supported when store is false. Skipping reasoning parts.

This is not only noisy terminal output: the warning says the request projection is lossy.

The asymmetric round trip

Response direction (provider -> Maka) works today:

DeepSeek response.reasoning_text.delta
  -> plaintext Responses transport added in #2328
  -> AI SDK reasoning delta
  -> Maka ThinkingItem { text, itemId, no encrypted content }
  -> event log / UI

Request direction (Maka -> provider) does not:

stored ThinkingItem
  -> AiSdkBackend reasoning part
  -> @ai-sdk/openai Responses input converter
  -> store=false + encrypted_content missing
  -> warning + reasoning item removed
  -> provider receives function_call / function_call_output without the plaintext reasoning

For example, after DeepSeek reasons and calls a tool, the desired second request is conceptually:

message, reasoning, function_call, function_call_output

The current request is:

message, function_call, function_call_output

DeepSeek can mask this in ordinary runs by recovering reasoning from its own call_id, but that is provider-specific behavior rather than a complete replay contract. The provider-free serialization gap is tracked upstream in vercel/ai#18511, with a proposed @ai-sdk/open-responses fix in vercel/ai#18546.

Why encrypted_content is involved

Maka sends Responses requests with store: false. The native OpenAI provider in the AI SDK therefore treats a replayable reasoning item as OpenAI's encrypted-content contract. DeepSeek's Open Responses dialect instead returns and consumes plaintext content[].reasoning_text; it cannot satisfy the OpenAI serializer merely by having an itemId.

The current replay capability is too broad: openai-responses-item says that a Responses reasoning item can be replayed, but does not say which wire dialect can encode it.

Proposed direction

I think this should be resolved at the ModelAdapter/request-projection seam, not by suppressing AI SDK warnings globally.

  1. Do not project plaintext Responses reasoning as an OpenAI encrypted reasoning item when reasoningEncryptedContent is absent. This preserves today's actual wire body while removing the misleading attempted replay and process warning.
  2. Keep replaying genuine encrypted reasoning unchanged.
  3. Once the upstream @ai-sdk/open-responses request-side fix is released, evaluate routing measured Open Responses providers such as DeepSeek through a dialect-specific codec/provider so plaintext reasoning is really serialized in the correct order.

The short-term containment and the full plaintext round trip can be separate PRs. I’m happy to implement this; before I start, I’d like to confirm whether maintainers prefer that split or want to wait for the upstream provider fix.

Acceptance checks

  • A DeepSeek tool-loop request emits no plaintext-as-encrypted warning.
  • A fake-fetch assertion checks the actual request body, not only Maka's intermediate model messages.
  • Encrypted OpenAI Responses reasoning still replays.
  • The request preserves message, reasoning, function_call, function_call_output ordering once plaintext replay is enabled.
  • No global warning suppression is introduced.

Related: #2328, vercel/ai#18511, vercel/ai#18546

中文说明

现象

#2328 已经修复了响应方向:DeepSeek 返回的明文 reasoning_text 能进入 Maka 的事件日志和界面。但是下一次工具回合组装请求时,Maka 又把这段明文 reasoning 当成 OpenAI 的加密 reasoning item 重放。

因为当前请求使用 store: false,@ai-sdk/openai 要求这种 reasoning item 携带 encrypted_content。DeepSeek 返回的是明文 reasoning_text,没有加密内容,所以 SDK 打印 warning 并在真正发请求前删除 reasoning item。

因此这不是单纯的日志噪音,而是说明请求投影发生了有损丢弃。

两个方向

  • 响应方向:Provider 返回内容给 Maka。这个方向已经能把 DeepSeek 的明文 reasoning 转换、保存并显示。
  • 请求方向:Maka 在工具调用后的下一轮,把历史消息重新发送给 Provider。这个方向目前尝试使用错误的 OpenAI 加密格式,随后被 SDK 丢弃。

普通运行有时仍然成功,是因为 DeepSeek 可以根据自己签发的 call_id 在服务端找回 reasoning;这不代表 Maka 已经正确发送了它。

建议

短期在 ModelAdapter/请求投影位置停止把缺少 reasoningEncryptedContent 的明文 reasoning 伪装成加密 item;真实的加密 reasoning 继续正常重放。长期等 @ai-sdk/open-responses 的请求侧修复发布后,再让 DeepSeek 使用能真正编码 reasoning_text 的 provider/codec。

不建议全局关闭 AI SDK warning,因为那只会隐藏有损请求。

我愿意实现这个修复;开始前想先确认维护者更希望拆成“短期止损 + 完整明文重放”两个 PR,还是等待上游 provider 修复。

Activity

  1. me2seeks commented on Aug 13, 2026

    @me2seeks
    ContributorAuthor

    Upstream update: vercel/ai#18515 has merged, and the plaintext assistant-reasoning serializer is now published in @ai-sdk/open-responses 2.0.27.

    This does not reach Maka through a dependency bump alone: current main still routes DeepSeek Responses through @ai-sdk/openai's createOpenAI(...).responses(). Consuming the upstream fix requires a dialect-specific adapter that routes measured Open Responses providers through createOpenResponses(), while keeping native OpenAI encrypted reasoning replay separate.

    @Astro-Han, would this direction be acceptable? If so, I’ll open a separate Draft PR for the complete integration and treat #2518 as the short-term containment/safety boundary. The new PR will assert the actual second request body and ordering:

    message → reasoning → function_call → function_call_output

  2. me2seeks commented on Aug 13, 2026

    @me2seeks
    ContributorAuthor

    Draft implementation is now available in #2972. It routes DeepSeek through @ai-sdk/open-responses@2.0.27, keeps native OpenAI encrypted replay separate, and covers the actual second-request ordering at the fetch boundary. DeepSeek custom ApplyPatch and hosted WebSearch compatibility are included as well. The remaining unverified item is a live credential smoke test.

  3. Astro-Han commented on Aug 13, 2026

    @Astro-Han
    Contributor

    Confirming the direction here so the follow-up PR doesn't have to guess.

    Take @ai-sdk/open-responses rather than building a dialect-specific adapter. For us it's a net deletion: it reads the standard content[].reasoning_text shape natively, so openai-responses-plaintext-reasoning-transport.ts retires with it, and 2.0.27 covers the request side. Carrying an upstream version beats maintaining a parallel implementation, and the AI SDK moves fast enough that anything missing is worth filing there instead of working around here.

    No reverse-engineering needed on the target shape — DeepSeek documents reasoning as an input item type carrying "a list of reasoning_text content parts", the mirror of what it emits:

    { "type": "reasoning", "content": [{ "type": "reasoning_text", "text": "..." }] }

    Plaintext throughout; encryption isn't part of this path at all.

    Worth recording why this matters, since it's easy to read as cosmetic. The thinking-mode guide says reasoning must be passed back when tool calls are present, and that omitting it returns a 400. We don't see those because DeepSeek recovers it server-side — we've tested that, so nothing is degrading today. But we're leaning on undocumented server behavior to satisfy a field the contract requires, and if that ever tightens it fails on tool loops specifically, which is the expensive kind of failure to reproduce.

    Two notes for whoever picks this up:

    • The switch reaches more than DeepSeek. volcengine-agent-plan, openai-responses-compatible, gpt-5* and grok-4.5 all resolve to this wire, so the change needs to keep genuine encrypted replay working rather than only the plaintext path.
    • reasoningReplayContract gains an open-responses-plaintext sibling next to the encrypted kind — a parallel member, not a rewrite. fix(runtime): gate Responses replay on encrypted reasoning #2518 stays as containment and doesn't close this issue.

    On timing: this changes the request body, so please keep it out of the current benchmark window. #2518 doesn't — its wire body is byte-identical — so that one can land whenever the wire-body test is in.

    The acceptance check already written above ("a fake-fetch assertion checks the actual request body") is the right gate for this one too, and it's the same test shape #2518 needs.

    中文

    先把方向确认下来,免得后续 PR 还要猜。

    用 @ai-sdk/open-responses,不要自建 dialect-specific adapter。 对我们来说这是净删代码:它原生读标准的 content[].reasoning_text,openai-responses-plaintext-reasoning-transport.ts 可以跟着退役,2.0.27 又覆盖了请求侧。跟一个上游版本好过维护一套并行实现,而且 AI SDK 迭代够快,那边缺什么值得提上去,而不是在我们这里绕。

    目标形状不用反推——DeepSeek 文档里 reasoning 就是 input item 类型之一,内容是「a list of reasoning_text content parts」,正是它吐出来那个形状的镜像:

    { "type": "reasoning", "content": [{ "type": "reasoning_text", "text": "..." }] }

    全程明文,这条路径根本不涉及加密。

    也记一下为什么这件事重要,否则容易被读成锦上添花。thinking mode 文档写明,存在工具调用时 reasoning 必须回传,不回传返回 400。我们没遇到,是因为 DeepSeek 在服务端替我们找回了——这个我们实测过,所以今天没有任何东西在掉分。但我们是在用一个未文档化的服务端行为,去顶一个契约要求的字段;一旦对方收紧,只会在工具回合上炸,而那是最难复现的一类故障。

    给接手的人两点:

    • 影响面不止 DeepSeek。volcengine-agent-plan、openai-responses-compatible、gpt-5* 和 grok-4.5 都解析到这条 wire,所以改动要保住真正的加密重放,而不只是明文那条路。
    • reasoningReplayContract 是在加密那个 kind 旁边新增一个 open-responses-plaintext 兄弟成员,不是重写。fix(runtime): gate Responses replay on encrypted reasoning #2518 作为围堵保留,不关闭本 issue。

    时间上:这个会改请求体,请避开当前的跑分窗口。#2518 不会——它的请求体逐字节不变——所以那个补完 wire-body 测试就可以随时合。

    上面验收标准里已经写了的那条「fake-fetch assertion checks the actual request body」,对这个 PR 同样是正确的门禁,而且和 #2518 需要的是同一种测试形状。

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions