Skip to content

[6/8] fix: vendors follow the upstream packages - #254

Merged
eric8810 merged 287 commits into
masterfrom
v4/6-vendors-follow-the-upstream-packages
Oct 17, 2026
Merged

eric8810 merged 287 commits into
masterfrom
v4/6-vendors-follow-the-upstream-packages

Conversation

@cunninghamcard-bit

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

Copy link
Copy Markdown
Contributor

Requested by li

Summary

Before: after the move to factories, a number of vendor behaviours still differ from the pinned upstream packages, some upstream vendor features are missing, and several upstream-ported tests were deleted. User agents carry hard-coded versions. Azure's token decorator adds a bearer token on a cross-origin redirect hop. Anthropic on AWS defaults to us-east-1 and signs only when told to. Stream error statuses are invented in some places and missed in others. openai-compatible has no supportsMultiPartToolContent, Vertex has no MaaS or xAI sub-provider, openai_output reads logprobs only under openai, and OpenAI image edits fetch URL files without the download guard. The #90 endpoint regression test and a set of hand-written unit tests are gone.

After: factories, request settings, base URLs, empty keys and provider names follow upstream; vendor option schemas live in each package; the missing vendor features are ported. Each package sends its own user agent, with its version and the provider-utils version from its own package.json, both read from the pins. Stream error events become error parts with upstream's statuses, and API requests follow cross-origin redirects without credentials, Azure's token included. Anthropic on AWS reads its region, workspace id and auth as upstream does, and the AWS signers follow aws4fetch. Vertex gains the MaaS and xAI sub-providers, openai-compatible gains supportsMultiPartToolContent, OpenAI and Azure expose their provider tools as tools, and OpenAI image edits download URL files through the guard. Models declare the URLs their package passes through, so those are not downloaded. The upstream-ported model tests are kept, and the deleted hand-written tests that still cover live code are restored.

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 [5/8] 87e652c this PR 58572b3 change vs [5/8] change vs master
ffi-x86_64-unknown-linux-gnu 3.77 MB 4.67 MB 4.99 MB +307.1 KiB (+6.73%) +1191.6 KiB (+32.41%)
ffi-aarch64-apple-darwin 3.12 MB 3.88 MB 4.13 MB +250.9 KiB (+6.63%) +990.9 KiB (+32.54%)
ffi-x86_64-pc-windows-msvc 3.03 MB 3.76 MB 4.04 MB +277.9 KiB (+7.57%) +994.8 KiB (+33.67%)
node-x86_64-unknown-linux-gnu 55.30 MB 62.84 MB 65.30 MB +2406.5 KiB (+3.92%) +9769.8 KiB (+18.09%)
node-aarch64-unknown-linux-gnu 53.25 MB 60.43 MB 62.74 MB +2257.1 KiB (+3.83%) +9261.9 KiB (+17.81%)
node-aarch64-apple-darwin 15.06 MB 18.33 MB 19.34 MB +982.3 KiB (+5.49%) +4173.7 KiB (+28.37%)
node-x86_64-apple-darwin 13.60 MB 16.60 MB 17.51 MB +888.0 KiB (+5.48%) +3818.5 KiB (+28.76%)
node-x86_64-pc-windows-msvc 10.57 MB 13.20 MB 13.97 MB +744.1 KiB (+5.77%) +3319.3 KiB (+32.17%)
node-aarch64-pc-windows-msvc 9.87 MB 12.40 MB 13.13 MB +721.5 KiB (+5.96%) +3184.4 KiB (+33.03%)

