Skip to content

feat(providers): add Alibaba Token Plan Responses compatibility - #3255

Open
MoonOld wants to merge 3 commits into
apache:mainfrom
MoonOld:feat/alibaba-responses-3162
Open

feat(providers): add Alibaba Token Plan Responses compatibility#3255
MoonOld wants to merge 3 commits into
apache:mainfrom
MoonOld:feat/alibaba-responses-3162

Conversation

@MoonOld

@MoonOld MoonOld commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Alibaba Token Plan Responses support for qwen3.8-max while keeping the other Token Plan models on Chat Completions.

Core owns only protocol facts and explicit delegation:

  • openAiAdapterApiProtocol declares the Alibaba Token Plan qwen3.8-max → openai-responses wire;
  • both Token Plan registry entries reference the stable alibaba-token-plan Runtime profile;
  • the adapter type forbids declaring both a Core responses contract and a Runtime profile.

Runtime owns the concrete execution profile:

  • selects @ai-sdk/open-responses;
  • forces store: false, so continuation never depends on a provider-stored response ID;
  • preserves Alibaba's documented required and allowed_tools tool choices when exactly one tool is selected, while rejecting invalid forced cardinality and unsupported object shapes;
  • verifies streamed reasoning against output_item.done.item.summary;
  • persists bounded local item identity and summary boundaries for restart-safe replay;
  • isolates invalid or late reasoning deltas from finalized durable state;
  • preserves provider failure classifications without widening retry eligibility;
  • leaves DeepSeek on its existing plaintext-content replay path.

Fixes #3162

Verification

Exact head 424619b35:

  • npm run format:check
  • npm run lint
  • npm run check:asf-headers
  • Core and Runtime builds passed
  • Runtime full suite: 3,023 passed, 13 skipped, 0 failed
  • focused compatibility and Responses wire suites: 20 passed, 0 failed
  • the wire test covers both SDK-emitted tool_choice: "required" and an allowed_tools request overlay, with store: false still finalized after caller customization
  • git diff --check

The current head includes the early-stop stream teardown fix from e88e07c24: sdk.response is always consumed, and the mismatch regression traps delayed unhandledRejection events. The exact fix head passed CI; an independent Windows re-run remains requested because the original scheduler-owned leak reproduced only on Windows.

Provider and live evidence

  • Alibaba's current Responses documentation explicitly defines store: false as not storing the response and making it unavailable to previous_response_id.
  • The same documentation defines string tool_choice: "required" and object allowed_tools mode, with forced choice available only when exactly one tool is selected: https://www.alibabacloud.com/help/en/model-studio/qwen-api-via-openai-responses
  • A protected branch-model-factory probe on head 34bed03f1 reached Alibaba Token Plan China /compatible-mode/v1/responses with qwen3.8-max; the same credential was rejected by Coding Plan Chat with HTTP 401 (request id 3f605e8f-be3a-992a-a443-f57c2a1b7387).
  • A real Maka TUI run on head 34bed03f1 completed streamed reasoning, a Maka-owned Read continuation, and the final answer: feat(providers): add Alibaba Token Plan Responses compatibility #3255 (comment)
  • Alibaba documents reasoning items with required id and summary_text entries on response.output_item.done in the Responses documentation linked above.
  • A raw-SSE fixture drives the pinned SDK and asserts streamed response.reasoning_text.delta text equals the final item summary before durable state is attached.

The live evidence is labeled with the head on which it ran; the exact current-head evidence is listed separately under Verification.

Review focus

  • Core is the sole owner of provider/model protocol routing and the Runtime-profile reference.
  • Runtime is the sole owner of SDK, replay carrier, and request compatibility configuration.
  • No Runtime code selects the Alibaba profile from providerType; it resolves only the profile explicitly referenced by Core.
  • Type-level exclusivity prevents Core Responses config and a Runtime profile from becoming competing authorities.
  • A two-way contract test prevents missing and orphan Runtime profile implementations.
  • Account-declared ModelInfo.apiProtocol still overrides the catalog default.

AI use

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

OpenAI Codex assisted with protocol research, implementation, automated tests, review, and PR drafting. The human contributor reviewed the scope and remains responsible for the contribution. The commits carry a Generated-by: OpenAI Codex trailer.

Checklist

  • Tests cover the changed behavior and fail without it
  • Lint, format, typecheck, build, and affected suites pass locally
  • Real user-facing reasoning and Maka-owned tool continuation evidence is attached

Does this PR entail a change in behavior?

  • Yes — described under Summary
  • No

@MoonOld

MoonOld commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

@Astro-Han Could you review the compatibility architecture when convenient? Current main owns parsing/serialization through @ai-sdk/open-responses; Maka adds typed request policies and bounded durable summary identity for Alibaba while keeping DeepSeek content-only. The automated contract and negative-path coverage is in place. A protected Token Plan rerun with retained evidence and the requested real TUI screenshot are still pending.

@MoonOld

MoonOld commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Review follow-up complete on final head 41a1047. Four independently identified boundaries are now covered: header-only request customization cannot bypass store:false/tool-choice policy; legacy and foreign-profile reasoning degrades without blocking; multiple and empty reasoning items retain identity and summary-part boundaries; and streamed reasoning must equal the provider's final summary before durable metadata is attached. A mismatch flushes visible partial text without makaResponses and a later Turn remains usable. DeepSeek is explicitly unchanged from #2972: content-only replay, no new item ID or durable state. Three independent subagent reviews now report no remaining P0-P2. Local evidence: Core 557 passed, focused Runtime 263 passed, provider matrix/thinking 168 passed, root build passed; full Runtime recorded 2,962 passed / 12 skipped with one unrelated PTY 10-second timeout reproduced in isolation. The PR and #3162 descriptions have been updated to match the final behavior.

@MoonOld

MoonOld commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

@Astro-Han The final review follow-ups are now on head 41a1047. DeepSeek has been restored exactly to main's content-only behavior, while Alibaba's request policy, profile-isolated durable replay, multi/empty item handling, and failure-safe summary verification now have focused regression coverage. Could you review this final architecture when convenient? GitHub still requires maintainer authorization before the external-fork Actions can start.

@Astro-Han

Copy link
Copy Markdown
Contributor

Reviewing! Thanks for asking~

@Astro-Han
Astro-Han marked this pull request as ready for review August 19, 2026 15:17

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

Thanks for the final architecture update. I re-reviewed this exact head and found one P2 in the reasoning-item failure path, added inline below.

The provider routing, final store: false override, profile isolation, durable summary validation, and AI provenance otherwise look sound. Approval also needs one author-provided screenshot from the actual user-facing surface: please attach a real terminal or Desktop capture showing qwen3.8-max reasoning followed by a Maka-owned tool continuation. Text-only live verification is not enough for this UI/UX gate.

The remaining exact-head CI gates are still pending, so this is not merge-ready yet.

AI-assisted review disclosure: OpenAI Codex performed the exact-head architecture, failure-recovery, provenance, UI-evidence, and CI analysis; I verified the reproduction, severity, smallest fix, and live GitHub state before posting.

中文说明

当前有一个 P2:第二个 reasoning item 的非法 ID 会让它的无 ID delta 追加到已完成的第一个 item,最终把持久化 summary 状态写坏,导致下一轮在发请求前失败。另请作者提供一张真实 terminal 或 Desktop 截图,展示 qwen3.8-max reasoning 后继续执行 Maka-owned tool;文字说明不能替代 UI/UX 截图。CI 也仍需全绿。

