Skip to content

[3/8] refactor!: messages and content follow the AI SDK - #251

Merged
eric8810 merged 110 commits into
masterfrom
v4/3-messages-and-content-follow-the-ai-sdk
Oct 17, 2026
Merged

eric8810 merged 110 commits into
masterfrom
v4/3-messages-and-content-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: core content, provider options, provider metadata and the provider prompt each have aimux-specific shapes, and vendors read options as untyped JSON. User messages are a role plus one content-part enum shared by every role, with no message-level provider options; reasoning parts carry a top-level signature; text results reuse a content shape generic over the provider layer. Provider namespace maps are unordered, and a custom content kind is any string. The prompt goes to the model without upstream's checks, so a tool call with no result or a response that breaks the tool choice is not reported. The router unit tests and the error variant-set test are gone, and a scratch MIGRATION-NS.md sits in the repository root.

After: user messages are the AI SDK's ModelMessage: a union by role, each role with its own content parts and message-level providerOptions, including tool approval responses and the deprecated tool output items (accepted with a deprecation warning). generate_text and stream_text standardize the prompt as upstream does: empty messages are an invalid prompt, consecutive tool messages are combined, a tool call without a result fails with MissingToolResults, and a response that breaks the tool choice fails with ToolChoiceViolation. Text results carry the AI SDK's core ContentPart (11 kinds, including tool errors and approval requests and responses), separate from the V4 provider content. There is one source, file and reasoning type per layer, and reasoning has no top-level signature. Provider options and metadata have one typed, insertion-ordered shape that each vendor package builds and reads; a non-object option is an error and unknown keys no longer leak. The provider prompt is modelled by role with one file part, using upstream's hyphenated part tags, and references, files and tool output convert as upstream does. A custom content kind must be ${string}.${string}, and CallOptions no longer carries a call id. FFI, bindings and their generated types follow; the router and variant-set 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

  • Position: base master, with PR 1 ([1/8] chore: CI on every PR and test fixture cleanup #249) and PR 2 ([2/8] refactor!: tool calls follow the AI SDK #250) merged in; next is PR 4 ([4/8] refactor!: results and streams follow the AI SDK #252).
  • Supersedes: [v4/p3-messages] refactor(core)!: messages, content, provider options and prompts follow the AI SDK V4 types #237, which GitHub closed when its branch was renamed to match the title.
  • Replaces: part of Draft refactor(core)!: align public core types with the AI SDK V4 shapes #219.
  • Aligns to: @ai-sdk/provider-utils 5.0.53 types/ (model messages and content parts; unchanged in 5.0.55); ai 7.0.127 prompt/convert-to-language-model-prompt, prompt/standardize-prompt, generate-text/content-part, to-response-messages; @ai-sdk/provider 4.0.21 shared provider options, metadata and references.
  • Closes: V4 alignment: core types, stream parts, results and call pipeline vs ai 7.x #229 B8, C31, C32, C36, C37, C38, C42, NP2-2 (custom content kind); V4 alignment: provider factory, HTTP and auth #230 B20; V4 alignment: public JSON, tools and lost tests #232 B7, B23 (router tests), N31-7.1-1.
  • In part:
  • Fork fixes included: none.
  • Not in this PR:
    • V4 alignment: core types, stream parts, results and call pipeline vs ai 7.x #229 C38, two upstream checks: system messages inside messages are still accepted, and instructions stays a string. Both would change the JSON that the bindings, CLI and web console send, so they wait for the binding migration.
    • Dedicated binding error classes for the two new errors also wait for the binding migration. Until then the C ABI reports them as AIMUX_E_INVALID_PROMPT and AIMUX_E_INVALID_RESPONSE_DATA, and Node and Python raise InvalidPromptError and InvalidResponseDataError.
    • tool-output-denied is only a stream part upstream, so it comes with the stream parts in PR 4. So do provider tool results with no matching call (NP2-3) and the stream tool error for an invalid call.
    • title and toolMetadata: aimux has no user-level tool set (as in PR 2).
    • Only the deprecated tool output items are converted here; URL downloads for user files come in PR 5.
  • Behaviour changes (breaking):
    • content::ContentPart, MessageContent and Role are replaced by UserContentPart, AssistantContentPart, ToolContentPart and their content types. The user tool result carries a typed output; the earlier result / is_error JSON is still read.
    • GenerateTextResult.content is Vec<ContentPart> (the core part). TextContent is gone in Rust; Python keeps the name for the new union.
    • ReasoningOutput has no signature, and extract_reasoning_signature is removed. Signatures live in each provider's own namespace.
    • SharedProviderOptions and SharedProviderMetadata are one NamespaceMap (IndexMap<String, JsonObject>); SharedProviderReference is ordered as well.
    • convert_to_language_model_prompt takes the provider id as a third argument.
    • CallOptions.call_id is removed; the id comes from the recording context. CallOptions::for_step is now crate-private.
    • New errors MissingToolResults and ToolChoiceViolation, mapped in the FFI, Node and Python.
  • Checks: fmt, clippy and cargo test --workspace pass; scripts/gen_ts_types.py --check is clean; the Node binding type-checks with tsc. The other bindings treat result content as opaque JSON; their suites run in CI.
  • Merged RFC / docs: NamespaceMap order and aliases (NP2-1); C32 line refs; C37 deprecation notes; C38 ToolChoiceViolation, TOOLDEF-3 line refs and empty tool input {}; part 2 Q1 scope note, the §3.5 speech and transcription tracing note, and the tool-call acceptance row; docs/api/node.md, docs/api/rust.md; CHANGELOG Breaking bullets.
  • Restored tests: the trace layer's tests were deleted in an earlier cut of this PR because they constructed the old result types; bfd9fdd restores all 10 on the new types (the time-to-first-output test now checks the value instead of matching null) and adds 4: the call id comes from the recording context on the success, stream and error paths, body/id/headers come from the result, a file part counts as the first output, and the raw usage object is kept.
  • Size: release artifacts before and after (CI artifacts, zip size):
CI artifact (zip) master a8e3a99 [2/8] 496ef24 this PR 35711cb change vs [2/8] change vs master
ffi-x86_64-unknown-linux-gnu 3.77 MB 3.78 MB 4.01 MB +232.0 KiB (+6.29%) +243.0 KiB (+6.61%)
ffi-aarch64-apple-darwin 3.12 MB 3.13 MB 3.31 MB +183.2 KiB (+6.00%) +190.5 KiB (+6.26%)
ffi-x86_64-pc-windows-msvc 3.03 MB 3.03 MB 3.21 MB +176.5 KiB (+5.96%) +181.0 KiB (+6.13%)
node-x86_64-unknown-linux-gnu 55.30 MB 55.60 MB 57.58 MB +1938.5 KiB (+3.57%) +2232.3 KiB (+4.13%)
node-aarch64-unknown-linux-gnu 53.25 MB 53.55 MB 55.42 MB +1834.4 KiB (+3.51%) +2121.4 KiB (+4.08%)
node-aarch64-apple-darwin 15.06 MB 15.11 MB 15.91 MB +778.4 KiB (+5.28%) +823.7 KiB (+5.60%)
node-x86_64-apple-darwin 13.60 MB 13.63 MB 14.36 MB +713.4 KiB (+5.36%) +743.5 KiB (+5.60%)
node-x86_64-pc-windows-msvc 10.57 MB 10.59 MB 11.21 MB +604.8 KiB (+5.85%) +623.7 KiB (+6.04%)
node-aarch64-pc-windows-msvc 9.87 MB 9.90 MB 10.48 MB +569.4 KiB (+5.89%) +592.7 KiB (+6.15%)

cunninghamcard-bit and others added 21 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)
…ow the AI SDK V4 types

