Skip to content

[2/8] refactor!: tool calls follow the AI SDK - #250

Merged
eric8810 merged 75 commits into
masterfrom
v4/2-tool-calls-follow-the-ai-sdk
Oct 17, 2026
Merged

eric8810 merged 75 commits into
masterfrom
v4/2-tool-calls-follow-the-ai-sdk

Conversation

@cunninghamcard-bit

@cunninghamcard-bit cunninghamcard-bit commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Requested by li

Summary

Before: the streaming tool-call tracker lives in aimux-stream as a port of provider-utils 5.0.51, so two parallel calls of the same tool whose first chunks carry empty arguments can merge into one. Tool types differ from upstream: tool_choice is required and mixes strings and objects, tool examples and provider-tool args are untyped, a provider tool id is any string, a V4 tool result may be null, and tool calls carry a separate thought signature. Invalid tool input is back-filled with a plain JSON parse, and a repaired call that still fails validation is wrapped in a repair error. The MoA and RFC-0026 converter unit tests are gone.

After: the tracker lives in aimux-provider-utils next to the vendors that use it and behaves as the provider-utils 5.0.55 tracker, which keeps such calls apart; only the tracker type is exported. A differential test compares it with fixtures produced by the pinned upstream tracker. Tool types match the pinned upstream types field for field: the model call carries the V4 tool-choice object union, filled by the call layer as upstream prepareToolChoice does; a provider tool id must be ${string}.${string}; a V4 tool result is never null. The thought signature travels in provider metadata and provider options under the vendor namespace. Invalid tool input is back-filled with the prototype-safe parse, and a repaired call that fails validation becomes an invalid call, as parse-tool-call does. The 18 MoA and 22 converter tests are back.

Type of change

  • Bug fix (non-breaking)
  • New feature (non-breaking)
  • Breaking change
  • New provider
  • New / updated binding
  • Docs / RFC

Checklist

  • cargo fmt --all -- --check passes
  • cargo clippy --workspace --all-targets -- -D warnings passes
  • cargo test --workspace passes (cassette playback — no network/keys)
  • Added or updated cassette tests / contract fixtures for behavior changes
  • Updated relevant docs (README.md, docs/, rfc/, bindings/README.md)
  • Commit messages follow Conventional Commits

Notes for reviewers

CI artifact (zip) master a8e3a99 [1/8] ca19e38 this PR 496ef24 change vs [1/8] change vs master
ffi-x86_64-unknown-linux-gnu 3.77 MB 3.76 MB 3.78 MB +11.4 KiB (+0.31%) +11.0 KiB (+0.30%)
ffi-aarch64-apple-darwin 3.12 MB 3.12 MB 3.13 MB +7.4 KiB (+0.24%) +7.3 KiB (+0.24%)
ffi-x86_64-pc-windows-msvc 3.03 MB 3.03 MB 3.03 MB +4.9 KiB (+0.17%) +4.5 KiB (+0.15%)
node-x86_64-unknown-linux-gnu 55.30 MB 55.30 MB 55.60 MB +287.9 KiB (+0.53%) +293.8 KiB (+0.54%)
node-aarch64-unknown-linux-gnu 53.25 MB 53.25 MB 53.55 MB +286.1 KiB (+0.55%) +287.0 KiB (+0.55%)
node-aarch64-apple-darwin 15.06 MB 15.06 MB 15.11 MB +51.3 KiB (+0.35%) +45.3 KiB (+0.31%)
node-x86_64-apple-darwin 13.60 MB 13.60 MB 13.63 MB +27.1 KiB (+0.20%) +30.1 KiB (+0.23%)
node-x86_64-pc-windows-msvc 10.57 MB 10.57 MB 10.59 MB +13.9 KiB (+0.14%) +19.0 KiB (+0.18%)
node-aarch64-pc-windows-msvc 9.87 MB 9.87 MB 9.90 MB +25.5 KiB (+0.26%) +23.2 KiB (+0.24%)

cunninghamcard-bit and others added 18 commits October 11, 2026 16:30
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 758d0bc)
The returned impl Iterator is already must_use, so clippy's
double_must_use rejected it.