@cunninghamcard-bit cunninghamcard-bit changed the title [V4 6/8] fix: vendors follow the upstream packages fix: vendors follow the upstream packages Oct 12, 2026
@cunninghamcard-bit cunninghamcard-bit changed the title fix: vendors follow the upstream packages [6/8] fix: vendors follow the upstream packages Oct 12, 2026
claude added 26 commits October 13, 2026 02:48
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
The AI SDK V4 interfaces carry `specificationVersion: 'v4'` on
`ProviderV4` and on every model interface, so the traits keep it too
(issue #230, B4):

- `Provider` and the model traits (language, embedding, image,
  transcription, speech, reranking, video, search, files, skills,
  decision) have a default `specification_version()` returning "v4";
  `&P` forwards to the provider it points to. Wrappers keep the default,
  as the upstream wrappers report 'v4' themselves.
- check_provider_boundaries.sh no longer forbids the method.
- The architecture alignment doc and the CHANGELOG no longer describe it
  as removed.
- The Anthropic and Google files tests check it again, and a new test
  checks providers from `create_provider` and from a registry, with
  their language models.

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
The comment claimed alignment with `LanguageModelV4CallOptions.timeout`,
but V4 call options have no timeout (@ai-sdk/provider 4.0.21
language-model-v4-call-options.ts:8-138). The type is the object form of
the AI SDK's `TimeoutConfiguration` (ai 7.0.127
src/prompt/request-options.ts:13-22), a request option of generateText /
streamText (`RequestOptions.timeout`, :126). Note the upstream keys aimux
does not carry (`toolMs`, `tools`, the bare-number `totalMs` form).

Closes #230 B20.

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
Remove designs from the alignment document that were never implemented
and are no longer planned:

- LanguageModelRef, ResolvedModel and RegistryModelReference. Model
  references follow upstream: an operation takes a string model ID,
  resolved through a registry, or a model instance; named methods come
  from the registration's extensions. The recording target keeps the
  string ID, and live replay asks for an explicit target when there is
  none.
- Registered sidecar metadata and its propagation through resolution,
  scope and wrap. Trace metadata comes from an explicit audit family or
  an injected descriptor index, and catalogue joins use explicit
  package/model links.
- The SessionInferer trait and registration API. Session inference keeps
  the existing opt-in prefix implementation in aimux.
- Session affinity middleware. Callers pass session headers per request
  through headers, as RFC-0019 describes.

Section 1.2 rulings and the appendix entry that pointed at these designs
now state what aimux does instead.

The size and performance gates (S1-S3, P, the CLI budget) and the
capability matrix move out of the alignment document to ROADMAP as work
after the migration. Their thresholds wait for the rewrite of #208
(RFC-0038); until then each migration layer's PR lists artifact sizes
before and after.

Section 9.1 gains a known-gaps list of the agreed designs the migration
layers do not implement, one line each.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
This layer added `ToolCallNotFoundForApproval` and dropped the variant-set
test instead of updating it. Restore the exhaustive match and the wire-tag
list for the current 17 variants, add the new variant's golden payload, and
pin its message against the AI SDK's ToolCallNotFoundForApprovalError
(ai 7.0.127 src/error/tool-call-not-found-for-approval-error.ts:22).

Closes #232 N31-7.1-1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
This layer deleted the 17 router tests that the tool-call layer still had
(origin/v4/2-tool-calls-follow-the-ai-sdk:aimux-core/src/router.rs). Bring
them back on the current result types (`GenerateResult.request` /
`.response`, `StreamResult.request` / `.response`): routing strategies,
fallback on error, no fallback for streams, child retries without
rerouting, a single composite run when every child fails, and
step-labelled child recording contexts.

Router is an aimux extension with no AI SDK counterpart, so these pin
aimux behaviour (RFC-0021) rather than upstream behaviour.

Closes #232 B23 (router half).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The AI SDK wraps only an error thrown by the repair function in
ToolCallRepairError (ai@7.0.127 src/generate-text/parse-tool-call.ts:90-96).
A repaired call is parsed with doParseToolCall (:103-106); when that throws,
the outer catch builds the invalid call from the original call and carries
the repaired call's own InvalidToolInputError / NoSuchToolError (:112-133).
Core wrapped that second failure in ToolCallRepair { original_error, cause }.
It now returns the original call, invalid, with the replacement's error, for
the closure path and for a host's `repaired` reply alike. This is the
parse/repair change the layer plan takes from layer D2 (fb0f79e).