- Content: one source, file and reasoning type per layer. A source is a
  url source or a document source; file citations and grounding documents
  map to document sources. The missing upstream content variants and usage
  fields are added.
- Provider options and provider metadata have one typed shape. Vendor
  packages build and read them; a provider option that is not an object is
  an error, unknown keys no longer leak, and option parse errors surface for
  Cohere, Google and Mistral. Bedrock reads amazonBedrock before bedrock.
- The provider prompt is modelled by role with one file part. Part tags use
  upstream's hyphenated names; references, files and tool output convert as
  upstream does, and file parts keep upstream's media-type and text-data
  rules.
- The #226 decision adapters and Jev follow the typed options, metadata and
  split usage types.
- Hand-written tests that built provider prompts by hand or pinned
  Rust-only behaviour are deleted; ported upstream tests are restored.
  FFI, bindings and their generated types follow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
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
Since messages follow the AI SDK V4 types, a reasoning part keeps its
signature in provider_options.anthropic.signature, as the Node test already
does. The Go round-trip test still sent the removed top-level `signature`, so
no thinking block reached the wire.

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

The content field was added to StreamTextResultAggregated and to its generated
fromJson, but not to the constructor, so lib/types.g.dart did not compile and
every test file importing the types failed to load.

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

Deleting the hand-written typed GenerateTextResult test left
_TypedArgs.maxOutputTokens and .temperature without a caller, which flutter
analyze reports as unused_element_parameter warnings and fails the job.

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 3/8] refactor!: messages and content follow the AI SDK refactor!: messages and content follow the AI SDK Oct 12, 2026
@cunninghamcard-bit cunninghamcard-bit changed the title refactor!: messages and content follow the AI SDK [3/8] refactor!: messages and content follow the AI SDK Oct 12, 2026
The file is a working note for the provider options / metadata namespace
migration and says on line 3 that it must not be committed. Nothing in
the tree links to it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
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
claude added 17 commits October 15, 2026 13:33
stream_text's pump stops polling at the terminal part and drops the
provider stream, so TraceRecordingStream never saw end of stream for a
completed call. Its Drop then traced every completed stream_text call as
"stream dropped before completion" and the LCP lookup was skipped.