Comment thread packages/runtime/src/ai-sdk-backend.ts Outdated
}
part.text += event.text;
} else if (stepResponsesThinkingParts.length > 0) {
stepResponsesThinkingParts.at(-1)!.text += event.text;

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] Keep an invalid next reasoning item from corrupting the finalized item

If item A has already finalized with makaResponses.summaryPartLengths and item B then supplies an ID rejected by safePlaintextResponsesReasoningItemId (for example, over 512 characters or containing C0), B's deltas arrive without an item ID and this branch appends them to A. B's reasoning-end fails, but the partial flush can persist A's old lengths with A+B text; the next turn then fails in reconstructSummaryParts() before provider dispatch. Please start a new undurable part when an idless delta follows a part with finalized provider metadata (or clear that durable metadata before the partial-error flush), and add a valid-A + invalid-B + next-turn recovery regression.

中文说明

已完成的 A 带有持久化长度元数据;非法 ID 的 B 会走无 ID 分支并把文本追加到 A。随后错误路径会保存“A 的旧长度 + A/B 合并文本”,下一轮重建时长度不一致,整个会话无法继续。最小修复是在已 finalized part 后遇到无 ID delta 时新建不带 durable metadata 的 part,并补恢复回归。

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.

Fixed in 34bed03. When an idless delta follows a finalized plaintext-summary item, Runtime now starts a separate undurable part instead of appending to the finalized item. The exact regression covers valid A → unsafe-ID B → provider error/partial flush → next-Turn recovery: A retains its original text and makaResponses boundary state, B persists without provider metadata, and the recovery prompt replays only A. Focused Runtime/Responses coverage is 264/264 green; Biome and diff checks pass.

@MoonOld

MoonOld commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Addressed Astro-Han’s P2 inline finding on head 34bed03. The finalized item can no longer be corrupted by deltas from a following unsafe-ID item, and the new restart/recovery regression proves the failed partial Turn does not brick the next request. The remaining reviewer request is the author-provided real Terminal/Desktop screenshot; code-side focused verification is green.

@MoonOld

MoonOld commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Correction to this correction: the credential owner has clarified that the supplied key is a Token Plan key; the brief Coding Plan classification was incorrect. The earlier temporary run still retained no raw HTTP artifact or request ID, so it remains withdrawn as merge evidence until the reproducible Token Plan probe and requested TUI screenshot complete.

@MoonOld

MoonOld commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Protected credential/routing probe completed on 2026-08-20 through the branch model factory. Coding Plan Chat returned HTTP 401 (invalid_api_key, request ID 3f605e8f-be3a-992a-a443-f57c2a1b7387), while Token Plan China /compatible-mode/v1/responses with qwen3.8-max completed successfully. This confirms the credential is Token Plan and the branch reaches the intended Responses endpoint. The probe used a 32-token output cap and produced no final text, so I am not treating it as reasoning/tool-loop evidence; the requested real TUI continuation screenshot remains pending.

@MoonOld

MoonOld commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Real Maka TUI verification on head 34bed03 using Alibaba Token Plan China with qwen3.8-max: streamed reasoning, a Maka-owned Read tool continuation, and the final answer completed successfully.
qwen3 8max

@MoonOld

MoonOld commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

@Astro-Han The P2 reasoning-item isolation finding is fixed on head 34bed03f1202, and the requested real Maka TUI screenshot is now attached above. Could you do a final re-review when convenient? The external-fork CI is still awaiting maintainer authorization.

jackwener
jackwener previously approved these changes Aug 20, 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.

Reviewed at 34bed03f. This does not work as written, and the reason is a single unverified assumption about the wire contract.

provider-registry.ts declares reasoningReplay: 'plaintext-summary' for the Alibaba Token Plan provider, which commits the adapter to receiving reasoning as a summary-channel event. The vendored @ai-sdk/open-responses@2.0.28 in this tree only handles response.reasoning_text.delta — the content channel — and emits nothing on the summary channel at all. Because ai-sdk-backend.ts:2394 checks the carrier with strict equality and throws on mismatch, every turn that produces reasoning ends in an error rather than a degraded rendering. That is a P0: the feature's primary path is the path that fails. The fix is either to declare the carrier the vendored SDK actually produces, or to verify against a live endpoint that the summary channel is emitted and pin the SDK version that does so.

Architecturally, the PR builds three things that already exist one seam over: a second provider-options key resolver alongside openAiCompatibleProviderName, a second body-rewriting fetch alongside request-customization-fetch.ts, and a STATE_VERSION that does not match the one sessions were written with. The first two are duplication that will drift; the third silently strands existing sessions. None of them is hard to fold back into the existing seam, and doing so now is much cheaper than after this ships.

Reviewed with Claude Opus as an analysis assistant. The carrier finding is confirmed by reading both this head and the vendored SDK source; it is not reproduced against a live Alibaba endpoint, and that verification is exactly what I am asking for. Reproduction status is stated per finding.

Comment thread packages/core/src/provider-registry.ts Outdated
name: 'provider',
responses: {
adapter: 'open-responses',
reasoningReplay: 'plaintext-summary',

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.

[P0] Verify the reasoning carrier before declaring it, or declare the one the SDK actually emits. reasoningReplay: 'plaintext-summary' commits the adapter to the Responses summary channel, but the vendored @ai-sdk/open-responses@2.0.28 in this tree handles only response.reasoning_text.delta — the content channel — and never emits a summary event. ai-sdk-backend.ts:2394 compares the carrier with strict equality and throws on mismatch, so every Alibaba Token Plan turn that produces reasoning terminates in an error instead of rendering. Confirmed by reading this head and the vendored SDK source; not reproduced against a live endpoint, which is the verification this line needs. Either set the carrier to what the pinned SDK produces, or capture a live transcript showing the summary channel and pin the SDK version that parses it. Regression test: a recorded Alibaba reasoning stream replayed through the adapter, asserting a rendered turn rather than a throw.

safePlaintextResponsesReasoningItemId(streamItemIdValue);
const summaryParts = plaintextSummaryParts(provider);
if (!itemId || !summaryParts) {
throw new Error('Plaintext Responses reasoning item is missing final summary metadata');

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] Do not throw on a reasoning-end that carries no providerMetadata. The SDK's flush() path emits reasoning-end with no metadata attached whenever the stream terminates early — including when the provider itself failed mid-turn — so this hard throw fires during the cleanup of an already-failing request and replaces the provider's real error with a generic plaintext-reasoning message. The user sees a misleading cause and the actual failure is lost. Confirmed by reading code at this head and the SDK's flush path; not executed. Treat missing metadata on reasoning-end as a no-op and let the underlying error surface. Regression test: abort a reasoning stream mid-flight, assert the surfaced error is the transport error and not this message.

Comment thread packages/runtime/src/model-runtime.ts Outdated
};
}

function responsesProviderOptionsKey(

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] Reuse openAiCompatibleProviderName instead of adding a second provider-options key resolver. responsesProviderOptionsKey reimplements the same provider-to-key mapping that openAiCompatibleProviderName already owns, so a provider added to one will silently take the wrong options path in the other — and the failure is a silently ignored provider option, not an error. Confirmed by reading code at this head. Derive the key from the existing resolver. Regression test: add a provider to the existing mapping only, assert the Responses path picks up the same key.

return body;
}

