Repository navigation
[2/8] refactor!: tool calls follow the AI SDK - #250
Merged
Merged
Conversation
Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a0eb91-5e79-775e-975e-b85e2ac49ab6 (cherry picked from commit 7f68ac3)
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
Amp-Thread-ID: https://ampcode.com/threads/T-01a0eb91-5e79-775e-975e-b85e2ac49ab6 Co-authored-by: Amp <amp@ampcode.com> (cherry picked from commit dc1cd14)
…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
6 of 12 tasks
7 of 12 tasks
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
10 of 12 tasks
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
Contributor
Author
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 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
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 升级)。
9 of 12 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_choiceis 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 benull, 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
prepareToolChoicedoes; a provider tool id must be${string}.${string}; a V4 tool result is nevernull. 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, asparse-tool-calldoes. The 18 MoA and 22 converter tests are back.Type of change
Checklist
cargo fmt --all -- --checkpassescargo clippy --workspace --all-targets -- -D warningspassescargo test --workspacepasses (cassette playback — no network/keys)README.md,docs/,rfc/,bindings/README.md)Notes for reviewers
@ai-sdk/provider-utils5.0.55 streaming tool-call tracker;ai7.0.127prepare-tool-choice,parse-tool-call;@ai-sdk/provider4.0.21 tool types.{}.titleandtoolMetadata: they come from user-level tool definitions, and aimux has no user-level tool set. That needs a design decision first.openai/model.rs), which serves OpenAI, Azure, OpenRouter and every registry provider, so every vendor gets the same tracker settings here. The per-vendor settings follow the upstream packages in PR 5 ([5/8] refactor!: providers are AI SDK factories #253: Groq validatesRequired; DeepSeek, openai-compatible and Mistral passgenerate_id) and PR 6 ([6/8] fix: vendors follow the upstream packages #254: OpenAI validatesIfPresentwithgenerate_id). Moving them down to this layer would make the layers in between rewrite the same code twice.mistral/model.rs) emits everytool_callsdelta as a complete call, and xAI (xai/model.rs) accumulates byindexalone. So the provider-utils 5.0.55 parallel-call fix (Security and bug fixes needed on master #228 B10), id de-duplication and the missing-name error do not apply to them yet. Both move in PR 5 ([5/8] refactor!: providers are AI SDK factories #253): Mistral drives the tracker as@ai-sdk/mistraldoes, and xAI chat is served byopenai_compatible, as@ai-sdk/xaihas no chat model. No OpenAI-format stream is left outside the tracker after PR 5.CallOptions.tool_choiceisOption<LanguageModelV4ToolChoice>.into_call_optionsalways sets it throughprepare_tool_choice, which givesautowhen the caller sets none. Providers drop the choice when there are no tools, except Open Responses, which forwards it as upstream does.ExactMatcherdoes not match recordings made before this change (CHANGELOG).ProviderTool.idis aNamespacedKind; an id without a dot is rejected.ToolResult.resultofnullfails to deserialize.StreamingToolCallTracker; its argument-state modules are private.fixtures/aisdk/VERSIONS.jsonmoves@ai-sdk/provider-utilsfrom 5.0.53 to 5.0.55. The pinned set is now mixed, becauseai7.0.127 depends on provider-utils 5.0.53 exactly. 5.0.55 is adopted for the tracker port only;@ai-sdk/providerstays at 4.0.21, theai7.0.127 baseline, until PR 7 moves it to 4.0.22.cargo test --workspacepass;scripts/gen_ts_types.py --checkis clean. No binding sends or reads the changed types; the binding suites run in CI.dynamic, the TypedToolResult / TypedToolErrordynamicrows and the deferred-result precondition; the RFC-0035 revision line; README anddocs/api/gaps.md; CHANGELOG Breaking bullets.StreamingToolCallTracker::new()generates ids with the random generator, as provider-utils@5.0.55 does (src/streaming-tool-call-tracker.ts:114);"tool-call"is only the fallback when a generator returns a blank id (:461).ffi-x86_64-unknown-linux-gnuffi-aarch64-apple-darwinffi-x86_64-pc-windows-msvcnode-x86_64-unknown-linux-gnunode-aarch64-unknown-linux-gnunode-aarch64-apple-darwinnode-x86_64-apple-darwinnode-x86_64-pc-windows-msvcnode-aarch64-pc-windows-msvc