Skip to content

fix(runtime): route DeepSeek reasoning through Open Responses with tool fallbacks - #2972

Merged
M4n5ter merged 12 commits into
apache:mainfrom
me2seeks:feat/2513-open-responses-plaintext-replay
Aug 19, 2026
Merged

fix(runtime): route DeepSeek reasoning through Open Responses with tool fallbacks#2972
M4n5ter merged 12 commits into
apache:mainfrom
me2seeks:feat/2513-open-responses-plaintext-replay

Conversation

@me2seeks

@me2seeks me2seeks commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • select the Responses SDK explicitly with responsesAdapter: 'openai' | 'open-responses'
  • keep reasoning continuation independent as encrypted-content or plaintext-content
  • route DeepSeek V4 through @ai-sdk/open-responses and delete Maka's protocol wrapper and private request rewriting
  • preserve old apply_patch work as a durable fact when the selected codec cannot replay the tool
  • resolve reasoning settings once for main, title, recap, memory, daily-review, and evaluator calls
  • merge the encrypted-replay containment from fix(runtime): gate Responses replay on encrypted reasoning #2518

Why

DeepSeek V4 returns plaintext reasoning.content[].reasoning_text. The OpenAI-specific Responses codec expects provider-issued IDs or encrypted content for stateless continuation, so DeepSeek reasoning could be visible in the UI but disappear from the next tool-loop request.

The runtime now models two independent choices:

responsesAdapter: 'openai' | 'open-responses';
responsesReasoningReplay: 'encrypted-content' | 'plaintext-content';

model-factory chooses the SDK only from the adapter contract. Durable replay chooses its representation only from the continuation contract.

Current capability boundary