(provider.rs part of commit 8397424)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…e removal

- scripts/gen_providers_doc.py: the `provider_name` module no longer exists;
  the docstrings and the section-header guard now name only `provider`.
  `--check` output is unchanged.
- docs/internal/phase4-audit.md: add a follow-up note (same style as
  RFC-0020) so the historical audit no longer reads as if the generated
  ProviderName types and their drift tests still exist.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
(cherry picked from commit dfcd54f)
The A2 line said Node would keep a generated string-literal union for
completion. It does not: overlay-registered provider names can never be
part of a compile-time union, so keeping one would keep the second name
path this PR removes. Record the actual A2 scope.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
(cherry picked from commit 3e2d9fc)
Pull requests into a branch other than master got no CI, so a stacked PR
could not show it was green against the PR below it. The pull_request
trigger now has no branch filter. A new push to a PR cancels that PR's
run still in progress; pushes to master are never cancelled.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…that pin Rust-only behaviour

The V4 migration that follows replaces these tests with tests ported from
the AI SDK. They are deleted first, on their own, so the PRs after this one
show only code changes. This commit removes only test files (Rust, the seven
bindings, the shared contract fixtures and their Node runner) and the CI
steps that ran the contract fixtures. No library code changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…ia alias

- Wire*.ts were hand-maintained mirrors of wire.rs and had already drifted
  (WireContentPart lacked provider_executed). Derive TS on the wire structs
  and export them to web/src/types/generated/ through gen_ts_types.py, which
  now handles two targets and checks both in CI.
- Core types resolve through the @aimux/types tsconfig alias instead of
  five-level relative paths into bindings/node.
- gen_ts_types.py sync no longer rewrites files that differ only by ts-rs
  end-of-line spaces.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 69c1351)
No crate in the workspace referenced the tracker; providers accumulate
streamed tool-call deltas themselves. Drops the module, its 787-line test
file and the re-exported companion types. Breaking for aimux-stream's
public Rust API; noted in CHANGELOG under Unreleased.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EYDYDWYcPsDmjBjFeuDVe
(cherry picked from commit fe8758f)
Re-adds the tracker as a port of the current
@ai-sdk/provider-utils streaming-tool-call-tracker.ts instead of the
older index-only version removed in the previous commit.

- Correlate deltas by wire id, index and function name; drop ambiguous ones
- Unique tool-call ids with bounded suffixes; blank generator output falls
  back to "tool-call"
- Ignore unmatched blank function names; retain blank-name continuations
- Flush in index order only when every call has an index
- New StreamingToolCallArgumentState / starts_with_structured_value port
- process_delta and flush return the emitted parts instead of buffering

Tests are ported case for case from the upstream suites (45 tracker cases,
9 argument-state cases). No caller in the workspace uses the tracker yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EYDYDWYcPsDmjBjFeuDVe
(cherry picked from commit 6035201)
…nd wire the OpenAI stream to it

The AI SDK keeps StreamingToolCallTracker in @ai-sdk/provider-utils, where
every provider that speaks the OpenAI chat-completions wire format imports
it. aimux had the tracker in aimux-stream (the eventsource-parser role),
with its own ToolCallStreamPart event type and no callers, while
openai/model.rs kept an index-only accumulator. This commit puts the
pieces where the AI SDK has them.

aimux-stream
- StreamingToolCallTracker, StreamingToolCallArgumentState,
  starts_with_structured_value and ToolCallStreamPart move out; the crate
  is SSE decoding only. serde_json becomes a dev-dependency (test
  fixture); the `tool-calls` keyword is dropped.

aimux-provider-utils
- streaming_tool_call_tracker.rs / streaming_tool_call_argument_state.rs
  land here, logic unchanged. The tracker emits aimux_core::StreamPart
  ToolInputStart / ToolInputDelta / ToolInputEnd / ToolCall directly, so
  there is no second event type (RFC-0036: ToolCallStreamPart must not be a
  second public protocol). The generic metadata parameter becomes
  serde_json::Value / ProviderMetadata; TrackerError converts into
  AiMuxError::InvalidResponseData. The ported upstream tests (45 + 9)
  move with it, projecting StreamPart back to the upstream event shape.