async function parseJsonBody(request: Request): Promise<Record<string, unknown>> {

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] Route this body rewrite through request-customization-fetch.ts rather than a second fetch wrapper. parseJsonBody plus the surrounding wrapper reimplements request-body interception that the existing customization fetch already performs, which means two layers can now rewrite the same request with no defined ordering between them, and a future change to one leaves the other stale. Confirmed by reading code at this head. Express this compatibility shim as a customization within the existing seam. Regression test: a request that both layers would touch, assert a single well-defined resulting body.

) {
return undefined;
}
throw new Error('Malformed durable plaintext Responses reasoning state');

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] Reconcile STATE_VERSION with the version existing sessions were written at, or migrate them. The durable plaintext-reasoning state is read back under a version that does not match what previously-persisted sessions carry, so this throw makes those sessions permanently unresumable — the user's only recovery is to abandon the conversation, and nothing in the error says so. Confirmed by reading code at this head; not executed. Either keep the version stable and tolerate the older shape, or add an explicit migration that upgrades stored state on read. Regression test: persist a session at the pre-PR version, resume it at this head, assert it resumes.

case 'force-store-false':
body = { ...body, store: false };
break;
case 'reject-forced-tool-choice': {

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] Remove the reject-forced-tool-choice branch or wire it up. Nothing in this PR ever produces that compatibility mode, so the case is unreachable as merged; leaving it in place suggests a behaviour the build does not have. Confirmed by reading code at this head. Delete it, and reintroduce it with its call site when it is actually needed.

@MoonOld

MoonOld commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

@Astro-Han Thanks for the exact-head review. I rechecked 34bed03f, the pinned @ai-sdk/open-responses@2.0.28 package source, and every commit in this PR.

The P0 premise does not match the pinned adapter's two-stage stream mapping:

  1. response.reasoning_text.delta becomes reasoning-delta; this is the visible streamed text.
  2. response.output_item.done for a reasoning item becomes reasoning-end. Its createReasoningProviderMetadata maps the final item.summary into providerMetadata[providerOptionsName].reasoningSummary.

In this PR, plaintext-summary names the durable replay carrier, not the raw delta event name. ModelAdapter accumulates the content deltas, reads the final summary from the reasoning-end metadata, and AiSdkBackend verifies strict equality before attaching makaResponses. Missing, empty, or mismatched final summary fails the turn by design.

The real Maka TUI run on this exact head completed streamed reasoning, a Maka-owned Read continuation, and the final answer. That path cannot complete if the final summary metadata is absent or differs from the streamed text. Both package.json and the lockfile pin the adapter exactly to 2.0.28.

On the three architecture points:

  • responsesProviderOptionsKey repeats the small raw-provider-identity rule used by openAiCompatibleProviderName. I agree that helper can be centralized, but the current values are identical and contract-tested; this is a maintainability cleanup, not a failing path.
  • The two fetch layers have different authority. request-customization-fetch applies caller-owned additive headers/body fields and rejects collisions. The compatibility layer is deliberately final: it must override store to false and reject unsupported forced tool_choice, including after caller customization. Their JSON plumbing could share a utility later, but folding their authority semantics together would weaken the invariant this PR tests.
  • makaResponses does not exist on main. Its STATE_VERSION has been 1 since the first commit in this PR and never changed across the five commits, so there is no earlier released or PR-head version for this decoder to strand. Legacy/foreign state without this key remains on the tested fail-open skip path.

Given the green exact-head CI, approval, pinned adapter, and live strict-summary evidence, I do not see a code change required for these findings. If you have a concrete 2.0.28 stream shape that bypasses response.output_item.done.item.summary, please share it and I will test that exact case.

Review assistance disclosure: OpenAI Codex rechecked the exact head, pinned npm tarball, and PR commit history; I reviewed and approved this response.

@MoonOld

MoonOld commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Architecture follow-up before the next head update: the new Alibaba Token Plan wire selection, plaintext-summary replay contract, and request compatibility modules (store:false / forced tool-choice policy) are Runtime execution policy, not shared @maka/core domain facts. I am moving the #3255 additions out of Core and into @maka/runtime. The existing ModelInfo.apiProtocol field remains in Core because it is an account-declared cross-process model fact; only the default/effective wire resolution moves. This keeps the provider catalog shared while preventing API-adapter mechanics from widening the Core dependency boundary. The next pushed head will include the migration and updated Runtime-owned contract tests.

@MoonOld

MoonOld commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up completed locally for the next head update: (1) the conformance matrix now resolves reasoning replay through the effective Runtime adapter and binds both alibaba-token-plan-cn and alibaba-token-plan to an executable qwen3.8-max Responses tool-continuation test, while retaining generated Chat coverage for the other Token Plan models; (2) the branch is merged with current main (a6f33c952), so the real CI planner now reports full=false and selects Runtime plus its actual reverse dependents only. Verification on the merged tree: root build:test passed after applying repository dependency patches; Runtime test:dist passed 2,990 / skipped 13 / failed 0; focused provider matrix and Responses contracts passed 155/155. Core remains absent from the PR diff.

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

COMMENT. My earlier P1 and P3 are properly fixed; the P0 has moved rather than been answered, and the way it moved introduces a new architecture problem that I think is the most important thing in this round.

Fixed, and well:

  • P1, the throw on a metadata-less reasoning-end. isUnfinalizedPlaintextSummaryReasoningEnd (model-adapter.ts:869-879) now detects exactly the SDK's flush() trailer and :364-372 defers the decision to the terminal outcome, so an existing provider failure wins while a clean stream still fails closed rather than silently losing replay state. That is a more precise fix than the one I asked for, and the comment explains why.
  • P3, the unreachable reject-forced-tool-choice branch. It is now reachable — ALIBABA_TOKEN_PLAN_RESPONSES.compatibility lists it.

Still open from the previous head, and unchanged by this one: the second fetch wrapper in open-responses-compatibility.ts (parseJsonBody plus a wrapping fetch) rather than routing the body rewrite through request-customization-fetch.ts, and the duplicate provider-to-key resolver in model-runtime.ts alongside openAiCompatibleProviderName. Both are P2 and both are now more clearly worth doing, because the new provider-runtime-policy.ts makes three separate places where this provider's special-casing lives. On the STATE_VERSION P2 I will hold rather than restate it — the durable state moved into a new responses-reasoning-state.ts at version 1 and I could not re-derive at this head whether any previously written session reads back under a different version. Please say either way in the PR body.

Two P1s inline. To be explicit about what would move me: for the second one, a captured qwen3.8-max Responses body showing the reasoning on the reasoningSummary channel would close it outright and is probably ten minutes of work. For the first one, putting the contract back in the registry — verified — closes it too, so the two are really one decision.

AI disclosure: this review was assisted by Claude (Opus) for code search and cross-checking. Everything above I re-derived myself against the source at b625a699c.

| Exclude<ProviderRuntimeAdapter, { kind: 'openai-compatible' }>
| OpenAiCompatibleRuntimeAdapter;