The wrapper now writes the record when Finish passes, with Finish's
usage. A natural end of stream without Finish and a drop mid-flight are
recorded as before, and a stream that is drained after Finish is still
recorded once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
cc5446f cut the TraceLayer half of cache_probe_test.rs only because it
stopped compiling after the Usage and GenerateResult type changes. The
ten tests come back on the current API: generate and stream records with
usage, TTFT and session chain; part errors, transport errors and failed
calls with their error text and counts; an early drop still recorded;
the LCP upper bound in aggregates; scope keys isolating providers that
share a model id; the R-2.2 route-affinity suppression and the C4-2
independent-evidence case.

The TTFT check now reads the value; the deleted test matched
"ttft_ms": in the JSONL, which a null also matches. Four new tests pin
the call id taken from the recording context, body id and headers read
from the result, a file part counted as first output, and the raw usage
object kept on the record.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Under an enabled recorder, generate_text and stream_text through a
TraceLayer trace each call under its recording's id, and replaying that
recording sends the call the model saw.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
cc5446f deleted the three Finding 1 tests when the file variants became
tuple variants, leaving their two Gemini image cassettes unreferenced.
They come back on the current types: one generated PNG with its full
base64 payload, text and image kept in order, and the streamed file part.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
cc5446f deleted the four in-crate request-replay tests, so
replay_with_model had no test at this layer. They come back on the
current types: a recording is rebuilt and resent, overrides apply, bad
input options fail with a parse error, and differing provider options
miss the exact matcher.

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
Brings in the tool call tracker's default generate_id. No conflicts.

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 #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 to the message,
content, result and stream types, is gated.

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
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 输出适配的四组端到端测试被删,没有替代(阻塞) 五个测试原样恢复,只把 mock 换成当前的结果类型;另加原文保真矩阵(非法 JSON、JSON 字符串、null、带空白、未知工具,各有无 input delta,非流式和流式)(7a726e33) aimux-core/tests/openai_output_tool_input_test.rs
trace 层回归套件被整组删除,而 call_id 来源恰好改了(阻塞) 按新接口恢复全部 10 个测试,另加 4 个:call_id 取自录制上下文(成功、流式、失败三条路径)、body/id/headers 取自结果、文件部件算首个输出、raw usage 原样保留(bfd9fddd)。恢复时发现并修了一个问题:正常结束的 stream_text 被 trace 记成"流提前丢弃"并跳过 LCP 审计,现在在 Finish 到达时记录(4b212066) aimux-core/tests/cache_probe_test.rs
metadata 保真缺测试 加了测试:未知 namespace、空 namespace、多层嵌套 null,经文本和流式推理两条路径写回 providerOptions 后和原值完全一致(88ce267f) aimux-core/src/response_messages.rs 的测试模块
Gemini inlineData 的测试和 cassette 三个测试恢复,两个 cassette 不再是孤儿(62e29675) aimux-providers/tests/data_loss_regression_test.rs
FFI 的组合、帧错误、会话输入测试 恢复(750f9a0b) aimux-ffi/src/lib.rs 的测试模块
ToolResultPartJson 还在写旧的 result / is_error,请写明何时移除 这是给绑定旧 JSON 的私有兼容,[8/8] 已经删掉读写两侧,ToolResultPart 只有 output(272a70b2) [8/8] aimux-core/src/content.rs
reasoning 的 provider_metadata 像是被吞了 没有被吞;那个 #[allow(unused)] 是 master 遗留的属性,[8/8] 已删 —
疑问:上游用户层 tool-result 还接受 result / isError 吗 不接受。provider-utils 5.0.55 的 content-part.ts:218-241 里用户层 ToolResultPart 只有 output;result/isError 只在 provider 层的 LanguageModelV4ToolResult —
疑问:call_id 改来源后,录制回放链路有没有自动化覆盖 有。新增 trace_recording_correlation_test.rs,在全局录制开启时经 TraceLayer 跑 generate_text 和 stream_text,断言 trace 用的就是录制的 id、能按 id 取到录制、回放时发出同样的请求(6faea338);四个 replay_with_model 单测也恢复了(9fd89058) aimux-core/tests/trace_recording_correlation_test.rs