Because that error carries the replacement's argument text, the
OpenAI-compatible output and the host-repair reconstruction no longer take
the error's text blindly: `original_tool_call_text` uses it only when it
back-fills to the call's own `input`, and falls back to `input` otherwise.

Tests: an_invalid_repair_is_not_repaired_again now expects the unwrapped
InvalidToolInput; a_repaired_reply_that_fails_validation_is_not_wrapped
covers the host path; invalid_repair_keeps_original_arguments_with_original_name
still shows the original arguments in both OpenAI outputs.

RFC-0035 takes the hunks from the doc corrections: the `repaired` row and
the control-flow sentence say the failure is not wrapped, and the
pre-evaluated `input_schema` and Swift's delayed abort are registered as
differences (line references checked against ai 7.0.127). Binding doc
comments that promised a ToolCallRepair error here are corrected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Re-applies fork PR #14 (RFC-0042 §3.2 vectors), keeping only the rows
whose result equals ai@7.0.127 parseToolCall:

- a provider-executed dynamic call with no tool set is parsed by
  parseProviderExecutedDynamicToolCall
  (src/generate-text/parse-tool-call.ts:199-224) with safeParseJSON
  (@ai-sdk/provider-utils@5.0.55 src/parse-json.ts:89);
- a JSON string stays a string, scalars and null parse, a repeated key
  keeps its last value, and ` 1e2 ` is 100 (compared by value, since
  serde_json keeps it as a float);
- unparseable text is an invalid call whose error and input keep the
  verbatim text (:112-133).

Dropped: the row expecting empty text to be invalid. Upstream parses
blank text as {} (:202-205). The fork's ignored/pending vectors are gone.

Also keeps duplicate_key_text_survives_in_the_error: the error's
toolInput is the original text (:258-263).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The retry wrapper's two texts put `getErrorMessage(error)` after "Last
error:" (@ai-sdk/provider-utils@5.0.53 retry-with-exponential-backoff.ts:97,
:103, :138; @ai-sdk/provider@4.0.21 errors/get-error-message.ts). For an API
failure that is `APICallError.message`, which never carries the HTTP status.
Aimux composed the text from `AiMuxError`'s Display, giving "Last error: API
call error: HTTP 429: ...".

The text now uses the attempt's own message: `ApiCallError.message` for an
API failure, the variant's message otherwise. JavaScript's `Error.toString()`
also prefixes the class name (`AI_APICallError: `); Aimux errors have no
class name, so only the message part is reproduced.

Issue item N31-6.4-1. Tests: retry::tests::retry_error_messages_match_the_ai_sdk,
aimux-ffi retry error test now pins the exact text.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Eight `max_retries` comments said `None` meant "provider default" or "the
model default", the per-provider retry design the alignment document
withdraws (docs/aisdk-architecture-alignment.md D14). They now name the AI
SDK's `maxRetries` and its default of 2 (ai@7.0.127
util/prepare-retries.ts), and say that a model's `retry_config()` can still
set another count until the layer that removes model-level retry settings
lands.

Issue item N4.5-1 (code comments).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The AI SDK types provider options, provider metadata and provider
references as JS objects (`@ai-sdk/provider@4.0.21`
shared-v4-provider-options.ts:24, shared-v4-provider-metadata.ts:24,
shared-v4-provider-reference.ts:20), which keep their keys in insertion
order. `SharedProviderOptions` and `SharedProviderMetadata` become one
alias, `NamespaceMap = IndexMap<String, JsonObject>`, and
`SharedProviderReference` becomes `IndexMap<String, String>`, so a value
serializes the way it was built or parsed. The inner objects already kept
order through serde_json's `preserve_order`.

The JSON shape and the generated TypeScript are unchanged; Rust callers
that built these maps from a `HashMap` now build an `IndexMap`
(`NamespaceMap::new()` / `::from`). `VideoOperationStatus` allows
clippy's large-variant lint: the larger map pushes `VideoResult` over the
threshold, and boxing would only add an allocation per poll.