const ALIBABA_TOKEN_PLAN_RESPONSES = {

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] This is a second declaration authority for provider capabilities, shadowing the registry. The previous head declared reasoningReplay: 'plaintext-summary', the two compatibility modules, and the qwen3.8-max Responses routing in packages/coreprovider-registry.ts and model-metadata.ts, the seam the whole codebase reads. This head deletes all three from core and re-declares them here: ProviderResponsesCompatibilityModule verbatim, plaintext-summary re-added to a runtime-local RuntimeProviderResponsesContract, and openAiAdapterApiProtocol's Alibaba branch reborn as defaultOpenAiApiProtocol wrapping the core function. resolveRuntimeProviderAdapter then overrides whatever the registry declared for these two provider types at model-construction time (model-runtime.ts:140). Two consequences. Anything reading PROVIDER_DEFAULTS[...].runtimeAdapter.responses — settings, capability display, any future consumer — now sees a contract the runtime does not actually use, with nothing at the boundary saying so. And the core test that pinned the routing rule (model-metadata.test.ts's "routes only Qwen3.8 Max through Alibaba Token Plan Responses") was deleted rather than moved, so the rule lost its coverage at the level where the rule lives. AGENTS.md asks for the closest existing seam rather than a parallel path, and the registry is that seam. If the concern was keeping an unverified carrier out of the published contract, the answer is to verify it, not to relocate it. Confirmed by reading the source at this head. Regression test needed: whatever replaces the deleted model-metadata.test.ts case, asserting the protocol routing at the level that owns it.


const ALIBABA_TOKEN_PLAN_RESPONSES = {
adapter: 'open-responses',
reasoningReplay: 'plaintext-summary',

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] The substance of my earlier P0 is still open — this declares the Responses summary channel and nothing in the PR establishes that the vendored @ai-sdk/open-responses emits it for this provider. Moving the declaration out of packages/core did not answer the question; it only changed which file makes the claim. Everything downstream is built on it: plaintextSummaryParts (model-adapter.ts:881-893) accepts only reasoningSummary entries shaped { type: 'summary_text', text } and returns undefined for anything else, and model-adapter.ts:837 then throws Plaintext Responses reasoning item is missing final summary metadata for every finalized item. So if the SDK actually delivers this provider's reasoning on the content channel rather than the summary channel, the failure mode is not degraded replay — it is a thrown error on the normal path. The tests I can see all construct the summary shape themselves, so they confirm the handler, not the premise. A captured response body from a real qwen3.8-max Responses call showing the reasoningSummary array would settle it; short of that, plaintext-content is the safe declaration. Confirmed by reading the source at this head. Regression test needed: a wire-level fixture recorded from the provider, not a hand-built chunk.

@MoonOld

MoonOld commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Final review/fix loop update on pushed head fc20601c0: response.failed now preserves its raw provider code through settlement and usage telemetry, so rate_limit_exceeded is classified as rate_limit instead of generic provider_unavailable. Existing policy remains unchanged: without Retry-After, the failure is classified correctly but is not automatically retried. The provider conformance matrix now enforces a duplicate-free bijection between override cells and executable bindings; DeepSeek/xAI/custom-Responses gaps use real two-request HTTP continuation tests. Current main 84fbe05db is merged; planner is full=false, asf_source=false, and Core diff is zero. Local evidence: root build:test passed; focused matrix/Responses tests passed; Runtime full suite passed 2,992 with 13 skipped, with only the known local Node 25 PTY 10-second child timeout failing. Independent production re-review reports no remaining P0-P2. Exact-head Actions still await external-fork maintainer authorization and renewed approval.

@MoonOld

MoonOld commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

@Astro-Han I updated the PR body with direct provider evidence for the two remaining architecture/carrier questions. Alibaba official Responses documentation defines reasoning items with required id and required summary entries of type summary_text, and publishes response.output_item.done examples carrying exactly that shape: https://help.aliyun.com/en/model-studio/qwen-api-via-openai-responses. The real Maka TUI run on this feature path also completed the strict streamed-text == final-summary gate before a Maka-owned Read continuation, so this is no longer based only on a synthetic fixture. Core remains the shared provider/catalog/account-fact authority; the Alibaba @ai-sdk choice, replay carrier, and body policy are intentionally Runtime execution policy, with one effective resolver and zero Core diff. The latest pushed head also preserves raw response.failed codes (rate_limit_exceeded now classifies as rate_limit) and closes the matrix override/binding gap. Could you re-review fc20601c0 when convenient? Exact-head Actions still await maintainer authorization.

@MoonOld

MoonOld commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Occam/razor cleanup pushed on b0d4362fe: compared with the prior head, the PR is 200 net lines smaller. The two nested fetch wrappers now share one JSON/Request customization seam with an explicit finalizer; provider identity has one Runtime authority; Alibaba uses one named compatibility profile instead of a hypothetical module list; and the unreleased durable state no longer carries unused content-carrier branches or a single-value carrier field. Root build:test and exact-head high-signal suites passed (199/199), with Biome and diff checks green. Current main advanced only through Windows CI workflow repairs (#3317); it does not touch Runtime and the merge tree is clean, so I did not add another merge commit or retrigger Actions solely for that unrelated change.

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

Re-reviewed at b0d4362fe. I started this round on fc20601c0, the head moved while I was verifying, and I re-derived every finding against the new head rather than carrying anything forward. I dispatched four scoped sub-reviews — security, correctness and resource bounds, integration, simplification and test quality — and used them as leads only; everything published below I confirmed myself by reading this head.

Four things are fixed, two of them the way I would have asked for.

The second fetch wrapper is gone. open-responses-compatibility.ts is now a fourteen-line body finalizer plugged into the existing request-customization-fetch.ts seam instead of a parallel wrapper with its own request reconstruction — that is exactly the right shape, and it removes the behavioural divergence I was about to file (the old copy hard-threw on GET/HEAD/null-body/non-JSON requests where the established wrapper passes them through). The carrier dimension is also gone: plaintext-summary no longer carries a carrier discriminant, which deletes a union arm no code path could produce. The reasoning-end-without-metadata throw is replaced by a deferral, and the yield in the finally does deliver the deferred error because the generator body completes normally there. reject-forced-tool-choice is now genuinely declared.

I have to correct my own P1 about the declaration split. Two of the three things I offered as evidence do not hold, and I would rather retract them than have you refute them one at a time:

  • I said the deleted core routing test was not moved. It was, and it is stronger than what it replaced: __tests__/responses-wire-contract.test.ts asserts both Token Plan providers route qwen3.8-max to Responses, that qwen3.7-max stays on chat, and that an account-declared apiProtocol override wins. The rule is covered at the layer it now lives in.
  • I said settings and capability display would see a contract the runtime does not use. There is no such consumer. Outside provider-registry.ts itself and packages/runtime/, nothing in the repo reads runtimeAdapter — not desktop, not the CLI. The divergence is latent, not observable today.

What survives is one thing, and I still think it matters: one fact has two declaration sites, and the core side states the opposite of the truth rather than a stale version of it. packages/core/src/provider-registry.ts gives both Alibaba Token Plan providers a runtimeAdapter with no responses at all, and ProviderResponsesContract has no plaintext-summary member, so core's declared position is "this provider does not support Responses" while the runtime routes it there. Relatedly, the docstring at packages/core/src/model-metadata.ts:94-98 now asserts two things that are false at this head — it calls itself the single declared source of the protocol split, and names the runtime model factory and the conformance matrix as its consumers, both of which have moved to defaultOpenAiApiProtocol. Whichever way the layering decision goes, that paragraph has to change.

The architectural question I would like a maintainer to settle, because it outlives this PR: are we adopting "core declares catalog facts, runtime declares execution policy, and runtime policy may add capabilities core says are absent" as a layering? Today it is neither documented nor annotated at runtimeAdapter. A smaller instance of the same question: model-protocol.ts adds reasoningItemId and reasoningSummaryText to the generic thinking event, filled only by the plaintext-summary path. Extending our own protocol is legitimate, but it sets the precedent that every new provider carrier adds fields to the shared event, and that is worth accepting or declining explicitly rather than by default.

For the record on the Runtime Host boundary: this PR does not touch packages/runtime-host at all, and settleModelStepOutcome decides a model step's terminal state, a kind already produced on main. The P2 below is intent bundling, not an authority violation.

One open thread I re-confirmed rather than re-filed, with a sharper mechanism than I first gave: the STATE_VERSION trap is worse than described. The profile is parsed before the version check, so a same-profile version mismatch decodes as malformed with a matching profile, misses the graceful return undefined in materializeRuntimeReplayPlan, and falls through to the throw. A future version bump therefore bricks every session written at version 1. It should degrade the way a profile mismatch already does — that is a two-line change and it is the difference between a migration and a data loss.

Carrying a P1 → COMMENT, not approval.

AI disclosure: reviewed with Claude Code, including four scoped sub-reviews used as leads. Every finding published here I re-derived at b0d4362fe myself — I read the pinned @ai-sdk/open-responses@2.0.28 stream mapping, the decode and replay paths, and the finish-classification diff, and I ran the repo-wide enumeration that retracts my two earlier evidence claims. Sub-review findings I could not confirm at this head are not included.

(candidate.providerOptions?.openai as { itemId?: unknown } | undefined)
?.itemId === itemId,
);
let part = stepResponsesThinkingPartsByItemId.get(itemId);

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] Guard this id-keyed branch against a delta that arrives after its item was finalized, the way the id-less branch below already is. When a reasoning-delta carries an itemId this map already resolves to a part whose providerOptions hold a finalized makaResponses state, the branch appends event.text to part.text and leaves summaryPartLengths untouched; deltas carry no reasoningSummaryText, so the mismatch check immediately below never runs. flushStep then persists text of length |T|+|X| against lengths summing to |T|, and on the next turn reconstructSummaryParts in responses-reasoning-state.ts throws Durable plaintext Responses reasoning summary boundaries do not match text from inside materializeRuntimeReplayPlan; nothing between there and provider dispatch catches it and the persisted RuntimeEvent is immutable, so every later turn in that session fails before reaching the provider. The id-less branch a few lines down handles exactly this hazard correctly — it decodes the last part and starts a fresh one when the state is valid — so the fix is to apply that same decode here and start a new part, or drop the durable metadata, instead of reusing the finalized one. Evidence: confirmed by reading code at this head; reachability depends on the provider emitting a stray or duplicated response.reasoning_text.delta after response.output_item.done, which the pinned SDK forwards unconditionally (@ai-sdk/open-responses@2.0.28, dist/index.js:803-808 tracks no finalization), so nothing in the stack defends it. Regression test: reasoning-start A → delta → reasoning-end A(metadata) → delta A → finish, assert the persisted part is either split or stripped of makaResponses, then assert the following turn replays instead of throwing.