Generated by Claude Code

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

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

复核通过,撤回修改请求。逐项核实:openai_output_tool_input_test.rs 恢复(5 个原测试 + 原文保真矩阵)✓;trace 层 10 个测试按新接口恢复并新增 4 个(call_id 取自录制上下文的三条路径、结果取值、文件部件、raw usage)✓;恢复过程中发现并修复"正常结束的 stream_text 被记为流提前丢弃、跳过 LCP 审计"的真实缺陷(4b212066)——这正说明该套件值得恢复 ✓;metadata 未知 namespace/嵌套 null 保真测试、Gemini inlineData 测试与 cassette、FFI 测试、trace_recording_correlation_test.rs(录制回放链路自动化)全部落地 ✓;ToolResultPartJson 旧写侧在 [8/8] 删除、上游 tool-result 语义疑问已答(用户层仅 output)✓。批准。

eric8810 and others added 3 commits October 17, 2026 09:27
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
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one
commit) through [2/8]. 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/2-tool-calls-follow-the-ai-sdk to master October 17, 2026 13:14
master 带入 #249/#250 的 squash;冲突文件取本分支版本
(层分支内容 = master 同内容 + 本层 messages/content 改写,为超集,
合并后净差异与层原始增量一致:268 files +14926/-10765)。
@eric8810
eric8810 merged commit bdf66d6 into master Oct 17, 2026
20 checks passed
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