aimux-providers
- openai/model.rs replaces its HashMap<usize, ToolCallAccumulator> with the
  tracker: tool_calls deltas are correlated by wire id, index and function
  name instead of index alone; id-less continuations follow their call;
  indices reused across parallel calls stay distinct; ambiguous deltas are
  dropped; a call without a wire id gets a generated `tool-call` /
  `tool-call-N` id instead of an empty string; a new call without a
  function name ends the stream with InvalidResponseData (AI SDK
  behaviour) instead of starting a call with an empty name.
- DeltaToolCall.index is Option<usize> (the AI SDK schema is
  `index: z.number().nullish()`).

Every registry-backed provider goes through openai/model.rs, so this is
the one plug point until S4-2 folds the mistral and xai chat copies into
the same implementation.

Verification: cargo test -p aimux-stream -p aimux-provider-utils; cargo test
-p aimux-providers (129 suites, 3041 passed, 0 failed, including the
cassette replays); cargo clippy -p aimux-stream -p aimux-provider-utils
-p aimux-providers --all-targets -- -D warnings; cargo fmt --all -- --check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
(cherry picked from commit df4d09f)
…-state port

Carried over from the SSE test port commit so the two split branches
leave the file identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
(cherry picked from commit 7bc200e)
Describes only the final API: the tracker lives in aimux-provider-utils
and emits StreamPart directly; the intermediate ToolCallStreamPart-
returning API from earlier commits is not mentioned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
(cherry picked from commit 264ade3)
… Rust

Same behaviour as the upstream port (checked differentially against the
previous implementation on ~1.1M random deltas), with Rust structure:

- Correlate by linear scan over per-call wire ids and indices instead of
  id/index hash maps re-sorted on every delta; no per-delta allocations
  besides the emitted parts.
- `finish(self)` consumes the tracker, so the never-true-before-flush
  `has_finished` state is gone and post-flush use cannot compile.
- `StreamingToolCallDelta<'a>` borrows from the provider wire type and
  carries `provider_metadata`, replacing `StreamingToolCallFunction`, the
  untyped `extra` bag, the test-only builders and the two boxed metadata
  hooks.
- `process` / `finish` append to a caller-owned buffer.
- Drop the unreachable `TrackerError::IdExhausted`.
- The argument state scans bytes and models completion as its own state.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a11e83-bf7b-75bb-900b-34851e6f12ee
(cherry picked from commit b0fd923)
…es and docs

With the tracker in aimux-provider-utils, aimux-stream only decodes SSE:
serde_json is needed by its tests alone, and the tool-calls keyword no
longer applies.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
- Drop the redundant #[must_use] on provider_names(); the returned
  impl Iterator is already must_use, so clippy::double_must_use failed CI.
- Gate the wire types' ts-rs export tests behind a ts-export feature that
  only gen_ts_types.py enables. A plain cargo test previously wrote
  Wire*.ts into bindings/node/src/types via the workspace-wide
  TS_RS_EXPORT_DIR, which made gen_ts_types.py --check fail on stale files.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 8397424)
One tool struct per layer, field for field with the pinned upstream types:
- CallOptions.tool_choice is optional. Vendors omit an unset choice, as
  their upstream packages do; generate_text and stream_text default it to
  auto when tools are given, as upstream prepare-tool-choice does.
- FunctionTool.input_examples is a list of { input: object } and
  ProviderTool.args is a JSON object.
- Tool calls carry no separate thought signature. The value travels in
  provider metadata on output and provider options on input, under the
  vendor namespace, where each upstream vendor package reads and writes it.
- Call sites pass the tool structs instead of their field lists.