if (finishReason === 'content-filter' || finishReason === 'error') {
const terminalFailure =
finishReason === 'error'
? providerFinishFailure(rawFinishReason)

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] Split this finish-reason reclassification out of the provider-compatibility PR, or at minimum cover it. On main a step with finishReason === 'error' is unconditionally terminal-failure carrying a non-retryable provider_unavailable; here it runs through providerFinishFailurenormalizeProviderFailure, and the outcome kind becomes terminalFailure.retryable ? 'retryable-failure' : 'terminal-failure'. That is a retry-behaviour change for every ai-sdk provider in the tree, not only Alibaba: any provider whose raw finish reason normalizes to a retryable code is now retried where it previously failed terminally. The PR body says the retry policy is unchanged — true of the policy, false of the classification fed into it. Under the one-revertable-intent rule that is the problem: reverting the Alibaba work would also roll back a cross-provider failure reclassification that has nothing to do with it. The content-filter arm is unaffected, since modelStepFailure always sets retryable: false; only the error arm moves. Evidence: confirmed by reading code at this head, not executed. Regression test: a non-Alibaba adapter finishing with finishReason: 'error' and a raw reason that maps to a retryable code, asserting both the outcome kind and the retry decision — the new retryable-failure arm has no test today, and the one test on this path asserts terminal-failure.


const ALIBABA_TOKEN_PLAN_RESPONSES = {
adapter: 'open-responses',
reasoningReplay: 'plaintext-summary',

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] Record which wire event this declaration actually depends on, and cover it with a raw-SSE fixture. This corrects my own earlier P1 on this line: I claimed nothing established that the summary channel carries reasoning here, and having now read the pinned SDK myself I no longer think that premise is in doubt — @ai-sdk/open-responses@2.0.28 builds reasoning-end's reasoningSummary from item.summary unconditionally (dist/index.js:883-895), so any provider populating summary on response.output_item.done satisfies plaintextSummaryParts. What survives is narrower and worth stating in a comment: the streamed text and the durable lengths arrive on two different channels. Deltas reach you through response.reasoning_text.delta (dist/index.js:803) while the lengths come from item.summary, and the SDK has zero handling for response.reasoning_summary_text.delta — 0 occurrences in dist/index.js, typed but unhandled in its source. So this contract holds only because Alibaba emits summary content on the content-channel delta event. If it ever moves to the standard summary delta, the streamed text is empty while the final summary is not, and the mismatch check in ai-sdk-backend.ts throws on every reasoning turn — a hard turn failure, not degraded replay. Evidence: confirmed by reading the pinned SDK at this head; the provider-side behaviour is inference from its published Responses shape, not executed here. Regression test: a wire-level fixture driving the real SDK from raw SSE frames rather than hand-built post-SDK chunks, asserting the streamed text equals the concatenated summary.

runtime?: ResolvedModelRuntime,
): ModelStreamEvent[] {
switch (chunk.type) {
case 'reasoning-start': {

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] Delete the now-dead second case 'reasoning-start' further down in this switch. That label is still grouped with start-step/tool-result/tool-error and their providerExecuted/toolCallId guard, but this new case precedes it, so the old arm is unreachable. Behaviour is unchanged today only because both returned [] for this chunk type — which is precisely why it will not be noticed later: TypeScript does not diagnose duplicate case labels, and biome.jsonc runs preset: none with noDuplicateCase off, so neither gate reports it. Evidence: confirmed by reading code at this head. No regression test needed; this is a deletion.

@MoonOld

MoonOld commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

@Astro-Han @jackwener Exact-head follow-up is pushed at d84bfc901, merged with current main fd9ad4baa (merge, not rebase).

The latest review findings are addressed:

  • late same-ID reasoning deltas now start an undurable part after a finalized item; the regression persists the split and proves the following turn replays instead of throwing;
  • raw finishReason: error classification keeps its diagnostic code but is forced non-retryable, with a provider-agnostic 503 settlement regression, so this PR no longer widens retry behavior;
  • an exact raw-SSE fixture drives pinned @ai-sdk/open-responses@2.0.28 and asserts response.reasoning_text.delta text equals response.output_item.done.item.summary;
  • the duplicate reasoning-start switch label is deleted;
  • well-formed future durable-state versions now decode as unsupported and degrade out of replay, while malformed current-version state remains fail-closed.

The earlier invalid-ID isolation, metadata-less trailer settlement, shared request-customization seam, single Runtime provider-name authority, forced-tool-choice declaration, and real TUI evidence remain intact. Core still has zero diff. Runtime now explicitly documents that its provider policy is layered over Core's catalog-level adapter default and that account ModelInfo.apiProtocol wins. @jackwener, the remaining question is the maintainer-level layering decision: whether this Runtime execution overlay is the intended boundary; I do not see a remaining functional divergence.

Current-main integration also exposed three stale legacy surface fields in owned-candidate.test.ts that no longer typecheck on main; this head removes only those fixture fields, without changing Runtime Host production behavior.

Verification on the pushed tree: root npm run build:test passed; exact high-signal Runtime/Runtime Host set passed 453/453; Biome and diff checks passed; CI planner reports full=false, Core is not selected. Exact-head Actions is currently action_required and needs maintainer authorization: https://github.com/apache/maka/actions/runs/32374114698

@Astro-Han

Copy link
Copy Markdown
Contributor

Hi — this PR conflicts with current main and cannot be merged as-is.

I tested a rebase onto current main locally (in a throwaway worktree — your branch was not touched). It stops on these files:

  • packages/runtime/src/__tests__/provider-contract-matrix.ts
  • packages/runtime/src/model-factory.ts
  • packages/runtime/src/model-runtime.ts

These are real source conflicts, so they need your judgement rather than a mechanical rebase — please rebase onto current main and resolve them yourself, then push. Once the branch is conflict-free and CI is green on the new head, I will pick it up for review.

git fetch upstream && git rebase upstream/main
# resolve, then
git push --force-with-lease

Thanks for the contribution — happy to help if any conflict is unclear.


AI-assisted maintenance note, not a review. It does not count as the required human review under CONTRIBUTING.md §Review.

@MoonOld
MoonOld force-pushed the feat/alibaba-responses-3162 branch from d84bfc9 to 41427b5 Compare August 23, 2026 14:33
@MoonOld

MoonOld commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

@Astro-Han @jackwener Rebase requested in the maintenance note is complete on head 41427b515, directly based on current main 8adb74b4f. GitHub now reports the PR mergeable.

The three conflicts were resolved semantically:

  • provider-contract-matrix.ts keeps main's retired/unavailable-provider guard, then applies the Runtime Alibaba adapter overlay;
  • model-runtime.ts keeps main's retired-provider refusal and layers the Alibaba default wire policy without restoring Core declarations;
  • model-factory.ts keeps main's camelCase OpenAI-compatible provider-options mapping and Fast serviceTier, while runtimeProviderName remains the one raw identity used by open-responses and provider metadata.

Core and Runtime Host both have zero diff. Verification on the rebased tree: root npm run build:test, repo lint, format check, full workspace typecheck, and the exact high-signal Runtime set (440/440) passed; CI planner remains full=false.

The new exact-head Actions run again needs external-fork authorization: https://github.com/apache/maka/actions/runs/32645781601. Once it is green, this head is ready for the promised review pickup.

@MoonOld
MoonOld force-pushed the feat/alibaba-responses-3162 branch from 41427b5 to 20ef5f6 Compare August 23, 2026 17:33
@MoonOld

MoonOld commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

@Astro-Han @jackwener The requested repair is complete on exact head 20ef5f632, directly rebased onto current main 6cfc888d9.

  • resolved the Runtime routing conflicts semantically, retaining current main's parallel-tool-call policy while layering the Alibaba Responses runtime policy;
  • restored all five ASF headers reported by the previous CI run;
  • the final diff is confined to packages/runtime; Core has zero diff;
  • GitHub now reports the PR mergeable.

Local verification on the rebased tree: ASF header check, lint, format, root build, and root typecheck passed; Runtime full suite passed 3,016 with 13 skipped and 0 failed. The remaining workspace suites passed. Eval's 75 Node tests passed; its Python suite requires Python 3.10+ (macOS system Python 3.9 cannot parse int | None), and all 68 Python cases passed under Python 3.13 with 6 expected skips.

The new exact-head CI run is action_required and needs external-fork authorization: https://github.com/apache/maka/actions/runs/32655238779. Once it is green, this head is ready for final review.

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

逐条核对 current head 20ef5f6 上的 7 条存活 finding(isOutdated=false):

  • provider-runtime-policy.ts:53 P1 未修:第二 capability 权威遮蔽 registry,resolveRuntimeProviderAdapter 覆盖 registry 的 responses 契约
  • provider-runtime-policy.ts:58 P1 未修:Responses summary 独立 policy 未回 registry
  • ai-sdk-backend.ts:2314 P1 未修:id-keyed finalize 后 delta 仍可追加
  • model-adapter.ts:807 P1 已修:reasoning-end 无 metadata 改为 defer
  • ai-sdk-backend.ts:3801 P2 悬置:STATE_VERSION 迁至 responses-reasoning-state.ts v1,旧会话兼容性待作者说明
  • model-adapter.ts:578 P2 部分修:fetch wrapper 已并入 request-customization-fetch,duplication 仍存
  • model-adapter.ts:907 P3 部分修

门禁:test success, mergeable true,卡点为上述未闭合 finding,非 CI。

@MoonOld
MoonOld force-pushed the feat/alibaba-responses-3162 branch from 20ef5f6 to 850ae74 Compare August 24, 2026 02:43
@MoonOld

MoonOld commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

@Astro-Han @jackwener I agreed with the surviving declaration-authority finding and reworked the architecture on exact head 850ae74a9, rebased onto current main 8e7fa68dc.

Proposed boundary for maintainer review:

  • Core owns protocol facts and delegation: openAiAdapterApiProtocol now owns the Token Plan qwen3.8-max → openai-responses default, and both provider registry entries reference one stable alibaba-token-plan Runtime profile.
  • Runtime owns concrete execution configuration: SDK choice, plaintext-summary replay, and request compatibility remain in one private Runtime profile table.
  • Runtime no longer selects Alibaba policy from providerType; it resolves only the profile explicitly referenced by Core.
  • The Core adapter type makes responses and runtimeProfile mutually exclusive, so they cannot become competing authorities.
  • A two-way contract test asserts that Core profile references and Runtime implementations are the same exact set, preventing both missing and orphan implementations.

I also rechecked the other surviving threads against this head:

  • finalized same-ID deltas are split at ai-sdk-backend.ts:2320; the exact reasoning-end → late same-ID delta → next-Turn recovery regression is at ai-sdk-backend.test.ts:11760;
  • makaResponses is unreleased PR-local state and has remained version 1 since introduction; well-formed future versions degrade out of replay and are covered in responses-reasoning-state.test.ts;
  • the raw-SSE fixture drives the pinned SDK and checks content deltas against output_item.done.item.summary;
  • raw finish errors preserve classification but are forced non-retryable and covered provider-agnostically;
  • the duplicate reasoning-start case is gone.

Exact-head local gates: clean build, full typecheck, ASF headers, lint, format, full npm test, and Runtime 3,022 passed / 13 skipped / 0 failed. The new full CI run needs external-fork authorization: https://github.com/apache/maka/actions/runs/32683983955

Could you review whether this protocol-reference / execution-profile split closes the authority concern, and recheck the late-delta thread against the current guard and regression?

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

#3255 850ae74 — review (bind exact head)

Gate: CI test success (run 32683983955). Sampling basis noted.

Scope: Alibaba Token Plan Responses compat — provider-runtime-policy, model-adapter, reasoning state, contract matrices.

Sampled checks (not full coverage):

  • Request/response field mapping sampled: Responses providerOptions and reasoning state wiring appear scoped to Alibaba token plan, not global.
  • Error code classification sampled: no broad retry-policy override observed in sample.
  • Abstraction reuse: new provider-runtime-policy.ts + responses-reasoning-state.ts — appears dedicated to this provider surface, not obvious duplication of existing Responses abstraction but full duplication check not completed in sample.

Limit: 26 files / 2503 lines — this review is sampled, not exhaustive; recommend second pass on contract matrices.

Verdict: COMMENT — no P0-P2 in sample, content tentatively GO within sampled scope, gate green.

中文抽样检查未见阻断,26文件量大建议二次全量。

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

Full-pass review at exact head 850ae74a91077d83a6cc37998de15f5e373cb908 (25 files, +2503/-116, MERGEABLE). I read every production file in full; the three open questions get full-coverage answers below, plus one finding.

The three open questions

1. Duplication vs the existing Responses abstractions (provider-runtime-policy.ts, responses-reasoning-state.ts) — no duplicate authority.

The split is a deliberate seam, not a parallel system: provider-registry.ts gains ProviderRuntimeProfileId where Core owns only that a runtime profile exists, and OpenAiCompatibleRuntimeAdapter makes responses and runtimeProfile mutually exclusive at the type level. Runtime's provider-runtime-policy.ts is the only resolver of profile → concrete contract, and responses-wire-contract pins that Core references and Runtime implementations match exactly (Core Runtime-profile references exactly match Runtime implementations). responses-reasoning-state.ts is the first durable plaintext-summary state carrier — the pre-existing plaintext-content mode stays untouched in place, and the new plaintext-item kind joins the existing ModelAdapterRuntimeEventReplaySupport union rather than forking it. The reasoning-item reader (responsesReasoningItemId) even keeps reading the legacy openai.itemId shape. This is extension along the existing seam.

2. Request/response field mapping — internally consistent and wire-pinned; provider-truth caveat below.

The wire-contract tests drive real doGenerate through the SDK against a mock fetch and assert the exact body: store: false survives a hostile requestBodyOverlay: { store: true } (the finalizer runs after caller overlays — correctly, since provider policy must win), reasoning.effort is preserved, and the replayed reasoning item is {type:'reasoning', id, summary} with no content field. tool_choice: 'required'/object shapes throw before dispatch. What I could not verify from here: that this matches Alibaba Token Plan's live API — I have no credentials for that endpoint, and the tests pin the SDK's own translation, not the provider's answers. The raw-SSE contract tests reference observed provider frames; I'm taking the pinned frames as the author measured them.

3. Error classification — no retry-policy widening. The one touched path (settleModelStepOutcome + providerFinishFailure) preserves the raw finish reason's classification for diagnostics but forces retryable: false, and the outcome kind follows that — so nothing retryable and non-retryable got mixed, and no provider error became newly retryable. The comment says as much and the code matches.

Finding

[P2][line-level, test gate] A new test fails deterministically on real Windows — post-test dangling rejection. packages/runtime/src/__tests__/ai-sdk-backend.test.ts:11537 (Alibaba Responses fails when streamed reasoning differs from the final summary) passes its assertions, but the process reports "generated asynchronous activity after the test ended … triggered an unhandledRejection event" — reproducible 3/3 runs on this Windows machine, rejection value undefined, no stack. CI's test job is green because it runs on Linux; this suite's evidence does not cover Windows. I could not determine in the time I had whether the leak lives in the test's mock-stream cancellation or in the production stream-error path — but this PR's own comment in model-adapter.ts (a generator return cannot strand the caller awaiting result.outcome) shows the author knows this failure mode is real, and a production unhandled rejection on a provider-mismatch path is a crash risk under default Node settings. It needs a repro and a verdict, not a shrug.

Executed evidence (real Windows x64, exact head)

Clean rebuild of core+runtime at the head, then: core model-metadata + provider-catalog-contract 20/20; runtime responses-reasoning-state + responses-wire-contract + open-responses-compatibility + request-customization-fetch 30/30; model-adapter + model-adapter-onerror + provider-contract-matrix 170/170; ai-sdk-backend + openai-responses-plaintext-reasoning 208/209 — the one failure is the finding above.

Checks: test green on the exact head; audit correctly absent (no manifest changes).

简体中文

全量复审完成。三个开放问题:新模块是沿既有 seam 的扩展而非重复抽象(core 只声明 profile 存在,runtime 独占解析,矩阵测试钉死两边一致);字段映射内部一致且 wire 测试钉住请求体(store:false 在用户 overlay 之后强制、tool_choice 强制形状拒发、reasoning 回放精确重建),但我无法验证与阿里云真实 API 的符合性(无凭据,这是诚实边界);错误归类没有放宽重试策略(分类保留为诊断,retryable 强制 false)。一条 P2:新增测试在真 Windows 上确定性失败(测试结束后出现未处理的 rejection,值 undefined),CI 绿是因为跑在 Linux;泄漏在测试 mock 还是生产路径未定论,需要作者复现定性。

A consumer that stops the adapter mid-stream (the reasoning-mismatch
throw, a user stop) leaves the SDK without a finish chunk, so teardown
rejects every result promise. usage and finishReason were already
consumed, but response was only read on the completed continuation
path — its rejection could surface as an unhandled rejection after the
turn unwound, scheduler-timing owned (observed post-test on Windows,
where Node makes it a crash). Sink it unconditionally in the teardown
finally, and pin the property with an unhandledRejection trap in the
mismatch test so every event loop proves the path leaves none behind.
@MoonOld

MoonOld commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

@zhiiw Thank you for the full Windows pass and the precise finding — addressed in e88e07c.

Verdict. I attempted the repro first: standalone probes of the exact mismatch scenario (unhandledRejection traps, long settle windows, stress loops) and the suite under node --test leak nothing on macOS/Linux — consistent with your observation that the settlement is scheduler-owned and only surfaces on Windows. So the fix targets the mechanism rather than the race.

Mechanism. The mismatch throw stops the adapter generator early, before any finish chunk exists; SDK teardown then rejects every streamText result promise. usage and finishReason were already sunk in the teardown finally, but response is only read on the completed-continuation path — on the error path its rejection had no handler, and when it lands is up to the event loop. That is the dangling rejection your Windows run observed after the test boundary; under Node defaults an unhandled rejection is a crash, so this needed closing regardless of reproduction.

Fix (production). The adapter teardown now sinks sdk.response unconditionally, so no early-stop path (mismatch throw, user stop) can leave an unhandled result-promise rejection on any platform.

Regression guard (test). The mismatch test now installs an unhandledRejection trap (removed via t.after), settles across macrotask cycles, and asserts zero leaked rejections — converting the Windows-only runner failure into a deterministic assertion on every event loop.

Evidence at exact head e88e07c (macOS). ai-sdk-backend + model-adapter + model-adapter-onerror + openai-responses-plaintext-reasoning + responses-wire-contract + responses-reasoning-state: 276/276. Honest boundary: I cannot execute the Windows repro from here — the new trap asserts exactly the activity you observed, so a re-run of ai-sdk-backend on your Windows machine (or any Windows runner) is the confirmation to beat.

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

Thanks for this. test is green on e88e07c2, and the 25 changed files all serve the Token Plan work — I checked each one against the merge base and found nothing riding along.

Two things in the request finalizer are worth another look before this lands; both are inline. They share a shape: the finalizer makes a claim about what the provider supports or does, and the tests assert the claim rather than the provider's behaviour.

One housekeeping note: the verification and live-probe comments in the description still refer to 850ae74a9, an earlier head. They cannot stand as evidence for e88e07c2 — worth refreshing so a later reader does not mistake them for current.

中文

感谢。e88e07c2test 已绿;25 个改动文件我对着 merge-base 逐个过了,都服务于 Token Plan 这条主题,没有夹带。

请求 finalizer 里有两处建议再看一下,都在行内。它们形状相同:finalizer 对供应商的支持范围或行为下了断言,而测试验证的是这个断言本身,不是供应商的实际行为。

另外一件小事:描述里的验证与 live probe 说明仍然指向更早的 head 850ae74a9,不能作为 e88e07c2 的证据,建议更新,免得后来的人误当成当前状态。

if (!profile) return undefined;
return (body) => {
const choice = body.tool_choice;
if (choice === 'required' || (choice !== null && typeof choice === 'object')) {

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] This rejects forced tool_choice unconditionally, including the forms Alibaba documents as supported.

The condition is choice === 'required' || (choice !== null && typeof choice === 'object'), and it throws before dispatch. So a request with exactly one tool and tool_choice: 'required' is refused, as is any object form.

Alibaba's Model Studio documentation for the OpenAI-compatible Responses API lists auto, none and required, with required supported when exactly one tool is present, and documents an object form as well. If that is accurate, this turns a supported call into a hard error.

Today the normal runtime path emits none or omits the field, so this is not reachable in the default flow — but requestBodyOverlay and any future caller can set it, which is why it is worth fixing rather than leaving as a latent trap.

Narrowing the check to the shapes the provider actually rejects — keyed on tool count for required — would keep the guard useful without refusing valid requests. If a live probe shows the documentation is wrong and forced choice genuinely fails, that is worth capturing in a comment here, since the current code reads as if that had already been established.

中文

[P2] 这里无条件拒绝强制 tool_choice,把阿里文档中标为支持的形态也拒掉了。

判断是 choice === 'required' || (choice !== null && typeof choice === 'object'),命中即在 dispatch 前抛错。所以"恰好一个 tool + required"这种组合也会被拒,对象形式同理。

阿里 Model Studio 关于 OpenAI 兼容 Responses 接口的文档列出了 auto/none/required,其中 required 在恰好一个 tool 时支持,也记载了对象形式。若文档属实,这段代码把一个受支持的调用变成了硬错误。

当前 runtime 默认只生成 none 或省略该字段,所以默认路径走不到——但 requestBodyOverlay 和将来的调用方都能设置它,所以值得现在收窄,而不是留成暗坑。

建议按工具数量对 required 收窄,只拒供应商真正不支持的形态。如果 live probe 证明文档有误、强制选择确实不可用,把这个结论写进注释——现在的代码读起来像是已经确认过了。

@MoonOld MoonOld Aug 24, 2026

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.

Agreed; fixed on exact head 424619b35.

The finalizer now follows the currently documented Alibaba Responses contract instead of rejecting every forced choice:

  • string required is preserved when the outgoing top-level tools list contains exactly one tool;
  • object { type: "allowed_tools", mode: "required", tools: [...] } is preserved when its selected list contains exactly one tool;
  • invalid forced cardinality still fails before dispatch;
  • object shapes not documented by Alibaba, such as the SDK specific-function form, remain rejected.

The product-level wire test drives real doGenerate through the pinned SDK for string required, and drives the documented allowed_tools object through the request-overlay/finalizer path. Both assert the exact outbound body and that store: false remains final.

Exact-head evidence: Runtime 3,023 passed / 13 skipped / 0 failed; focused compatibility + wire suites 20/20; format, lint, headers, Core/Runtime builds, and diff check passed. The PR description now binds these results to 424619b35.

if (choice === 'required' || (choice !== null && typeof choice === 'object')) {
throw new Error('Alibaba Token Plan Responses does not support forced tool_choice');
}
return { ...body, store: false };

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] store: false is asserted as a stateless guarantee, but nothing here shows the provider honours it.

The description states that the runtime applies a final request policy of store: false, the test is named for a stateless request, and responses-wire-contract.test.ts:284 describes the field as the wire switch for it. What the test actually asserts is that the field appears in the outgoing body.

Alibaba's parameter table for this endpoint does not list store, and its compatibility rules say only explicitly listed parameters are processed. The pinned @ai-sdk/open-responses@2.0.29 does not emit the field on its own either. So if the provider ignores it, the stateless and restart-safe-replay properties claimed in the description do not hold — and no test would notice, because every test checks our side of the wire.

This is not "the field might be ignored" as a hypothetical. The PR makes a retention claim, and the evidence offered supports a weaker statement: that we send a particular byte sequence.

Either use a retention control the provider documents and verify it against the real endpoint, or drop the stateless/privacy wording and the assertions that depend on it. Sending the field as best-effort is fine; promising the property is what needs backing.

中文

[P2] store: false 被当作 stateless 保证写了出来,但没有任何证据表明供应商会遵守它。

描述里写了 runtime 施加 store: false 的最终请求策略,测试名带 stateless,responses-wire-contract.test.ts:284 也把它说成 wire 上的开关。但测试实际断言的只是这个字段出现在发出的 body 里

阿里该接口的参数表没有列 store,而兼容性规则写明未列出的参数不处理;锁定的 @ai-sdk/open-responses@2.0.29 自己也不生成该字段。所以若供应商忽略它,描述里声称的 stateless 与 restart-safe replay 就不成立,而且没有任何测试会发现——所有测试查的都是我们这一侧。

这不是"字段可能被忽略"的假设。是这个 PR 做了关于数据留存的承诺,而它给出的证据只支持一个更弱的陈述:我们发出了某段字节。

要么改用供应商文档中存在的留存控制并对真实端点验证,要么去掉 stateless/隐私措辞和依赖它的断言。字段照发没问题,需要判据的是那个承诺。

@MoonOld MoonOld Aug 24, 2026

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.

Rechecked against the current Alibaba OpenAI-compatible Responses reference. The premise that store is absent from the parameter table is not true in the current documentation:

https://www.alibabacloud.com/help/en/model-studio/qwen-api-via-openai-responses

The request-body table lists store as an optional boolean with default true, and explicitly defines false as: the response is not stored and cannot be referenced by previous_response_id. The Retrieve Response reference also echoes the same semantics.

I kept the provider-owned store: false finalizer on that documented basis. I did make the wording more precise on exact head 424619b35: the test formerly named “stateless request” is now “non-stored request”, and the PR body says continuation does not depend on a provider-stored response ID. It does not claim that Alibaba performs no other operational retention.

The restart-safe replay statement is a separate Maka-local property: bounded item identity and summary boundaries are persisted in the local durable event state and reconstructed into the next request. It does not rely on provider storage.

The PR description now separates exact-current-head verification from older live evidence and labels every live run with its actual head.

Permit documented required and allowed_tools choices when they select exactly one tool, while keeping provider-owned store:false finalization and rejecting unsupported forced shapes.\n\nGenerated-by: OpenAI Codex
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.

feat(providers): add Alibaba Token Plan Responses wire with durable Qwen reasoning replay

4 participants