The alignment document's Part 2 sketch and inventory row now describe the
same aliases (#229 NP2-1).

Tests: shared::tests::provider_options_keep_insertion_order_through_serde,
shared::tests::provider_reference_keeps_insertion_order_through_serde.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Upstream keeps poll pacing on the operation: `generateVideo` takes
`poll: { intervalMs, timeoutMs }` with defaults 5000 / 600000
(ai@7.0.127 generate-video/generate-video.ts:55-80, :575-576), and
`VideoModelV4` has no poll settings (@ai-sdk/provider@4.0.21
video-model/v4/video-model-v4.ts). `VideoModel::poll_config()` and
`VideoPollConfig` are removed; the poll loop reads `VideoCallOptions::poll`
and falls back to the upstream defaults. RunwayML's config-level
`poll_interval` / `timeout` go with it (there is no @ai-sdk/runwayml
package; the per-call `poll` option replaces them).

Each status check, retries included, now races the time left in the poll
budget as upstream does (generate-video.ts:585-622): when the budget runs out
the pending request is dropped and the call fails with the polling timeout
text "Video generation timed out after {timeoutMs}ms." (:579-581). Before,
only the gap between checks was bounded, so a hanging status GET or a long
retry delay ran past the poll timeout.

Issue items: #230 C19, #229 U5(a).
Tests: video_model::tests::a_hanging_status_check_is_bounded_by_the_poll_timeout,
status_retries_race_the_remaining_poll_timeout,
poll_pacing_defaults_to_the_ai_sdk_values.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Rust's f64 parser accepts `inf`, `Infinity` and `NaN`, and `as i64`
saturates infinity, so `retry-after-ms: Infinity` produced an i64::MAX
hint. The core retry loop dropped it through its window check, but the
public hint (FFI `aimux_error_retry_ms`, the Node and Python `retry_ms`)
reported it. A non-finite value, including seconds that overflow once
converted to milliseconds, is now no hint, as upstream never uses it either
(ai@7.0.127 util/retry-with-exponential-backoff.ts:47-55 keeps a delay only
when `0 <= ms && (ms < 60000 || ms < exponentialBackoffDelay)`).

Carries the master fix 13fe482 for #228 N2 to this layer.
Test: error::tests::non_finite_retry_after_is_no_hint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Closes #229 C39. `CallOptions.tool_choice` is now
`LanguageModelV4ToolChoice`, the object union of @ai-sdk/provider@4.0.21
src/language-model/v4/language-model-v4-tool-choice.ts:1-5
({type:'auto'|'none'|'required'} | {type:'tool', toolName}). The
user-level `ToolChoice` (ai@7.0.127 src/types/language-model.ts:102-106)
stays on `GenerateTextOptions`.

`prepare_tool_choice` is the AI SDK's prepareToolChoice
(ai@7.0.127 src/prompt/prepare-tool-choice.ts:4-15): no choice is
{type:'auto'}, a string becomes {type:<string>}, a tool choice keeps its
toolName. `into_call_options` always sends the prepared choice, as
generateText / streamText do on every step
(src/generate-text/generate-text.ts:953-955, :1048;
stream-text.ts:2382-2384), so a call without tools now carries
{type:'auto'} too. Every provider except open-responses already drops the
choice when there are no tools; open-responses forwards
`toolChoice?.type` as upstream does
(@ai-sdk/open-responses@2.0.58
src/responses/open-responses-language-model.ts:310, :379-380).

Providers match on `LanguageModelV4ToolChoice`. Replay maps the
recorded V4 choice back to the user-level one when it re-runs a call;
recordings now store the object form, so ExactMatcher no longer matches
recordings made before this change (CHANGELOG).

Tests: tool_choice_test (v4_tool_choice_is_an_object_union,
v4_tool_choice_rejects_the_user_level_string_form,
prepare_tool_choice_follows_the_ai_sdk,
a_call_without_a_choice_carries_auto_even_without_tools),
open_responses_test tool_choice_is_sent_without_tools. Generated TS:
LanguageModelV4ToolChoice.ts, CallOptions.ts, ToolChoice.ts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The AI SDK's `ReasoningPart` is `{ type, text, providerOptions? }`
(`@ai-sdk/provider-utils@5.0.53` types/content-part.ts:107), and
`toResponseMessages` replays reasoning as
`{ type: 'reasoning', text, providerOptions: part.providerMetadata }`
(`ai@7.0.127` generate-text/to-response-messages.ts:62). A provider's
signature stays in its own namespace.

`ContentPart::Reasoning.signature` is removed, with
`extract_reasoning_signature`, which copied `anthropic` / `bedrock` /
`amazonBedrock` signatures out of the metadata into it. The prompt
conversion already ignored the field, and the providers read the signature
from `provider_options`, so nothing that reaches a provider changes. Input
JSON that still carries `signature` is accepted and the key is ignored
(#229 B8).

Test: response_messages::tests::reasoning_signature_is_replayed_only_in_provider_options.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Closes the tool part of #229 NP2-2. `LanguageModelV4ProviderTool.id` is
`${string}.${string}` (@ai-sdk/provider@4.0.21
src/language-model/v4/language-model-v4-provider-tool.ts:17), so
`ProviderTool.id` is now `types::NamespacedKind`: a string newtype whose
constructor and deserializer reject text without a `.`. Like the
template literal type, it admits empty segments ("x.", ".y").

The same type is meant for the custom content `kind`
(language-model-v4-custom-content.ts:13, language-model-v4-prompt.ts:138),
which is the rest of NP2-2 and belongs to the messages and content layer.

The JSON stays a plain string; the generated TS type is
`NamespacedKind = \`${string}.${string}\``.

Tests: tool_wire_test (a_provider_tool_id_needs_a_namespace,
a_namespaced_kind_is_any_text_with_a_dot).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Closes #229 NP2-6. `LanguageModelV4ToolResult.result` is
`NonNullable<JSONValue>` (@ai-sdk/provider@4.0.21
src/language-model/v4/language-model-v4-tool-result.ts:23), so
`tool::ToolResult.result` rejects JSON `null` on deserialize. Every other
JSON value, falsy ones included, is kept. The generated TS type stays
`JsonValue` (the doc comment states the rule).

Test: tool_wire_test a_tool_result_is_never_null.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The tool-layer part of #229 C41, which is documentation and one test; the
code for the rest lands with the core content and stream layers.

Alignment doc, core output section:
- the parsed tool-call example has no `dynamic` key: a static call omits
  it (ai@7.0.127 src/generate-text/parse-tool-call.ts:279-288);
  test a_static_call_serializes_without_dynamic pins that;
- TypedToolResult / TypedToolError carry `dynamic?: false` when static and
  `dynamic: true` when dynamic (src/generate-text/tool-result.ts:20, :35;
  tool-error.ts:16, :30);
- a deferred provider tool result (input undefined,
  convert-language-model-content.ts:105-138) is allowed only for a
  provider tool with `supportsDeferredResults`; otherwise "Tool call …
  not found" (:96-103).

Not done here: the core `tool-error`, `tool-output-denied` and
`tool-approval-response` parts belong to the core ContentPart /
TextStreamPart (C42, C43); `title` and `toolMetadata` come from the
user-level tool definition (@ai-sdk/provider-utils src/types/tool.ts),
which aimux does not have yet; deferred results go with NP2-3.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Closes the MoA part of #232 B23: the 18 tests in moa.rs that layer 1 had
(fan-out and aggregation, per-field usage, best-effort and fail-fast
references, child retries without replaying the fan-out, aggregator
stream setup retry, stream error relay, non-text part forwarding,
reference tool stripping, a router as a reference) were dropped by the
tool-call refactor. They are back, adapted to this layer's types:

- reference tool stripping now clears the optional
  `LanguageModelV4ToolChoice` instead of resetting it to Auto;
- the aggregator's streamed tool call is a provider-layer
  `StreamPart::ToolCall(RawToolCall)` whose input is the raw text, and is
  checked to pass through unchanged.

The router tests are the other half of B23 and belong to the messages
layer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
claude added 10 commits October 16, 2026 10:27
Brings in the CHANGELOG wording from [4/8]: only generate_text checks a
tool result without its call; stream_text passes it on, as streamText does.

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 the CHANGELOG wording from [4/8]: only generate_text checks a
tool result without its call; stream_text passes it on, as streamText does.

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
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: rfc/0029-web-console.md and tools/aimux-web/README.md keep
this layer's text, which already says what #266 says and adds which
providers cannot take a custom base_url and where Replay sends.

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 的意见 我们的处理 位置
Mistral 的 model_length 映射成 Length,缺 fixture 直测 补了 model_length_is_a_length_finish,非流式和流式各断言一次 Length、raw 保留原值。映射和上游 mistral 4.0.54 map-mistral-finish-reason.ts:9-11 一致(a6935483) aimux-providers/tests/mistral_upstream_e2e_test.rs
OpenAI chat、Groq、DeepSeek 发回非对象参数的编码缺回归测试 新增测试:一条 assistant 消息带六种历史工具调用(null、字符串、数组、数字、布尔、对象),断言三家实际发出的 arguments(bd2d041f) aimux-providers/tests/non_object_tool_input_test.rs
Anthropic 的 rawInvalidInput 要上游出处并补测试 出处是 anthropic 4.0.71 convert-to-anthropic-prompt.ts:1668-1673 的 toAnthropicToolInput,和 Bedrock 同一规则。测试 anthropic_wraps_non_object_input_as_raw_invalid_input 在同一个新文件里;RFC-0042 第 6.2 节改成 Anthropic 和 Bedrock 都包 rawInvalidInput(bd2d041f) 同上;rfc/0042-*.md 第 6.2 节
PR 描述里 "Aligns to" 少写了版本 描述已补成 15 个厂商包加 3 个基础包,和 VERSIONS.json 的 18 个 pin 一一对应 PR 描述
(不是 eric 提的,核对时我们发现的)computer toolset 成员调用的往返 和上游一致了:input 原样发回,缺 action 时发出以 toolset 命名的调用并给警告(007ad306) aimux-providers/tests/anthropic_toolset_test.rs
(不是 eric 提的,核对时我们发现的)内置工具参数校验 上游校验的 10 个内置工具参数,现在 Anthropic、Vertex、Bedrock 共用一份校验,不合格的在发请求前以 InvalidArgument 失败(a9a97fdc) aimux-providers/src/anthropic/prepare_tools.rs、aimux-providers/tests/anthropic_provider_tool_args_test.rs

Generated by Claude Code

claude and others added 15 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
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
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
…ings

#252 took max_retries, timeout and session_id off CallOptions and made
them operation settings, but RFC-0015, RFC-0016, RFC-0017, RFC-0023,
RFC-0024, RFC-0029 and RFC-0041 still named them as CallOptions fields.

Each of those RFCs gets a later-change line in its header: the three are
set on GenerateTextOptions, generate_text and stream_text put them in
OperationSettings, and code below the model interface reads them with
OperationSettings::current(), as the AI SDK keeps maxRetries and timeout
on generateText and not in LanguageModelV4CallOptions; recordings still
write them under the old keys. The passages that describe the current
shape are rewritten (RFC-0015 §5 and §8, RFC-0017 §2.4, the RFC-0023 §1
table, RFC-0024 §1.2 and §3.1, RFC-0029 §5.4, the RFC-0041 §10.2 row);
dated implementation records keep the field where it was then.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in 38b6cde: the RFCs that named max_retries, timeout and session_id as CallOptions fields now say they are operation settings.

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 38b6cde: the RFCs that named max_retries, timeout and session_id as CallOptions fields now say they are operation settings.

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
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
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one
commit) through [3/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
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one
commit) through [4/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
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one
commit) through [5/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/5-providers-are-ai-sdk-factories to master October 17, 2026 14:03
master 带入下方各层的 squash;冲突为 squash 不对称,取本分支版本
(层分支内容 = master 同内容 + 本层改写),删除型按本层意图 git rm。
@eric8810
eric8810 merged commit 951ef05 into master Oct 17, 2026
20 checks passed
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