The fifteen tool-choice tests ported from upstream (OpenAI, Groq, Mistral,
Bedrock) are kept and adapted; hand-written tests that pinned the old
shapes are deleted. CHANGELOG and docs/api/gaps.md record the wire-format
changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
@cunninghamcard-bit cunninghamcard-bit changed the title [V4 2/8] refactor!: tool calls follow the AI SDK refactor!: tool calls follow the AI SDK Oct 12, 2026
@cunninghamcard-bit cunninghamcard-bit changed the title refactor!: tool calls follow the AI SDK [2/8] refactor!: tool calls follow the AI SDK Oct 12, 2026
fixtures/aisdk/VERSIONS.json lists the seven AI SDK packages that the
upstream comparison tests are pinned to: ai 7.0.127, @ai-sdk/provider
4.0.21, @ai-sdk/provider-utils 5.0.53, @ai-sdk/anthropic 4.0.71,
@ai-sdk/google 4.0.87, @ai-sdk/openai 4.0.83 and
@ai-sdk/openai-compatible 3.0.62.

Every entry has one shape, {"version": "<x.y.z>", "integrity":
"sha512-<base64>"}, where integrity is the sha512 SRI of the npm tarball
(the registry's dist.integrity). A bare version string is not a valid
entry. The file used to arrive later in the stack, together with the
provider factories, as bare version strings; it comes first now so that
every later change can be checked against the pinned upstream sources.

Ported from fork commit 9d7508c.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
scripts/fetch-aisdk.sh downloads every package pinned in
fixtures/aisdk/VERSIONS.json with npm pack and unpacks it into
target/aisdk/<package>@<version>/, so the upstream sources that the
comparison tests follow can be read locally at exactly the pinned
versions.

- The whole VERSIONS.json is checked before anything is downloaded:
  every entry must be {"version", "integrity": "sha512-..."}, with a
  plain npm package name (it becomes a path) and an exact version.
- Each tarball's sha512 must equal the pinned integrity, and its
  package.json must name the pinned package and version.
- A package is unpacked into a work directory under target/aisdk and
  renamed into place only after its checks pass, so a failure never
  leaves a partial tree. A rerun replaces each tree with identical
  content.

AISDK_VERSIONS and AISDK_DEST override the input file and the
destination. scripts/fetch-aisdk.test.mjs (node --test) runs the script
against an npm registry served from the test process, with a temporary
npm cache and empty npm config files, so it needs no network.

Ported from fork commit 19db928. Unlike the fork, a bare version string
or a missing integrity is an error instead of a fallback to npm view,
because VERSIONS.json has a single format.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
scripts/fetch-aisdk.sh now verifies every package before it places any.
Phase 1 fetches, checks and unpacks all packages into a staging
directory; phase 2 moves the old trees aside, places the new ones and
puts the old ones back if a move fails. A network error, a missing
version or an integrity or version mismatch in any package leaves
target/aisdk exactly as it was. The new test "a failing package leaves
every destination as it was" fails on the per-package version.

scripts/aisdk-fixtures/versions.mjs is what the fixture recorder uses to
read and rewrite fixtures/aisdk/VERSIONS.json:

- readVersions / checkVersions accept only the object form, with the
  same name, version and sha512 checks as fetch-aisdk.sh.
- versionsDocument keeps the pinned integrity when the version is
  unchanged, takes it from package-lock.json when the lock pins the same
  version, and fails when neither has it or when the two disagree. It
  never writes an entry without integrity.

versions.test.mjs (node --test) covers this and checks the committed
VERSIONS.json. Its lockfile comparison is skipped until
scripts/aisdk-fixtures/package-lock.json exists.

Ported from fork commits ed1564b and 1ce4eb6. Unlike the fork,
versionsDocument fails instead of writing a version-only entry, because
VERSIONS.json has a single format. The recorder (record.mjs) is not part
of this change; it switches to versionsDocument when it is added.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
… 5.0.55 does

The tracker followed @ai-sdk/provider-utils 5.0.53, which merges a second
parallel call into the first when the stream reuses or omits labels: a
named delta with an unseen id and empty arguments resolved to the matching
call even after that call's structured arguments were complete, and an
unlabeled continuation with several open calls was dropped as ambiguous.

5.0.55 (5094ebd) adds `hasEmptyArguments`
(src/streaming-tool-call-tracker.ts:131, :140-141), passes
`name != null && hasEmptyArguments` as the start evidence on the
unseen-id path (:275-285), resolves the unlabeled path through
`resolveMatchingToolCall` (:305-308), and lets that function pick the only
candidate without complete structured arguments (:324-354). The Rust
tracker now does the same.

The three test cases added upstream in 5.0.55
(src/streaming-tool-call-tracker.test.ts:248-302, :304-341, :343-381 at
tag @ai-sdk/provider-utils@5.0.55) are ported; the first and third fail
on the previous tracker.

fixtures/aisdk/VERSIONS.json pins provider-utils 5.0.55 with its registry
integrity; the other entries are the object-form pins of the fork's
VERSIONS.json. The user-agent version string is left alone. The alignment
doc's #185 row records the new baseline.

Closes #228 B10.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…private

Re-applies fork PR #41 on this layer. The `streaming_tool_call_tracker`
and `streaming_tool_call_argument_state` modules are private; the crate
root re-exports `StreamingToolCallTracker`, `StreamingToolCallDelta`,
`TypeValidation` and `TrackerError`.

`@ai-sdk/provider-utils@5.0.55` exports only the tracker and its delta and
options types from `src/index.ts:115-119`; the argument state is internal.
So `StreamingToolCallArgumentState` and `starts_with_structured_value` are
no longer re-exported (the fork hid them with `#[doc(hidden)]` instead),
and their port of `streaming-tool-call-argument-state.test.ts` moves into
the module. The tracker's correlation table moves from the now private
module docs onto the type. The unreleased CHANGELOG entry says so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
… SDK tracker

Re-applies fork PRs #44 and #47 on this layer: twelve delta sequences
(unattributable and ambiguous deltas, blank names, typeValidation
'if-present' / 'required' / 'none', interleaved index and id reuse) with
what the pinned upstream StreamingToolCallTracker emits for each, per
delta, on error and on flush. The test drives aimux's tracker the same way
and compares.

scripts/gen-t3-tracker-fixture.mjs no longer hardcodes 5.0.53: it reads
the "@ai-sdk/provider-utils" pin from fixtures/aisdk/VERSIONS.json,
refuses any other installed version, and records the version and the
sha256 of src/streaming-tool-call-tracker.ts in `source`.

The fixture holds the 5.0.55 results. It was not regenerated by running
the script here (upstream code is not executed in this environment); the
5.0.53 recording was updated by reading the 5.0.55 source. Of the twelve
cases only two reach a path 5.0.55 changed (resolveMatchingToolCall with
several candidates and no explicit start, or the unseen-id path with a
named empty fragment). `ambiguous_delta_across_open_calls_is_dropped` is
unchanged: both open calls are incomplete, so it is still ambiguous
(streaming-tool-call-tracker.ts:305-308, :349-351).
`index_reuse_with_new_id` changes: the index-only continuation "2}" now
goes to call_b, the only call at index 0 whose structured arguments are
incomplete (:291-298, :345-347), so call_b ends as {"b":2} instead of
{"b":. Running the script against a node_modules holding 5.0.55 should
reproduce the file byte for byte.

The typeValidation per case follows the upstream callers:
@ai-sdk/openai@4.0.83 openai-chat-language-model.ts:541 ('if-present'),
@ai-sdk/groq@4.0.52 groq-chat-language-model.ts:393 ('required'),
@ai-sdk/openai-compatible@3.0.62 openai-compatible-chat-language-model.ts:555
(default 'none').

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
When a tool call is invalid, Core still fills `input` with the parsed
argument text when that text is valid JSON, as the AI SDK's catch-all does
(ai@7.0.127 src/generate-text/parse-tool-call.ts:112-133). That back-fill
used plain serde_json, while the main path rejects a `__proto__` key or a
`constructor.prototype` (contains_forbidden_prototype). Upstream uses
`safeParseJSON` for the back-fill too (:116-117), which goes through
`secureJsonParse` (@ai-sdk/provider-utils@5.0.53
src/secure-json-parse.ts:49-67), so such text stays the verbatim string.

The back-fill now applies the same check. Test:
invalid_call_input_is_not_back_filled_with_forbidden_prototype_keys covers
a known tool (rejected by the main path) and an unknown tool (NoSuchTool)
with both forbidden shapes; it fails without the change.

Closes #229 C48.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Replace the integration-branch atomic cutover in the alignment document
(header, section 0.4 Q3, D31, the section 0.7 ABI row, section 9.1 and
appendix I1) with layer-by-layer merges into master. Each layer is
CI-green with all its consumers updated, and nothing is released from
master until every layer is merged and the P2 gates pass; fixes for
released versions ship from a maintenance branch.

The new C ABI, codegen, the full recording schema 3 and the binding
migration come last, before the release. Until then the existing C
constructors keep their signatures and call the new factories, and ts-rs
stays as the interim generator (section 0.5 and the Part 2 generator
note). The Part 2 implementation order moves codegen, the new C ABI and
the full recording schema to the end.

ROADMAP defers the ops protocol schema and L0 passthrough until after
the migration and stops fixing C1 at nine exports.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…, Bedrock SigV4 and secret redaction (#248), roadmap status (#205), RFC-0033, RFC-0036 §12-§14, RFC-0039 to RFC-0043

The CI speed-up branch below this one has master merged in, so this brings
in master as well.

ROADMAP.md §4 conflicted: master rewrote it as "近期入口与依赖门" (#205),
and this branch had annotated the old item 2 (S1 + S2) as deferred. The
merge takes master's section as written; the size and performance gates
are reconciled with RFC-0043 in the next commit.

.github/workflows/ci.yml: the contract-tests and swift-contract-tests jobs
keep the "changes" gate and the master-only cache saving from below, with
this branch's steps (generator drift checks only; the Rust, Node and Swift
contract tests were removed here). The comments in the "changes" job now
use the jobs' new names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
#208 is merged as RFC-0043, which sets the gates without waiting for
the migration, and master's ROADMAP §4 (#205) lists S1 / S2 / P as near
term. This branch had deferred them until the migration was done and
until #208 was rewritten. S1, S2, S3 and P are no longer marked as
deferred in ROADMAP §2 and §3, the alignment doc §0.7 puts them back in
the unaffected row with a pointer to RFC-0043, and the list of the
maintainer's RFC drafts names RFC-0043 instead of RFC-0038. Each
migration layer's PR still lists artifact sizes before and after; the
0.9 capability matrix stays after the migration.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…rged RFCs

#207 merged RFC-0036 §12-§14 and asks that the alignment doc's matching
section be updated with this first layer and link back to the §12.2
replacement table. §0.7 now links RFC-0036 §12.2 in both places that
cited "#207 §12.2", and links the topic RFCs, all of them now merged
(RFC-0033, RFC-0036 §12-§14, RFC-0039 to RFC-0043).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
scripts/fetch-aisdk.test.mjs and scripts/aisdk-fixtures/versions.test.mjs
ran nowhere. Both are offline: the fetch test serves its own registry on
127.0.0.1, and the versions test reads local JSON. They now run in the
generator drift job, which already runs for every code change. The
lock-file test skips until scripts/aisdk-fixtures/package-lock.json
exists and then holds the lock to VERSIONS.json.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
RFC-0011, RFC-0013, RFC-0016 and RFC-0035 still pointed at
contract-tests/fixtures/wire-format.json, tool-call-repair.json and the
binding contract tests this layer deletes. Each gets a 后续变更 line
saying what no longer applies and what pins wire behaviour now, and
the changelog gets a Removed entry for the retired fixtures.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
aimux-core's #[ts(export)] derives are not feature-gated, and
TS_RS_EXPORT_DIR points at bindings/node/src/types/, so a plain cargo
test rewrites that tracked directory. scripts/gen_ts_types.py is the
source of truth; the debt goes away when ts-rs retires with D8.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…docs, the RFC-0036 §12.2 back-link and the review follow-ups

The alignment doc §0.7 conflicted where [2/8] extends the #185 row and
[1/8] rewrites the two rows after it; both changes are kept.
bindings/python/src/lib.rs: get_model_specs keeps this branch's optional
`source_url` signature and releases the GIL around the blocking call, as
#248 does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
openai-chat-language-model.ts forwards an error chunk as an error part
(:561-568) and still flushes the tool-call tracker before finish
(:656-670). Nothing pinned that order here: the existing mid-stream
error test streams text only. The new test streams a tool-call start,
an error frame and [DONE], and requires tool-input-start, -delta, error,
tool-input-end, the tool call with its incomplete raw arguments, then a
finish whose reason is error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
unique_id searched `generated-1, generated-2, ...` without a bound and
ended in `expect`. The search only runs when `generated` is one of the
n ids in use, so one of `generated-1 ..= generated-n` is free; it now
searches that range, as streaming-tool-call-tracker.ts:468-485 does,
and cannot panic. The ids it returns are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The only conflict was scripts/gen_ts_types.py. It keeps #247's
multi-target structure (TARGETS, per-target sync and check) and drops
the EXCLUDED set and manifest's exclude parameter: #246 deletes
bindings/node/src/types/ProviderName.ts, so nothing is left to exclude.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
aimux-core's #[ts(export)] attributes were ungated, so every plain
cargo test rewrote bindings/node/src/types through the TS_RS_EXPORT_DIR
in .cargo/config.toml. aimux-core now has the same ts-export feature as
tools/aimux-web (tools/aimux-web/Cargo.toml:21-23), and each of its 158
exports is #[cfg_attr(feature = "ts-export", ts(export))].

scripts/gen_ts_types.py enables the feature for the core target, and
--check also fails on any ts(export attribute outside that gate in either
crate's source, printing file:line. The .cargo/config.toml comment and the
web README now say that cargo test writes nothing and the generator is
the only writer. gen_ts_types.py --check passes: 159 node files and 8 web
files are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
StreamingToolCallTracker::new() used "tool-call" as its id generator, so
an OpenAI chat stream without wire ids produced tool-call, tool-call-1,
and so on. Upstream defaults to the random generator
(provider-utils@5.0.55 src/streaming-tool-call-tracker.ts:114) and uses
"tool-call" only when a generator returns a blank id (:461). new() now
defaults to generate_id (provider-utils@5.0.55 src/generate-id.ts), and
FALLBACK_TOOL_CALL_ID is only the blank-output fallback.

generate_id.rs is the same file a later layer of this stack adds, so the
restack has nothing to merge there. The tracker tests that assert ids pass
their generator explicitly; a new test checks that new() gives a call
without a wire id a 16-character id.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Stacks the series on #247 (which carries #246): aimux-core's ts-rs exports
run only under the ts-export feature that scripts/gen_ts_types.py enables,
and the generated ProviderName types are gone. Each later layer gates the
exports it adds in its own merge, so no layer leaves an ungated export.

Resolution: scripts/local-ci.sh keeps this layer's version without the
retired contract section; CHANGELOG keeps both sides, the ts-rs entry as
a Fixed section before Removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #247 (with #246) through the layer below: ts-rs exports run
only under the ts-export feature, and the generated ProviderName types
are gone.

Resolution: the aimux-core files keep this layer's code, and every
export in them, including the ones this layer adds (ToolChoice, the
tool and stream-part types), is gated as #[cfg_attr(feature =
"ts-export", ts(export))]; CHANGELOG keeps both sides.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #266 (a blank key on a custom base URL is sent as an empty
key, as the AI SDK does for apiKey: '').

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #266 (a blank key on a custom base URL is sent as an empty
key, as the AI SDK does for apiKey: '').

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #266 (a blank key on a custom base URL is sent as an empty
key, as the AI SDK does for apiKey: '').

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #266 (a blank key on a custom base URL is sent as an empty
key, as the AI SDK does for apiKey: '').

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa

Copy link
Copy Markdown
Contributor Author
eric 的意见 我们的处理 位置
OpenAI 这一族的流没有按厂商配置 tracker 这一层只有一个接线点,同时服务 OpenAI、Azure 和全部 registry 厂商。按厂商的配置在 [5/8] 和 [6/8] 补上,到 [8/8] 每个厂商的参数都和上游一致;不下移到这一层,是因为 [5/8] 会拆开这个共用函数。另外 tracker 的默认 id 生成器改成和上游一样的随机生成器,"tool-call" 只在生成器返回空串时兜底(e4104aaa) aimux-provider-utils/src/streaming_tool_call_tracker.rs:134
xAI 和 Mistral 的 chat 流在这一层还没接 tracker 迁移在 [5/8](#253),不是 [6/8]。Mistral 按 @ai-sdk/mistral 接 tracker,xAI 的 chat 由 openai_compatible 承担(@ai-sdk/xai 没有 chat 模型)。PR 描述已补一段说明 PR 描述
OpenAI 兼容流在流结束时的行为,以及首帧错误预读的范围 name 晚到的缓冲在 [5/8] 加入;"先 flush 工具调用,再报 error 并 finish(error)"和错误帧之后继续读在 [6/8] 加入,[8/8] 的顺序与上游 openai-compatible 3.0.62 一致。首帧预读只看第一条 SSE 事件,作为产品选择写在对齐文档第 5 节;上游只有 @ai-sdk/openai 读到第一个输出块,兼容包、groq、deepseek、mistral 都不预读 docs/aisdk-architecture-alignment.md 第 5 节
缺"error 帧之后照常 flush"的顺序测试 补了 tool_calls_are_flushed_after_an_error_frame,断言 Error、ToolInputEnd、ToolCall(保留不完整原文)、Finish(error) 的顺序(d0058715) aimux-providers/tests/provider_error_test.rs:343
unique_id 里的 expect 改成和上游一样的有界搜索,不会 panic(f0ee3026) aimux-provider-utils/src/streaming_tool_call_tracker.rs:400
VERSIONS.json 里 @ai-sdk/provider 还是 4.0.21(请确认) 有意分层:这一层只把 provider-utils 升到 5.0.55 以对齐 tracker 修复;@ai-sdk/provider 到引入 DecisionModelV4 的 [7/8] 才升 4.0.22,那次只改了 evaluate 到 decide 的命名,tracker 用到的类型没有变化 VERSIONS.json

Generated by Claude Code

claude and others added 4 commits October 16, 2026 11:18
The fetch test pointed npm's user and global config at the same empty
file. npm refuses that ("double-loading config ... as global, previously
loaded as user") and exits before any request, so on CI every test that
downloads failed with "npm pack failed". It passed in the cloud only
because the environment's NPM_CONFIG_USERCONFIG overrode the test's own
setting.

The test now writes two files, and drops inherited npm_config_* variables
in any case before setting its own, so it runs the same everywhere.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in 6ca248e: the fetch test gives npm separate user and global config files, so the AI SDK pin scripts step in the generator drift job passes on CI.

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
master 带入 #262/#246/#247 的 squash;三个冲突文件取本分支版本
(ci.yml 为已调和 #262 任务表 + web-typecheck + pin 脚本测试的超集),
gen_ts_types.py 自动合并。
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one
commit) through [1/8], whose master merge eric made (2a0b02b). The tree
is unchanged: this branch already had all three.

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
@eric8810
eric8810 changed the base branch from v4/1-ci-on-every-pr-and-test-fixture-cleanup to master October 17, 2026 12:49
master 带入 #249 的 squash;两个冲突文件取本分支版本
(对齐文档含本层 §0.7 增量;VERSIONS.json 为本层的 provider-utils 5.0.55 升级)。
@eric8810
eric8810 merged commit 19bb199 into master Oct 17, 2026
20 checks passed
eric8810 added a commit that referenced this pull request Oct 17, 2026
master 带入 #249/#250 的 squash;冲突文件取本分支版本
(层分支内容 = master 同内容 + 本层 messages/content 改写,为超集,
合并后净差异与层原始增量一致:268 files +14926/-10765)。
eric8810 added a commit that referenced this pull request Oct 17, 2026
master 带入 #249/#250/#251 的 squash;82 个冲突取本分支版本,
其中 3 个 node 类型文件按本层意图删除(Role/StreamResponse/
StreamTextResultAggregated 为 delete/modify 冲突,git rm)。
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.

3 participants