@ai-sdk/open-responses@2.0.28 includes vercel/ai#18844, so history round trips preserve heterogeneous item order, item IDs, reasoning summaries/encrypted content, reasoning boundaries, and URL citations. The remaining boundaries fail closed:

  • DeepSeek exposes portable Write and Edit; the provider's custom apply_patch capability remains declared, but the generic Open Responses codec does not advertise it until it can serialize provider-defined custom tools
  • a valid historical freeform apply_patch is converted to a bounded assistant fact instead of being sent as an undeclared tool or silently dropped
  • DeepSeek model-native WebSearch is disabled; Tavily is available only when the user explicitly configures and selects it
  • provider-executed tool history falls back to grounded text instead of replaying an unmatched tool call (upstream seam tracked in @ai-sdk/open-responses: support registered namespaced tool and item extensions vercel/ai#18899)
  • empty reasoning records are omitted

DeepSeek's declared reasoning effort is passed through verbatim under the provider-native namespace, so max is sent literally instead of being clamped by the SDK's xhigh mapping.

Verification

  • merged latest main into the branch and resolved conflicts (provider registry contracts, wire-contract tests, notices, lockfile)
  • full workspace npm run typecheck passes
  • @maka/runtime suite: 2877 passed
  • @maka/runtime-host execution-model composition + protocol: 50 passed
  • @maka/core provider-contract matrix / model-web-search / provider-registry: 5 passed
  • request-level checks cover Authorization, high/max, unsupported effort filtering, auxiliary-call reasoning, freeform ApplyPatch downgrade, empty reasoning, and hosted-tool replay fallback
  • formatter and diff checks passed

A live DeepSeek credential smoke test has not been run.

Fixes #2513

Follow-up to #2328 and #2518.

中文说明

这个 PR 把两个概念拆开:使用哪个 Responses SDK adapter,以及 reasoning 历史按什么契约继续。DeepSeek V4 使用通用 @ai-sdk/open-responses 和 plaintext continuation;OpenAI、xAI、custom relay 继续使用 OpenAI adapter 和 encrypted-content continuation。

Maka 自己的 Responses 协议实现和私有请求改写已经删除。@ai-sdk/open-responses@2.0.28 已包含上游 vercel/ai#18844 的无损回放修复。剩余的能力边界显式收缩:DeepSeek 暂时使用 Write/Edit,旧 apply_patch 会保留为事实,模型原生 WebSearch 暂不启用,provider-hosted tool 历史会回退到 grounded text(上游 vercel/ai#18899 跟踪中)。

DeepSeek 声明的 reasoning effort 通过 provider-native namespace 原样下发,max 不再被 SDK 的 xhigh 映射降级。

@M4n5ter

M4n5ter commented Aug 13, 2026

Copy link
Copy Markdown
Member
English — Request changes

Conclusion

This PR addresses a real bug, and routing DeepSeek through @ai-sdk/open-responses is directionally correct. However, the current implementation is not ready to merge.

The root cause is not that DeepSeek uses a separate “plaintext Responses dialect.” It is a mismatch between:

  1. the reasoning continuation model implemented by @ai-sdk/openai;
  2. the partial, currently lossy implementation in @ai-sdk/open-responses;
  3. DeepSeek-specific hosted-tool and custom-tool behavior.

The PR currently collapses those concerns into responsesDialect: 'open-responses' and a single wrapper. That abstraction is misleading, is not reusable for arbitrary Responses providers, and introduces two deterministic correctness regressions.

Root cause

1. @ai-sdk/openai implements OpenAI-specific reasoning continuation

Open Responses defines one reasoning item that may carry any combination of:

  • content for plaintext reasoning;
  • encrypted_content for opaque continuation;
  • summary for displayable reasoning summaries.

These are representations within the same item model, not separate wire dialects. See the Open Responses reasoning specification and API reference.

OpenAI currently uses summaries plus item IDs or encrypted content for stateless continuation. Its AI SDK provider consequently reads summary and encrypted_content, but does not consume plaintext reasoning.content[].reasoning_text. See the current @ai-sdk/openai response decoder and reasoning replay encoder.

I reproduced this with the latest @ai-sdk/openai@4.0.41: a DeepSeek-shaped reasoning item containing plaintext content was decoded as an empty reasoning part with no encrypted content, and the reasoning item disappeared from the following request.

That is the original failure: an OpenAI-specific continuation codec is being used against a provider that returns and consumes plaintext reasoning.

2. The latest @ai-sdk/open-responses only fixes the narrow reasoning-loss case

The PR already uses the latest stable @ai-sdk/open-responses@2.0.27. Version 2.0.27 added serialization of an assistant reasoning part as content[].reasoning_text, fixing the earlier behavior where reasoning disappeared completely during a tool loop. See upstream PR #18515.

That fix is necessary, but it is not a lossless Responses item round trip. The upstream test covers only a single isolated reasoning part; it does not cover mixed reasoning, tool, and grounded-text chronology. See the upstream regression test.

The latest converter collects assistant parts into three buckets and always emits:

  1. reasoning items;
  2. assistant text;
  3. function calls.

It does this regardless of the original part order. See convertToOpenResponsesInput.

The decoder is also lossy: it maps only plaintext reasoning content and message text. It does not preserve reasoning summaries, encrypted_content, reasoning item IDs, or text annotations in provider metadata. See the non-streaming decoder.

I independently ran the published 2.0.27 package. Given assistant content ordered as:

reasoning
provider-hosted search
grounded assistant text

the actual request body was:

reasoning
grounded assistant text
function_call

The same probe confirmed that summary, encrypted_content, item metadata, and URL citation annotations were not preserved.

The streaming implementation has the same limitations. It handles plaintext response.reasoning_text.delta, but does not project reasoning-summary or annotation events, and it classifies every completed function_call as a client tool call. See the stream implementation.

The AI SDK documentation also treats the packages as separate implementations: use @ai-sdk/open-responses for third-party or self-hosted compatible endpoints, and @ai-sdk/openai when OpenAI-specific tools and options are required. It does not currently provide one complete provider-neutral adapter with OpenAI extensions. See the AI SDK Open Responses provider documentation.

3. The PR turns a hosted tool into a client tool to fit the upstream codec

The wrapper translates DeepSeek web_search_call into a sentinel function_call before the upstream codec sees it. See the response projection.

The upstream codec unconditionally sets hasToolCalls = true for a function_call and maps a completed response to tool-calls. See the decoder and finish-reason mapper.

The wrapper later converts the sentinel content back into a providerExecuted WebSearch call/result, but it does not restore the finish reason. See projectGenerateResult and projectStreamResult.

This is the immediate cause of the first blocking regression.

Blocking correctness issues

1. A successful hosted WebSearch can fail the run as step_limit

Maka correctly excludes providerExecuted calls from the client tool loop. However, the sentinel has already caused the SDK finish reason to become tool-calls.

When maxSteps is configured, the backend converts that finish reason into step_limit. See ai-sdk-backend.ts.

step_limit is then classified as tool_step_cap_reached, and the runtime flow marks the run as failed. See events.ts and ai-sdk-flow.ts.

Therefore, a response where DeepSeek has already completed WebSearch and returned the final answer can be reported as an exhausted and failed run.

The adapter must preserve the distinction between provider-hosted tools and client-executed function calls. If a response contains only completed hosted calls and no real client call, its finish reason must remain stop.

The current streaming test does not assert the finish event, so it cannot detect this regression.

2. WebSearch replay reverses the required chronology

Maka intentionally materializes a provider-hosted tool before the grounded assistant text:

reasoning
provider tool call/result
grounded assistant text

See the chronology documented and implemented in ai-sdk-backend.ts.

The upstream converter then regroups that assistant content as:

reasoning
grounded assistant text
sentinel function_call

Finally, the wrapper converts the sentinel call into an item_reference, preserving the already incorrect position. See projectRequestBody.

The final wire request is therefore approximately:

reasoning
grounded assistant text
item_reference

This reverses the relationship between the hosted search and the answer grounded in that search.

The wrapper’s comment currently claims that presenting the custom tools as functions lets the upstream codec retain ordering. That claim is contradicted by the upstream implementation. See the wrapper comment and the upstream bucketed conversion.

The replay implementation must preserve ordered Responses items. This can be fixed upstream or owned explicitly by the DeepSeek adapter, but it cannot rely on the current upstream grouping behavior.

Design issue: responsesDialect models the wrong boundary

The provider registry describes responsesDialect: 'open-responses' as selecting a “standard plaintext Open Responses dialect instead of OpenAI’s extension.” See provider-registry.ts.

That description conflates two independent decisions:

  1. which SDK adapter/codec is used;
  2. how reasoning is continued.

Plaintext and encrypted reasoning are not mutually exclusive dialects. A conforming reasoning item may contain plaintext content, encrypted content, summaries, or a supported combination. The runtime should preserve what was actually returned and apply the provider’s continuation contract.

A more accurate minimal model would separate:

responsesAdapter: 'openai' | 'open-responses';

from:

reasoningReplay:
  | 'item-reference'
  | 'encrypted-content'
  | 'plaintext-content';

The exact names can follow repository conventions, but adapter selection must not be inferred from reasoningReplay.kind, as it currently is in model-factory.ts.

I am not suggesting that Maka should implement the entire Open Responses specification in this PR. The scope-controlled design is:

  1. use @ai-sdk/open-responses as the generic third-party Responses base;
  2. represent the adapter choice explicitly rather than calling encryption a dialect;
  3. keep reasoning continuation independent;
  4. name and scope the wrapper as a DeepSeek-specific compatibility adapter;
  5. keep WebSearch, custom apply_patch, and DeepSeek effort mapping in that provider-specific layer;
  6. contribute generally applicable fixes—ordered item replay, annotations, and reasoning metadata—to the upstream AI SDK where practical.

This is more honest and maintainable than exposing the current wrapper as a generic plaintext Responses implementation. The wrapper currently recognizes only openai.custom/apply_patch and openai.web_search/WebSearch, rejects every other provider tool, and forces store: false. See projectTool and projectRequestBody.

A future provider with ordinary Responses reasoning but different hosted tools should use the generic base without inheriting those DeepSeek assumptions.

Additional simplification

The private x-maka-open-responses-reasoning-effort header creates two representations of the request: the body recorded by the delegated model and the bytes sent after fetch mutation.

DeepSeek accepts both xhigh and max, and its documentation states that xhigh maps to max. Unless the literal wire value max is a required external contract, Maka should map max to the SDK-supported xhigh value and remove the private header and request-body mutation. See the DeepSeek thinking-mode documentation.

Required validation

Please add behavior-level coverage for:

  • hosted WebSearch followed by a final answer produces stop, not tool-calls;
  • the same path with finite maxSteps completes successfully;
  • replay preserves reasoning → hosted search reference → grounded text;
  • a response containing reasoning content, summary, and encrypted_content does not silently conflate or discard representations;
  • annotations/citations are preserved if DeepSeek emits them;
  • ordinary client function calls still produce tool-calls;
  • a second synthetic Open Responses provider can use the generic base without DeepSeek extensions;
  • custom relay support is enabled only after its adapter and continuation contract can be declared explicitly.

Final assessment

  • Correctness: not acceptable as submitted because hosted WebSearch can turn a successful response into a failed run, and replay changes item chronology.
  • Design: not acceptable as submitted because responsesDialect conflates adapter selection, reasoning continuation, and DeepSeek-specific extensions.
  • Direction: keep the move to @ai-sdk/open-responses; do not revert to the OpenAI-specific codec.
  • Minimum mergeable direction: make adapter selection explicit, keep continuation independent, scope the wrapper to DeepSeek, and fix the two hosted-tool regressions.
中文 — 请求修改

结论

这个 PR 修复的是真实问题,将 DeepSeek 路由到 @ai-sdk/open-responses 的方向也是正确的。但是,当前实现还不适合合并。

根因并不是 DeepSeek 使用了另一种“明文 Responses 方言”,而是以下三者之间的契约不匹配:

  1. @ai-sdk/openai 实现的 reasoning continuation 模型;
  2. @ai-sdk/open-responses 当前仍然有损的部分实现;
  3. DeepSeek-specific 的 hosted tool 和 custom tool 行为。

当前 PR 将这些问题全部压缩进 responsesDialect: 'open-responses' 和一个 wrapper 中。这个抽象具有误导性,无法安全复用于任意 Responses provider,并且引入了两个确定性的正确性回归。

根因

1. @ai-sdk/openai 实现的是 OpenAI-specific reasoning continuation

Open Responses 定义了统一的 reasoning item,其中可以包含任意组合:

  • content:明文 reasoning;
  • encrypted_content:不透明但可回放的 reasoning;
  • summary:可展示的 reasoning 摘要。

它们是同一个 item model 中的不同表示,不是不同 wire dialect。参考 Open Responses reasoning specificationAPI reference

OpenAI 当前使用 summary,并通过 item ID 或 encrypted content 进行无状态 continuation。因此,它的 AI SDK provider 会读取 summaryencrypted_content,但不会读取明文 reasoning.content[].reasoning_text。参见当前的 @ai-sdk/openai response decoderreasoning replay encoder

我使用最新的 @ai-sdk/openai@4.0.41 进行了复现:包含 plaintext content 的 DeepSeek-shaped reasoning item 被解析成一个空 reasoning part,没有 encrypted content,并且该 reasoning item 在下一次请求中完全消失。

这就是原始故障:一个 OpenAI-specific continuation codec 被用于一个返回并消费明文 reasoning 的 provider。

2. 最新 @ai-sdk/open-responses 只修复了最窄的 reasoning 丢失问题

这个 PR 已经使用了最新稳定版 @ai-sdk/open-responses@2.0.27。2.0.27 增加了将 assistant reasoning part 序列化成 content[].reasoning_text 的逻辑,修复了此前 reasoning 在 tool loop 中完全消失的问题。参见 上游 PR #18515

这个修复是必要的,但它并没有实现无损的 Responses item round trip。上游测试只覆盖了单独一个 reasoning part,没有覆盖 reasoning、tool 和 grounded text 混排的时间顺序。参见 上游回归测试

最新 converter 会把 assistant part 收集到三个 bucket 中,并始终按照以下顺序输出:

  1. reasoning item;
  2. assistant text;
  3. function call。

它不会保留原始 part 顺序。参见 convertToOpenResponsesInput

Decoder 同样是有损的:它只投影 plaintext reasoning content 和 message text,没有在 provider metadata 中保留 reasoning summary、encrypted_content、reasoning item ID 或文本 annotation。参见 非流式 decoder

我直接运行了 npm 发布的 2.0.27。给定如下 assistant content:

reasoning
provider-hosted search
grounded assistant text

实际 request body 变成:

reasoning
grounded assistant text
function_call

同一复现还确认,summaryencrypted_content、item metadata 和 URL citation annotation 都没有被保留。

流式实现存在相同限制。它能够处理 plaintext response.reasoning_text.delta,但不会投影 reasoning summary 或 annotation event,并且会把每个已完成的 function_call 都分类为客户端工具调用。参见 stream implementation

AI SDK 文档本身也把这两个包定位为不同实现:第三方或自托管兼容 endpoint 使用 @ai-sdk/open-responses;需要 OpenAI-specific tool 和 option 时使用 @ai-sdk/openai。目前并不存在一个同时覆盖 OpenAI extension 的完整 provider-neutral adapter。参见 AI SDK Open Responses provider documentation

3. PR 为了适配上游 codec,把 hosted tool 变成了 client tool

Wrapper 在上游 codec 看到响应之前,将 DeepSeek web_search_call 转换成 sentinel function_call。参见 response projection

上游 codec 会对任意 function_call 无条件设置 hasToolCalls = true,并把已完成响应映射为 tool-calls。参见 decoderfinish-reason mapper

Wrapper 随后会把 sentinel content 转换回 providerExecuted WebSearch call/result,但不会恢复 finish reason。参见 projectGenerateResultprojectStreamResult

这是第一个阻塞性回归的直接原因。

阻塞性正确性问题

1. 成功的 hosted WebSearch 可能把 run 错误标记为 step_limit

Maka 正确地将 providerExecuted call 排除在客户端工具循环之外。但是,sentinel 已经让 SDK finish reason 变成了 tool-calls

当配置了 maxSteps 时,backend 会把这个 finish reason 转换为 step_limit。参见 ai-sdk-backend.ts

step_limit 随后被分类为 tool_step_cap_reached,runtime flow 会把整个 run 标记为失败。参见 events.tsai-sdk-flow.ts

因此,即使 DeepSeek 已经完成 WebSearch 并返回最终答案,这次请求仍可能被报告为步数耗尽和运行失败。

Adapter 必须保留 provider-hosted tool 与客户端执行 function call 之间的区别。如果响应只包含已完成的 hosted call,而没有真正的 client call,finish reason 必须保持为 stop

当前 streaming 测试没有断言 finish event,因此无法发现这个回归。

2. WebSearch 回放颠倒了必需的时间顺序

Maka 有意将 provider-hosted tool 放在 grounded assistant text 之前:

reasoning
provider tool call/result
grounded assistant text

参见 ai-sdk-backend.ts 中记录并实现的 chronology。

上游 converter 随后将 assistant content 重新分组为:

reasoning
grounded assistant text
sentinel function_call

最后,wrapper 将 sentinel call 转换为 item_reference,但保留了已经错误的位置。参见 projectRequestBody

因此最终 wire request 大致变成:

reasoning
grounded assistant text
item_reference

这颠倒了 hosted search 与基于该搜索结果生成的回答之间的关系。

Wrapper 的注释当前声称,将 custom tool 表示成 function 可以让上游 codec 保留顺序。这个说法与上游实现直接矛盾。参见 wrapper commentupstream bucketed conversion

Replay 必须保留有序 Responses item。这个问题可以在上游修复,也可以由 DeepSeek adapter 显式承担,但不能继续依赖当前上游的分组行为。

设计问题:responsesDialect 表达了错误的边界

Provider registry 将 responsesDialect: 'open-responses' 描述为选择“standard plaintext Open Responses dialect instead of OpenAI’s extension”。参见 provider-registry.ts

这个描述混合了两个独立决策:

  1. 使用哪个 SDK adapter/codec;
  2. 如何 continuation reasoning。

Plaintext 和 encrypted reasoning 不是互斥 dialect。一个符合规范的 reasoning item 可以包含 plaintext content、encrypted content、summary 或受支持的组合。Runtime 应当保留实际返回的内容,再应用 provider 的 continuation contract。

更准确的最小模型应当将:

responsesAdapter: 'openai' | 'open-responses';

与:

reasoningReplay:
  | 'item-reference'
  | 'encrypted-content'
  | 'plaintext-content';

分开表达。

具体命名可以遵循仓库约定,但 adapter 选择不应像当前 model-factory.ts 那样由 reasoningReplay.kind 推导。

我并不是建议 Maka 在这个 PR 中自行实现完整的 Open Responses 规范。范围受控的设计应该是:

  1. 使用 @ai-sdk/open-responses 作为第三方 Responses 的通用基础;
  2. 显式表达 adapter 选择,而不是把 encryption 称为 dialect;
  3. 将 reasoning continuation 保持为独立契约;
  4. 将 wrapper 明确命名和限定为 DeepSeek-specific compatibility adapter;
  5. 将 WebSearch、自定义 apply_patch 和 DeepSeek effort mapping 保留在 provider-specific 层;
  6. 在可行时,将 ordered item replay、annotation 和 reasoning metadata 等通用修复贡献到 AI SDK 上游。

这比把当前 wrapper 暴露为通用 plaintext Responses 实现更诚实、更易维护。当前 wrapper 只识别 openai.custom/apply_patchopenai.web_search/WebSearch,会拒绝其他所有 provider tool,并且强制 store: false。参见 projectToolprojectRequestBody

未来具有普通 Responses reasoning、但使用不同 hosted tool 的 provider,应当能够使用通用基础,而不继承这些 DeepSeek 假设。

可以进一步简化的部分

私有 x-maka-open-responses-reasoning-effort header 会产生同一个请求的两份表示:delegated model 记录的 body,以及经过 fetch mutation 后实际发送的字节。

DeepSeek 同时接受 xhighmax,其文档也说明 xhigh 会映射为 max。除非字面上的 wire value max 是外部强制契约,否则 Maka 应将 max 映射为 SDK 支持的 xhigh,并删除私有 header 和 request-body mutation。参见 DeepSeek thinking-mode documentation

必需的验证

请增加以下行为级测试:

  • hosted WebSearch 后已经产生最终答案时,finish reason 是 stop,不是 tool-calls
  • 同一路径配置有限 maxSteps 时成功完成;
  • replay 保持 reasoning → hosted search reference → grounded text
  • 包含 reasoning contentsummaryencrypted_content 的响应不会被静默混淆或丢弃;
  • 如果 DeepSeek 返回 annotation/citation,应当保留它们;
  • 普通客户端 function call 仍然返回 tool-calls
  • 第二个不带 DeepSeek extension 的虚构 Open Responses provider 可以使用通用基础;
  • 只有当 custom relay 能显式声明 adapter 和 continuation contract 时,才为它开放这条路径。

最终判断

  • Correctness: 当前不可接受,因为 hosted WebSearch 可以把成功响应变成失败 run,并且 replay 改变了 item chronology。
  • Design: 当前不可接受,因为 responsesDialect 混合了 adapter 选择、reasoning continuation 和 DeepSeek-specific extension。
  • 方向: 保留迁移到 @ai-sdk/open-responses 的方向,不要回退到 OpenAI-specific codec。
  • 最小可合并方向: 显式表达 adapter 选择、独立保留 continuation、将 wrapper 限定为 DeepSeek-specific,并修复两个 hosted-tool 回归。

@M4n5ter

M4n5ter commented Aug 13, 2026

Copy link
Copy Markdown
Member

Follow-up: I opened vercel/ai#18839 for the underlying @ai-sdk/open-responses round-trip problem:

vercel/ai#18839

If the schedule allows us to wait for an upstream fix, I strongly recommend doing so before landing this adapter. A correct upstream implementation that preserves item order, item identity, and opaque reasoning state would let us remove a substantial amount of local replay/dialect compensation and avoid owning a parallel Responses codec in Maka.

After that fix, the Maka-side design could be much smaller:

  • explicitly select the generic @ai-sdk/open-responses adapter for compatible endpoints;
  • keep only genuine DeepSeek extensions in a narrowly scoped DeepSeek adapter, such as hosted WebSearch mapping or an exact max effort mapping if upstream still cannot express them; and
  • delete generic reasoning-history reconstruction and ordering workarounds that belong in the upstream codec.

This would also make the abstraction boundary clearer: encrypted versus plaintext reasoning is not a provider “dialect” distinction—the Responses/Open Responses item model permits both. The generic adapter should preserve either representation, while provider-specific code should cover only actual provider extensions.

If waiting is not possible, the current workaround should be treated as temporary, isolated behind a DeepSeek-specific boundary, and covered by round-trip tests so that it can be deleted when the upstream issue is fixed.

@me2seeks me2seeks changed the title fix(runtime): route plaintext Responses through its dialect fix(runtime): route DeepSeek through an explicit Responses adapter Aug 13, 2026

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anchored to f531b0f58; I posted this 39 minutes after 9e9d1461c landed, which is the same version slip I attribute to another review below, with less excuse. Re-checked against 9e9d1461c: all five inline findings still hold and the design note is now obsolete — corrected in place. The upstream table is unaffected, being probe results against the published 2.0.27 rather than against this branch.

The direction is right and the core claim holds. I drove a full AiSdkBackend.send() against a DeepSeek connection with a real getAIModel and a capturing fetch; the second-round body is what you say it is:

POST https://api.deepseek.com/responses
input: [reasoning{content:[reasoning_text]}, function_call, function_call_output, message]

The OpenAI encrypted path is not regressed. Deleting the self-maintained transport is the right call — upstream reads response.reasoning_text.delta and now writes content[].reasoning_text back, which is exactly the half the old transport could not fix.

Most of what came out of review is upstream's to fix, not yours. Splitting it that way leaves you with a short list.

Yours: one blocker

Stored apply_patch history replays as a malformed call. Sessions created on main stored freeform patch strings — ApplyPatch is the default routing for v4-flash/pro there. With the profile now null, normalizeApplyPatchReplayInput returns the stored input untouched and it reaches the wire as non-JSON arguments naming a tool that is no longer declared. Two reviewers reproduced the same body independently. downgradedApplyPatchCalls is the intended handler for exactly this and is unreachable behind an early return. Detail inline; this is the only finding no upstream change can rescue.

Three P1s, each small, all in seams this PR moved:

  • session thinking level now bypasses resolveThinkingLevel, so undeclared efforts reach the wire (model-adapter.ts:281)
  • the plaintext reasoning branch has no empty-text guard, unlike both paths it replaces (ai-sdk-backend.ts:3756)
  • execution-model-authority.ts:499 never got the second effort channel, so DeepSeek's title/memory/evaluator calls now send no reasoning at all

One P2 in the same family: upstream's assistant branch only recognises reasoning / text / tool-call, so a provider-executed tool-result is dropped while its tool-call still serialises — I reproduced a function_call with no matching function_call_output. canReplayProviderNative (ai-sdk-backend.ts:3686) does not screen for this. Model switching isn't from this PR, but routing DeepSeek through this codec is what makes "used hosted search on OpenAI, then switched to DeepSeek" reach it.

Upstream's: file these, don't work around them

Each of these is reproducible against the published 2.0.27. Several overlap with what @M4n5ter is already filing, so they are probably one issue rather than seven.

# Behaviour Probe result
1 effort: 'max' removes reasoning from the body entirely max → body.reasoning undefined + one unsupported warning; enum is minimal/low/medium/high/xhigh (open-responses-language-model.ts:159-173)
2 Non-function tools are dropped with zero warning 1 function + 2 provider-defined in → body.tools has 1, warnings: [] (:120-128)
3 Converter regroups assistant parts, losing interleaved order text→reasoning→toolcall→reasoning→text out as [reasoning, reasoning, message(merged), function_call] (convert-to-open-responses-input.ts:105-161)
4 Serializer hardcodes summary: [] and emits no id / encrypted_content part providerOptions discarded (:115-119)
5 Decoder drops summary, encrypted_content, item ids, annotations a reasoning item with only summary decodes to 0 parts; providerMetadata hardcoded undefined (:262-297, :336)
6 Stream and non-stream disagree on the same response stream keeps item ids, non-stream has none; stream reads reasoning only from response.reasoning_text.delta, which upstream's own comment calls a non-spec LM Studio extension (:507-519)
7 Unknown incomplete_details.reason + tool calls → tool-calls a truncated response is reported as a normal tool turn; raw keeps the real reason, unified does not (map-open-responses-finish-reason.ts:10-20)
8 Truncation flush emits reasoning-end with a hardcoded reasoning-0 real id was rs_REAL_ID; AI SDK core then injects error: reasoning part reasoning-0 not found (:567-571)
9 No way to send store, and no previous_response_id support body has neither

#1 is why max maps to xhigh here, and that mapping is correct — do not change it. Passing max through does not clamp, it deletes the whole reasoning field, so the model runs with thinking off. DeepSeek's table is xhigh → high, max → max, so today max runs at high where main ran at max. That is a measurable regression for every DeepSeek run at max, benchmark baseline included, and it should be stated in the description rather than left implicit — the PR already notes no live credential smoke test was run.

Correcting myself on the fix for #1. I first wrote that upstream should add max to the effort enum. That enum is in @ai-sdk/provider (reasoning?: 'provider-default' | 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh') — the cross-provider abstraction, which should not carry each vendor's private ladder. The real gap is that @ai-sdk/open-responses exposes exactly one provider-specific field, reasoningSummary, and nothing equivalent for effort, so effort can only arrive through the standard enum and a hardcoded effortMap. That is the issue I will file. Separately, we do not have to wait for it: requestBodyOverlay (llm-connections.ts:122, applied at the fetch layer in model-factory.ts:62-66) can put a literal { reasoning: { effort: 'max' } } on the wire per connection, so restoring real max for DeepSeek is available to this PR today.

#2 is why apply_patch and native web search are off, and the zero-warning part is what makes it dangerous: a provider tool that silently never ships looks identical to one the model chose not to call. Both capabilities return on their own once upstream serializes provider-defined tools — worth a comment at the two disable sites recording that recovery condition.

#7 and #8 do not stay upstream — Maka amplifies both. model-adapter.ts:897 turns the SDK error chunk from #8 into a ModelFailure, so hitting max_output_tokens mid-reasoning ends the step with an unintelligible provider error instead of length. And model-adapter.ts:567 only treats other/unknown as truncated, so #7's fake tool-calls is accepted as success and the tool loop continues. Both are worth a local guard even before upstream lands — the raw finish reason is already available on streamedFinishReason.

Two things about the description

The capability regression belongs in the title. Swapping applyPatchProtocol for responsesDialect is one line, but it changes the headless coding tool surface from apply_patch to Write/Edit and rewrites HEADLESS_CODING_V1_TOOLS_HASH. That is material user-visible behaviour under AGENTS.md and refactor: does not carry it. Equivalence on terminal-bench cannot be assumed — freeform V4A patches edit multiple files per call, while Write/Edit requires reading first and re-matching strings, costing turns and input tokens. An A/B on one or two tasks, or an explicit accepted-regression note, would settle it.

Tavily is not actually still available. That only holds when defaultProvider is explicitly tavily; at the default 'model', native-web-search-tool.ts:83 removes the client WebSearch tool entirely, so a DeepSeek session has no search at all and nothing is logged. Benchmarks are unaffected — eval blocks web tools regardless.

On @M4n5ter's review

Thorough, and the upstream reproductions in it are the most valuable thing anyone has produced on this. Both of its blocking findings point at code that no longer exists — it landed 32 minutes after f531b0f58 deleted open-responses-plaintext-model.ts. Its design argument about responsesDialect was the part that still stood, and 2c3f18620 has since answered it: the concern is split into responsesAdapter and responsesReasoningReplay, and model-adapter.ts:283 gates on runtime.responsesAdapter rather than deriving the adapter from the reasoning replay shape. That is the right shape, and it is also what one of my reviewers arrived at independently. Nothing left to answer there.

Remaining P2 notes are inline. Nothing else blocks.


Review assistance: Claude Code (Opus) ran seven independent fresh-eye passes (runtime correctness, capability regression, test coverage, architecture, upstream package behaviour, persistence compatibility, dependency/security), each blind to the others' findings. I verified the DeepSeek effort table against the official docs, confirmed the store: false filter in @ai-sdk/openai at dist/index.js:5328-5338, and dropped two agent conclusions that did not survive that check.

中文

方向是对的,核心主张也成立。我用真实 getAIModel 加捕获 fetch 驱动了完整的 AiSdkBackend.send(),第二轮请求体确实如你所说:

POST https://api.deepseek.com/responses
input: [reasoning{content:[reasoning_text]}, function_call, function_call_output, message]

OpenAI 加密路径没有回归。删掉自维护的 transport 是对的——上游已经读 response.reasoning_text.delta,现在请求侧也会写回 content[].reasoning_text,正是旧 transport 修不了的那一半。

评审出来的问题大部分该由上游修,不是你的事。这样切开之后,留给你的清单很短。

你的:一个阻塞项

存量 apply_patch 历史会重放成非法调用。main 上创建的会话存的是 freeform 补丁字符串——而 ApplyPatch 正是 v4-flash/pro 在那边的默认路由。现在 profile 为 nullnormalizeApplyPatchReplayInput 原样返回存储内容,最终以非 JSON 的 arguments 调用一个已不再声明的工具上线。两位评审独立复现出同一个请求体。downgradedApplyPatchCalls 正是为这种情况准备的处理路径,却被一个提前 return 挡在外面够不着。细节在行内;这是唯一一条上游怎么改都救不了的发现。

另有三条 P1,都很小,都落在本 PR 挪动过的 seam 上:

  • 会话 thinking level 现在绕过了 resolveThinkingLevel,模型未声明的档位会直接上线(model-adapter.ts:281
  • 明文 reasoning 分支没有空文本护栏,而它取代的两条路径都有(ai-sdk-backend.ts:3756
  • execution-model-authority.ts:499 没跟上新增的第二条 effort 通道,DeepSeek 的 title/memory/evaluator 调用现在完全不带 reasoning

同一类的还有一条 P2:上游的 assistant 分支只认 reasoning / text / tool-call,所以 provider-executed 的 tool-result 会被丢弃、而它的 tool-call 仍被序列化——我复现出了一个没有对应 function_call_outputfunction_callcanReplayProviderNativeai-sdk-backend.ts:3686)没有筛掉这种情况。会话内换模型不是本 PR 引入的,但让 DeepSeek 走上这条 codec,才使得「在 OpenAI 上用过 hosted 搜索、然后切到 DeepSeek」这条路径变得可达。

上游的:提上去,不要在我们这边绕

以下每条都能对已发布的 2.0.27 复现。其中几条和 @M4n5ter 正在提的重叠,估计合成一个 issue 就够,不需要七个。

# 行为 探针结果
1 effort: 'max' 会让 reasoning 字段整个从请求体消失 max → body.reasoning undefined + 一条 unsupported warning;枚举只有 minimal/low/medium/high/xhighopen-responses-language-model.ts:159-173
2 function 工具被丢弃且 warnings 为空数组 传入 1 个 function + 2 个 provider-defined → body.tools 只剩 1 个,warnings: []:120-128
3 converter 重新分组 assistant part,丢失交错顺序 text→reasoning→toolcall→reasoning→text 发出为 [reasoning, reasoning, message(合并), function_call]convert-to-open-responses-input.ts:105-161
4 序列化侧写死 summary: [],不发 idencrypted_content part 上的 providerOptions 被丢弃(:115-119
5 decoder 丢掉 summaryencrypted_content、item id、annotation 只带 summary 的 reasoning item 解出 0 个 part;providerMetadata 写死 undefined:262-297:336
6 流式与非流式对同一份响应解码结果不同 流式保留 item id,非流式完全没有;流式response.reasoning_text.delta 读 reasoning,而上游自己的注释说这是非 spec 的 LM Studio 扩展(:507-519
7 未知 incomplete_details.reason + 有 tool call → tool-calls 被截断的响应被报告成正常工具轮次;raw 里留着真实原因,unified 已经说谎(map-open-responses-finish-reason.ts:10-20
8 截断时 flush 出的 reasoning-end 用硬编码的 reasoning-0 真实 id 是 rs_REAL_ID;AI SDK core 随即注入 error: reasoning part reasoning-0 not found:567-571
9 无法发送 store,也不支持 previous_response_id 请求体里两者都没有

第 1 条就是这里把 max 映射成 xhigh 的原因,而这个映射是对的,不要改。 直传 max 不是钳位,是把整个 reasoning 字段删掉,模型会在关闭思考的状态下运行。DeepSeek 的映射表是 xhigh → highmax → max,所以现在 max 实际跑在 high,而 main 上跑的是 max。这对所有以 max 运行的 DeepSeek 会话(含跑分基线)都是可度量的退化,应当写进描述而不是留白——PR 自己也写了没有做真实凭据的 smoke test。

关于第 1 条的修法,更正我自己。 我最初写的是让上游给 effort 枚举补上 max。那个枚举在 @ai-sdk/providerreasoning?: 'provider-default' | 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'),是跨 provider 的标准化抽象,不应该承载各家私有的档位。真正的缺口是 @ai-sdk/open-responses 的 provider 专属选项里只开了 reasoningSummary 一个字段,effort 没有对应通道,只能走标准枚举加一张写死的 effortMap。我要提的是这个 issue。另外这件事不必等上游:requestBodyOverlayllm-connections.ts:122,在 model-factory.ts:62-66 的 fetch 层生效)可以按连接把 { reasoning: { effort: 'max' } } 原样打到请求体上,所以在这个 PR 里就能把 DeepSeek 的真 max 恢复回来。

第 2 条就是 apply_patch 和原生 web search 被关掉的原因,而「零 warning」才是真正危险的地方:一个永远发不出去的 provider 工具,和一个模型选择不调用的工具,从外部看完全一样。一旦上游支持序列化 provider-defined 工具,这两项能力会自己回来——建议在两个关闭点各写一句记录恢复条件。

第 7、8 条不会止步于上游——Maka 把两者都放大了。 model-adapter.ts:897 会把第 8 条产生的 SDK error chunk 转成 ModelFailure,于是在 reasoning 未闭合时撞上 max_output_tokens,这一步不是以 length 结束,而是以一条用户看不懂的 provider 错误终结。而 model-adapter.ts:567 只把 other/unknown 判为截断,所以第 7 条那个假的 tool-calls 会被当成功接受,工具循环继续跑下去。即便上游还没修,这两处都值得在本地加个守卫——原始 finish reason 在 streamedFinishReason 上已经拿得到。

关于描述的两点

能力回退应当体现在标题里。responsesDialect 顶掉 applyPatchProtocol 只是一行,但它把 headless coding 的工具面从 apply_patch 换成了 Write/Edit,并重写了 HEADLESS_CODING_V1_TOOLS_HASH。按 AGENTS.md 这属于 material user-visible behaviour,refactor: 承载不了。terminal-bench 上的等价性不能假定:freeform V4A 补丁一次调用可以改多个文件,而 Write/Edit 需要先读再精确匹配字符串,这会多花轮次和输入 token。在一两个 task 上做个 A/B,或者写明这是已接受的退化,都可以。

Tavily 其实并没有保住。 只有在 defaultProvider 显式设为 tavily 时才成立;默认值是 'model',此时 native-web-search-tool.ts:83 会把客户端 WebSearch 工具整个删掉,DeepSeek 会话完全没有搜索能力,且没有任何日志。跑分不受影响——eval 本来就禁用 web 工具。

关于 @M4n5ter 的评审

很扎实,其中对上游的复现是目前所有人在这件事上产出的最有价值的东西。两条 blocking 发现指向的代码已经不存在了——它比 f531b0f58 删除 open-responses-plaintext-model.ts 晚了 32 分钟。真正仍然成立的是关于 responsesDialect 的设计论点,而 2c3f18620 已经回应了它:关注点被拆成 responsesAdapterresponsesReasoningReplaymodel-adapter.ts:283 也改为按 runtime.responsesAdapter 判断,不再从 reasoning 回放形状反推 adapter。这个形状是对的,也正是我这边一位评审独立得出的结论。这条没有遗留问题了。

其余 P2 在行内。没有别的阻塞项。


评审协助说明:Claude Code (Opus) 跑了七轮相互隔离的 fresh-eye 审查(运行时正确性、能力退化、测试覆盖、架构边界、上游包实际行为、持久化兼容、依赖与安全),彼此不知道对方的发现。DeepSeek 的 effort 映射表我对照官方文档核实过,@ai-sdk/openaistore: false 的过滤逻辑在 dist/index.js:5328-5338 也已确认,另有两条 agent 结论没通过复核,已被我剔除。

Comment thread packages/core/src/provider-registry.ts Outdated
Comment thread packages/runtime/src/model-adapter.ts Outdated
Comment thread packages/runtime/src/ai-sdk-backend.ts Outdated
Comment thread packages/runtime/src/__tests__/responses-wire-contract.test.ts
Comment thread packages/runtime/src/model-adapter.ts Outdated
@M4n5ter

M4n5ter commented Aug 14, 2026

Copy link
Copy Markdown
Member

Upstream update: vercel/ai#18839 has now been fixed by vercel/ai#18844 and closed:

The upstream fix preserves heterogeneous item order, item IDs, reasoning summaries and encrypted content, reasoning-content boundaries, and URL-citation annotations across generated/streamed responses and manual-history replay. These are the generic codec responsibilities that currently account for much of the complexity in this PR.

@ai-sdk/open-responses is still published as 2.0.27, so the fix is on main but not yet in the latest release. We can simply wait for the next release containing #18844, upgrade the dependency, and then simplify this PR to the genuinely DeepSeek-specific behavior instead of carrying a local implementation of the upstream round-trip fix.

@me2seeks me2seeks changed the title fix(runtime): route DeepSeek through an explicit Responses adapter fix(runtime): route DeepSeek reasoning through Open Responses with tool fallbacks Aug 14, 2026
@me2seeks

Copy link
Copy Markdown
Contributor Author

Thanks for the upstream update — waiting for the release containing vercel/ai#18844 makes sense.

I pushed b093de9 with the fixes that are independent of that release:

  • historical DeepSeek freeform apply_patch calls now become bounded facts instead of undeclared tool calls
  • reasoning settings are resolved once and shared by the main backend and auxiliary title/recap/memory/evaluator calls
  • unsupported thinking levels and empty reasoning records are omitted
  • provider-executed tool history falls back to grounded text while @ai-sdk/open-responses@2.0.27 cannot replay the complete pair safely
  • the provider capability remains declared separately from the current codec capability

I also updated the PR description to record the temporary maxxhigh/high downgrade and that Tavily is available only when explicitly configured and selected.

I’ll keep this Draft, upgrade once the release containing #18844 is available, and then remove any safeguards made obsolete by the released codec. Local affected suites are green; GitHub CI is still running.

@Astro-Han

Copy link
Copy Markdown
Contributor

vercel/ai#18880
vercel/ai#18884
vercel/ai#18844
vercel/ai#18879

All the upstream issues has been fixed and published in the newest version. Most of previous issues could be fixed by introduce the newer version.

@me2seeks

Copy link
Copy Markdown
Contributor Author

Thanks — that matches what I see: @ai-sdk/open-responses@2.0.28 includes all four. Upgraded and pushed b8b334d36.

What the upgrade lets me remove/simplify:

  • max now passes through verbatim. 2.0.28 resolves a provider-native reasoningEffort from providerOptions ahead of the cross-provider reasoning enum, which can't express DeepSeek's max (its documented mapping sends xhigh to high). I key the open-responses namespace by the same provider name passed to createOpenResponses, so the request body now carries reasoning: { effort: 'max' } unchanged. The xhigh downgrade and the buildModelCallSettings dual channel (providerOptions + top-level reasoning) are gone; buildProviderOptions is the single seam, and the auxiliary title/recap/memory/evaluator calls go through it too instead of silently dropping reasoning.
  • providerExecutedTools stays fail-closed. I verified 2.0.28's replay now preserves item order and IDs, but a provider-executed result embedded in the assistant message is still dropped, which would leave a dangling function_call on the wire. Provider-executed tool history keeps replaying as grounded text until the upstream extension seam (@ai-sdk/open-responses: support registered namespaced tool and item extensions vercel/ai#18899) can round-trip the pair — I've linked it as a tracking item.

Tests updated to pin max → max at both the buildProviderOptions unit seam and the wire contract, plus 2.0.28 item-ID preservation for ordered replay. Local runtime and runtime-host suites are green.

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@me2seeks, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Limit details: You’ve used all 3 included reviews currently available under your plan.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e287b792-9a0a-430d-a14f-5bb3442ef60f

📥 Commits

Reviewing files that changed from the base of the PR and between 0695cf3 and 95b0f5e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (18)
  • packages/cli/THIRD_PARTY_NOTICES.txt
  • packages/core/src/llm-connections.ts
  • packages/core/src/provider-registry.ts
  • packages/runtime/src/__tests__/ai-sdk-backend.test.ts
  • packages/runtime/src/__tests__/apply-patch-profile.test.ts
  • packages/runtime/src/__tests__/model-factory-thinking.test.ts
  • packages/runtime/src/__tests__/openai-responses-plaintext-reasoning.test.ts
  • packages/runtime/src/__tests__/provider-contract-matrix.ts
  • packages/runtime/src/__tests__/responses-wire-contract.test.ts
  • packages/runtime/src/ai-sdk-backend.ts
  • packages/runtime/src/apply-patch-profile.ts
  • packages/runtime/src/codex-v4a-patch.ts
  • packages/runtime/src/model-adapter.ts
  • packages/runtime/src/model-factory.ts
  • packages/runtime/src/model-runtime.ts
  • packages/runtime/src/openai-apply-patch.ts
  • packages/runtime/src/test-connection.ts
  • packages/runtime/src/tool-runtime.ts
📝 Walkthrough

Summary

This PR fixes DeepSeek V4 reasoning replay during tool loops.

DeepSeek plaintext reasoning no longer enters the OpenAI encrypted-reasoning path. The runtime now supports separate encrypted and plaintext Responses replay modes. Genuine encrypted reasoning remains replayable.

The PR separates Responses adapter selection from reasoning replay format. OpenAI-compatible providers select openai or open-responses explicitly. DeepSeek uses @ai-sdk/open-responses@2.0.28. The PR removes the Maka Responses wrapper, private request rewriting, and the plaintext transport module.

The implementation preserves replay chronology and metadata through the updated Open Responses SDK. Historical apply_patch operations become bounded assistant facts when replay is unsafe. Provider-executed tool history fails closed, with grounded-text fallback where required. DeepSeek model-native WebSearch remains disabled because the current serializer supports function tools only.

Reasoning settings resolve once and apply consistently to primary and auxiliary calls. DeepSeek reasoning effort, including max, passes through the deepseek provider namespace without downgrade.

Design assessment

The PR extends the existing provider registry, model runtime, adapter, and replay abstractions. It does not create a parallel runtime path. The Open Responses SDK provides the protocol implementation, while the runtime selects the adapter and replay representation.

This is the smallest coherent solution supported by the current evidence. Adapter selection, reasoning continuation, provider-executed tools, and apply_patch history have different constraints. The explicit states and branches are necessary to handle those constraints without lossy replay or private request mutation.

The deleted plaintext transport and removed private request rewriting simplify the implementation. No additional deletion is supported by the current evidence without weakening coverage for ordered replay, item IDs, reasoning metadata, citations, ordinary client tools, hosted-tool fallback, and generic-provider compatibility.

Complexity delta

The PR adds:

  • ResponsesAdapter values: openai and open-responses.
  • Three reasoning replay states: none, encrypted-content, and plaintext-content.
  • Provider-registry fields for adapter and replay selection.
  • unavailable runtime handling.
  • URL normalization helpers.
  • Adapter-specific replay and wire-contract tests.
  • Branches for unsupported provider-executed tools and unsafe history replay.

The PR removes:

  • The Maka Responses protocol wrapper.
  • Private request rewriting.
  • The plaintext reasoning transport.
  • The OpenAI-specific encrypted-thinking boolean.
  • Implicit adapter and replay selection.

The public type surface and test-maintenance burden increase. Protocol-specific translation and implicit replay behavior decrease. Overall maintenance complexity decreases because the removed private path is replaced by explicit provider configuration and the upstream Open Responses implementation. The remaining complexity is justified by the provider differences.

Validation and risks

Reported validation includes workspace typechecking, runtime, runtime-host, and core suites, request-level checks, formatter checks, and diff checks.

The tests cover endpoint normalization, explicit adapter selection, exact max propagation, plaintext and encrypted reasoning, ordered replay, item-ID preservation, provider-native options, tool-output conversion, provider-executed tool fallback, and WebSearch routing.

No live DeepSeek credential smoke test was run. A known intermittent streaming-remount.spec.ts failure remains. Required check status is unverified from the available evidence.

Review-relevant risks

  • The PR changes public runtime types, including ResponsesAdapter, ResolvedModelRuntime, ModelRuntimeWire, ReasoningReplayContract, and replay-support fields. Material public-contract changes require independent human review under repository policy.
  • DeepSeek WebSearch behavior changes to unsupported-tool handling. This can affect user-visible capabilities. Material user-visible behavior changes require independent human review under repository policy.
  • The PR adds @ai-sdk/open-responses@2.0.28 and third-party license text. Release and licensing effects require independent human review under repository policy.
  • The PR changes provider request serialization and reasoning continuation behavior. Security and provider-compatibility effects require independent human review under repository policy.

The person performing the merge reviews the final diff. A maintainer makes the final determination.

Walkthrough

The runtime adds Open Responses SDK routing, typed Responses adapter metadata, dialect-aware reasoning replay, normalized endpoints, and DeepSeek fallbacks. Tests cover provider options, tool serialization, reasoning replay, web-search availability, and auxiliary model calls.

Changes

Responses adapter contracts and runtime resolution

Layer / File(s) Summary
Adapter contracts and runtime resolution
packages/core/src/provider-registry.ts, packages/runtime/src/model-runtime.ts, packages/runtime/src/model-adapter.ts, packages/runtime/src/provider-urls.ts
OpenAI-compatible adapters now declare their Responses SDK adapter and reasoning replay mode. Runtime wires distinguish OpenAI and Open Responses protocols, plaintext and encrypted reasoning, and unavailable adapters.
Model creation and replay projection
packages/runtime/package.json, apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt, packages/runtime/src/model-factory.ts, packages/runtime/src/ai-sdk-backend.ts, packages/runtime/src/apply-patch-profile.ts, packages/runtime/src/test-connection.ts, packages/runtime-host/src/server/execution-model-authority.ts
The runtime uses @ai-sdk/open-responses for compatible models. Backend replay resolves provider options once, preserves supported reasoning content, rejects unsupported provider tools, and normalizes Responses endpoints.

DeepSeek and validation behavior

Layer / File(s) Summary
DeepSeek and unavailable-provider fallbacks
packages/core/src/model-web-search.ts, packages/core/src/__tests__/model-web-search.test.ts, packages/runtime/src/__tests__/native-web-search-tool.test.ts, packages/runtime/src/__tests__/ai-sdk-backend.test.ts, packages/runtime-host/src/__tests__/execution-model-composition.test.ts
DeepSeek hosted web search is reported as unavailable. Unsupported native search and ApplyPatch tools are omitted. Auxiliary title generation uses provider-native reasoning effort and top-level Responses instructions. Replay preserves supported history while degrading unsupported tool pairs.
Integration and contract validation
packages/runtime/src/__tests__/*, packages/core/src/__tests__/model-web-search.test.ts
Tests cover adapter contracts, endpoint normalization, Open Responses function-tool serialization, plaintext and encrypted reasoning replay, ApplyPatch fallback behavior, provider options, and DeepSeek request handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🔵 Low · up to 0695c

This change updates reasoning routing and replay behavior across providers. A remaining provider-matrix mismatch could select the wrong replay assumptions for some provider combinations, so the PR is mergeable with explicit owner awareness or follow-up.

Sequence Diagram(s)

sequenceDiagram
  participant ProviderRegistry
  participant ModelRuntime
  participant ModelFactory
  participant AiSdkBackend
  participant OpenResponsesSDK
  ProviderRegistry->>ModelRuntime: resolve Responses adapter and reasoning contract
  ModelRuntime->>ModelFactory: select wire and normalized endpoint
  ModelFactory->>OpenResponsesSDK: create Open Responses model
  AiSdkBackend->>OpenResponsesSDK: send reasoning and function-tool request
  OpenResponsesSDK-->>AiSdkBackend: return reasoning and tool events
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Ai Use Disclosure ⚠️ Warning The PR description selects neither required AI-use declaration, and all 15 introduced commits lack a valid Generated-by trailer. If generative tooling made a substantive contribution, name the tool and scope; otherwise state that none did. See CONTRIBUTING.md, “Human ownership and AI attribution.” Ensure required trailers survive squash or amend.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary runtime change: routing DeepSeek reasoning through Open Responses with tool fallbacks.
Description check ✅ Passed The description clearly explains the problem, implementation, capability boundaries, linked issue, and verification results.
Linked Issues check ✅ Passed The changes address issue #2513 by separating replay contracts, preserving encrypted reasoning, enabling plaintext replay, testing wire bodies, and avoiding global warning suppression.
Out of Scope Changes check ✅ Passed The changes support the stated runtime routing, replay, fallback, provider-contract, and verification objectives without unrelated code changes.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@me2seeks
me2seeks marked this pull request as ready for review August 17, 2026 17:05
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Caution

CodeRabbit couldn't update its existing comment. The review summary may be out of date.

Error details
No server is currently available to service your request. Sorry about that. Please try resubmitting your request and contact us if the problem persists.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/runtime/src/__tests__/provider-contract-matrix.ts (1)

344-355: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align wireDimensionCell with runtime wire resolution

When kind === 'openai-compatible', apiProtocol === 'openai-responses', and supportsOpenAiResponses is absent, the runtime selects openai-chat, but wireDimensionCell marks exact-model-id and tool-loop as overrides. Require supportsOpenAiResponses === true in this branch. If you extract a shared helper, pass the effective protocol because usesOpenAiResponsesWire also resolves `openAiAdapterApiProtocol(modelId, providerType).

Source: Path instructions

🧹 Nitpick comments (5)
packages/core/src/__tests__/model-web-search.test.ts (1)

102-105: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This assertion now duplicates the earlier test.

Lines 102-105 are identical to lines 8-11. The unique behavior of this test is the protocol-selection path at lines 106-134. Remove the duplicated leading assertion so the test states only what it owns.

As per path instructions: "Flag tests that duplicate existing coverage".

Source: Path instructions

packages/runtime/src/__tests__/ai-sdk-backend.test.ts (1)

3092-3095: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer structured absence checks over whole-prompt substring matching.

JSON.stringify(prompt).includes('tool-call') matches any occurrence anywhere in the serialized prompt, including tool names or text content. The sibling test at Lines 289-300 already filters message.content by part.type. Use the same structured form here so the assertion stays stable when unrelated prompt fields change.

packages/runtime/src/model-runtime.ts (1)

97-106: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Compute responsesAdapterContract(adapter) once.

The same call runs twice in one return expression. A single local keeps the two consumers in sync by construction.

♻️ Proposed simplification
   const wire = resolveModelRuntimeWire(connection.providerType, modelId, adapter, apiProtocol);
+  const responsesAdapter = wire === 'openai-responses' ? responsesAdapterContract(adapter) : undefined;
   return {
     adapter,
     baseUrl:
       connection.providerType === 'kimi-coding-plan' && apiProtocol === 'openai-chat'
         ? kimiOpenAiBaseUrl(resolvedBaseUrl)
         : resolvedBaseUrl,
     ...(apiProtocol ? { apiProtocol } : {}),
     wire,
-    ...(wire === 'openai-responses' ? { responsesAdapter: responsesAdapterContract(adapter) } : {}),
+    ...(responsesAdapter ? { responsesAdapter } : {}),
     reasoningReplay: reasoningReplayContract(adapter, wire),
     applyPatchProfile: resolveApplyPatchProfile(
       {
         wire,
-        ...(wire === 'openai-responses'
-          ? { responsesAdapter: responsesAdapterContract(adapter) }
-          : {}),
+        ...(responsesAdapter ? { responsesAdapter } : {}),
         applyPatchProtocol: adapter.applyPatchProtocol,
       },
       modelId,
     ),
   };

Source: Path instructions

packages/runtime/src/__tests__/model-factory-thinking.test.ts (1)

504-518: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This test cannot distinguish the two namespace helpers.

The chat path uses openAiCompatibleProviderOptionsKey and the Responses path uses openAiCompatibleProviderName. For deepseek both resolve to the same string, so both assertions expect { deepseek: { reasoningEffort: 'high' } }, which the assertion at Line 210 already pins. The test therefore duplicates coverage instead of protecting the raw-name-versus-camelCase contract the production comment relies on. Either drop the Responses half or use a connection whose slug-derived key differs from its raw name.

Source: Path instructions

packages/runtime/src/__tests__/provider-conformance.test.ts (1)

658-671: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The $schema assertion pins a dependency detail, not provider behavior.

$schema: 'http://json-schema.org/draft-07/schema#' comes from the ai package schema conversion, not from the Open Responses wire contract. An ai or zod upgrade that changes the emitted draft breaks this test without any provider-behavior regression. Assert the fields this test owns (type, name, description, and the parameters shape) and drop $schema.

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e280b3f7-c56d-467d-bb32-a7f69af2ed9b

📥 Commits

Reviewing files that changed from the base of the PR and between 5d9ce0d and 3cf50eb.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (25)
  • apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt
  • packages/core/src/__tests__/model-web-search.test.ts
  • packages/core/src/model-web-search.ts
  • packages/core/src/provider-registry.ts
  • packages/runtime-host/src/__tests__/execution-model-composition.test.ts
  • packages/runtime-host/src/server/execution-model-authority.ts
  • packages/runtime/package.json
  • packages/runtime/src/__tests__/ai-sdk-backend.test.ts
  • packages/runtime/src/__tests__/apply-patch-profile.test.ts
  • packages/runtime/src/__tests__/model-adapter.test.ts
  • packages/runtime/src/__tests__/model-factory-thinking.test.ts
  • packages/runtime/src/__tests__/native-web-search-tool.test.ts
  • packages/runtime/src/__tests__/openai-responses-plaintext-reasoning.test.ts
  • packages/runtime/src/__tests__/provider-conformance.test.ts
  • packages/runtime/src/__tests__/provider-contract-matrix.ts
  • packages/runtime/src/__tests__/provider-contract-overrides.ts
  • packages/runtime/src/__tests__/responses-wire-contract.test.ts
  • packages/runtime/src/ai-sdk-backend.ts
  • packages/runtime/src/apply-patch-profile.ts
  • packages/runtime/src/model-adapter.ts
  • packages/runtime/src/model-factory.ts
  • packages/runtime/src/model-runtime.ts
  • packages/runtime/src/openai-responses-plaintext-reasoning-transport.ts
  • packages/runtime/src/provider-urls.ts
  • packages/runtime/src/test-connection.ts
💤 Files with no reviewable changes (1)
  • packages/runtime/src/openai-responses-plaintext-reasoning-transport.ts

Included review availability: Your plan includes up to 3 reviews per rolling hour; 1 remains after this review.

Comment thread packages/runtime/src/__tests__/ai-sdk-backend.test.ts Outdated
Comment thread packages/runtime/src/ai-sdk-backend.ts Outdated
@qodo-code-review

qodo-code-review Bot commented Aug 17, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Endpoint probe doubles responses ✓ Resolved 🐞 Bug ≡ Correctness
Description
Fix now — the new send path accepts a fully qualified /responses endpoint, but connection
validation still appends another /responses. Such configurations send model calls to the correct
URL yet consistently fail validation against /responses/responses.
Code

packages/runtime/src/model-factory.ts[155]

+            url: openResponsesUrl(baseURL),
Relevance

●●● Strong

Deterministic endpoint normalization mismatch causes validation to probe the wrong URL; contract
tests make the intended behavior explicit.

PR-#2518

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The model factory now passes a normalized full endpoint to Open Responses, and the normalizer
explicitly removes an existing suffix before restoring it. The connection probe for the same runtime
wire independently appends the suffix without normalization, while the new contract test explicitly
accepts URLs already ending in /responses.

packages/runtime/src/model-factory.ts[150-157]
packages/runtime/src/provider-urls.ts[39-44]
packages/runtime/src/test-connection.ts[185-200]
packages/runtime/src/test-connection.ts[236-255]
packages/runtime/src/tests/responses-wire-contract.test.ts[37-46]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Connection validation appends `/responses` even when the configured URL already names that endpoint, while the model send path normalizes it exactly once.

## Issue Context
Reuse the existing `openResponsesUrl` seam; no new configuration or URL authority is needed.

## Fix Focus Areas
- packages/runtime/src/model-factory.ts[151-156]
- packages/runtime/src/provider-urls.ts[39-44]
- packages/runtime/src/test-connection.ts[236-255]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Frozen provider options overwritten ✓ Resolved 🐞 Bug ≡ Correctness
Description
Fix now — runHostAuxiliaryModelCall spreads request-supplied provider options and then overwrites
them with newly resolved options. Memory proposal/extraction calls therefore lose the frozen source
turn's provider-visible options, potentially changing reasoning or provider-specific request
semantics.
Code

packages/runtime-host/src/server/execution-model-authority.ts[507]

+              providerOptions,
Relevance

●●● Strong

This is a deterministic spread-precedence bug affecting frozen auxiliary-call semantics; similar
provider-option preservation fixes were accepted.

PR-#1755

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The backend deliberately snapshots the main call's provider options, and memory extraction copies
those frozen options into the auxiliary request to preserve the source provider prefix. Because the
newly added property occurs after ...request, JavaScript spread precedence replaces that frozen
value before generateProviderPrefixModelCall forwards it.

packages/runtime-host/src/server/execution-model-authority.ts[157-178]
packages/runtime-host/src/server/execution-model-authority.ts[486-514]
packages/runtime/src/ai-sdk-backend.ts[1201-1217]
packages/runtime/src/tool-free-model-call.ts[45-80]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The auxiliary call overwrites frozen request-supplied provider options after spreading the request.

## Issue Context
Consolidate on the existing request option authority: use the newly resolved options only when the request does not already carry frozen source options. No new merge state or public surface is required.

## Fix Focus Areas
- packages/runtime-host/src/server/execution-model-authority.ts[486-514]
- packages/runtime/src/ai-sdk-backend.ts[1213-1217]
- packages/runtime/src/tool-free-model-call.ts[45-80]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Stale webSearch metadata after disabling DeepSeek search 🐞 Bug ≡ Correctness
Description
providerHostedWebSearchAdapter now hard-codes { adapter: 'openai-responses', implemented: false }
for DeepSeek because the generic @ai-sdk/open-responses codec only serializes function tools, so
routeWebSearchTools will never select DeepSeek's native WebSearch tool regardless of settings. The
DeepSeek model metadata in model-metadata.ts still declares `capabilities: {
...REASONING_FUNCTION_CALLING, webSearch: true } for both deepseek-v4-flash` and
deepseek-v4-pro, leaving a stale capability contract that advertises a feature the routing layer
can no longer deliver; any other code path reading
lookupModelMetadata(...).capabilities?.webSearch directly (UI capability badges, model pickers,
docs generation) will keep showing native web search as supported for DeepSeek even though it is now
unreachable.
Code

packages/core/src/model-web-search.ts[R60-64]

    case 'deepseek':
+      // @ai-sdk/open-responses currently serializes function tools only.
+      // Mark native search unavailable so routing never hands it a provider
+      // tool that would be silently filtered from the request.
+      return { adapter: 'openai-responses', implemented: false };
Relevance

●●● Strong

Accepted capability-boundary findings are common; raw metadata remains inconsistent with explicit
disabled routing.

PR-#3168

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
model-metadata.ts (read via lookupModelMetadata) still sets webSearch: true for deepseek-v4-flash
and deepseek-v4-pro while model-web-search.ts now unconditionally returns implemented: false for the
deepseek providerType, so any consumer of the raw model-metadata capability flag (outside of
resolveHostedWebSearchCapability's own null-return path) will disagree with the actual routing
outcome.

packages/core/src/model-metadata.ts[394-404]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
DeepSeek's model metadata declares `capabilities.webSearch: true` for `deepseek-v4-flash` and `deepseek-v4-pro`, but `providerHostedWebSearchAdapter` in `model-web-search.ts` now hard-codes `implemented: false` for the `deepseek` providerType because the generic Open Responses codec cannot serialize the native web-search provider tool. This leaves two sources of truth disagreeing about whether DeepSeek supports native web search.

## Issue Context
`resolveHostedWebSearchCapability` masks the discrepancy for routing decisions (it returns `implemented: false` regardless of the stored capability), but any other consumer reading the raw model metadata capability flag directly (UI capability badges, docs generation, model pickers, other capability checks) will still see `webSearch: true` and could present or rely on a capability that is actually unreachable.

## Fix Focus Areas
- packages/core/src/model-metadata.ts[394-404]
- packages/core/src/model-web-search.ts[60-64]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. normalizeApplyPatchReplayInput broadens downgrade beyond codec gap 🐞 Bug ≡ Correctness
Description
normalizeApplyPatchReplayInput now returns null (routing to the durable-fact downgrade path)
whenever profile is falsy, whereas previously it returned the historical input unchanged; this
is intentional for the new DeepSeek open-responses codec gap, but resolveApplyPatchProfile also
returns null for pre-existing reasons unrelated to that gap (wire !== 'openai-responses', or no
applyPatchProtocol declared), so any session that transitions to such a model/provider now has its
historical apply_patch calls silently downgraded to assistant-fact text instead of replaying
verbatim as before.
Code

packages/runtime/src/apply-patch-profile.ts[R82-85]

+  // A missing profile means the target request does not advertise ApplyPatch.
+  // Returning the historical input would serialize a call to an undeclared
+  // tool; route it through the durable-fact downgrade instead.
+  if (!profile) return null;
Relevance

●● Moderate

The concern is semantically plausible, but no close rejection precedent establishes preserving
history for non-Responses downgrade cases.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
resolveApplyPatchProfile returns null both when runtime.wire !== 'openai-responses' (a session
switched off the Responses wire entirely) and when runtime.responsesAdapter === 'open-responses';
normalizeApplyPatchReplayInput's new null-returns-null behavior applies uniformly to both cases,
broadening the downgrade path beyond the PR's stated narrow scope of 'the selected codec cannot
replay the tool'.

packages/runtime/src/apply-patch-profile.ts[20-38]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`normalizeApplyPatchReplayInput` now returns `null` (triggering the durable-fact downgrade in `ai-sdk-backend.ts`) for any falsy `profile`, not just the new DeepSeek open-responses codec gap. `resolveApplyPatchProfile` returns `null` for multiple distinct reasons (wrong wire, no declared apply_patch protocol, or the new open-responses codec gap), and all of them now take the downgrade path uniformly.

## Issue Context
The PR's stated goal is narrowly about preserving old `apply_patch` work when the *selected codec* cannot replay the tool (DeepSeek + open-responses). Verify that downgrading to a text fact is also the desired behavior for the pre-existing null-profile cases (switched wire / no protocol), and add a regression test covering a non-DeepSeek provider transition to confirm the downgrade path is intentional there too.

## Fix Focus Areas
- packages/runtime/src/apply-patch-profile.ts[76-98]
- packages/runtime/src/ai-sdk-backend.ts[4179-4242]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Web pages:
  +11 more
Review mode: 🧠 Deep: This is a high-density runtime and provider-contract change spanning model selection, Responses codecs, reasoning replay, tool/history fallbacks, routing, auxiliary calls, and many independent behavioral paths, making redundant review materially useful.

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread packages/runtime/src/model-factory.ts
Comment thread packages/runtime-host/src/server/execution-model-authority.ts Outdated
@me2seeks

Copy link
Copy Markdown
Contributor Author

Note on CI: the only failing check is the e2e streaming-remount.spec.ts test ("keeps a completed reply after an interrupted turn and conversation remount"). This is a known intermittent flake we have observed failing on several unrelated PRs (2521, 2641, 3027) — the failure is not related to this change. All other checks (typecheck, unit, workspaces) are green, and all review threads are resolved.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed exact head 5ee70e5d2021df786ca992bc60d762f372d5aad7, including the complete diff, upstream adapter behavior, all nine resolved review threads, and the current CI logs. I found no P0–P3 code issue.

The root fix is at the right boundary. SDK adapter selection and reasoning-continuation representation are separate provider contracts; DeepSeek uses the upstream Open Responses codec for plaintext replay, while OpenAI’s encrypted continuation remains separate. Unsupported ApplyPatch, hosted-tool history, and WebSearch capabilities fail closed at the codec boundary. The local response-rewriting transport is deleted rather than retained as a parallel implementation.

I also independently rechecked the earlier findings. Historical ApplyPatch replay, unsupported thinking levels, empty reasoning, missing auxiliary options, provider-tool orphaning, endpoint normalization, and frozen provider-option handling are fixed on this head. I would not split this cohesive provider-contract change.

The remaining stop is operational rather than a diff finding: CI is still red on streaming-remount.spec.ts waiting for the Stop button. The same locator and line have failed on unrelated PRs, and this diff does not touch that lifecycle, so the evidence points to the known flake; nevertheless, the branch should rerun to green before merge.

The PR also explicitly notes that no live DeepSeek credential smoke was run. Recorded live fixtures and the fake-fetch wire tests are strong, but I would prefer one real V4 tool loop confirming plaintext reasoning replay against /responses, or an explicit maintainer decision to accept that residual validation risk.

Code recommendation: go. Merge recommendation: wait for green CI.

Disclosure: Codex performed the read-only adversarial source, upstream package, test, CI, and feedback-ledger analysis. The human contributor remains responsible for independently verifying the final diff and deciding whether to merge.

中文

代码层面没有 P0–P3,adapter/replay 的职责拆分和 fail-closed fallback 都合理。当前仅因已知 E2E flake 保持合并 STOP,需 rerun green;真实 DeepSeek V4 tool loop 建议补一次,但不是代码 finding。

Comment thread packages/runtime/src/ai-sdk-backend.ts
Comment thread packages/runtime/src/model-factory.ts Outdated
Comment thread packages/runtime/src/apply-patch-profile.ts Outdated
Comment thread packages/core/src/provider-registry.ts Outdated

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English — Request changes

I reviewed exact head 5ee70e5d2021df786ca992bc60d762f372d5aad7, including the full diff, the published SDK behavior, current CI, and all existing review threads.

The adapter/reasoning separation is directionally correct, and the previously reported ApplyPatch, reasoning-option, empty-reasoning, auxiliary-call, and provider-orphan issues are fixed. Two new correctness issues remain:

  1. P1: the provider-tool fallback is plan-wide and discards unrelated client tool results. I reproduced a mixed history where a completed Read result disappeared solely because the same plan contained a historical provider WebSearch.
  2. P2: an endpoint-form custom Responses relay passes connection validation but the native OpenAI adapter sends its model request to /responses/responses.

The first issue can lose the only record of completed work and should be fixed before merge. The second makes a configuration accepted by the probe unusable at runtime.

The simplification audit also found two non-blocking opportunities: remove the production-unreachable DeepSeek freeform ApplyPatch target path, and replace the duplicated Responses capability fields with a discriminated contract that excludes lossy adapter/replay pairings.

Focused verification on the exact head:

  • Responses wire-contract tests: 10/10 passed.
  • Existing hosted-tool fallback test: 1/1 passed, but it covers only provider-tool history with grounded text.
  • Mixed client/provider history reproduction: client result was dropped.
  • Endpoint capture: actual URL was https://relay.example/v1/responses/responses.
  • Diff check passed.
  • GitHub E2E remains red on the existing streaming-remount Stop-button failure; this diff does not touch that path.
  • No live DeepSeek credential smoke was performed.
简体中文 — 请求修改

我审查了精确 head 5ee70e5d2021df786ca992bc60d762f372d5aad7,包括完整 diff、已发布 SDK 的实际行为、当前 CI,以及全部已有 review thread。

adapter 与 reasoning continuation 的职责拆分方向正确;此前报告的 ApplyPatch、reasoning option、空 reasoning、辅助调用和 provider orphan 问题也已修复。但仍有两个新的 correctness 问题:

  1. P1: provider-tool fallback 是 plan 级的,会连带丢弃无关的客户端工具结果。我已复现:只因同一历史中存在旧的 provider WebSearch,一次已完成的 Read 结果就从 prompt 中消失。
  2. P2: 使用完整 endpoint 的自定义 Responses relay 可以通过连接测试,但原生 OpenAI adapter 会把实际模型请求发送到 /responses/responses

第一个问题可能丢失已完成工作的唯一记录,应在合并前修复。第二个问题则使 probe 明确认可的配置无法正常运行。

简化审计还发现两个非阻塞机会:删除生产环境不可达的 DeepSeek freeform ApplyPatch 目标路径;将重复的 Responses capability 字段合并为 discriminated contract,从类型层排除有损的 adapter/replay 组合。

针对精确 head 的聚焦验证:

  • Responses wire-contract 测试:10/10 通过。
  • 现有 hosted-tool fallback 测试:1/1 通过,但只覆盖带 grounded text 的 provider-tool 历史。
  • 混合客户端/provider 历史复现:客户端工具结果被丢弃。
  • Endpoint 捕获:实际 URL 为 https://relay.example/v1/responses/responses
  • Diff check 通过。
  • GitHub E2E 仍因既有 streaming-remount Stop-button 失败而红;本 diff 未修改该路径。
  • 未执行带真实 DeepSeek credential 的 smoke test。

@me2seeks

Copy link
Copy Markdown
Contributor Author

Both blocking findings are fixed on 0695cf3c4:

  • P1 — replay degradation is now per item (dropUnsupportedReplayItems): the unsupported provider-executed pair drops out, unrelated client tool call/result history still materializes natively. Regression test reproduces your mixed Read + provider WebSearch scenario and fails against the previous code.
  • P2openAiResponsesBaseUrl reduces endpoint-form overrides to the base before createOpenAI; a behavior-level test captures the request URL and asserts no /responses/responses.

Runtime suite green locally (2886 tests, 0 fail). The two simplification opportunities are acknowledged in their threads and tracked as follow-ups so this head stays reviewable.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/runtime/src/__tests__/ai-sdk-backend.test.ts (1)

3202-3208: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Recommended: assert on the prompt structure, not the serialized string.

compactPrompt(model) already returns structured messages. Line 3202 flattens it to JSON, so line 3207 must accept two escape shapes and the checks cannot pin the surviving Read call and its result to the correct message roles or order. A structural assertion fails for the right reason when replay regresses. Disposition: optional; the current assertions do protect the observable behavior.

♻️ Proposed structural assertions
-    const wire = JSON.stringify(compactPrompt(model));
-    // The unsupported provider-executed pair degrades away…
-    assert.equal(wire.includes('latest Maka'), false, wire);
-    // …but the unrelated client Read call and its result survive (`#2972`).
-    assert.match(wire, /CLIENT_READ_SENTINEL_CONTENT/);
-    assert.match(wire, /"toolName":"Read"|\\"toolName\\":\\"Read\\"/);
-    assert.match(wire, /Maka shipped the feature/);
+    const prompt = compactPrompt(model) as Array<{
+      role: string;
+      content: Array<{ type: string; toolName?: string; text?: string }>;
+    }>;
+    const parts = prompt.flatMap((message) => message.content);
+    // The unsupported provider-executed pair degrades away…
+    assert.equal(
+      parts.some((part) => part.toolName === 'WebSearch'),
+      false,
+      JSON.stringify(prompt),
+    );
+    // …but the unrelated client Read call and its result survive (`#2972`).
+    assert.equal(
+      parts.some((part) => part.type === 'tool-call' && part.toolName === 'Read'),
+      true,
+      JSON.stringify(prompt),
+    );
+    assert.equal(
+      parts.some((part) => part.type === 'tool-result' && part.toolName === 'Read'),
+      true,
+      JSON.stringify(prompt),
+    );
+    assert.match(JSON.stringify(prompt), /CLIENT_READ_SENTINEL_CONTENT/);
+    assert.equal(
+      parts.some((part) => part.text?.includes('Maka shipped the feature')),
+      true,
+      JSON.stringify(prompt),
+    );

Source: Path instructions

packages/runtime/src/provider-urls.ts (1)

53-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consolidate the shared /responses normalization.

openResponsesUrl and openAiResponsesBaseUrl duplicate the same normalization. Extract it into one private helper and reuse it in both functions. This is an optional maintainability improvement; no correctness issue is present.

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c7c5819-1d19-496a-b1c0-842ac3c6e94d

📥 Commits

Reviewing files that changed from the base of the PR and between 5ee70e5 and 0695cf3.

📒 Files selected for processing (5)
  • packages/runtime/src/__tests__/ai-sdk-backend.test.ts
  • packages/runtime/src/__tests__/responses-wire-contract.test.ts
  • packages/runtime/src/ai-sdk-backend.ts
  • packages/runtime/src/model-factory.ts
  • packages/runtime/src/provider-urls.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/runtime/src/model-factory.ts
  • packages/runtime/src/ai-sdk-backend.ts

Included review availability: Your plan includes up to 3 reviews per rolling hour; 2 remain after this review.

@ai-sdk/open-responses@2.0.28 resolves a provider-native reasoningEffort
from providerOptions ahead of the cross-provider top-level `reasoning`
enum, which cannot express DeepSeek's `max` (its documented mapping sends
`xhigh` to high). Key the open-responses providerOptions namespace by the
same provider name passed to createOpenResponses so `max` reaches the wire
unchanged, and fold the top-level reasoning channel into buildProviderOptions
as the single reasoning seam.
…nostic readers

- AiSdkBackend stores one resolvedProviderOptions value (caller-supplied or
  derived from buildProviderOptions) and reads it for the main call, the
  memory-extraction snapshot, and the request-shape diagnostics, so every
  reader describes the request actually sent.
- runHostAuxiliaryModelCall lets request-carried frozen provider options win
  over freshly resolved ones, preserving the source turn's reasoning and
  provider-specific semantics for memory proposal/extraction.
- The Open Responses connection probe normalizes through openResponsesUrl so
  a base URL that already names the endpoint is not probed as
  /responses/responses, with a provider-conformance regression pinning the
  single /responses path.
- Drop the providerOptions: undefined assertion from the plaintext replay
  test; assert part count, type, and text instead of coupling to AI SDK
  normalization.
@me2seeks
me2seeks force-pushed the feat/2513-open-responses-plaintext-replay branch from 0695cf3 to 865cd54 Compare August 18, 2026 12:07

@me2seeks me2seeks left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Publishing the inline responses for the current implementation and validation.

Comment thread packages/runtime/src/__tests__/ai-sdk-backend.test.ts Outdated
Comment thread packages/runtime/src/ai-sdk-backend.ts Outdated
Comment thread packages/runtime/src/model-factory.ts
Comment thread packages/runtime-host/src/server/execution-model-authority.ts Outdated
Comment thread packages/runtime/src/ai-sdk-backend.ts
Comment thread packages/runtime/src/model-factory.ts Outdated
Comment thread packages/runtime/src/apply-patch-profile.ts Outdated
Comment thread packages/core/src/provider-registry.ts Outdated
Comment thread packages/runtime/src/apply-patch-profile.ts Outdated
Comment thread packages/core/src/provider-registry.ts Outdated
@me2seeks

Copy link
Copy Markdown
Contributor Author

Follow-up on the two findings added to Qodo’s aggregate review for 865cd54:

  • Stale webSearch metadata: no code change. ModelMetadata.capabilities.webSearch records provider/model support, while resolveHostedWebSearchCapability(...).implemented is the Maka adapter-readiness authority. Production search routing only consumes that resolver; repository search found no UI or routing consumer reading the raw webSearch flag. The existing reports provider support separately from Maka adapter readiness test pins this separation.
  • ApplyPatch downgrade: confirmed intentional for every target with no ApplyPatch profile, not only DeepSeek. Replaying the call would serialize an undeclared tool. 21be06c adds a behavior regression for a non-Responses Anthropic target and shares the existing DeepSeek fixture helper; restoring the old null-profile passthrough makes it fail by emitting an apply_patch tool call.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest head now has a coherent provider contract from first principles: each Responses dialect owns its continuation format, provider options are keyed where the selected SDK adapter actually reads them, and unsupported hosted-tool replay is degraded per item instead of discarding valid client-tool history. The endpoint-form URL fix also removes the previous /responses/responses failure without adding a compatibility branch.

I found no reproducible P0-P2 issue on the latest head. The focused checks are green.

Review performed with Codex reviewer agents and DeepSeek V4 Flash as advisory tools; I verified the final claims against the latest head and current main.

中文评论

最新 head 的 provider contract 已形成清晰闭环:每种 Responses 方言管理自己的 continuation 格式,provider options 使用实际 SDK adapter 读取的 namespace,不支持的 hosted-tool replay 按 item 降级,不再误删有效的 client-tool 历史。endpoint-form URL 也修复了此前的 /responses/responses 问题,没有增加兼容分支。

在最新 head 上未发现可复现的 P0-P2 问题,相关检查均已通过。

本次审查使用了 Codex reviewer agents 与 DeepSeek V4 Flash 作为辅助工具;我已依据最新 head 和当前 main 复核最终结论。

@me2seeks

Copy link
Copy Markdown
Contributor Author

@M4n5ter The two issues from your changes-requested review were fixed on the current head 95b0f5e: provider-tool replay now degrades per item without dropping unrelated client-tool results, and endpoint-form Responses URLs no longer append a second /responses segment. All review threads are resolved, focused checks are green, and Astro-Han has approved this exact head. GitHub does not allow this fork author to use the reviewer-request API; could you re-review the current head when convenient so the stale aggregate CHANGES_REQUESTED state can be cleared?

@M4n5ter
M4n5ter merged commit 054ff04 into apache:main Aug 19, 2026
21 checks passed
Joob1n added a commit to Joob1n/maka-agent that referenced this pull request Aug 19, 2026
…signature

Main's apache#2972 landed a test constructing createHostSessionEffectModel
with claudeDeviceId, which this branch removes with the provider that
needed it. The merge is otherwise clean; only the field goes.

Generated-by: Claude Code
Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants