Skip to content

feat(runtime): adopt Open Responses extension codecs for DeepSeek tools - #5350

Open
Lxr-max wants to merge 8 commits into
apache:mainfrom
Lxr-max:feat/deepseek-open-responses-codecs
Open

Lxr-max wants to merge 8 commits into
apache:mainfrom
Lxr-max:feat/deepseek-open-responses-codecs

Conversation

@Lxr-max

@Lxr-max Lxr-max commented Sep 15, 2026

Copy link
Copy Markdown

Summary

Fixes #4107

Rebased onto current apache/maka main (feb9cf22f). DeepSeek Open Responses codecs stay registered at the createOpenResponses construction boundary; after the openResponsesSdkModel() helper extraction on main, the wrap and experimental_extensions now live in that helper so both Open Responses call sites stay aligned.

@ai-sdk/open-responses is already at 2.0.44 on main (newer than the issue’s 2.0.35), so this PR does not bump the package. It registers DeepSeek hosted web_search extension codecs:

  • Encode { type: "web_search" } (DeepSeek ignores search_context_size / user_location)
  • Decode web_search_call into provider-executed WebSearch call/result pairs
  • Stream response.web_search_call.in_progress|searching|completed without entering the client tool loop
  • Replay the original item, including opaque fields, exactly once
  • Drop those hosted pairs when the target adapter cannot recognize the exchange (DeepSeek chat / Anthropic web_search), so mid-session model switch does not emit dangling tool_calls or invalid server_tool_use

Until vercel/ai#19939 (allowBareTypes) ships a parser-safe change, @ai-sdk/open-responses@2.0.44 only accepts namespaced <implementor>:<type> registrations. This PR keeps namespaced registration plus the DeepSeek-only discriminator wrap. The unreachable bare allowBareTypes branch is deleted so a flag-only upstream cannot silently stop decoding.

Hosted-search product routing remains implemented: false (sibling #3689). Tavily and Anthropic-compatible DeepSeek paths are unchanged. apply_patch / custom_tool_call is out of scope.

Verification

  • npm --workspace @maka/{core,storage,runtime} run build
  • npx biome check on the touched files — clean
  • ai-sdk-backend.test.js hosted-pair / model-switch tests: 6 pass
  • deepseek-open-responses-extensions.test.js: 16 pass
  • model-adapter Open Responses / replay-gate / abort-isolation tests: pass
  • Did not run the full workspace npm test suite

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Cursor Cloud Agent (Grok 4.6) implemented the codecs, discriminator wrap, mid-session replay gate, tests, rebase onto current main, and this PR.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

@Lxr-max
Lxr-max marked this pull request as ready for review September 15, 2026 12:27
@github-actions github-actions Bot added the effort/XL Under 2500 readable lines label Sep 15, 2026

@hqhq1025 hqhq1025 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.

Reviewed exact head 1877b4ae4b37654a3aa77ac4aed9e5e17e26b641.

This adds a DeepSeek-only Open Responses extension codec plus a fetch-layer discriminator adapter for web_search, web_search_call, and related SSE events. Product routing remains disabled.

I found two correctness gaps in the codec lifecycle; both are included as inline P2 comments. The main issue is that the exact-once replay test exercises an SDK-local carrier that Maka does not persist, so the claimed replay behavior does not survive the production event/history boundary.

Checks run on this head: core/storage/runtime builds; 74 focused tests passed; Biome on all four touched files; ASF header audit; git diff --check; clean merge-tree with current main 6105ae726079b456f63ebe17f624bf76f4d45bff. The full workspace typecheck was not conclusive because downstream packages were invoked before all referenced workspace dist exports were built. Hosted checks currently contain only the label job, not a test gate.

Not ready to merge until the inline findings are resolved.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

): Experimental_OpenResponsesExtensionItem | undefined {
const part = options.part;
if (part.type !== 'tool-call') return undefined;
const stored =

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.

[P2] Preserve the replay carrier across the durable runtime boundary

storedReplayItem cannot recover the original item from the shape Maka actually persists. The Open Responses SDK puts the full opaque item on a separate custom replay carrier; the tool-call metadata contains only { id, itemId }. Maka drops custom stream chunks in model-adapter.ts and persists only that reference metadata on the tool call. I reproduced a second request from this persisted shape: the request contained no web_search_call item and emitted unsupported: provider-defined tool openai.web_search tool-result history. The current exact-once test passes only because it feeds first.content directly back before the runtime boundary. Please persist/reconstruct the full carrier and add a RuntimeEvent -> ModelMessage -> request regression test.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed on later commits (ed77ed1 and following). The opaque Open Responses item is now merged onto the provider-executed tool-call providerOptions so it survives RuntimeEvent persistence. deepseek-open-responses-extensions.test.ts covers RuntimeEvent → ModelMessage → request replay of the original web_search_call (including opaque fields). Please re-review when you have a moment.

providerExecuted: true,
},
];
if (item.status === 'completed' || item.status === 'failed' || options.mode === 'generate') {

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.

[P2] Propagate failed search status as an error result

This branch emits the same ordinary tool-result for status: "failed" as for a completed search, without isError: true. Maka only marks provider results as failures when the SDK chunk is tool-error or carries isError, so a failed hosted search is persisted and displayed as a successful tool result. A focused doGenerate probe with a failed web_search_call produced a provider-executed result containing status: "failed" but no error flag. Please mark failed items as errors and cover the runtime mapping.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed on later commits. Failed web_search_call items now emit isError: true on the provider-executed tool-result, and model-adapter maps that through as an error result. Covered by marks a failed hosted search item as an error result and marks failed provider-executed tool results as errors.

@hqhq1025 hqhq1025 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.

Reviewed exact head 59cd3256ad498e58a360ff46cc520138c952a7f7.

This follow-up fixes both previously reported issues: the opaque Open Responses extension item now survives the RuntimeEvent/history boundary, and failed hosted searches are marked as error results. The durable replay test exercises the persisted event shape and sends the reconstructed item through the provider request path.

I found one remaining concurrency/lifecycle correctness issue in the new carrier state, included as an inline P2 comment. The ModelAdapter is shared by concurrent turns, but the pending replay map is shared across every physical stream.

Checks run on this head: clean npm ci; core/storage/runtime builds; 292 focused Runtime tests passed; changed-file Biome; ASF header audit; git diff --check; and a clean merge-tree with fetched main ec59d42f6021a4b40e18f10dad184a1821e6f0c4. GitHub reports MERGEABLE / BLOCKED, REVIEW_REQUIRED, and no hosted status checks for this head. The PR API still reports base OID 6105ae726079b456f63ebe17f624bf76f4d45bff, while the live main ref is newer. I did not call the real DeepSeek API.

Not ready to merge until the inline finding is resolved.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

Comment thread packages/runtime/src/model-adapter.ts Outdated
private readonly runtime: ResolvedModelRuntime;
private readonly openAiChatReasoningTransportState: OpenAiChatReasoningTransportState;
private readonly openAiResponsesTransportState: OpenAiResponsesTransportState;
private readonly pendingOpenResponsesExtensionReplay = new Map<string, ProviderOptions>();

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.

[P2] Scope replay carriers to one physical stream

This map lives on the session-wide ModelAdapter, although AiSdkBackend explicitly permits multiple concurrent send() calls and routes every stream through the same translateChunk(). I reproduced the production startStream() path with two simultaneous streams using the same provider-controlled item id: after carrier A, carrier B, then tool-call A, A persisted B's opaque provider_trace; tool-call B then had no item at all. Entries also have no finish/error/dispose cleanup, so an interrupted stream can leave stale state behind. Keep this association inside toModelStreamResult() (one map per physical request, cleared in finally) and add a concurrent-stream/aborted-stream regression.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed on later commits. The pending replay map now lives inside toModelStreamResult() (one map per physical stream) and is cleared on every iterator termination path. Concurrent same-id streams and aborted-then-later-request regressions are in model-adapter.test.ts.

@hqhq1025 hqhq1025 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.

Reviewed exact head bf65b76940b4e4ec40a0e2e2934676379cf0990c.

This follow-up resolves the previously reported concurrency/lifecycle issue. toModelStreamResult now owns the pending Open Responses replay-carrier map per physical provider stream (packages/runtime/src/model-adapter.ts:350-372) and clears it on every iterator termination path (:408-415). The regressions exercise concurrent streams using the same provider item id and an aborted stream followed by a later request (packages/runtime/src/__tests__/model-adapter.test.ts:806-900). I found no remaining P0-P3 issue in the current diff.

Validation on this head: clean Node 24.18.1 install; build:test; full workspace typecheck; Runtime 3495 passed / 13 skipped; focused 294/294; lint; format; ASF headers; and git diff --check. The merge tree with current main 4410c3a2d19d20cfdc6b7815bdd2d72d33a7460f is clean. Its focused run had one sandbox retry-count failure, which reproduces unchanged on pure current main and is not attributable to this PR. GitHub reports MERGEABLE / BLOCKED, REVIEW_REQUIRED, and no hosted status checks. I did not call the real DeepSeek API.

No technical blocker found on this head; final merge readiness still depends on repository review policy and live gates.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

@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.

Review

Scope: codec correctness / round-trip fidelity, not a line-by-line pass. Most of the risk here is in how the codec lines up with the SDK's extension seam, so I cross-checked the diff against the pinned @ai-sdk/open-responses@2.0.44 and @ai-sdk/provider@4.0.14 in this checkout (node_modules/@ai-sdk/open-responses/dist/index.js / index.d.ts) rather than only against the patch.

Verified

  1. Registration shape is valid for the pinned SDK. createOpenResponsesExtensionRegistry throws unless toolType+encodeTool, itemTypes+decodeItem and eventTypes+decodeEvent are provided in pairs, and assertNamespacedType requires the namespace to match the extension id. deepseek-open-responses-extensions.ts:221 registers all three pairs under openai.web_search with openai:* types — consistent in both the bare and namespaced branches.

  2. The extension id is the real provider-tool id in production, not just in tests. compileProviderTool('openai-web-search') returns openai.tools.webSearch(...) (model-adapter.ts:1255), i.e. provider-tool id openai.web_search, which is exactly DEEPSEEK_OPEN_RESPONSES_WEB_SEARCH_EXTENSION_ID. So the extension binds to the tool Maka actually lowers, and the wrap maps it to DeepSeek's bare web_search.

  3. Content-part shapes are V4-correct. The tool-result part uses result + isError (LanguageModelV4ToolResult, @ai-sdk/provider d.ts:556), not the prompt-side output. providerExecuted: true on that part is load-bearing: Maka drops tool-result chunks unless providerExecuted === true (model-adapter.ts:1126) and reads chunk.output ?? chunk.result (:1140). The as Experimental_OpenResponsesExtensionContentPart cast is therefore justified (the type omits providerExecuted on tool-results) — a one-line comment would stop it reading as a type escape hatch.

  4. Carrier contract matches what the SDK emits — kind open-responses.extension-replay, payload providerMetadata[<provider name>].openResponsesExtension = { id, item } (dist:1451). Scanning all provider keys instead of assuming deepseek (deepseek-open-responses-extensions.ts:137) is the right call, since runtimeProviderName() returns the connection slug for openai-compatible connections (provider-runtime-policy.ts:81).

  5. No duplicate decode in streaming. decodeItem is invoked only from the non-streaming path (dist:1058) and response.output_item.done (dist:1256); output_item.added for extension items is ignored, so the in_progress item in the stream fixture cannot produce a second tool-call.

  6. Ordering works because the SDK emits the carrier first. decodeExtensionItem returns [carrier, ...decoded] (dist:1414), so pendingOpenResponsesExtensionReplay is populated before the matching tool-call chunk. The per-request map (model-adapter.ts:366, cleared at :414) plus the concurrency/abort tests (model-adapter.test.ts:806, :860) cover the shared-state risk properly.

  7. Exactly-once history replay. The SDK pushes an item carrying the full item once (dedup on type:id) and silently skips parts carrying only {id, itemId} (dist:344-360). The projection's split — carrier keeps the item, tool-call/tool-result keep the reference (ai-sdk-message-projection.ts:419-441) — is exactly what that encoder expects, and the durable test (deepseek-open-responses-extensions.test.ts:473) asserts a single web_search_call with the opaque fields on the wire.

  8. Persistence round-trip closes. tool_start persists providerOptions into function_call content (session-event-runtime-mapper.ts:268), the core schema allows it (runtime-event.ts:1097), and buildRuntimeEventModelReplayPlan carries it onto the tool_call item (model-history.ts:870).

  9. Feature detection is tight. Only providerType === 'deepseek' gets the wrap and extensions, so Anthropic-compatible and OpenAI-compatible DeepSeek endpoints are untouched; unregistered provider tools still warn and drop (…extensions.test.ts:256); createRequestCustomizationFetch returns upstream unchanged when there is nothing to customize (request-customization-fetch.ts:40), so the model-factory.ts:174 change adds no wrapper in the no-customization case.

  10. The providerExecutedTools flip is scoped. With the short-circuit at model-adapter.ts:177, non-Responses DeepSeek adapters were already true via the kind !== 'responses' clause, so the only behavior change is DeepSeek + open-responses. streamText filters providerExecuted out of clientToolCalls and skips execution for it, so the "no client tool loop" claim holds.

The one item I'd like addressed

DeepSeek's hosted-pair replay degradation lost its only test (ai-sdk-backend.test.ts:3346, :3425). Those two tests were the regression guard for "Open Responses cannot round-trip a provider-executed pair → drop it, keep the grounded text". Because providerExecutedTools is now true for DeepSeek, they were re-pointed at alibaba-token-plan-cn instead of being updated, so:

  • nothing asserts what DeepSeek now does with a persisted provider-executed pair without carrier providerOptions — i.e. history written before this change, or any turn where the carrier merge didn't happen. That path takes encodeInputItem's synthesis branch (deepseek-open-responses-extensions.ts:375) and the SDK then warns provider-defined tool openai.web_search tool-result history while dropping the result. Maka consumes no SDK warnings anywhere in the model path, so that signal is invisible.
  • the test name ("Open Responses cannot replay a hosted tool pair") no longer describes the fixture provider.

Latent today (routing stays implemented: false, so no such history can exist in product), but it becomes load-bearing when #3689 flips the flag. Cheapest fix: keep one of the two tests on DeepSeek with updated expectations (synthesized web_search_call item present, result dropped, grounded text retained).

Nits

  • Dead code: OUTGOING_EVENT_TYPES (deepseek-open-responses-extensions.ts:81) is never read — events only flow inbound. Neither gate catches it: biome.jsonc is an explicit allowlist (preset: "none") without noUnusedVariables, and tsconfig.base.json sets no noUnusedLocals.
  • The allowBareTypes branch is unreachable and untested (:221; the rewrite tests early-return when the probe is true, …extensions.test.ts:139/:191). Note the SDK's own guards require a : in the type for items and events (dist:167-180), so if upstream ships allowBareTypes without relaxing those, this branch would register successfully and then silently stop decoding. A comment or a unit test constructing the bare variant directly would pin the assumption.
  • decodeEvent no-ops on searching/completed (:403): the call/result materialize only from output_item.done. Fine for DeepSeek's documented sequence, but if output_item.done is ever omitted, the turn ends with a tool-input-start and no call/result.
  • generate vs stream parity (:362): mode === 'generate' emits the tool-result even for a non-terminal status (in_progress), which streaming deliberately does not. Intentional? A comment would help.
  • Body handling (:262, :492): every DeepSeek JSON request body is buffered and re-serialized even when no type matches OUTGOING_TOOL_TYPES (rewriteDeepSeekOpenResponsesOutgoingBody always returns a fresh object, so the no-op case isn't detectable). With a request customization configured the body is serialized twice per request. Cheap win: return undefined when nothing changed.
  • translateChunk's new third parameter defaults to a fresh Map (model-adapter.ts:526), so a future caller that forgets it silently loses carriers with no error. Consider making it required.
  • usesDeepSeekOpenResponsesExtensions(providerType: string) (:125) accepts a plain string; the repo has a ProviderType union and both callers pass it.
  • The streaming test bypasses streamText (…extensions.test.ts:670 drives model.doStream directly). Maka's runtime always goes through streamText, where parseToolCall validates against the provider tool's (absent → empty-object) schema. The carrier path is covered at the startStream level with hand-crafted chunks, but no test drives an SDK-generated carrier through startStream/AiSdkBackend; worth adding alongside #3689.
  • Durable payload size: the opaque item is now persisted inside every provider-executed function_call event's providerOptions, so it also flows through the history-compaction/recap paths that clone providerOptions. Bounded by web_search_call.action today — flagging for whoever owns compaction.

@cursor
cursor Bot force-pushed the feat/deepseek-open-responses-codecs branch from ae646fb to ac9bd40 Compare September 18, 2026 04:50
@Lxr-max

Lxr-max commented Sep 18, 2026

Copy link
Copy Markdown
Author

Following up on @Astro-Han's review (DeepSeek hosted-pair replay degradation coverage + nits).

Addressed on the latest head (ec176e2 and parents after rebase onto current main):

  • Restored/tightened DeepSeek coverage for the no-carrier synthesis path (synthesized web_search_call present, result dropped, grounded text retained); Alibaba keeps its own degradation case
  • Removed unused OUTGOING_EVENT_TYPES; no-op body rewrite returns undefined; comments for generate vs stream parity and allowBareTypes
  • Rebased cleanly (only model-factory.ts conflict — DeepSeek wrap now lives in openResponsesSdkModel())

Would appreciate a re-review when you have a moment. Thanks!

@cursor
cursor Bot force-pushed the feat/deepseek-open-responses-codecs branch from ec176e2 to 60476fc Compare September 20, 2026 14:20
@Lxr-max

Lxr-max commented Sep 20, 2026

Copy link
Copy Markdown
Author

@Astro-Han @hqhq1025 — re-review request after the DeepSeek hosted-pair follow-up and a rebase onto current main (205a06efb). Head is 60476fcef on Lxr-max:feat/deepseek-open-responses-codecs (same PR, no new branch).

Astro-Han must-fix: DeepSeek no-carrier replay coverage is back in ai-sdk-backend.test.ts as synthesizes a DeepSeek hosted tool call when replay metadata is missing. Persisted provider-executed pair without carrier providerOptions now asserts:

  • synthesized web_search_call present (encodeInputItem synthesis)
  • tool-result dropped (function_call_output / web_search_result / tool-result absent)
  • grounded text retained (Maka shipped the feature.)
  • test name matches the DeepSeek fixture

Alibaba keeps keeps unrelated client tool history when degrading a hosted tool pair. Product routing is still implemented: false (#3689 untouched).

Cheap nits: unused OUTGOING_EVENT_TYPES removed; no-op body rewrite returns undefined; comments pin generate vs stream result parity and the allowBareTypes parser assumption. Larger nits (streamText carrier, compaction) left for #3689.

hqhq1025 threads: carrier persistence, failed-search isError, and per-stream carrier scoping are in place on later commits; replied on those threads.

Focused checks on this head:

  • npm --workspace @maka/{core,storage,runtime} run build
  • npx biome check on the touched files — clean
  • synthesizes a DeepSeek hosted tool call when replay metadata is missing + Alibaba mixed-tool test — 2 pass
  • deepseek-open-responses-extensions.test.js — 15 pass
  • model-adapter Open Responses / failed-result / abort-isolation tests — pass

Would appreciate a re-review. Thanks!

@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 codec work itself is solid — verified clean on the trust boundary that mattered most: the discriminator wrap is exact Map.get membership both directions (no prefix/substring leaks), scoped to providerType === 'deepseek' only, content-length deleted on rewrite, non-JSON/SSE bytes pass through untouched; replay consumes the carrier per physical stream with item.id === toolCallId matching; provider-executed calls never reach returnedToolCalls so no phantom client tool calls; history degradation is honest (unmatched calls dropped with diagnostics, grounded text preserved); encode emits exactly {type:'web_search'} with search_context_size/user_location never reaching the wire; implemented:false scope holds and Tavily/Anthropic paths are untouched.

One P1 and one P2 below — both are consequences of DeepSeek becoming a producer of provider-executed exchanges, which the surrounding machinery wasn't built for.

P2 — provider-executed web_search exchanges replay malformed on non-Responses wires. emitStep (ai-sdk-message-projection.ts:407-432) emits provider-executed call/result parts into any admitted plan with no origin-provider check, and providerExecutedTools now admits them for deepseek on every wire. Two concrete breakages on mid-session model switch (②):

  • DeepSeek → deepseek-chat/deepseek-reasoner (same connection, chat wire): convert-to-openai-compatible-chat-messages emits every tool-call part as assistant tool_calls ignoring providerExecuted, while the carrier and tool-result parts drop silently — the wire gets tool_calls:[{name:'WebSearch'}] with no matching tool message, which OpenAI-compatible endpoints reject → every subsequent request fails until history compacts past the step.
  • DeepSeek → Anthropic with hosted web_search offered: the call becomes server_tool_use{name:'web_search', input:<deepseek action>} and the result fails webSearch_20250305OutputSchema validation → prompt conversion throws every turn.

Verified safe directions: native OpenAI Responses drops the pair cleanly; generic open-responses providers stay fail-closed; Anthropic without the tool offered warns and drops. The defect class predates the PR, but this creates the first routine producer of provider-executed calls for deepseek — worth fixing before sibling #3689 flips implemented. Minimal fix: gate provider-executed exchange emission on the target adapter recognizing the exchange's replay state, or at minimum drop them on openai-chat-plaintext wires instead of emitting as client tool_calls.

P3s:

  • ai-sdk-message-projection.ts:431replayReference propagates the call's providerOptions onto the result for all providers, not just extension ones: an Anthropic provider-executed result now inherits {anthropic:…} caller options where main attached none. Possibly benign, possibly a latent fix, but a behavior change outside stated scope — have openResponsesExtensionReplayReferenceOptions return undefined unless it actually projected an extension reference.
  • openResponsesSupportsBareExtensionTypes() probes by construction — a flag-only upstream release (accepts allowBareTypes, parsers still require :) flips registration to bare types and silently stops decoding; the file's own comment notes this. Simplest fix is deleting the bare branch — the namespaced registration + discriminator wrap already emits the correct bare wire regardless; alternatively make the probe exercise a real decode path.
  • Replay dedup assumes provider item ids are unique across the whole request history (${type}:${id}) — if DeepSeek reuses web_search_call ids per response, a later genuinely-different search is silently deduped. Unverifiable without DeepSeek's id scheme; worth pinning in the test plan.

// replay is open for its hosted items; other Open Responses providers
// stay fail-closed.
providerExecutedTools:
usesDeepSeekOpenResponsesExtensions(this.input.connection.providerType) ||

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.

P1 — provider-executed web_search_call ends the turn as tool-callsstep_limit → recorded failed. With this extension, @ai-sdk/open-responses counts provider-executed parts in hasToolCalls with no providerExecuted exclusion (open-responses-language-model.ts:459,620-622 — contrast @ai-sdk/openai's hasFunctionCall, which deliberately counts only client calls). So a DeepSeek response carrying web_search_call + a final answer maps to finishReason:'tool-calls'; ai-sdk-turn.ts:2447-2451 then reports step_limit whenever maxSteps is defined — which is always for handoff continuations, child executions, and eval budgets (the repo's own deepseek eval fixtures set maxSteps). mapCompleteStopReason('step_limit')failedtool_step_cap_reached. A turn that successfully answered via hosted search records as failed on the exact model this PR targets. Decode is live today even with implemented:false (a web_search_call in any response decodes regardless of whether the tool was offered), and it becomes ① the moment #3689 flips. Minimal fix: only label step_limit when client work was actually pending — finishReason === 'tool-calls' && returnedToolCalls.length > 0 — or suppress when the step's only tool activity was provider-executed (attemptSawToolActivity is already tracked). The upstream fix belongs in @ai-sdk/open-responses (match hasFunctionCall semantics), but Maka needs the guard regardless until that lands.

toolCallId: result.toolCallId,
toolName: result.toolName,
output: await materializeReplayToolResult(result, call.toolName),
...(replayReference !== undefined ? { providerOptions: replayReference } : {}),

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.

P3 — replayReference now propagates the call's providerOptions onto results for all providers. openResponsesExtensionReplayReferenceOptions returns the options unchanged when there's no openResponsesExtension inside, so a non-extension provider-executed result (e.g. Anthropic web_search_tool_result) now inherits the call's provider options where main attached none — getAnthropicCaller/getCacheControl then read them downstream. Return undefined from the helper unless it actually projected an extension reference.

Register DeepSeek hosted web_search codecs on createOpenResponses. The
runtime already pins @ai-sdk/open-responses@2.0.44; until vercel/ai#19939
ships allowBareTypes, map only DeepSeek's documented bare discriminators
at the network boundary. Hosted search stays fail-closed.

Generated-by: Cursor Cloud Agent (Grok 4.6)
…tatus

Merge the Open Responses custom replay carrier onto provider-executed
tool-call metadata so the opaque web_search_call item survives RuntimeEvent
persistence, reconstruct it on replay, and mark failed hosted searches as
errors. Keep product hosted-search routing fail-closed.

Generated-by: Cursor Cloud Agent (Grok 4.6)
Keep Open Responses extension-replay pending items on the per-request
stream in toModelStreamResult instead of the session-wide ModelAdapter, and
clear the map on success, error, and abort so concurrent send() calls with
the same provider item id cannot mix opaque carriers.

Generated-by: Cursor Cloud Agent (Grok 4.6)
Drop the unused outgoing event map, return undefined from the fetch-layer
rewrite when no allowlisted discriminator changed, and document generate
vs stream result parity plus the allowBareTypes parser assumption.

Generated-by: Cursor Cloud Agent (Grok 4.6)
A no-op discriminator wrap must not hand the upstream fetch an
ArrayBuffer; request mocks and JSON.parse(String(body)) still expect
the original text payload.

Generated-by: Cursor Cloud Agent (Grok 4.6)
Gate provider-executed WebSearch emission on the target adapter recognizing
the exchange. Chat Completions no longer emit dangling tool_calls, and
Anthropic no longer coerces DeepSeek search into server_tool_use. Also
return undefined from replay-reference projection unless an extension
reference was produced, delete the unreachable allowBareTypes branch, and
pin SDK ${type}:${id} replay dedup.

Generated-by: Cursor Cloud Agent (Grok 4.6)
@cursor
cursor Bot force-pushed the feat/deepseek-open-responses-codecs branch from 60476fc to 194e438 Compare September 21, 2026 00:17
Provider-executed calls often carry no origin metadata; the matching
tool-result does. Gate both items from the paired exchange so Anthropic
hosted search still replays when only the result is schema-shaped.

Generated-by: Cursor Cloud Agent (Grok 4.6)
@Lxr-max

Lxr-max commented Sep 21, 2026

Copy link
Copy Markdown
Author

@Astro-Han — follow-up on your 2026-09-20 re-review (5261344309). Same PR, head 55346a9fb on Lxr-max:feat/deepseek-open-responses-codecs, rebased onto current main (feb9cf22f).

P2. Provider-executed hosted web_search is now gated on the target adapter recognizing the exchange (ModelAdapter.canReplayProviderExecutedExchange, used from canReplayProviderNative / dropUnsupportedReplayItems / emitStep):

  • DeepSeek → deepseek-chat (openai-chat-plaintext): pair dropped, grounded text kept, no dangling tool_calls / missing tool message
  • DeepSeek → Anthropic with hosted web_search offered: pair dropped, no server_tool_use + webSearch_20250305OutputSchema throw
  • Anthropic-shaped hosted search still replays on Anthropic (judged on the whole call/result exchange)
  • DeepSeek Open Responses synthesis path unchanged

Regressions: drops DeepSeek hosted search when replaying onto deepseek-chat and drops DeepSeek hosted search when replaying onto Anthropic web_search.

P3.

  1. openResponsesExtensionReplayReferenceOptions returns undefined unless it actually projected an extension reference (Anthropic caller options no longer copy onto results)
  2. Deleted the unreachable allowBareTypes bare-registration branch; namespaced registration + discriminator wrap stay
  3. Pinned SDK ${type}:${id} dedup in a comment + replays distinct hosted search items when ids differ (DeepSeek same-id reuse across responses still unverified)

Focused checks: core/storage/runtime build; Biome clean; hosted-pair / switch tests 6/6; extension suite 16/16; model-adapter replay-gate / carrier / abort tests pass. #3689 routing still implemented: false.

Would appreciate another look. Thanks!

@github-actions github-actions Bot added effort/XXL Over 2500 readable lines and removed effort/XL Under 2500 readable lines labels Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XXL Over 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(runtime): adopt Open Responses extension codecs for DeepSeek tools

3 participants