Skip to content

[11/13] refactor: provider config is plain data - #259

Merged
eric8810 merged 409 commits into
masterfrom
v4/11-provider-config-is-plain-data
Oct 17, 2026
Merged

eric8810 merged 409 commits into
masterfrom
v4/11-provider-config-is-plain-data

Conversation

@cunninghamcard-bit

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

Copy link
Copy Markdown
Contributor

Requested by li

Summary

Before: each model's private config carries the AI SDK's provider callbacks as Rust closures: a url() closure that builds the endpoint, a headers() closure that adds auth and fixed headers, endpoint closures for Vertex, Bedrock and Polly, supported-URL closures, and a provider-name match that picks the Alibaba or Moonshot AI usage shape.

After: package-private configs hold plain data, and where a shared layer needs a vendor's behaviour, the shared layer defines a small trait and the vendor implements it in its own module.

  • Inside one package: base URLs are enums (OpenAIUrl, MessagesUrl, AnthropicEndpoint, the compatible BaseUrl::Preset); provider headers are ProviderHeaders / EndpointHeaders.
  • Across packages: shared defines ResolveEndpoint (with HttpEndpoint for the common case); the Vertex Gemini, Bedrock and Polly endpoints implement it in vertex/, bedrock/ and aws_polly/. openai_compatible defines ResolveBaseUrl and ResolveFetchHeaders; the Vertex partner sub-providers implement them in vertex/. Google's supported-URL patterns are passed in as a function.
  • Usage: the Alibaba and Moonshot AI converters live with their registry rows in preset.rs and reach the chat model through the existing convert_usage hook, chosen by the row's profile.convert_usage rather than the provider name.
  • ChatSettings::error_structure stays a capturing Arc<dyn Fn>, the same shape as convert_usage.
  • SigV4Fetch::with_clock takes a fn pointer.

scripts/check_provider_boundaries.sh gains a rule: code in shared/ and openai_compatible/ may only name the shared layers, preset and runtime_decision, so vendor modules added later are caught too. Special auth stays composed inside each vendor package (D11). Public settings and public JSON do not change.

This re-derives the earlier "provider config is plain data" layer of the RFC-0036 stack (branch rfc-0036/provider-config) onto the migrated code, keeping what the project decided to keep: compat transformRequestBody, metadataExtractor, supportsMultiPartToolContent and specification_version.

Type of change

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

Refactor of private internals.

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; [10/13] ([10/13] refactor: tool-call repair is a trait #258) is merged, and this is the next PR of the Rust-adaptation series.
  • Direction: structure only. Every public setting, request, error and JSON field is unchanged, including the camelCase provider-config JSON (baseUrl, with base_url still accepted), the by-name provider entries, the C ABI, the bindings and the generated TS types.
  • Dependencies point one way: no shared → vertex/bedrock/aws_polly/google or openai_compatible → vertex imports; the first version of this PR had them and the boundary rule now forbids them.
  • Kept caller closures: transform_request_body, generate_id, and the compatible supported_urls, convert_usage, error_structure and metadata_extractor.
  • Not ported from the old layer, because those parts change behaviour or the public API: the by-name/FFI config path, a feature commit, the removal of transform_request_body, the narrowing of generate_id, and the rewrite of the compatible public settings.
  • Edge case: PresetDescriptor gains a pub(crate) field. Code outside the crate could previously build one with a struct literal; it is built from the registry JSON in practice. Noted in CHANGELOG.
  • Docs: CHANGELOG and docs/contributing/adding-a-provider.md updated.

Verification

Check Result
cargo test --workspace 3194 passed, 0 failed, 7 ignored: the same per test binary as #258, plus the new SearXNG and DataForSEO env tests (the ts-rs export tests run only under the ts-export feature since [1/8])
Upstream fixture replays ({openai,anthropic,google,openai_compatible}_factory_test) unchanged
Test edits decision_support.rs native_decision_url_passes_endpoint_to_url_callback builds its config with ProviderHeaders and OpenAIUrl::Base (the url closure it overrode no longer exists); a_lazy_base_url_is_evaluated_per_request_and_its_error_fails_the_request uses a preset whose base URL comes from an env var. New: searxng_test.rs instance_url_comes_from_searxng_url_per_request covers the BaseUrl::Env path (not in the request differential: search providers have no cassettes); the DataForSEO EndpointHeaders::Basic path is covered by the existing do_search_sends_correct_basic_auth_header, and dataforseo_test.rs login_comes_from_dataforseo_login_per_request covers the login read from DATAFORSEO_LOGIN per request (missing: LoadApiKey, no request sent)
Boundary rule reports all 8 vendor references on the first version of this PR; passes now
Old vs new, request by request #258 against #260 (this PR and the next together): 2,542 cassettes plus all 282 registry presets replayed through every public language-model route with a recording transport. 20,173 requests (URL, headers, body), 8,464 generate results and 2,497 errors compared: 0 differ once the process-wide source-N / id-N counters of the Responses code are normalised. Not covered: cassettes for non-language-model endpoints, response bodies over 256 KiB, and search, speech, transcription, image and video providers (no cassettes). The probe (one test file copied into both worktrees, plus a diff script) is not committed; it can be shared on request
Gate fmt, clippy, rustdoc -D warnings, gen_ts_types.py --check, gen_providers_doc.py --check, check_provider_boundaries.sh pass on stable 1.99

@cunninghamcard-bit
cunninghamcard-bit marked this pull request as ready for review October 13, 2026 07:20
claude added 28 commits October 13, 2026 09:16
The V4 layer writes schema 3 with an identity-only ProviderRecord and
replays live from the host registry (decision recordings for any
provider). A call that lost an event or was marked inconsistent is
written with complete: false, and the sensitive headers include
x-prodia-key and *subscription-key. Until the V4 layers merge, the RFC
body still describes master.

Refs #232.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
SpeechResult gains usage: Option<JsonObject>, as SpeechModelV4Result's
usage?: JSONObject (@ai-sdk/provider@4.0.22
src/speech-model/v4/speech-model-v4-result.ts:23-27). Google speech
passes its usageMetadata through unchanged and leaves usage unset when
the response has none or null (@ai-sdk/google@4.0.87
src/google-speech-model.ts:356-358, google-speech-api.ts:35). The other
vendors report none. Telemetry of speech usage is not part of this
change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Supported question types are a property of the model, as upstream
DecisionModelV4.supportedQuestionTypes (@ai-sdk/provider@4.0.22
src/decision-model/v4/decision-model-v4.ts:11), so the recording input
no longer stores input.decision_capabilities and replay no longer reads
it. The live recording fixture drops the field; the replay mock accepts
every question type and size and takes rounding and the probability
source from the recorded result. Recording schema stays 3.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Upstream `createOpenResponses` wraps its headers in
`withUserAgentSuffix(.., options.userAgentSuffix ??
"ai-sdk-open-responses/" + VERSION)` (open-responses-provider.ts:114-127),
and postJsonToApi then appends the provider-utils part of the
provider-utils 2.0.58 depends on. aimux sent no package part.

`@ai-sdk/open-responses` 2.0.58 is now pinned: VERSIONS.json carries the
registry `dist.integrity` (sha512-VF48lc...), which equals the sha512 of
the registry tarball, whose contents equal the extracted sources the
port was checked against. The recorder's package.json, package-lock.json
(regenerated with `npm install --package-lock-only`, which only adds this
entry) and PINNED_ONLY list it too. aisdk_pins gains OPEN_RESPONSES
(2.0.58, provider-utils 5.0.53, its package.json dependency), covered by
the existing drift tests.

The provider sends `ai-sdk-open-responses/2.0.58` through the package
transport, and `OpenResponsesProviderSettings::user_agent_suffix` replaces
the package part as `userAgentSuffix` does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Upstream parses `choices[].logprobs.content` with a zod schema of
`{token, logprob, top_logprobs: [{token, logprob}]}` (openai-chat-api.ts
:71-87, chunk :159-175), which strips `bytes` and any other key, and sets
`providerMetadata.openai.logprobs` only when `content` is not null
(openai-chat-language-model.ts:459-461 generate, :613-615 stream). aimux
copied the raw list, `bytes` included, and set `logprobs: null` for a
null `content`.

Both paths now go through `logprobs_content`, which keeps exactly the
schema's keys. `to_chat_completion` therefore returns `"bytes": null` for
an OpenAI chat result, and `round_trip_with_logprobs` expects that.
DeepSeek's own schema keeps `bytes` and is unchanged.

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

Merges fx3/p3 (layer 3 at ed0e436, which already contains layers 1 and 2).

Conflicts resolved keeping both sides:
- Step content is layer 3's core ContentPart: StepResult.content and the
  text result accessors use ContentPart, tool_results return
  TypedToolResult, files come from the File { file, .. } part, and reasoning
  converts to AssistantContentPart (no signature).
- response_messages.rs is layer 3's builder (tool message sorted by tool
  call order, tool_error), plus this layer's is_deep_equal_data approval
  filter and its tests, typed_tool_result and approval_response for the
  stream path. The C47 tool-message sorting is not added a second time.
- generate_text keeps this layer's step pipeline and gains layer 3's tool
  choice check (generate-text.ts:1155-1176) and the invalid-call tool error
  after the content, built with this layer's invalid_tool_call_error.
- stream_text checks the tool choice on the finish chunk: the step finishes
  with an error finish reason (raw reason kept) and an error part follows
  (stream-language-model-call.ts:644-687). A consumed stream records tool
  results, tool errors and approval responses where they arrive
  (stream-text.ts:1529-1538, :1648-1650).
- TextStreamPart::Custom.kind is a NamespacedKind, as StreamPart::Custom.
- apply_tool_call_repair_to_result works on the typed steps and also
  rewrites or drops the invalid call's tool error and its tool-message
  result.
- The restored RFC-0026 OpenAI output, MoA and router tests are ported to
  TextStreamPart, ResponseInfo and the text() accessor; the AiMuxError
  variant-set test includes NoOutputGenerated; layer 3's content and
  tool-choice tests read the step content and the FinishStep / Finish parts.
- FFI, Node and Python error mappings keep NoOutputGenerated and add
  ToolChoiceViolation to the response-data code / class.
- MIGRATION-NS.md stays deleted; generated TS regenerated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
google-vertex's Node MaaS and xAI wrappers spread the resolved provider
headers over `init.headers` in their fetch, after postToApi built the
user-agent (google-vertex-maas-provider-node.ts:43-49,
google-vertex-xai-provider-node.ts:41-47; provider-utils
post-to-api.ts:100-104). A provider-level `user-agent` therefore leaves
nothing of the package part or the `ai-sdk-provider-utils` part. aimux
layered it over the per-call headers but the helper layer still appended
`ai-sdk-provider-utils/5.0.51`.

`CompatModelConfig::request_headers` now also returns the fetch headers'
`user-agent`, and `with_transport` sends it through a per-request
transport that sets it as the whole header after the helper layer. The
headers are still resolved once, at the config level, so a header or
token error keeps its type. `transport_user_agent` leaves a header with
no provider-utils part and no suffix exactly as it is, so the package
transport after it does not touch the caller's value.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
… replay (U5, K1, K7, C45, C47)

- §3.5: object generation is the deprecated wrapper over `output`
  (generate-object.ts:120, stream-object.ts:190); the separate object spans
  are withdrawn.
- §4.5 / §9.3: async jobs follow generateVideo's start/status model, the
  caller's idempotency key and the poll budget (generate-video.ts:520-536,
  :576-625); doGenerate-only models are retried whole as upstream does
  (generate-image.ts:206, transcribe.ts:243).
- §5: per-batch idempotency keys (product difference, :338-372), stream
  retries and the first-chunk timeout's start (not aligned yet,
  stream-text.ts:374, :2228-2243).
- Part 2: the result convenience fields are deprecated upstream
  (generate-text-result.ts:52-160, stream-text-result.ts:148-330); the
  response-message table gains the empty-reasoning, invalid-input,
  inputSchemaInput, tool-message order and tool-error encoding rows
  (to-response-messages.ts:43, :93-94, :122, :133-146, :203, :220-265); the
  deferred-result paragraph records aimux's provider-tool approximation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Upstream `downloadBlob` calls `fetchUntrustedUrl({url, abortSignal})`
with no headers (download-blob.ts:106-142, fetch-untrusted-url.ts:46-86),
so nothing adds a user-agent; only `getFromApi` and `postToApi` append
`ai-sdk-provider-utils/<v>` (get-from-api.ts:77-90). The OpenAI-compatible
image model downloads URL files with `downloadBlob(file.url)`
(openai-compatible-image-model.ts:241-244). aimux routed that download
through `get_from_api`, which sent `ai-sdk-provider-utils/5.0.53`.

aimux-provider-utils gains `download_blob`: a guarded GET with no caller
headers and no provider-utils part (an internal flag on the prepared
request skips the append). The OpenAI-compatible image model uses it.
Validated `get_from_api` downloads (Luma, Replicate, fal, Recraft, BFL,
Gladia; upstream `getFromApi({validateUrl: true})`) keep the part.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Upstream reads a URL image file or mask with `downloadBlob(file.url,
{abortSignal})` (openai-image-model.ts:166, :357), which goes through
`fetchUntrustedUrl` (provider-utils download-blob.ts:106-142): the target
is validated, redirects are checked hop by hop, and no headers are sent.
aimux fetched it with a plain `get_from_api` on the default transport, so
a private-network URL was fetched and `ai-sdk-provider-utils/5.0.53` was
sent.

`image_file_to_bytes` now calls `download_blob(url, abort_signal)`. A
non-public target is refused before any request, as upstream; a failed
download is a status-code error. CHANGELOG has a Breaking (security)
bullet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Upstream `GoogleFiles.uploadFile` starts the resumable upload and sends
the bytes with the provider's plain `fetch`, not `postToApi`
(google-files.ts:84-92, :118-126), so neither request gets the
`ai-sdk-provider-utils/<v>` part that postToApi appends
(post-to-api.ts:100-104). The init request carries the provider headers,
whose user-agent is `ai-sdk-google/4.0.87` (google-provider.ts:258-269);
the byte upload carries only its two `X-Goog-Upload-*` headers, so no
user-agent. aimux sent the part on both.

`HttpRequest` gains a public `omit_provider_utils_user_agent` flag, which
sends the user-agent as given (or none). The Google upload sets it on both
requests (the upload stays validated, deliberately stricter than
upstream), and `download_blob` now sets it too instead of its private
prepared-request tweak. The poll stays a `getFromApi` and keeps the part.

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

`CallOptions` loses `max_retries`, `timeout` and `session_id`. The V4 call
options have none of them (@ai-sdk/provider language-model-v4-call-options.ts:8-138);
`maxRetries` and `timeout` are request options of generateText /
streamText (ai prompt/request-options.ts:104-127), reported on the
operation's start event (generate-text-events.ts:54-62).

`GenerateTextOptions` keeps all three, so the JSON bindings are unchanged.
`generate_text` and `stream_text` run their model calls inside
`operation::OperationSettings::scope`, a task-local scope. The router and
MoA take the child retry count from `OperationSettings::current()`, the
trace layer takes the session id from it, and the recorder writes the
three settings into `input.options` under the keys they had, so replay
restores them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings [6/8] (and the [5/8] and [1/8] work it merged) ahead of [7/8],
so [8/8] can start while [7/8] is finished. Conflicts: ApiCallError
keeps [8/8]'s optional status_code serialization with [6/8]'s doc text;
xAI keeps [8/8]'s realtime web_socket with [6/8]'s package fetch;
ApiCallError.ts keeps [8/8]'s shape (regenerated later in this layer).

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

Merges fx3/p4 (layer 4 at 822def8, which contains layers 1 to 3). Layer 5
already had layer 1; layers 2 and 3 arrive through layer 4.

Conflicts resolved keeping both intents:
- Retry: layer 5 deleted RetryConfig and retry_config(), so the core
  operations, the router and MoA call prepare_retries(max_retries, abort)
  with the operation's count (OperationSettings, layer 4's C7). The MoA
  test's retry_config() assertion is dropped.
- CallOptions has neither layer 4's max_retries / timeout / session_id nor
  layer 5's body_overrides. Replay restores the three operation settings
  from the record; InputRecord's doc names neither call_id nor
  body_overrides.
- trace/layer.rs keeps layer 5's with_scope_name and layer 4's session id
  from the operation scope.
- VideoModel has neither poll_config() (layer 4's C19) nor retry_config()
  (layer 5). RunwayML keeps layer 5's factory settings and loses its poll
  constants; its tests pass poll per call (fast_poll).
- AiMuxError has both LoadApiKey / LoadSetting and MissingToolResults /
  ToolChoiceViolation; the FFI, Node and Python mappings carry all four.
- Provider code keeps layer 5's factories and exchange helpers, with layer
  3's IndexMap namespace maps (anthropic and google files) and layer 2's
  LanguageModelV4ToolChoice imports. Layer 5's removals (groq tools and
  chat option parsing in openai/convert.rs, the xAI chat converter) stay
  removed.
- provider-utils keeps layer 5's sigv4_fetch and drops the retry module;
  the tracker's argument state stays private as layer 2 made it.
- Layer 5 removed the hand-synced web types; the files layers 2 to 4 added
  there are not kept, and the console imports TextStreamPart from
  @aimux/types.
- Bindings and docs: layer 4's result and stream shapes with layer 5's
  RequestInfo / ResponseInfo (Swift), deepseek mocks (Node, Go) and the
  removed body_overrides. CHANGELOG: layer 4's "All bindings" section goes
  before "Added".
- fixtures/aisdk/VERSIONS.json takes layer 2's provider-utils 5.0.55 entry.

Compile fixes for the merged code follow in separate commits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Layer 5 removed the model-level retry settings and added the LoadApiKey /
LoadSetting errors; layer 4 brought tests and comments written against
the old sets.

- The video test that races status retries against the poll budget set a
  60 s delay through retry_config(). The failed status check now carries
  `retry-after-ms: 30000`, which the retry honours exactly below 60 s, so
  the delay does not depend on the jittered backoff.
- The router test no longer asserts retry_config() of the router.
- The error variant-set test lists LoadApiKey and LoadSetting, and builds
  NoSuchProvider with its layer 5 fields.
- The max_retries comments of the call options and the composite child
  count say that None retries twice, the AI SDK default, without the
  retry_config() clause.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Provider code that layer 5 added or rewrote compiled against the old
types. After the merge:

- The tool choice of a model call is LanguageModelV4ToolChoice (layer 2,
  C39): the Anthropic, Groq, DeepSeek and OpenAI-compatible tool
  preparation and their tests. Each still drops the choice when no tool
  is sent.
- Namespace maps are IndexMaps (layer 3, NP2-1): OpenAI chat options,
  OpenAI-compatible chat metadata, the Anthropic skills reference and the
  speech and Hugging Face tests. The Anthropic options rewrite uses
  shift_remove, so the other namespaces keep their order. Imports of
  HashMap that became unused are removed.
- Provider tool ids are NamespacedKind (layer 2, NP2-2) in the Groq tests.
- Tests read GenerateTextResult through its accessors (layer 4, C44) in
  the DeepSeek test, and the factory tests map fixture roles from strings,
  since layer 3 removed Role.
- The ElevenLabs speech test gets back the HashMap import its helper uses.
- rustfmt on the video poll test of the previous commit.

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

The merge took layer 2's `@ai-sdk/provider-utils` entry in
fixtures/aisdk/VERSIONS.json (5.0.55, the tracker baseline), while the
recorder's package.json and package-lock.json still pinned 5.0.53, so the
check that the lock pins the same integrity as VERSIONS.json could not
pass. package.json now pins 5.0.55 and the lock is regenerated with
`npm install --package-lock-only --ignore-scripts`; nothing else in the
tree changes.

The pinned set stays mixed, as layer 2 recorded: ai 7.0.127 and the
@ai-sdk/* packages depend on provider-utils 5.0.53 exactly, so the lock
nests 5.0.53 under each of them, and 5.0.55 nests @ai-sdk/provider
4.0.23. The top-level provider-utils is 5.0.55 with the same integrity as
VERSIONS.json.

`node --test scripts/fetch-aisdk.test.mjs
scripts/aisdk-fixtures/versions.test.mjs`: 12 passed, none skipped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
aimux's Decision API had its own shape: a question list with ids, Choice
options and Score levels, a capability struct, refusals and skips as
answers, and extension fields on the call and the result. The AI SDK now
has DecisionModelV4 (@ai-sdk/provider@4.0.22 src/decision-model/v4/) and
decide (ai@7.0.128 src/decide/decide.ts). This commit makes aimux's
Decision that contract, in one step, with no transitional code (D31). It
folds N5, N6, N7, C29 and N8 of layer 7.

Model and operation (N5, C29):
- One DecisionModel per decision-model-v4.ts:6-16: specification_version
  ("v4"), provider, model_id, supported_question_types and
  do_decide(&DecisionCallOptions). Call options, questions, answers and the
  result follow decision-model-v4-call-options.ts:7-14,
  decision-model-v4-question.ts and decision-model-v4-result.ts:7-56.
- decide follows decide.ts:76-185: validate the input
  (validate-decision.ts:50-125), reject unsupported question types before
  any I/O with the DecisionUnsupportedQuestionTypeError message
  (decision-unsupported-question-type-error.ts:18-31), retry (default 2),
  validate the answers once after the retries (validate-decision.ts:176-298,
  tolerance 1e-6 and rounding decimals 0..15), complete total_tokens only
  when both counts are known and default the response timestamp and model.

Provider lookup (N7):
- Provider::evaluation_model and the EvaluationModel trait are removed.
  Provider::decision_model returns Option<Result<Arc<dyn DecisionModel>>>,
  None by default. The registry and custom_provider have one decision
  lookup and report None as NoSuchModel("decisionModel")
  (provider-registry.ts:447-461, custom-provider.ts:241-258). The
  deprecated evaluationModel / doEvaluate aliases are not ported.
- EvaluationLanguageModel is DecisionLanguageModel, with the default
  provider "{language provider}.decision" and the response format name
  "decision" (provider-utils 5.0.55 decision-language-model.ts:24, :158).
  Google's decision_model is that adapter ("google.evaluation",
  google 4.0.87 google-provider.ts:438-441); OpenAI's stays the native
  Decisions API, and OpenAIProvider::evaluation builds the Responses
  adapter ("openai.evaluation", openai 4.0.83 openai-provider.ts:355-358).

API (N6):
- No capability struct. Limits are checked in do_decide before HTTP.
- Request extensions move to provider_options.<provider>: images, OpenAI
  choice_values (typed native values) and safety_identifier, runtime
  protocol. Result extensions move to provider_metadata.<provider>:
  probability_source, per-answer confidence, OpenAI typed values, Laya
  abstentions, latency_ms.
- An OpenAI refusal or a vLLM skipped question fails the decision; a V4
  decision answers every question.
- Recordings hold the redacted DecideOptions and the DecideResult, with no
  capability snapshot; mock replay returns the recorded result.
- FFI: aimux_decide takes DecideOptions and returns DecideResult;
  aimux_decision_capabilities keeps its export and returns the model's V4
  properties. Bindings: capabilities() is describe() (Node
  describeDecisionModel, Python describe_decision_model).

Fixtures (N8): the decision fixtures and the live Jev recordings were
converted to the V4 shape by a deterministic script. Re-decoding both live
Jev wire responses through the new adapter gives the converted results
(decision_recording_test).

Tests pinned to upstream: decision_test (questions_have_the_v4_shape,
invalid_input_is_rejected_before_the_model_is_called,
unsupported_question_types_fail_before_the_model_is_called,
transient_failures_are_retried_twice_by_default,
answers_are_validated_once_after_the_retries,
the_result_completes_usage_and_response,
invalid_answers_report_the_upstream_messages, ...); custom_provider_test
(decision_model::*, decision_*, registry_reports_missing_decision_models);
decision_language_model_test
(provider_defaults_to_the_language_provider_with_a_decision_suffix,
errors_use_the_upstream_decision_messages,
the_language_model_call_joins_the_decision_recording); provider tests for
OpenAI, Google, Jev and the runtimes; FFI, Node and Python tests.

Docs: docs/api/decision.md describes the V4 API; the alignment doc §3.1,
§3.5, §5 and §9.1 (decide telemetry is a known gap); revision lines on
RFC-0037 and RFC-0038; CHANGELOG Breaking entries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
- CHANGELOG: layer 4's video bullet named RunwaymlConfig, which layer 5
  replaced with RunwaymlProviderSettings; it now says RunwayML no longer
  paces its own polling. Layer 5's single-modality bullet no longer says
  that VideoModel::poll_config() stays, since layer 4 removed it (#230
  C19).
- Alignment doc §5: the video poll pacing row becomes an upstream-aligned
  row. Neither the provider nor VideoModel has poll settings, and
  VideoCallOptions.poll overrides the AI SDK defaults of 5000 ms / 600000
  ms per field (ai 7.0.127 generate-video/generate-video.ts:543-544), as
  §8.1's delete list already says.
- Generated TS (scripts/gen_ts_types.py): AiMuxError gains LoadApiKey and
  LoadSetting next to MissingToolResults and ToolChoiceViolation;
  CallOptions and GenerateTextOptions have no body_overrides; the
  max_retries comments lose the retry_config() clause; the InputRecord
  comment no longer names body_overrides.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The AI SDK's JSON carries a `Uint8Array` as a base64 string
(`@ai-sdk/provider-utils@5.0.53` uint8-utils.ts:12-23) and reads one
with `atob`'s forgiving decode (:6-10). The public JSON wrote bytes as
arrays of numbers and read either form. Now:

- `ContentPart` image `image` and file `data` (`DataContent`,
  types/content-part.ts:42, :75) are base64 strings
  (`wire::base64_bytes`).
- `FileBytes`, `AudioData`, `AudioInput`, `AudioChunk`,
  `ImageFileData` and `VideoFileData` (the SDK's `Uint8Array | string`,
  shared-v4-file-data.ts:7-10, speech-model-v4-result.ts:16) serialize
  `Binary` as base64 and `Base64` verbatim, and parse a string as
  `Base64` (`wire::base64_wire_enum!`). `ImageOutputs` is an array of
  base64 strings (image-model-v4-result.ts:23), video `binary` data a
  base64 string (video-model-v4-result.ts:28-29). An array of numbers
  is rejected. `FileBytes` equality compares bytes across the two forms.

Bindings keep their byte-valued APIs and encode at the JSON boundary:
Python `Base64Bytes` (bytes are encoded), Go `Bytes` (base64 only, read
as atob reads it), Java `Types.Base64Bytes` and field serializers,
Kotlin `Base64Bytes` / `Base64ByteListSerializer`, Swift `Base64Bytes`,
Dart `encodeBase64Bytes` / `decodeBase64Bytes`. Generated TypeScript
types are `string`. docs/api byte notes and CHANGELOG are updated.

Tests: aimux-core ai_sdk_json_test (prompt_bytes_are_base64_strings,
file_bytes_are_one_base64_string, model_result_bytes_are_base64_strings),
wire::tests::base64_matches_btoa_and_atob; Node e2e image prompt and
speech audio; Python test_image_bytes_are_base64_on_the_wire and speech;
Go TestE2E_SpeechResultParsing; Java promptAndFileBytesAreBase64Strings
and the serializer tests; Kotlin FileBytes/content part tests and speech;
Swift and Dart tests updated (not run here: no toolchain).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
`DecisionImage.data` is `FileBytes`, which the previous commit puts on
the wire as one base64 string, as the AI SDK writes inline image bytes
(`ImagePart.image: DataContent`, `@ai-sdk/provider-utils@5.0.53`
types/content-part.ts:42; uint8-utils.ts:12-23). This pins that shape
for decisions and makes the providers read the string the same way the
wire does: `inline_image` encodes binary with `wire::encode_base64` and
validates a string with the forgiving `wire::decode_base64` (atob rules,
uint8-utils.ts:6-10), sending the string on verbatim as
`convertToBase64` does.

Python `decide` encodes `bytes` image data to base64 before building
the JSON; the other bindings take the options as JSON and the Node type
is `string` already. docs/api/decision.md and CHANGELOG say so.

Tests: aimux-core ai_sdk_json_test::decision_image_bytes_are_a_base64_string;
aimux-providers decision_support::tests::inline_image_reads_bytes_as_the_ai_sdk_does;
Python test_openai_images_native_values_and_score_descriptions[as_bytes].

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

The user operations now download where the AI SDK's do, through a
transport the caller can replace (D26). The new public `download` module
has `Download` (one URL, the `download` option of `transcribe` and
`generateVideo`), `create_download(max_bytes)` (the guarded download,
`createDownload`), `DownloadFunction` (the prompt download, the AI SDK's
`experimental_download`) and `create_default_download_function`.

- generate_text / stream_text: inside the operation, before the model
  call, the prompt's file URLs (user FileUrl parts and `file` items of
  tool outputs; not data: URLs, assistant file parts or deprecated
  file-url items) are planned against the model's `supported_urls()`
  (`SupportedUrls::is_supported`, provider-utils `isUrlSupported`) and
  passed to `GenerateTextOptions::download`; downloaded files are sent
  inline (ai 7.0.127 convert-to-language-model-prompt.ts:36-55,
  :444-540, :567-578; generate-text.ts:963-971). The downloads count
  against the operation timeout and observe the abort signal.
- transcribe: new `AudioInput::Url`, downloaded once with
  `TranscriptionCallOptions::download` before the retries
  (transcribe.ts:56, :109-117, :192-199); the downloaded media type
  replaces the given one. Transcription models return
  `AudioInput::url_error()` for a URL.
- generate_video: each video returned as a URL is downloaded with
  `VideoCallOptions::download` and returned as `VideoData::Binary`, with
  upstream's media type order minus byte sniffing (generate-video.ts:136,
  :253-261, :383-410).

Without a download each operation uses the guarded download. Provider
video tests inject a recording download. Docs, CHANGELOG and the
generated TS types follow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The AI SDK's JSON omits undefined and keeps null. Fields that are
`unknown` upstream read an explicit `null` as a present value and write it
back, and are omitted only when they were missing: response and request
`body` (language-model-v4-generate-result.ts:41, :56 and the speech,
transcription, embedding, reranking and search results),
`APICallError.data` (errors/api-call-error.ts:18) and the tool approval
request's `inputSchemaInput` (tool-approval-request.ts:41).

`aimux_core::wire::present` is the `deserialize_with` for those
`Option<Value>` fields: `None` is missing, `Some(Value::Null)` is null.

Bindings:
- Python serializes without `exclude_none`; a field that was set to
  `None` and is typed `Any` is written as `null`.
- Go reads the result bodies as `json.RawMessage`.
- Kotlin adds `JsonValuePresentSerializer`.
- Swift adds a `decodeIfPresent(JSONValue.self)` overload that keeps null.
- Dart adds `hasBody` / `hasInputSchemaInput`.
- Java's `JsonNode` fields already read null as `NullNode`; a test pins it.

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

The Breaking entry for 52bd723, left out of that commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Merges fx3/p5 (layer 5 at 06bb478, which contains layers 1 to 4).

Conflicts resolved keeping both intents:
- Tool choice: layer 2's LanguageModelV4ToolChoice in the bedrock,
  deepseek, groq, google, openai-compatible, OpenAI Responses and xAI
  helpers, with this layer's code: Google sends ANY (no VALIDATED) for
  required and a named tool, xAI selects the last tool of a name, and the
  Responses tools go through prepare_responses_tools_for.
- Namespaces: layer 3's IndexMap maps (SharedProviderMetadata::new()) in
  the openai-compatible chat metadata, which keeps this layer's order
  (empty namespace, extractor, prediction tokens). The Responses
  compaction part lives in provider_events as this layer moved it.
- Google image: this layer's delegation to the Gemini language model; the
  Imagen helpers layer 3 retyped stay deleted.
- OpenAI: layer 5's base_url_setting (an invalid OPENAI_BASE_URL fails each
  request) with this layer's supported_urls and file_id_prefixes on the
  model config. Anthropic: layer 5's options-based stream core (raw
  chunks) with this layer's custom options key, JSON response tool and
  citation documents.
- openai-compatible: layer 5's async SupportedUrlsFn; the chat model reads
  the dialect's function, else the profile's URLs. Presets: layer 5's
  owned descriptors with this layer's user_agent and the alibaba /
  moonshotai usage conversion.
- mistral and xAI export their base URL and key variable as layer 5 made
  them; DEFAULT_NAME stays removed.
- provider-utils exports download_blob next to layer 5's same_origin.
- openai_output keeps this layer's provider-namespace logprobs lookup and
  takes layer 5's finish_reason_to_openai; the stray first line of the
  chat_logprobs doc is gone.
- Recorder pins: provider-utils 5.0.55 at the top as VERSIONS.json has it,
  plus this layer's vendor and open-responses pins; package-lock.json was
  regenerated with npm --package-lock-only (each package keeps its own
  provider-utils, nested).
- Docs: the alignment table keeps this layer's executable-credential row
  and takes layer 5's video poll and download rows. CHANGELOG: layer 4's
  "All bindings" section goes before "Added"; both layers' bullets kept.

Compile fixes for the merged code follow in separate commits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The merged layer 6 code still used the types layers 2 to 5 replaced:

- Bedrock, Anthropic, Mistral and the OpenAI Responses tool helpers take
  LanguageModelV4ToolChoice, as CallOptions.tool_choice now is.
- Provider tool ids and custom content kinds are NamespacedKind: the
  OpenAI tool factories build `openai.<kind>` through one helper, the
  compaction, container upload and Gemini image search ids are checked
  constants, and the prefix checks read the id as a string.
- supported_urls is async: the Bedrock chat model implements it so, the
  openai-compatible dialect holds layer 5's SupportedUrlsFn and the Vertex
  xAI partner returns a ready future.
- xai::convert::resolve_provider_reference takes SharedProviderReference.
- Tests: model-level calls carry no max_retries (an operation setting
  now), supported_urls tests await, ProviderTool ids parse, and layer 5's
  reasoning test calls convert_to_responses_input with this layer's five
  arguments.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Layer 5 added AudioInput::Url, which only `transcribe` downloads. The Azure
speech model and the Vertex Gemini transcription model return
AudioInput::url_error() for it, as the other models do.

Layer 5 also moved the shared `{ error: { message } }` handler out of
provider-utils. The Azure speech model gets its own handler with the same
mapping (azure-speech-transcription-model.ts:72-79 for the message).

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…tool-call-repair-is-a-trait

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…config-is-plain-data

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…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
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

Copy link
Copy Markdown
Contributor Author

谢谢评审。这一层已随整套栈更新,请以更新后的为准。

你提到 我们的处理 位置
(我们自己的改动)shared 和 openai_compatible 原先反向依赖厂商模块 共享层定义 ResolveEndpoint、ResolveBaseUrl、ResolveFetchHeaders,各厂商自己实现,边界检查脚本加了对应规则;Alibaba 和 Moonshot 的 usage 换算跟着 registry 行放进 preset.rs;UsageDialect 去掉 aimux-providers/src/shared/、preset.rs、scripts/check_provider_boundaries.sh
SearXNG 从 SEARXNG_URL 读地址、DataForSEO 用 Basic 头,这两条新路径应有测试 都补了。SearXNG:instance_url_comes_from_searxng_url_per_request,设了变量请求发到 mock,删掉后返回 LoadSetting 且不发请求。DataForSEO:Basic 头原有 do_search_sends_correct_basic_auth_header 覆盖;从 DATAFORSEO_LOGIN 读 login 的分支新增 login_comes_from_dataforseo_login_per_request,设了变量带正确 Basic 头,删掉后返回 LoadApiKey 且不发请求 aimux-providers/tests/searxng_test.rs、dataforseo_test.rs
CHANGELOG 应写明 PresetDescriptor 多了 crate 私有字段,crate 外不能再用结构体字面量构造 已补这一句 CHANGELOG.md
validate_base_url 允许地址带 userinfo,以后收紧校验时要记得保留 确认是有意保留(自托管服务的 basic-auth),日后收紧时显式保留这一条 无代码改动

Generated by Claude Code

eric8810 pushed a commit that referenced this pull request Oct 17, 2026
…ndk (#262)

* ci: speed up test builds, deduplicate caches, and use prebuilt cargo-ndk

Cargo.toml configures [profile.release] with fat LTO (lto = true),
codegen-units = 1, and opt-level = "z" so published FFI binaries stay
small. In CI, 11 test-only jobs (Java, Kotlin, Go, Python, Flutter,
Android example, contract tests, Swift) were running full fat-LTO release
builds from scratch even though they upload no release artifacts, while
writing separate 275 MB cache entries per job and exceeding GitHub's 10 GB
repository cache quota.

- Skip fat LTO and -Oz in test-only jobs via top-level CARGO_PROFILE_*
  overrides, while keeping Cargo.toml's full release profile (lto = true,
  codegen-units = 1, opt-level = "z") on node-binding and ffi-build so
  uploaded release artifacts keep their exact sizes.
- Strip debug symbols from dev/test profiles (CARGO_PROFILE_DEV_DEBUG=0,
  CARGO_PROFILE_TEST_DEBUG=0) to speed up linking the 162 integration
  test binaries.
- Build only the cdylib target (cargo rustc --crate-type cdylib) in jobs
  that only link the shared library, avoiding archiving a redundant
  staticlib.
- Enable cache-all-crates on Swatinem/rust-cache and share a single
  "ffi-test" cache key across the FFI test jobs to reduce cache churn.
- Replace `cargo install cargo-ndk` (source build) with
  taiki-e/install-action@v2 and enable Flutter/Gradle/npm/pip caches.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* ci: keep uploaded artifacts unchanged and stop PR runs from evicting caches

Fixes on top of the previous commit:

- node-binding builds with `napi build` without `--release`, so its
  uploaded node-* artifacts are dev-profile builds. The workflow-level
  CARGO_PROFILE_DEV_DEBUG=0 stripped their debug info (Linux artifact
  55.3 MB -> 17.6 MB) and its CARGO_PROFILE_RELEASE_* overrides had no
  effect. Debug info is now dropped in rust-test only, where the test
  binaries are linked, and the dead overrides are gone.
- ffi-build goes back to `cargo build`, as release.yml does. Building
  only the cdylib changes the shipped library (x86_64 Linux .so
  9,446,848 -> 7,637,144 bytes, same 129 exports), so the ffi-*
  artifacts would no longer match the release build or earlier runs.
- rust-cache `cache-all-crates` keeps unused registry crates, it does
  not cache workspace crates; dropped. Caches are saved from master only
  (`save-if`); pull requests restore master's caches instead of each
  writing their own, which is what pushed the repository past 10 GB.
- The two node-binding legs on each runner OS (x64 and arm64 targets)
  shared one cache key: one leg failed to save and the other restored a
  cache built for the wrong target. The key now includes the target.
- CARGO_INCREMENTAL=0 is already set by rust-cache; dropped.

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

* ci: fewer jobs per run so pull requests stop queueing for runners

With every stacked pull request running CI, runs wait far longer for a
runner than they spend building: each run had 6 macOS jobs against a
5-job macOS limit and 18 Linux jobs against a 20-job total limit, and
the macOS jobs of one run sat queued for 15 minutes.

- Java (JDK 8, 11, 17, 21) and Kotlin (JDK 17, 21) run in one job
  that builds the FFI library once and runs each JDK in turn: 6 jobs
  and 6 FFI builds become 1.
- The Go binding is tested in the ffi-build legs on Linux and macOS,
  against the static archive that job already builds: the 2 Go jobs
  and their FFI builds go away.
- The x86_64 macOS Node binding is cross-built on Apple Silicon and
  tested with x64 Node under Rosetta instead of on the Intel runner,
  where its build took 371 of the job's 465 seconds.
- scripts/build-ios-xcframework.sh builds both iOS slices in one cargo
  invocation, so they compile in parallel; the output is unchanged.

A run goes from 24 jobs (6 macOS) to 17 (5 macOS).

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

* ci: restore the Flutter SDK cache everywhere, save it from master only

- subosito/flutter-action saves a 1.5-2.2 GB SDK cache per OS. A pull
  request can only restore caches saved on its own branch or on master,
  so with the action's cache on, every open pull request saved its own
  copy and the 10 GB budget filled up again. The action's `cache` input
  turns restore and save off together, so the two are split: a dry run
  of the action gives the key and path, actions/cache/restore runs on
  every branch, the action installs the SDK (a no-op when the restore
  hit), and actions/cache/save runs on master only, at the end of the
  job so the engine artifacts the build downloads are in the cache. The
  pub cache stays off, as on master. The Gradle cache already saves from
  the default branch only.
- bindings/node and bindings/python are their own Cargo workspaces and
  build into bindings/<name>/target, which rust-cache's default (the
  repository's target directory) never covered: the Node and Python jobs
  rebuilt aimux-core and aimux-providers on every run. `workspaces` now
  points rust-cache at each binding's directory.
- The ffi-build comment said the Go binding links the static archive;
  on macOS ld64 picks the .dylib next to it. Comment only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* ci: build test-only FFI libraries at opt-level 1, not 0

aimux_stream_text runs the whole stream future on the caller's thread
(ffi_block_on). With the test-only override at opt-level 0, one stream
through the C ABI needs about 530 KB of stack on the AI SDK branches
(about 300 KB on master), more than the 512 KB stack of a Swift
cooperative thread: the async Swift tests died with SIGBUS on #255 and
#259 once they carried this workflow. At opt-level 1 the same stream
needs about 110 KB; the shipping profile needs about 160 KB. The
library is also 40% smaller, and the build is still far from the
shipping profile's fat LTO with one codegen unit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* ci: pin cargo-ndk and keep pull requests from saving two more caches

cargo-ndk is installed through cargo-binstall, which took whatever was
newest; pin it to 4.1.2, the version CI has been installing.

The contract and Swift jobs were the last two that saved a Rust cache
on every pull request. Like the other jobs they now restore on pull
requests and save only on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

---------

Co-authored-by: chenhaonan <chenhaonan@fazhitech.cn>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
eric8810 and others added 11 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
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
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one
commit) through [6/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 [7/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
…ite-json-repair-in-idiomatic-rust

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…tool-call-repair-is-a-trait

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…config-is-plain-data

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
eric8810 added a commit that referenced this pull request Oct 17, 2026
* refactor(providers): remove generated provider name types

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)

* docs(changelog): record removal of generated ProviderName as breaking

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 758d0bc)

* fix(providers): drop the redundant #[must_use] on provider_names()

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

* chore: drop stale provider_name references left after the ProviderName 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)

* docs(roadmap): Node drops the ProviderName string-literal union too

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)

* ci: run checks on every pull request and cancel superseded PR runs

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

* test: delete self-generated contract fixtures and hand-written tests 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

* refactor(web): consume generated core types directly

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)

* refactor(web): generate wire types with ts-rs and import core types via 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)

* fix: address review findings on provider_names and wire export

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

* chore(fixtures): pin the AI SDK packages with sha512 integrity

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

* chore(scripts): fetch the pinned AI SDK sources into target/aisdk

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

* chore(scripts): fetch all-or-nothing and keep the pinned integrity

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

* docs: merge the migration into master layer by layer

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

* docs: drop never-built aimux-only designs, move size gates to ROADMAP

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

* ci: speed up test builds, deduplicate caches, and use prebuilt cargo-ndk

Cargo.toml configures [profile.release] with fat LTO (lto = true),
codegen-units = 1, and opt-level = "z" so published FFI binaries stay
small. In CI, 11 test-only jobs (Java, Kotlin, Go, Python, Flutter,
Android example, contract tests, Swift) were running full fat-LTO release
builds from scratch even though they upload no release artifacts, while
writing separate 275 MB cache entries per job and exceeding GitHub's 10 GB
repository cache quota.

- Skip fat LTO and -Oz in test-only jobs via top-level CARGO_PROFILE_*
  overrides, while keeping Cargo.toml's full release profile (lto = true,
  codegen-units = 1, opt-level = "z") on node-binding and ffi-build so
  uploaded release artifacts keep their exact sizes.
- Strip debug symbols from dev/test profiles (CARGO_PROFILE_DEV_DEBUG=0,
  CARGO_PROFILE_TEST_DEBUG=0) to speed up linking the 162 integration
  test binaries.
- Build only the cdylib target (cargo rustc --crate-type cdylib) in jobs
  that only link the shared library, avoiding archiving a redundant
  staticlib.
- Enable cache-all-crates on Swatinem/rust-cache and share a single
  "ffi-test" cache key across the FFI test jobs to reduce cache churn.
- Replace `cargo install cargo-ndk` (source build) with
  taiki-e/install-action@v2 and enable Flutter/Gradle/npm/pip caches.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* ci: keep uploaded artifacts unchanged and stop PR runs from evicting caches

Fixes on top of the previous commit:

- node-binding builds with `napi build` without `--release`, so its
  uploaded node-* artifacts are dev-profile builds. The workflow-level
  CARGO_PROFILE_DEV_DEBUG=0 stripped their debug info (Linux artifact
  55.3 MB -> 17.6 MB) and its CARGO_PROFILE_RELEASE_* overrides had no
  effect. Debug info is now dropped in rust-test only, where the test
  binaries are linked, and the dead overrides are gone.
- ffi-build goes back to `cargo build`, as release.yml does. Building
  only the cdylib changes the shipped library (x86_64 Linux .so
  9,446,848 -> 7,637,144 bytes, same 129 exports), so the ffi-*
  artifacts would no longer match the release build or earlier runs.
- rust-cache `cache-all-crates` keeps unused registry crates, it does
  not cache workspace crates; dropped. Caches are saved from master only
  (`save-if`); pull requests restore master's caches instead of each
  writing their own, which is what pushed the repository past 10 GB.
- The two node-binding legs on each runner OS (x64 and arm64 targets)
  shared one cache key: one leg failed to save and the other restored a
  cache built for the wrong target. The key now includes the target.
- CARGO_INCREMENTAL=0 is already set by rust-cache; dropped.

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

* ci: fewer jobs per run so pull requests stop queueing for runners

With every stacked pull request running CI, runs wait far longer for a
runner than they spend building: each run had 6 macOS jobs against a
5-job macOS limit and 18 Linux jobs against a 20-job total limit, and
the macOS jobs of one run sat queued for 15 minutes.

- Java (JDK 8, 11, 17, 21) and Kotlin (JDK 17, 21) run in one job
  that builds the FFI library once and runs each JDK in turn: 6 jobs
  and 6 FFI builds become 1.
- The Go binding is tested in the ffi-build legs on Linux and macOS,
  against the static archive that job already builds: the 2 Go jobs
  and their FFI builds go away.
- The x86_64 macOS Node binding is cross-built on Apple Silicon and
  tested with x64 Node under Rosetta instead of on the Intel runner,
  where its build took 371 of the job's 465 seconds.
- scripts/build-ios-xcframework.sh builds both iOS slices in one cargo
  invocation, so they compile in parallel; the output is unchanged.

A run goes from 24 jobs (6 macOS) to 17 (5 macOS).

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

* ci: restore the Flutter SDK cache everywhere, save it from master only

- subosito/flutter-action saves a 1.5-2.2 GB SDK cache per OS. A pull
  request can only restore caches saved on its own branch or on master,
  so with the action's cache on, every open pull request saved its own
  copy and the 10 GB budget filled up again. The action's `cache` input
  turns restore and save off together, so the two are split: a dry run
  of the action gives the key and path, actions/cache/restore runs on
  every branch, the action installs the SDK (a no-op when the restore
  hit), and actions/cache/save runs on master only, at the end of the
  job so the engine artifacts the build downloads are in the cache. The
  pub cache stays off, as on master. The Gradle cache already saves from
  the default branch only.
- bindings/node and bindings/python are their own Cargo workspaces and
  build into bindings/<name>/target, which rust-cache's default (the
  repository's target directory) never covered: the Node and Python jobs
  rebuilt aimux-core and aimux-providers on every run. `workspaces` now
  points rust-cache at each binding's directory.
- The ffi-build comment said the Go binding links the static archive;
  on macOS ld64 picks the .dylib next to it. Comment only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* ci: build test-only FFI libraries at opt-level 1, not 0

aimux_stream_text runs the whole stream future on the caller's thread
(ffi_block_on). With the test-only override at opt-level 0, one stream
through the C ABI needs about 530 KB of stack on the AI SDK branches
(about 300 KB on master), more than the 512 KB stack of a Swift
cooperative thread: the async Swift tests died with SIGBUS on #255 and
#259 once they carried this workflow. At opt-level 1 the same stream
needs about 110 KB; the shipping profile needs about 160 KB. The
library is also 40% smaller, and the build is still far from the
shipping profile's fat LTO with one codegen unit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* ci: pin cargo-ndk and keep pull requests from saving two more caches

cargo-ndk is installed through cargo-binstall, which took whatever was
newest; pin it to 4.1.2, the version CI has been installing.

The contract and Swift jobs were the last two that saved a Rust cache
on every pull request. Like the other jobs they now restore on pull
requests and save only on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* chore: delete the web console's generated ProviderName copy

tools/aimux-web/web/src/types/ProviderName.ts was the eighth copy written
by scripts/gen_provider_names.py, which this branch deletes. Nothing
imports it (it is an orphan on master too), and its header still claimed
the deleted generator. RFC-0036 §12.3 A2 requires every Rust, binding
and web copy and the generator to go.

The three quality-audit reports that cite `gen_provider_names.py --check`
as a current gate get a later-change note, as docs/internal/phase4-audit.md
already has.

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

* ci: type-check the web console against the generated types

The console imports the core types from bindings/node/src/types through
@aimux/types. gen_ts_types.py --check only compares the generated files,
so a renamed or removed Rust type regenerates cleanly and leaves a
dangling import that no pull request check catches. RFC-0036 §13.1 A4
asks for every import to be type-checked.

A new "Web console typecheck" job runs `npm ci && npm run typecheck`
(vue-tsc --noEmit) in tools/aimux-web/web. It runs when the shared code
changes (which includes tools/aimux-web and the Rust crates whose types
are generated) or when bindings/node changes, where the generated types
live; documentation-only changes skip it. "CI result" checks it.
Removing bindings/node/src/types/Recording.ts locally makes it fail on
the two files that import it.

RFC-0029 §9.1 gets a later-change note: the console no longer has its
own copies, and TS_RS_EXPORT_DIR no longer points at the web directory.

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

* docs: size and performance gates follow RFC-0043

#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

* docs: the alignment doc links back to RFC-0036 §12.2 and names the merged 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

* ci: run the AI SDK pin script tests

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

* docs: note the retired contract fixtures in the RFCs and the changelog

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

* docs(web): note that cargo test rewrites the tracked node types

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

* build(core): ts-rs exports run only from the generator

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

* test(scripts): give the pin fetch test its own user and global npmrc

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

---------

Co-authored-by: chenhaonan <chenhaonan@fazhitech.cn>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eric8810 <ericted8810@gmail.com>
eric8810 added a commit that referenced this pull request Oct 17, 2026
* refactor(providers): remove generated provider name types

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)

* docs(changelog): record removal of generated ProviderName as breaking

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 758d0bc)

* fix(providers): drop the redundant #[must_use] on provider_names()

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

* chore: drop stale provider_name references left after the ProviderName 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)

* docs(roadmap): Node drops the ProviderName string-literal union too

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)

* ci: run checks on every pull request and cancel superseded PR runs

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

* test: delete self-generated contract fixtures and hand-written tests 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

* refactor(web): consume generated core types directly

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)

* refactor(web): generate wire types with ts-rs and import core types via 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)

* refactor(stream)!: remove unused StreamingToolCallTracker

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)

* feat(stream)!: align StreamingToolCallTracker with the current AI SDK

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)

* feat(provider-utils)!: move the tool-call tracker next to its users and 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)

* test(provider-utils): note the None/undefined mapping in the argument-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)

* docs(changelog): record the tracker move to aimux-provider-utils

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)

* refactor(provider-utils)!: rewrite the tool-call tracker in idiomatic 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)

* chore(stream): drop the tool-call tracker from the crate's dependencies 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

* fix: address review findings on provider_names and wire export

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

* refactor(core)!: tool types follow the AI SDK V4 tool types

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

* chore(fixtures): pin the AI SDK packages with sha512 integrity

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

* chore(scripts): fetch the pinned AI SDK sources into target/aisdk

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

* chore(scripts): fetch all-or-nothing and keep the pinned integrity

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

* fix(provider-utils): keep parallel tool calls apart as provider-utils 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

* refactor(provider-utils)!: export only the tracker, keep its modules 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

* test(provider-utils): differential tracker test against the pinned AI 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

* fix(core): back-fill invalid tool input with the prototype-safe parse

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

* docs: merge the migration into master layer by layer

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

* docs: drop never-built aimux-only designs, move size gates to ROADMAP

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

* fix(core)!: a repaired call that fails validation is not wrapped

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

* test(core): tool-argument vectors checked against the AI SDK parse

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

* refactor(core)!: model calls carry the V4 tool choice object union

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

* fix(core)!: a provider tool id must be namespaced

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

* fix(core)!: a V4 tool result is never null

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

* docs: core tool results follow the AI SDK tool result family

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

* test(core): restore the MoA unit tests

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

* test(core): restore the RFC-0026 OpenAI output converter tests

Closes #232 B24: the 22 unit tests in openai_output.rs that layer 1 had
(non-streaming text, tool calls, raw argument pass-through, blank and
null arguments, reasoning, usage and finish-reason mapping; streaming
text, tool-call open/argument chunks and indices, back-filled arguments,
reasoning, usage, errors, recoverable frame errors, SSE encoding) were
dropped by the tool-call refactor. They are back, adapted to the split
tool-call types:

- `to_chat_completion` reads a provider-layer
  `GenerateContent::ToolCall(RawToolCall)` whose input is the raw text;
- `to_chat_completion_stream` reads core `TextStreamPart`s, so streamed
  calls are `StreamPart::ToolCall(ToolCall)` with parsed input;
- the thought signature field is gone (it travels in provider metadata).

Assertions are unchanged.

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

* ci: speed up test builds, deduplicate caches, and use prebuilt cargo-ndk

Cargo.toml configures [profile.release] with fat LTO (lto = true),
codegen-units = 1, and opt-level = "z" so published FFI binaries stay
small. In CI, 11 test-only jobs (Java, Kotlin, Go, Python, Flutter,
Android example, contract tests, Swift) were running full fat-LTO release
builds from scratch even though they upload no release artifacts, while
writing separate 275 MB cache entries per job and exceeding GitHub's 10 GB
repository cache quota.

- Skip fat LTO and -Oz in test-only jobs via top-level CARGO_PROFILE_*
  overrides, while keeping Cargo.toml's full release profile (lto = true,
  codegen-units = 1, opt-level = "z") on node-binding and ffi-build so
  uploaded release artifacts keep their exact sizes.
- Strip debug symbols from dev/test profiles (CARGO_PROFILE_DEV_DEBUG=0,
  CARGO_PROFILE_TEST_DEBUG=0) to speed up linking the 162 integration
  test binaries.
- Build only the cdylib target (cargo rustc --crate-type cdylib) in jobs
  that only link the shared library, avoiding archiving a redundant
  staticlib.
- Enable cache-all-crates on Swatinem/rust-cache and share a single
  "ffi-test" cache key across the FFI test jobs to reduce cache churn.
- Replace `cargo install cargo-ndk` (source build) with
  taiki-e/install-action@v2 and enable Flutter/Gradle/npm/pip caches.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* ci: keep uploaded artifacts unchanged and stop PR runs from evicting caches

Fixes on top of the previous commit:

- node-binding builds with `napi build` without `--release`, so its
  uploaded node-* artifacts are dev-profile builds. The workflow-level
  CARGO_PROFILE_DEV_DEBUG=0 stripped their debug info (Linux artifact
  55.3 MB -> 17.6 MB) and its CARGO_PROFILE_RELEASE_* overrides had no
  effect. Debug info is now dropped in rust-test only, where the test
  binaries are linked, and the dead overrides are gone.
- ffi-build goes back to `cargo build`, as release.yml does. Building
  only the cdylib changes the shipped library (x86_64 Linux .so
  9,446,848 -> 7,637,144 bytes, same 129 exports), so the ffi-*
  artifacts would no longer match the release build or earlier runs.
- rust-cache `cache-all-crates` keeps unused registry crates, it does
  not cache workspace crates; dropped. Caches are saved from master only
  (`save-if`); pull requests restore master's caches instead of each
  writing their own, which is what pushed the repository past 10 GB.
- The two node-binding legs on each runner OS (x64 and arm64 targets)
  shared one cache key: one leg failed to save and the other restored a
  cache built for the wrong target. The key now includes the target.
- CARGO_INCREMENTAL=0 is already set by rust-cache; dropped.

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

* ci: fewer jobs per run so pull requests stop queueing for runners

With every stacked pull request running CI, runs wait far longer for a
runner than they spend building: each run had 6 macOS jobs against a
5-job macOS limit and 18 Linux jobs against a 20-job total limit, and
the macOS jobs of one run sat queued for 15 minutes.

- Java (JDK 8, 11, 17, 21) and Kotlin (JDK 17, 21) run in one job
  that builds the FFI library once and runs each JDK in turn: 6 jobs
  and 6 FFI builds become 1.
- The Go binding is tested in the ffi-build legs on Linux and macOS,
  against the static archive that job already builds: the 2 Go jobs
  and their FFI builds go away.
- The x86_64 macOS Node binding is cross-built on Apple Silicon and
  tested with x64 Node under Rosetta instead of on the Intel runner,
  where its build took 371 of the job's 465 seconds.
- scripts/build-ios-xcframework.sh builds both iOS slices in one cargo
  invocation, so they compile in parallel; the output is unchanged.

A run goes from 24 jobs (6 macOS) to 17 (5 macOS).

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

* ci: restore the Flutter SDK cache everywhere, save it from master only

- subosito/flutter-action saves a 1.5-2.2 GB SDK cache per OS. A pull
  request can only restore caches saved on its own branch or on master,
  so with the action's cache on, every open pull request saved its own
  copy and the 10 GB budget filled up again. The action's `cache` input
  turns restore and save off together, so the two are split: a dry run
  of the action gives the key and path, actions/cache/restore runs on
  every branch, the action installs the SDK (a no-op when the restore
  hit), and actions/cache/save runs on master only, at the end of the
  job so the engine artifacts the build downloads are in the cache. The
  pub cache stays off, as on master. The Gradle cache already saves from
  the default branch only.
- bindings/node and bindings/python are their own Cargo workspaces and
  build into bindings/<name>/target, which rust-cache's default (the
  repository's target directory) never covered: the Node and Python jobs
  rebuilt aimux-core and aimux-providers on every run. `workspaces` now
  points rust-cache at each binding's directory.
- The ffi-build comment said the Go binding links the static archive;
  on macOS ld64 picks the .dylib next to it. Comment only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* ci: build test-only FFI libraries at opt-level 1, not 0

aimux_stream_text runs the whole stream future on the caller's thread
(ffi_block_on). With the test-only override at opt-level 0, one stream
through the C ABI needs about 530 KB of stack on the AI SDK branches
(about 300 KB on master), more than the 512 KB stack of a Swift
cooperative thread: the async Swift tests died with SIGBUS on #255 and
#259 once they carried this workflow. At opt-level 1 the same stream
needs about 110 KB; the shipping profile needs about 160 KB. The
library is also 40% smaller, and the build is still far from the
shipping profile's fat LTO with one codegen unit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* ci: pin cargo-ndk and keep pull requests from saving two more caches

cargo-ndk is installed through cargo-binstall, which took whatever was
newest; pin it to 4.1.2, the version CI has been installing.

The contract and Swift jobs were the last two that saved a Rust cache
on every pull request. Like the other jobs they now restore on pull
requests and save only on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* chore: delete the web console's generated ProviderName copy

tools/aimux-web/web/src/types/ProviderName.ts was the eighth copy written
by scripts/gen_provider_names.py, which this branch deletes. Nothing
imports it (it is an orphan on master too), and its header still claimed
the deleted generator. RFC-0036 §12.3 A2 requires every Rust, binding
and web copy and the generator to go.

The three quality-audit reports that cite `gen_provider_names.py --check`
as a current gate get a later-change note, as docs/internal/phase4-audit.md
already has.

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

* ci: type-check the web console against the generated types

The console imports the core types from bindings/node/src/types through
@aimux/types. gen_ts_types.py --check only compares the generated files,
so a renamed or removed Rust type regenerates cleanly and leaves a
dangling import that no pull request check catches. RFC-0036 §13.1 A4
asks for every import to be type-checked.

A new "Web console typecheck" job runs `npm ci && npm run typecheck`
(vue-tsc --noEmit) in tools/aimux-web/web. It runs when the shared code
changes (which includes tools/aimux-web and the Rust crates whose types
are generated) or when bindings/node changes, where the generated types
live; documentation-only changes skip it. "CI result" checks it.
Removing bindings/node/src/types/Recording.ts locally makes it fail on
the two files that import it.

RFC-0029 §9.1 gets a later-change note: the console no longer has its
own copies, and TS_RS_EXPORT_DIR no longer points at the web directory.

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

* docs: size and performance gates follow RFC-0043

#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

* docs: the alignment doc links back to RFC-0036 §12.2 and names the merged 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

* ci: run the AI SDK pin script tests

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

* docs: note the retired contract fixtures in the RFCs and the changelog

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

* docs(web): note that cargo test rewrites the tracked node types

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

* test(openai): tool calls are flushed after an error frame

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

* refactor(provider-utils): bound the tracker's id suffix search

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

* build(core): ts-rs exports run only from the generator

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

* fix(provider-utils): the tool call tracker generates ids as upstream

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

* test(scripts): give the pin fetch test its own user and global npmrc

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

---------

Co-authored-by: chenhaonan <chenhaonan@fazhitech.cn>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eric8810 <ericted8810@gmail.com>
eric8810 added a commit that referenced this pull request Oct 17, 2026
* refactor(providers): remove generated provider name types

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0eb91-5e79-775e-975e-b85e2ac49ab6
(cherry picked from commit 7f68ac3686d78f1f614e1f313cccce4c10aa9222)

* docs(changelog): record removal of generated ProviderName as breaking

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 758d0bcbee451f48b8daa47703281403bb1a2369)

* fix(providers): drop the redundant #[must_use] on provider_names()

The returned impl Iterator is already must_use, so clippy's
double_must_use rejected it.

(provider.rs part of commit 83974246716cf8e1585c02177b0c87fa4917701e)

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

* chore: drop stale provider_name references left after the ProviderName 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 dfcd54f4da70f1d02866f3027363f6359cad0bb8)

* docs(roadmap): Node drops the ProviderName string-literal union too

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 3e2d9fc7dce2bcb8b91407617d5fcd7e49a33cb0)

* ci: run checks on every pull request and cancel superseded PR runs

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

* test: delete self-generated contract fixtures and hand-written tests 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

* refactor(web): consume generated core types directly

Amp-Thread-ID: https://ampcode.com/threads/T-01a0eb91-5e79-775e-975e-b85e2ac49ab6
Co-authored-by: Amp <amp@ampcode.com>
(cherry picked from commit dc1cd1439e330e8fd44833984b4ebfb139a6b502)

* refactor(web): generate wire types with ts-rs and import core types via 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 69c135122136ad0815949d68a83bfa74841ab7ef)

* refactor(stream)!: remove unused StreamingToolCallTracker

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

* feat(stream)!: align StreamingToolCallTracker with the current AI SDK

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 6035201dbec549f5d77fa39390b070e3f1552f3f)

* feat(provider-utils)!: move the tool-call tracker next to its users and 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 df4d09f0353b47ad56926f1c3a623e7a123e880e)

* test(provider-utils): note the None/undefined mapping in the argument-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 7bc200eae6cdb57f9af4ce58428b4a48e79ad0d3)

* docs(changelog): record the tracker move to aimux-provider-utils

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 264ade36cb15d6801a443cac8cb084415604210c)

* refactor(provider-utils)!: rewrite the tool-call tracker in idiomatic 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 b0fd923a3f312ee179c2e72a62e795ea44fb6cff)

* chore(stream): drop the tool-call tracker from the crate's dependencies 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

* fix: address review findings on provider_names and wire export

- 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 83974246716cf8e1585c02177b0c87fa4917701e)

* refactor(core)!: messages, content, provider options and prompts follow 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

* refactor(core)!: tool types follow the AI SDK V4 tool types

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

* test(go): pass the prompt reasoning signature in provider_options

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

* fix(flutter): accept content in the StreamTextResultAggregated constructor

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

* test(flutter): drop the typed-call helper options no test passes any 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

* chore: remove the MIGRATION-NS.md scratch recipe

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

* chore(fixtures): pin the AI SDK packages with sha512 integrity

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 9d7508ce.

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

* chore(scripts): fetch the pinned AI SDK sources into target/aisdk

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 19db928e. 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

* chore(scripts): fetch all-or-nothing and keep the pinned integrity

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 ed1564bb and 1ce4eb6b. 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

* fix(provider-utils): keep parallel tool calls apart as provider-utils 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

* refactor(provider-utils)!: export only the tracker, keep its modules 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

* test(provider-utils): differential tracker test against the pinned AI 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

* fix(core): back-fill invalid tool input with the prototype-safe parse

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

* docs(core): say what TimeoutConfiguration mirrors

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

* docs: merge the migration into master layer by layer

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

* docs: drop never-built aimux-only designs, move size gates to ROADMAP

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

* test(core): restore the AiMuxError variant-set test

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

* test(core): restore the RouterModel unit tests

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

* fix(core)!: a repaired call that fails validation is not wrapped

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 (fb0f79e5).

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

* test(core): tool-argument vectors checked against the AI SDK parse

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

* refactor(core)!: provider namespace maps keep insertion order

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

* refactor(core)!: model calls carry the V4 tool choice object union

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

* refactor(core)!: drop the reasoning part's top-level signature

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

* fix(core)!: a provider tool id must be namespaced

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

* fix(core)!: a V4 tool result is never null

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

* docs: core tool results follow the AI SDK tool result family

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

* test(core): restore the MoA unit tests

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

* test(core): restore the RFC-0026 OpenAI output converter tests

Closes #232 B24: the 22 unit tests in openai_output.rs that layer 1 had
(non-streaming text, tool calls, raw argument pass-through, blank and
null arguments, reasoning, usage and finish-reason mapping; streaming
text, tool-call open/argument chunks and indices, back-filled arguments,
reasoning, usage, errors, recoverable frame errors, SSE encoding) were
dropped by the tool-call refactor. They are back, adapted to the split
tool-call types:

- `to_chat_completion` reads a provider-layer
  `GenerateContent::ToolCall(RawToolCall)` whose input is the raw text;
- `to_chat_completion_stream` reads core `TextStreamPart`s, so streamed
  calls are `StreamPart::ToolCall(ToolCall)` with parsed input;
- the thought signature field is gone (it travels in provider metadata).

Assertions are unchanged.

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

* refactor(core)!: user messages are a union by role with per-role content

The AI SDK's `ModelMessage` is a union by role in which every message has
`providerOptions` (`@ai-sdk/provider-utils@5.0.53` types/model-message.ts:10,
system-model-message.ts:19, user-model-message.ts:16,
assistant-model-message.ts:25, tool-model-message.ts:17), and each role has
its own content union (`UserContent` user-model-message.ts:22,
`AssistantContent` assistant-model-message.ts:32, `ToolContent`
tool-model-message.ts:23).

- `ModelMessage` becomes `System / User / Assistant / Tool` with
  message-level `provider_options`; `convert_to_language_model_prompt`
  copies them onto the provider message instead of dropping them
  (ai@7.0.127 convert-to-language-model-prompt.ts:211-216, :219-245,
  :252-258, :362, :405) (#229 C32).
- `ContentPart`, `MessageContent` and `Role` are replaced by
  `UserContentPart`, `AssistantContentPart`, `ToolContentPart`,
  `UserContent`, `AssistantContent` and `ToolContent`. A part a role does
  not allow no longer parses. The file variants stay as they are on this
  layer (#229 C31).
- `ToolResultPart` carries the upstream typed `output`
  (`ToolResultOutput`, content-part.ts:218-247). The bindings' JSON keeps
  working: the earlier `result` / untyped `output` value with `is_error` is
  read as `createToolModelOutput` reads a raw value
  (prompt/create-tool-model-output.ts:4), and text / json / error outputs
  are written that way. `tool_name` stays optional here and is taken from
  the preceding call.
- A tool message can hold `tool_approval_response`; the conversion passes
  it on only for a provider-executed tool (:374-378, :395-402), still drops
  the assistant's approval requests (:270-281), and drops a tool message
  left empty (:177-183) (#229 C36).
- Response messages replay a provider-executed tool result as upstream
  does (`errorMode: 'none'`, or `'json'` for an error;
  generate-text/to-response-messages.ts:103-127).

In TypeScript the provider `ToolResultPart` / `ToolResultOutput` are
renamed `LanguageModelToolResultPart` / `LanguageModelToolResultOutput`,
next to the existing `LanguageModelReasoningPart`. The aimux-web console
builds the new messages from its own wire format. The alignment
document's line references for message-level providerOptions are fixed.

Tests: aimux-core/tests/prompt_conversion_test.rs
(message_provider_options_reach_the_provider_prompt,
content_parts_are_checked_by_role,
empty_text_parts_are_dropped_as_upstream_drops_them,
tool_approvals_reach_the_provider_only_for_provider_executed_tools,
typed_tool_result_output_reaches_the_provider,
earlier_tool_result_json_still_reads_and_writes,
assistant_tool_result_converts_like_a_tool_message_result).

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

* feat(core)!: accept the AI SDK's deprecated tool output items (#229 C37)

A `content` tool output now accepts upstream's deprecated `file-data`,
`file-url`, `file-id`, `file-reference`, `image-data`, `image-url`,
`image-file-id` and `image-file-reference` items
(provider-utils types/content-part.ts:361-532), and the prompt conversion
maps each to a `file` part as `mapToolResultOutput` does
(ai prompt/convert-to-language-model-prompt.ts:610-797): media type
inference from the URL extension (:821-865), the given URL kept as
`original_url` when normalizing changes it, and a string `file_id` turned
into a reference for the called provider (:799-818).

`convert_to_language_model_prompt` takes the provider id, the part of
`LanguageModel::provider()` before the first dot, as generate-text.ts:970
and stream-language-model-call.ts:311 pass it. These items and a user
`image` part (:229-238) log upstream's deprecation warnings through
`tracing`, in logWarnings' wording (logger/log-warnings.ts:71-73).

Docs: the image row and the user-layer tool output paragraph of the
alignment doc note the upstream deprecations.

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

* feat(core)!: standardize the prompt and check tool results and tool choice (#229 C38)

As the AI SDK does around the model call:

- `standardize_prompt` rejects an empty message list ("messages must not
  be empty", ai prompt/standardize-prompt.ts:78-83). The system-message
  opt-in (:85-96) and system-message instructions (:55-65) are not ported:
  the bindings, CLI and web tool still send system messages in the
  messages, and instructions stay a string.
- The prompt conversion combines consecutive tool messages, merging the
  earlier message's providerOptions into its last part with mergeObjects
  (convert-to-language-model-prompt.ts:104-130, util/merge-objects.ts), and
  raises the new `AiMuxError::MissingToolResults` for tool calls the
  provider did not execute that have neither a tool result nor an approval
  response (:57-87, :132-175; message from
  error/missing-tool-result-error.ts:15-20).
- A response that breaks a `required` or `tool` tool choice raises the new
  `AiMuxError::ToolChoiceViolation` with upstream's fields and messages
  (error/tool-choice-violation-error.ts): `generate_text` fails
  (generate-text.ts:1155-1176); `stream_text` yields the error and then a
  Finish part with unified reason `error`, the raw reason kept
  (stream-language-model-call.ts:644-687, stream-text.ts:2984-2989), the
  error carrying the streamed provider content (:892-953).

The C ABI, Node and Python report the new errors through the existing
invalid-prompt and invalid-response-data codes and classes until the
binding migration.

Docs: TOOLDEF-3 line references and the empty tool input rule.

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

* refactor(core)!: text results carry the AI SDK's core ContentPart (#229 C42, C41 content parts)

Text results now hold the core `ContentPart` of ai generate-text/
content-part.ts:12-33, eleven kinds, instead of the provider's
`GenerateContent` with other type parameters: text, custom, reasoning,
reasoning file, source, file (wrapped as `{ file, provider_metadata }`),
tool call, tool result, tool error, tool approval request and tool
approval response. `GenerateContent` is the provider content only and is
no longer generic.

As convertLanguageModelContent builds them
(convert-language-model-content.ts:55-176), a provider tool result becomes
a `TypedToolResult` with the input of its call, or a `TypedToolError` when
the provider marks it as an error; a result without a call in the
response keeps no input. As generateText does (generate-text.ts:1303-1323),
a client tool call that could not be parsed is followed by a tool error
holding its error message, and the response messages put it in a tool
message as error text, ordered by tool call (to-response-messages.ts:
158-265). Provider-executed results stay in the assistant message.

`toolMetadata` and `title` are absent: they come from user-level tool
definitions, which aimux does not have. `tool-output-denied` is a stream
part only (the alignment doc says so too) and goes with the stream parts.

Node exports `ContentPart`, `TypedToolResult`, `TypedToolError` and
`ToolApprovalResponseOutput` instead of `TextContent`; the Python wrapper
validates the new parts (checked against Rust output with the native
module stubbed).

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

* fix(core)!: a custom content kind must be namespaced (#229 NP2-2)

Closes the content half of #229 NP2-2. The custom content `kind` is
`${string}.${string}` everywhere the AI SDK defines it:
- @ai-sdk/provider@4.0.21 language-model-v4-prompt.ts:138 (prompt part)
  and language-model-v4-custom-content.ts:13 (model output);
- @ai-sdk/provider-utils@5.0.53 src/types/content-part.ts:133 (user part);
- ai@7.0.127 generate-text/content-part.ts:16 and
  stream-text-result.ts:475 (core result and stream parts).

`AssistantContentPart::Custom`, the provider `CustomPart`,
`GenerateContent::Custom`, `ContentPart::Custom` and `StreamPart::Custom`
now carry the `types::NamespacedKind` layer 2 added for the provider tool
id, so a kind without a `.` no longer deserializes. The JSON stays a
plain string; the generated TS uses `NamespacedKind`.

Tests: custom_kind_test (a_custom_kind_needs_a_namespace,
a_message_with_an_unnamespaced_custom_kind_does_not_deserialize,
a_custom_part_reaches_the_provider_with_its_kind).

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

* refactor(core)!: call options carry no call id or step helper (#230 C7, partial)

`LanguageModelV4CallOptions` (@ai-sdk/provider@4.0.21
language-model-v4-call-options.ts:8-138) has no call id, so
`CallOptions.call_id` is removed. It always equalled the recording
context's id; the entry points no longer copy it, session grouping and the
trace layer read `recording_context.call_id`, and the `ExchangeContext`
for `CallOptions` derives it the way `DecisionCallOptions` already did.
The bedrock, codex and vertex models build their requests with
`HttpRequest::new`, which carries abort, call id and recording context.

`CallOptions::for_step` becomes the crate-private
`composite::step_options`, used by the router and MoA only.

Not done here: `max_retries`, `timeout` and `session_id` stay on
`CallOptions`. The router, MoA and trace layer are `LanguageModel`s that
see only the call options, and they read these fields; moving them needs
the operation runtime and task-local scope the alignment doc puts them in
(§2.3), which comes with the call pipeline in layer 4.

Tests: http_recording_test
(an_exchange_takes_the_call_id_from_the_recording_context); recording
input_record_serializes_options_without_internal_fields now sets a
recording context and an abort signal.

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

* docs: scope note for part 2 and the speech and transcription tracing note

From the merged-RFC correction patch, for the messages and content layer:
- Part 2 opens with the §0.4 Q1 scope note: tool execution, approval state
  transitions and multi-step aggregation only need their types and wire
  format in place.
- §3.5 notes that ai 7.0.122 had no speech or transcription tracing and
  that 7.0.127 adds generateSpeech / transcribe / streamTranscribe tracing
  types (ai@7.0.127 src/telemetry/tracing-channel.ts:12-14).
- The tool-call acceptance row lists empty or blank arguments, which the
  AI SDK parses as `{}` (parse-tool-call.ts:202-205).

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

* test(node): the reasoning replay test gives its tool call a result

"assistant without reasoning omits reasoning_content" sent an assistant
tool call with no tool result. Since 252371bf (#229 C38) the prompt is
checked as the AI SDK's convertToLanguageModelPrompt checks it, and that
prompt fails with MissingToolResultsError before any request. The
conversation now carries the tool message with the call's result, as
the other replay tests do; the assertion on the assistant message is
unchanged.

No other binding test sends a tool call without its result.

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

* ci: speed up test builds, deduplicate caches, and use prebuilt cargo-ndk

Cargo.toml configures [profile.release] with fat LTO (lto = true),
codegen-units = 1, and opt-level = "z" so published FFI binaries stay
small. In CI, 11 test-only jobs (Java, Kotlin, Go, Python, Flutter,
Android example, contract tests, Swift) were running full fat-LTO release
builds from scratch even though they upload no release artifacts, while
writing separate 275 MB cache entries per job and exceeding GitHub's 10 GB
repository cache quota.

- Skip fat LTO and -Oz in test-only jobs via top-level CARGO_PROFILE_*
  overrides, while keeping Cargo.toml's full release profile (lto = true,
  codegen-units = 1, opt-level = "z") on node-binding and ffi-build so
  uploaded release artifacts keep their exact sizes.
- Strip debug symbols from dev/test profiles (CARGO_PROFILE_DEV_DEBUG=0,
  CARGO_PROFILE_TEST_DEBUG=0) to speed up linking the 162 integration
  test binaries.
- Build only the cdylib target (cargo rustc --crate-type cdylib) in jobs
  that only link the shared library, avoiding archiving a redundant
  staticlib.
- Enable cache-all-crates on Swatinem/rust-cache and share a single
  "ffi-test" cache key across the FFI test jobs to reduce cache churn.
- Replace `cargo install cargo-ndk` (source build) with
  taiki-e/install-action@v2 and enable Flutter/Gradle/npm/pip caches.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* ci: keep uploaded artifacts unchanged and stop PR runs from evicting caches

Fixes on top of the previous commit:

- node-binding builds with `napi build` without `--release`, so its
  uploaded node-* artifacts are dev-profile builds. The workflow-level
  CARGO_PROFILE_DEV_DEBUG=0 stripped their debug info (Linux artifact
  55.3 MB -> 17.6 MB) and its CARGO_PROFILE_RELEASE_* overrides had no
  effect. Debug info is now dropped in rust-test only, where the test
  binaries are linked, and the dead overrides are gone.
- ffi-build goes back to `cargo build`, as release.yml does. Building
  only the cdylib changes the shipped library (x86_64 Linux .so
  9,446,848 -> 7,637,144 bytes, same 129 exports), so the ffi-*
  artifacts would no longer match the release build or earlier runs.
- rust-cache `cache-all-crates` keeps unused registry crates, it does
  not cache workspace crates; dropped. Caches are saved from master only
  (`save-if`); pull requests restore master's caches instead of each
  writing their own, which is what pushed the repository past 10 GB.
- The two node-binding legs on each runner OS (x64 and arm64 targets)
  shared one cache key: one leg failed to save and the other restored a
  cache built for the wrong target. The key now includes the target.
- CARGO_INCREMENTAL=0 is already set by rust-cache; dropped.

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

* ci: fewer jobs per run so pull requests stop queueing for runners

With every stacked pull request running CI, runs wait far longer for a
runner than they spend building: each run had 6 macOS jobs against a
5-job macOS limit and 18 Linux jobs against a 20-job total limit, and
the macOS jobs of one run sat queued for 15 minutes.

- Java (JDK 8, 11, 17, 21) and Kotlin (JDK 17, 21) run in one job
  that builds the FFI library once and runs each JDK in turn: 6 jobs
  and 6 FFI builds become 1.
- The Go binding is tested in the ffi-build legs on Linux and macOS,
  against the static archive that job already builds: the 2 Go jobs
  and their FFI builds go away.
- The x86_64 macOS Node binding is cross-built on Apple Silicon and
  tested with x64 Node under Rosetta instead of on the Intel runner,
  where its build took 371 of the job's 465 seconds.
- scripts/build-ios-xcframework.sh builds both iOS slices in one cargo
  invocation, so they compile in parallel; the output is unchanged.

A run goes from 24 jobs (6 macOS) to 17 (5 macOS).

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

* ci: restore the Flutter SDK cache everywhere, save it from master only

- subosito/flutter-action saves a 1.5-2.2 GB SDK cache per OS. A pull
  request can only restore caches saved on its own branch or on master,
  so with the action's cache on, every open pull request saved its own
  copy and the 10 GB budget filled up again. The action's `cache` input
  turns restore and save off together, so the two are split: a dry run
  of the action gives the key and path, actions/cache/restore runs on
  every branch, the action installs the SDK (a no-op when the restore
  hit), and actions/cache/save runs on master only, at the end of the
  job so the engine artifacts the build downloads are in the cache. The
  pub cache stays off, as on master. The Gradle cache already saves from
  the default branch only.
- bindings/node and bindings/python are their own Cargo workspaces and
  build into bindings/<name>/target, which rust-cache's default (the
  repository's target directory) never covered: the Node and Python jobs
  rebuilt aimux-core and aimux-providers on every run. `workspaces` now
  points rust-cache at each binding's directory.
- The ffi-build comment said the Go binding links the static archive;
  on macOS ld64 picks the .dylib next to it. Comment only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* ci: build test-only FFI libraries at opt-level 1, not 0

aimux_stream_text runs the whole stream future on the caller's thread
(ffi_block_on). With the test-only override at opt-level 0, one stream
through the C ABI needs about 530 KB of stack on the AI SDK branches
(about 300 KB on master), more than the 512 KB stack of a Swift
cooperative thread: the async Swift tests died with SIGBUS on #255 and
#259 once they carried this workflow. At opt-level 1 the same stream
needs about 110 KB; the shipping profile needs about 160 KB. The
library is also 40% smaller, and the build is still far from the
shipping profile's fat LTO with one codegen unit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* ci: pin cargo-ndk and keep pull requests from saving two more caches

cargo-ndk is installed through cargo-binstall, which took whatever was
newest; pin it to 4.1.2, the version CI has been installing.

The contract and Swift jobs were the last two that saved a Rust cache
on every pull request. Like the other jobs they now restore on pull
requests and save only on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* chore: delete the web console's generated ProviderName copy

tools/aimux-web/web/src/types/ProviderName.ts was the eighth copy written
by scripts/gen_provider_names.py, which this branch deletes. Nothing
imports it (it is an orphan on master too), and its header still claimed
the deleted generator. RFC-0036 §12.3 A2 requires every Rust, binding
and web copy and the generator to go.

The three quality-audit reports that cite `gen_provider_names.py --check`
as a current gate get a later-change note, as docs/internal/phase4-audit.md
already has.

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

* ci: type-check the web console against the generated types

The console imports the core types from bindings/node/src/types through
@aimux/types. gen_ts_types.py --check only compares the generated files,
so a renamed or removed Rust type regenerates cleanly and leaves a
dangling import that no pull request check catches. RFC-0036 §13.1 A4
asks for every import to be type-checked.

A new "Web console typecheck" job runs `npm ci && npm run typecheck`
(vue-tsc --noEmit) in tools/aimux-web/web. It runs when the shared code
changes (which includes tools/aimux-web and the Rust crates whose types
are generated) or when bindings/node changes, where the generated types
live; documentation-only changes skip it. "CI result" checks it.
Removing bindings/node/src/types/Recording.ts locally makes it fail on
the two files that import it.

RFC-0029 §9.1 gets a later-change note: the console no longer has its
own copies, and TS_RS_EXPORT_DIR no longer points at the web directory.

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

* docs: size and performance gates follow RFC-0043

#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

* docs: the alignment doc links back to RFC-0036 §12.2 and names the merged 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

* ci: run the AI SDK pin script tests

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

* docs: note the retired contract fixtures in the RFCs and the changelog

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

* docs(web): note that cargo test rewrites the tracked node types

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

* test(openai): tool calls are flushed after an error frame

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…
@eric8810
eric8810 changed the base branch from v4/10-tool-call-repair-is-a-trait to master October 17, 2026 15:03
master 带入下方各层的 squash;全部冲突与自动合并差异收敛到本层树
(层树为作者维护、CI 全绿的真相;squash 内容已包含于层树),零差异。
@eric8810
eric8810 merged commit b716d21 into master Oct 17, 2026
14 checks passed
eric8810 added a commit that referenced this pull request Oct 17, 2026
* fix(core): tests and comments follow the merged retry and error sets

Layer 5 removed the model-level retry settings and added the LoadApiKey /
LoadSetting errors; layer 4 brought tests and comments written against
the old sets.

- The video test that races status retries against the poll budget set a
  60 s delay through retry_config(). The failed status check now carries
  `retry-after-ms: 30000`, which the retry honours exactly below 60 s, so
  the delay does not depend on the jittered backoff.
- The router test no longer asserts retry_config() of the router.
- The error variant-set test lists LoadApiKey and LoadSetting, and builds
  NoSuchProvider with its layer 5 fields.
- The max_retries comments of the call options and the composite child
  count say that None retries twice, the AI SDK default, without the
  retry_config() clause.

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

* fix(providers): layer 5 provider code takes layers 2 to 4 types

Provider code that layer 5 added or rewrote compiled against the old
types. After the merge:

- The tool choice of a model call is LanguageModelV4ToolChoice (layer 2,
  C39): the Anthropic, Groq, DeepSeek and OpenAI-compatible tool
  preparation and their tests. Each still drops the choice when no tool
  is sent.
- Namespace maps are IndexMaps (layer 3, NP2-1): OpenAI chat options,
  OpenAI-compatible chat metadata, the Anthropic skills reference and the
  speech and Hugging Face tests. The Anthropic options rewrite uses
  shift_remove, so the other namespaces keep their order. Imports of
  HashMap that became unused are removed.
- Provider tool ids are NamespacedKind (layer 2, NP2-2) in the Groq tests.
- Tests read GenerateTextResult through its accessors (layer 4, C44) in
  the DeepSeek test, and the factory tests map fixture roles from strings,
  since layer 3 removed Role.
- The ElevenLabs speech test gets back the HashMap import its helper uses.
- rustfmt on the video poll test of the previous commit.

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

* chore(fixtures): the recorder pins provider-utils 5.0.55 as VERSIONS.json does

The merge took layer 2's `@ai-sdk/provider-utils` entry in
fixtures/aisdk/VERSIONS.json (5.0.55, the tracker baseline), while the
recorder's package.json and package-lock.json still pinned 5.0.53, so the
check that the lock pins the same integrity as VERSIONS.json could not
pass. package.json now pins 5.0.55 and the lock is regenerated with
`npm install --package-lock-only --ignore-scripts`; nothing else in the
tree changes.

The pinned set stays mixed, as layer 2 recorded: ai 7.0.127 and the
@ai-sdk/* packages depend on provider-utils 5.0.53 exactly, so the lock
nests 5.0.53 under each of them, and 5.0.55 nests @ai-sdk/provider
4.0.23. The top-level provider-utils is 5.0.55 with the same integrity as
VERSIONS.json.

`node --test scripts/fetch-aisdk.test.mjs
scripts/aisdk-fixtures/versions.test.mjs`: 12 passed, none skipped.

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

* refactor(core)!: decisions follow DecisionModelV4

aimux's Decision API had its own shape: a question list with ids, Choice
options and Score levels, a capability struct, refusals and skips as
answers, and extension fields on the call and the result. The AI SDK now
has DecisionModelV4 (@ai-sdk/provider@4.0.22 src/decision-model/v4/) and
decide (ai@7.0.128 src/decide/decide.ts). This commit makes aimux's
Decision that contract, in one step, with no transitional code (D31). It
folds N5, N6, N7, C29 and N8 of layer 7.

Model and operation (N5, C29):
- One DecisionModel per decision-model-v4.ts:6-16: specification_version
  ("v4"), provider, model_id, supported_question_types and
  do_decide(&DecisionCallOptions). Call options, questions, answers and the
  result follow decision-model-v4-call-options.ts:7-14,
  decision-model-v4-question.ts and decision-model-v4-result.ts:7-56.
- decide follows decide.ts:76-185: validate the input
  (validate-decision.ts:50-125), reject unsupported question types before
  any I/O with the DecisionUnsupportedQuestionTypeError message
  (decision-unsupported-question-type-error.ts:18-31), retry (default 2),
  validate the answers once after the retries (validate-decision.ts:176-298,
  tolerance 1e-6 and rounding decimals 0..15), complete total_tokens only
  when both counts are known and default the response timestamp and model.

Provider lookup (N7):
- Provider::evaluation_model and the EvaluationModel trait are removed.
  Provider::decision_model returns Option<Result<Arc<dyn DecisionModel>>>,
  None by default. The registry and custom_provider have one decision
  lookup and report None as NoSuchModel("decisionModel")
  (provider-registry.ts:447-461, custom-provider.ts:241-258). The
  deprecated evaluationModel / doEvaluate aliases are not ported.
- EvaluationLanguageModel is DecisionLanguageModel, with the default
  provider "{language provider}.decision" and the response format name
  "decision" (provider-utils 5.0.55 decision-language-model.ts:24, :158).
  Google's decision_model is that adapter ("google.evaluation",
  google 4.0.87 google-provider.ts:438-441); OpenAI's stays the native
  Decisions API, and OpenAIProvider::evaluation builds the Responses
  adapter ("openai.evaluation", openai 4.0.83 openai-provider.ts:355-358).

API (N6):
- No capability struct. Limits are checked in do_decide before HTTP.
- Request extensions move to provider_options.<provider>: images, OpenAI
  choice_values (typed native values) and safety_identifier, runtime
  protocol. Result extensions move to provider_metadata.<provider>:
  probability_source, per-answer confidence, OpenAI typed values, Laya
  abstentions, latency_ms.
- An OpenAI refusal or a vLLM skipped question fails the decision; a V4
  decision answers every question.
- Recordings hold the redacted DecideOptions and the DecideResult, with no
  capability snapshot; mock replay returns the recorded result.
- FFI: aimux_decide takes DecideOptions and returns DecideResult;
  aimux_decision_capabilities keeps its export and returns the model's V4
  properties. Bindings: capabilities() is describe() (Node
  describeDecisionModel, Python describe_decision_model).

Fixtures (N8): the decision fixtures and the live Jev recordings were
converted to the V4 shape by a deterministic script. Re-decoding both live
Jev wire responses through the new adapter gives the converted results
(decision_recording_test).

Tests pinned to upstream: decision_test (questions_have_the_v4_shape,
invalid_input_is_rejected_before_the_model_is_called,
unsupported_question_types_fail_before_the_model_is_called,
transient_failures_are_retried_twice_by_default,
answers_are_validated_once_after_the_retries,
the_result_completes_usage_and_response,
invalid_answers_report_the_upstream_messages, ...); custom_provider_test
(decision_model::*, decision_*, registry_reports_missing_decision_models);
decision_language_model_test
(provider_defaults_to_the_language_provider_with_a_decision_suffix,
errors_use_the_upstream_decision_messages,
the_language_model_call_joins_the_decision_recording); provider tests for
OpenAI, Google, Jev and the runtimes; FFI, Node and Python tests.

Docs: docs/api/decision.md describes the V4 API; the alignment doc §3.1,
§3.5, §5 and §9.1 (decide telemetry is a known gap); revision lines on
RFC-0037 and RFC-0038; CHANGELOG Breaking entries.

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

* docs: video polling, retry comments and generated types after the merge

- CHANGELOG: layer 4's video bullet named RunwaymlConfig, which layer 5
  replaced with RunwaymlProviderSettings; it now says RunwayML no longer
  paces its own polling. Layer 5's single-modality bullet no longer says
  that VideoModel::poll_config() stays, since layer 4 removed it (#230
  C19).
- Alignment doc §5: the video poll pacing row becomes an upstream-aligned
  row. Neither the provider nor VideoModel has poll settings, and
  VideoCallOptions.poll overrides the AI SDK defaults of 5000 ms / 600000
  ms per field (ai 7.0.127 generate-video/generate-video.ts:543-544), as
  §8.1's delete list already says.
- Generated TS (scripts/gen_ts_types.py): AiMuxError gains LoadApiKey and
  LoadSetting next to MissingToolResults and ToolChoiceViolation;
  CallOptions and GenerateTextOptions have no body_overrides; the
  max_retries comments lose the retry_config() clause; the InputRecord
  comment no longer names body_overrides.

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

* fix(core)!: bytes are base64 strings in the public JSON (#232 C33)

The AI SDK's JSON carries a `Uint8Array` as a base64 string
(`@ai-sdk/provider-utils@5.0.53` uint8-utils.ts:12-23) and reads one
with `atob`'s forgiving decode (:6-10). The public JSON wrote bytes as
arrays of numbers and read either form. Now:

- `ContentPart` image `image` and file `data` (`DataContent`,
  types/content-part.ts:42, :75) are base64 strings
  (`wire::base64_bytes`).
- `FileBytes`, `AudioData`, `AudioInput`, `AudioChunk`,
  `ImageFileData` and `VideoFileData` (the SDK's `Uint8Array | string`,
  shared-v4-file-data.ts:7-10, speech-model-v4-result.ts:16) serialize
  `Binary` as base64 and `Base64` verbatim, and parse a string as
  `Base64` (`wire::base64_wire_enum!`). `ImageOutputs` is an array of
  base64 strings (image-model-v4-result.ts:23), video `binary` data a
  base64 string (video-model-v4-result.ts:28-29). An array of numbers
  is rejected. `FileBytes` equality compares bytes across the two forms.

Bindings keep their byte-valued APIs and encode at the JSON boundary:
Python `Base64Bytes` (bytes are encoded), Go `Bytes` (base64 only, read
as atob reads it), Java `Types.Base64Bytes` and field serializers,
Kotlin `Base64Bytes` / `Base64ByteListSerializer`, Swift `Base64Bytes`,
Dart `encodeBase64Bytes` / `decodeBase64Bytes`. Generated TypeScript
types are `string`. docs/api byte notes and CHANGELOG are updated.

Tests: aimux-core ai_sdk_json_test (prompt_bytes_are_base64_strings,
file_bytes_are_one_base64_string, model_result_bytes_are_base64_strings),
wire::tests::base64_matches_btoa_and_atob; Node e2e image prompt and
speech audio; Python test_image_bytes_are_base64_on_the_wire and speech;
Go TestE2E_SpeechResultParsing; Java promptAndFileBytesAreBase64Strings
and the serializer tests; Kotlin FileBytes/content part tests and speech;
Swift and Dart tests updated (not run here: no toolchain).

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

* fix(decision): decision image bytes are a base64 string (#232 C33a)

`DecisionImage.data` is `FileBytes`, which the previous commit puts on
the wire as one base64 string, as the AI SDK writes inline image bytes
(`ImagePart.image: DataContent`, `@ai-sdk/provider-utils@5.0.53`
types/content-part.ts:42; uint8-utils.ts:12-23). This pins that shape
for decisions and makes the providers read the string the same way the
wire does: `inline_image` encodes binary with `wire::encode_base64` and
validates a string with the forgiving `wire::decode_base64` (atob rules,
uint8-utils.ts:6-10), sending the string on verbatim as
`convertToBase64` does.

Python `decide` encodes `bytes` image data to base64 before building
the JSON; the other bindings take the options as JSON and the Node type
is `string` already. docs/api/decision.md and CHANGELOG say so.

Tests: aimux-core ai_sdk_json_test::decision_image_bytes_are_a_base64_string;
aimux-providers decision_support::tests::inline_image_reads_bytes_as_the_ai_sdk_does;
Python test_openai_images_native_values_and_score_descriptions[as_bytes].

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

* feat(core)!: the operations take an injectable download, as the AI SDK's (#230 C10)

The user operations now download where the AI SDK's do, through a
transport the caller can replace (D26). The new public `download` module
has `Download` (one URL, the `download` option of `transcribe` and
`generateVideo`), `create_download(max_bytes)` (the guarded download,
`createDownload`), `DownloadFunction` (the prompt download, the AI SDK's
`experimental_download`) and `create_default_download_function`.

- generate_text / stream_text: inside the operation, before the model
  call, the prompt's file URLs (user FileUrl parts and `file` items of
  tool outputs; not data: URLs, assistant file parts or deprecated
  file-url items) are planned against the model's `supported_urls()`
  (`SupportedUrls::is_supported`, provider-utils `isUrlSupported`) and
  passed to `GenerateTextOptions::download`; downloaded files are sent
  inline (ai 7.0.127 convert-to-language-model-prompt.ts:36-55,
  :444-540, :567-578; generate-text.ts:963-971). The downloads count
  against the operation timeout and observe the abort signal.
- transcribe: new `AudioInput::Url`, downloaded once with
  `TranscriptionCallOptions::download` before the retries
  (transcribe.ts:56, :109-117, :192-199); the downloaded media type
  replaces the given one. Transcription models return
  `AudioInput::url_error()` for a URL.
- generate_video: each video returned as a URL is downloaded with
  `VideoCallOptions::download` and returned as `VideoData::Binary`, with
  upstream's media type order minus byte sniffing (generate-video.ts:136,
  :253-261, :383-410).

Without a download each operation uses the guarded download. Provider
video tests inject a recording download. Docs, CHANGELOG and the
generated TS types follow.

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

* fix(core)!: JSON null is kept apart from a missing value (#232 C34)

The AI SDK's JSON omits undefined and keeps null. Fields that are
`unknown` upstream read an explicit `null` as a present value and write it
back, and are omitted only when they were missing: response and request
`body` (language-model-v4-generate-result.ts:41, :56 and the speech,
transcription, embedding, reranking and search results),
`APICallError.data` (errors/api-call-error.ts:18) and the tool approval
request's `inputSchemaInput` (tool-approval-request.ts:41).

`aimux_core::wire::present` is the `deserialize_with` for those
`Option<Value>` fields: `None` is missing, `Some(Value::Null)` is null.

Bindings:
- Python serializes without `exclude_none`; a field that was set to
  `None` and is typed `Any` is written as `null`.
- Go reads the result bodies as `json.RawMessage`.
- Kotlin adds `JsonValuePresentSerializer`.
- Swift adds a `decodeIfPresent(JSONValue.self)` overload that keeps null.
- Dart adds `hasBody` / `hasInputSchemaInput`.
- Java's `JsonNode` fields already read null as `NullNode`; a test pins it.

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

* docs(changelog): note that JSON null is kept apart from missing (#232 C34)

The Breaking entry for 52bd7237, left out of that commit.

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

* fix(providers): layer 6 provider code takes layer 5's types

The merged layer 6 code still used the types layers 2 to 5 replaced:

- Bedrock, Anthropic, Mistral and the OpenAI Responses tool helpers take
  LanguageModelV4ToolChoice, as CallOptions.tool_choice now is.
- Provider tool ids and custom content kinds are NamespacedKind: the
  OpenAI tool factories build `openai.<kind>` through one helper, the
  compaction, container upload and Gemini image search ids are checked
  constants, and the prefix checks read the id as a string.
- supported_urls is async: the Bedrock chat model implements it so, the
  openai-compatible dialect holds layer 5's SupportedUrlsFn and the Vertex
  xAI partner returns a ready future.
- xai::convert::resolve_provider_reference takes SharedProviderReference.
- Tests: model-level calls carry no max_retries (an operation setting
  now), supported_urls tests await, ProviderTool ids parse, and layer 5's
  reasoning test calls convert_to_responses_input with this layer's five
  arguments.

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

* fix(providers): the layer 6 transcription models refuse URL audio

Layer 5 added AudioInput::Url, which only `transcribe` downloads. The Azure
speech model and the Vertex Gemini transcription model return
AudioInput::url_error() for it, as the other models do.

Layer 5 also moved the shared `{ error: { message } }` handler out of
provider-utils. The Azure speech model gets its own handler with the same
mapping (azure-speech-transcription-model.ts:72-79 for the message).

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

* fix(core)!: file content parts are the AI SDK FilePart (#232 C35)

A file part has one tag, `file`, and its `data` is what the AI SDK's
`FilePart.data` is in JSON (provider-utils types/content-part.ts:62-102,
file-data.ts:44-48): a tagged `FileData`
(`{type: "data" | "url" | "reference" | "text"}`), a string (base64 or a
URL) or a bare `{provider: id}` reference. The aimux-only `file-base64`,
`file-url` and `file-reference` parts are removed. `reasoning-file` data
also takes a bare string (content-part.ts:164).

The prompt conversion follows `convertToLanguageModelV4FilePart`
(ai prompt/file-part-data.ts:89-141):
- a string is a URL when it parses as one, otherwise base64;
- a URL string that parsing changes keeps the text as `originalUrl`;
- a `data:` URL becomes inline data, and its media type wins over the
  part's `mediaType` (convert-to-language-model-prompt.ts:294-327);
- a `data:` URL inside `{type: "data"}` is rejected.
Upstream rejects that last case with InvalidDataContentError; aimux reports
AI_InvalidPromptError.

Response messages write generated files as bare base64 strings
(to-response-messages.ts:69-84).

The constructors `ContentPart::file`, `file_base64`, `file_url` and
`file_reference` stay and build the tagged form.

Bindings:
- Python, Java, Kotlin, Swift and Dart replace the three part classes with
  a typed `FilePartData` / `ReasoningFilePartData`.
- Node regenerates its types.
- Go documents the shape and gains an e2e test.

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

* test(anthropic): an unparsable stream chunk is an error part

Layer 5's raw-chunks test expected the chunk that does not parse to come
out as Err. Layer 6 emits it as StreamPart::Error and keeps streaming, as
anthropic-language-model.ts:1960-1963 enqueues `{ type: 'error' }`. The
test now expects the error part.

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

* fix(providers): Cohere, Open Responses and xAI Responses declare their supported URLs

Layer 5 made generate_text and stream_text download every prompt URL the
model's supported_urls() does not cover. These three layer 6 models kept
the empty default, so their image URLs (and xAI's PDF and text URLs) were
downloaded and sent inline, where their packages pass them through:

- Cohere chat: HTTP(S) images (cohere-chat-language-model.ts:51-53).
- Open Responses: HTTP(S) images (open-responses-language-model.ts:101-103).
- xAI Responses: HTTP(S) images, PDFs and text, `xaiResponsesSupportedUrls`
  (xai-responses-language-model.ts:142-149, :181).

Their converters already send these URLs as they are.

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

* fix(core)!: generated files are {base64, mediaType} (#232 C46)

A generated file is the AI SDK's `GeneratedFile`, whose JSON is
`{base64, mediaType, providerMetadata?}` (ai generate-text/generated-file.ts:10-32).
The text results' `files[]` held the provider's `{type: "data" | "url", ...}`
shape; they now hold `GeneratedFile`. The `file` and `reasoning-file` parts
of `content`, `reasoning` and the text stream are
`{type, file: GeneratedFile, providerMetadata?}`
(convert-language-model-content.ts:55-70, stream-language-model-call.ts:569-590):
the part keeps the provider metadata and the file has none, as upstream.

Types:
- `LanguageModelFile` is the provider-level file (`LanguageModelV4File`:
  `mediaType`, tagged `data`), used by `GenerateContent`, `StreamPart` and
  the provider crates.
- `GeneratedFile` is the text-level file; `base64()` and `bytes()` read it.
- `GeneratedFileOutput` replaces `ReasoningFileOutput` and also carries
  plain files.
- `StreamPart::map_reasoning_file` becomes `map_file` and maps both.

Upstream's stream file objects are `DefaultGeneratedFileWithType`, which
add `type: "file"` to the file's JSON; aimux does not write that field.

Bindings:
- Python, Java, Kotlin, Swift, Dart and Go gain a `GeneratedFile` type
  with a bytes accessor, and their file stream parts wrap it.
- Node regenerates its types.
- Go types `GenerateTextResult.Files` as `[]GeneratedFile`.

Tests pin the JSON to upstream:
- core `generated_files_are_base64_and_media_type`;
- providers `generated_file_is_base64_and_media_type_in_text_results`
  (Google image cassette);
- a test in each binding.

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

* test: restore the hand-written unit tests 48a92aaf deleted that still cover live code

48a92aaf removed hand-written tests along with the code it replaced. These
still exercise code that is live on layer 6, so they come back, verbatim
where they compile and adapted where layer 5/6 moved the API:

- aimux-core retry: prepared_retries_use_the_fixed_delay_and_backoff
- aimux-ffi exports smoke: provider_from_env_handle
- anthropic tool_name_mapping: unmapped_names_pass_through,
  function_tools_are_ignored
- bedrock event_stream: the six frame round-trip / CRC rejection tests
  (decode_messages backs decode_stream, which the Converse stream uses)
- google utils: smoke (GoogleJsonAccumulator, used by Vertex)
- openai-compatible chat: an_error_structure_decides_the_message_and_code
  and the lazy-base-URL test (ChatProfile/package fields added)
- moonshotai: the three top-level cached_tokens usage tests
- provider_error anthropic_stream_errors: both mid-stream error tests,
  adapted to upstream's behaviour (an error event is an error part and no
  finish follows)
- bedrock conformance: do_stream_returns_parts

The Bedrock conformance stream test only passed before because the old
parser ignored bytes it could not frame. The eight nova-lite stream
cassettes from rig store the base64 event stream under `response.body`,
so replay served base64 text. They now use `response.body_base64`, as
tests/common/replay.rs documents for non-UTF-8 bodies; the bytes are
unchanged.

The two OpenAI embedding cassette tests are not restored: their
cassettes were recorded with encoding_format "base64", while
@ai-sdk/openai sends "float" and accepts only number arrays.

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

* fix(codex): Codex models declare the OpenAI Responses supported URLs

Codex speaks the OpenAI Responses API through the shared Responses model
and converter, but its config kept the empty default. Since layer 5,
generate_text and stream_text download every prompt URL a model does not
declare, so Codex prompts had their image and PDF URLs downloaded and sent
inline. The OpenAI Responses model passes those URLs through
(openaiResponsesSupportedUrls, openai-responses-language-model.ts:209-212).

The Codex model config now takes the same patterns as the OpenAI
provider's responses config (HTTP(S) images and PDFs), and CodexModel
reports them on both channels.

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

* fix(huggingface): pin @ai-sdk/huggingface 2.0.62 and declare its supported URLs

@ai-sdk/huggingface was not pinned. 2.0.62 is the version whose
dependencies match our pins: @ai-sdk/openai-compatible 3.0.62,
@ai-sdk/provider-utils 5.0.53 and @ai-sdk/provider 4.0.21 (2.0.61 still
has openai-compatible 3.0.61, and 2.0.63 moves to 3.0.63 and 5.0.54).
VERSIONS.json carries the registry `dist.integrity` (sha512-FCjGyr...),
which equals the sha512 of the packed tarball the port was checked
against. The recorder's package.json, package-lock.json and PINNED_ONLY
list it too. The lock was regenerated with `npm install
--package-lock-only`, which adds only this entry and its nested
provider-utils 5.0.53.

aisdk_pins gains HUGGINGFACE (2.0.62, provider-utils 5.0.53), covered by
the existing drift tests, plus a test that its openai-compatible
dependency is the openai-compatible pin. The provider's transport is now
the package transport for that pin. Upstream `createHuggingFace` adds no
package part (huggingface-provider.ts:68-75), so requests still carry only
`ai-sdk-provider-utils/5.0.53`; a test fixes that.

The Responses model declares HTTP(S) image URLs, as upstream does
(huggingface-responses-language-model.ts:67-69). It used to keep the empty
default, so since layer 5 generate_text downloaded image URLs that the
converter already sends as `input_image` URLs.

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

* fix(core)!: timestamps are toISOString() strings (#232 C48)

Response timestamps are a `Date` upstream
(provider language-model-v4-response-metadata.ts:10; ai
types/language-model-response-metadata.ts:22 and the image, speech,
transcription, video and reranking metadata), and its JSON is
`toISOString()`: `2025-03-06T13:50:19.000Z`. aimux wrote each provider's
own string instead: chrono's `+00:00` with nanoseconds, Bedrock's raw HTTP
`Date` header, and Open Responses' Unix seconds as digits.

`aimux_core::Timestamp` replaces `Option<String>` on every response
timestamp. It holds a UTC time at millisecond precision in years 0000-9999,
as a `Date` does:
- serializing writes `toISOString()`;
- parsing reads any RFC 3339 date-time and rejects any other text, so a
  value read and written back is `new Date(text).toISOString()`;
- the TypeScript type stays `string`.

Providers build timestamps as upstream does:
- `Timestamp::now()`;
- `from_unix_seconds(created)` for `new Date(created * 1000)` (openai
  responses/openai-responses-language-model.ts:1509, open-responses
  open-responses-language-model.ts:579);
- `from_http_date` for Bedrock's `new Date(headers.date)`
  (amazon-bedrock-chat-language-model.ts:864-867).

The unused `util::rfc3339_now` helper is removed, and core gains a chrono
dependency (already in the lock file).

Tests:
- core `timestamps_are_iso_strings` (golden JSON and rejected forms);
- `timestamp::tests` (`toISOString`, `new Date(seconds * 1000)`, RFC 3339,
  HTTP dates);
- OpenAI and Mistral upstream e2e expectations move to the `.000Z` form;
- Go, Java, Kotlin, Python and Node each check a mocked `created` in the
  result's `response.timestamp`.

Binding types stay strings; their docs say how to parse one.

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

* chore(node): export the generated Timestamp type (#232 C48)

`scripts/gen_ts_types.py` lists `types/Timestamp.ts` (`string`) in
`types.ts`; the C48 commit added the file but not that line.

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

* fix(core)!: text and reasoning stream deltas carry text (#229 B9)

The AI SDK's `TextStreamPart` names the delta of a `text-delta` and a
`reasoning-delta` part `text`
(ai generate-text/stream-text-result.ts:435-440, :466-471). It copies the
field from the V4 part's `delta` (stream-language-model-call.ts:496-501).
`tool-input-delta` keeps `delta` (stream-text-result.ts:496-501).

aimux's `TextStreamPart::TextDelta` and `ReasoningDelta` now have a `text`
field. `into_text_stream_part` maps the provider-level `StreamPart`, which
keeps V4's `delta`, onto it.

Bindings:
- Go: `TextDeltaPayload.Text`.
- Java: `getText()` / `text(...)`.
- Kotlin, Swift and Dart: a `text` property or associated value.
- Python: stream parts read `text`.
- Node: regenerated types.
The web console, examples and docs read `part.text`.

Tests:
- core `text_stream_deltas_are_text` pins the three delta parts and rejects
  a `delta` key on a text delta.
- Each binding checks the wire keys or round-trips the parts. The Swift and
  Dart tests were edited without a toolchain to run them.

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

* fix: layer 7 code takes layers 2-6's types

The merge of [6/8] left layer 7's decision and transcription code on
the pre-merge shapes:

- indexmap is a workspace dependency since layer 3; the duplicate key
  in aimux-core is gone and the other crates take the workspace entry.
- WrapProvider forwards decision_model with the provider factory's
  Option<Result<..>> signature.
- CallOptions no longer carries call_id (layer 4): the decision
  language model's exchanges share the decision's recording context,
  which supplies the call id.
- Provider metadata and options are IndexMap namespaces (layer 3).
- The google, groq, mistral and xai transcription models refuse
  AudioInput::Url (layer 5) with AudioInput::url_error(), as every
  transcription model does; only transcribe downloads a URL.
- Tests: SharedProviderOptions literals, download: None in
  TranscriptionCallOptions, do_stream's Option (C21), and the replay
  tool's decision_model probe; the two test modules in
  transcription_model.rs are one.

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

* test(providers): the layer 7 transcription models refuse URL audio

google, groq, mistral and xai each get url_audio_is_refused_before_any_request:
an AudioInput::Url fails with the url error before any request is sent,
as layer 5 pins for the transcription models it already had.

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

* fix(core): decide runs its model call in the operation settings

Layer 4 moved max_retries and timeout out of CallOptions into the
operation's OperationSettings, which generate_text and stream_text
scope around their model calls. decide now scopes its model call the
same way, so a language model under DecisionLanguageModel (a router or
MoA reading the child retry count, a TraceLayer) sees the decision's
settings. A direct do_decide call has none, as a direct do_generate.

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

* refactor(core)!: provider namespace maps keep insertion order (#229 NP2-1)

The AI SDK types provider options, provider metadata and provider
references as JS objects (`@ai-sdk/provider@4.0.22`
shared-v4-provider-options.ts:24, shared-v4-provider-metadata.ts:24,
shared-v4-provider-reference.ts:20). JS objects keep their keys in
insertion order.

Rust: this is 4197573e, already on the lower layers, ported by hand onto
this layer.
- `SharedProviderOptions` and `SharedProviderMetadata` become one alias,
  `NamespaceMap = IndexMap<String, JsonObject>`.
- `SharedProviderReference` becomes `IndexMap<String, String>`.
- The inner objects already kept order through serde_json's
  `preserve_order`.
- Provider code and tests written on this layer that still built a
  `HashMap` now build an `IndexMap`.
- The Anthropic options-name rewrite uses `shift_remove`.

Bindings: the order must hold end to end.
- Node (`JSON.parse`), Python (`json.loads`, pydantic dicts), Java
  (Jackson `JsonNode`), Kotlin and Dart already keep it.
- Go maps sort their keys when marshaled, so:
  - `Tool.ProviderOptions` and `TranscriptionSessionOpts.ProviderOptions`
    become `json.RawMessage`;
  - `UploadFileResult.ProviderReference` becomes `ProviderReference` (raw
    JSON with `Get(provider)`).
- Java's `UploadFileResult` defaults to a `LinkedHashMap`.
- Swift's typed values are `Dictionary`, which has no order. The Swift API
  doc says so, and that the JSON strings keep the order.

The generated TypeScript is unchanged.

Tests:
- core `shared::tests` (outer and inner keys, out of alphabetical order);
- Node, Python, Go, Java and Kotlin send an invalid tool call whose
  `providerMetadata` keys are out of order through the native
  `applyToolCallRepair`, decode the result into their typed values and
  check the order;
- Go, Java and Kotlin also round-trip an upload `providerReference`;
- a Dart test decodes and re-encodes a tool call's metadata. It was
  written without a toolchain to run it.

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

* test(kotlin): the OpenAI decision test sends the fixture's options in order

V4 decision questions and choice criteria are maps, and the OpenAI
request lists them in the caller's order. The test re-serialized the
fixture's options through org.json's JSONObject, which does not keep key
order, so the request listed score before choice and failed the
comparison with the fixture. It now passes the options' raw text.

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

* docs(node): regenerate index.d.ts for applyToolCallRepair's doc

Layer 4 rewrote the Rust doc of applyToolCallRepair (it repairs the
call in the result's steps) without regenerating index.d.ts, so every
napi build rewrote the file. This is the napi build's output for the
merged source; only that doc comment changes.

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

* test(node): the reasoning replay test gives its tool call a result

"assistant without reasoning omits reasoning_content" sent an assistant
tool call with no tool result. Since 252371bf (#229 C38) the prompt is
checked as the AI SDK's convertToLanguageModelPrompt checks it, and that
prompt fails with MissingToolResultsError before any request. The
conversation now carries the tool message with the call's result, as
the other replay tests do; the assertion on the assistant message is
unchanged.

No other binding test sends a tool call without its result.

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

* test(bindings): the DeepSeek usage.raw tests read the step usage

Since d3fd8207 a result's `usage` is the usage summed over its steps,
as the AI SDK's addLanguageModelUsage, which carries no raw object
(generate-text.ts:1526-1544, types/usage.ts:122-155). The provider's raw
usage, with DeepSeek's prompt_cache_hit_tokens / _miss_tokens, is on
each step's `usage.raw`. The Node and Python cassette tests (RFC-0016
M10) read steps[0].usage.raw and check that the summed usage has none.
The other bindings read raw usage only from stream parts.

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

* docs(node): regenerate index.d.ts for applyToolCallRepair's doc

d3fd8207 rewrote the Rust doc of applyToolCallRepair (it repairs the
call in the result's steps) without regenerating index.d.ts, so every
napi build rewrote the file. This is the napi build's output; only that
doc comment changes.

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

* test(swift): the OpenAI decision test sends the fixture's options in order

The same order loss as the Kotlin test before efd10d65: the test
re-encoded the fixture's options with JSONSerialization, whose
dictionaries keep no key order, and compared the request as an
NSDictionary, whose arrays are ordered. V4 questions and choice
criteria are maps, and the OpenAI request lists them in the caller's
order. The test now passes the raw text of the fixture's options object.

Not compiled here (no Swift toolchain); the brace matcher was checked
against the fixture with an equivalent script.

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

* fix(core)!: standalone tool calls and sources carry their type tag (#229 NP2-7)

Outside `content` the AI SDK writes a tool call as `TypedToolCall`,
`{type: 'tool-call', ...}` (ai generate-text/tool-call.ts:8-14), and a source
as `Source`, a `LanguageModelV4Source` tagged
`{type: 'source', sourceType, ...}` (ai types/language-model.ts:92; provider
language-model-v4-source.ts:8, :36). This covers:
- the text results' `toolCalls` and `sources` (generate-text-result.ts:73,
  :78; stream-text-result.ts:173, :180);
- an approval request's `toolCall` (tool-approval-request-output.ts:20).

The repair function's `toolCall` is a tagged `LanguageModelV4ToolCall`
(language-model-v4-tool-call.ts:7). aimux wrote all of these untagged.

aimux's `ToolCall`, `RawToolCall` and `Source` stay untagged, because the
`content` and stream unions write their tag. New `wire` helpers write the
tag where a value stands alone:
- `TypeTag`;
- `typed` / `typed_vec` for fields;
- `Tagged` for a value serialized on its own.

The tag comes first. Reading accepts a value without the tag (JSON written
before it) and rejects a different one.

Tagged fields:
- `GenerateTextResult` and `StreamTextResultAggregated`: `toolCalls` and
  `sources`;
- `ToolApprovalRequestOutput.toolCall`;
- the repair context's `toolCall`, and the call `apply_tool_call_repair`
  returns through the C ABI, Node and Python.
`apply_tool_call_repair_to_result` writes the patched entry tagged. A
repair reply's call may carry the tag.

TypeScript types these fields as the new `TypedToolCall` and `TypedSource`
(generated, export only). Node's `ToolCallRepairContext.toolCall` is tagged.

Bindings: each one's standalone `ToolCall`, `RawToolCall` and `Source` now
writes the tag and checks it on read.
- Go: `MarshalJSON` / `UnmarshalJSON`.
- Java: a fixed `type` field plus a checking setter.
- Kotlin: an `@EncodeDefault` property checked in `init`.
- Swift: hand-written `Codable`.
- Dart: `toJson` / `fromJson`.
- Python: a `Literal` field with a default.

Tests:
- core `standalone_tool_calls_and_sources_are_tagged` (golden JSON, key
  order, content written once, untagged read, mistag rejected);
- FFI smoke test (context, repaired call, patched result);
- one test in each binding.
The Swift and Dart tests were written without a toolchain to run them.

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

* fix(core)!: integral tool input numbers are written as JavaScript writes them (#229)

The AI SDK parses a tool call's argument text with `JSON.parse`
(ai generate-text/parse-tool-call.ts:116, :256; provider-utils
parse-json.ts:97, secure-json-parse.ts:31). JS has one number type, so
`JSON.stringify(JSON.parse('{"n":1e2}'))` is `{"n":100}`. serde_json keeps
a number written with a fraction or exponent as an `f64` and writes it back
as `100.0`, so aimux's `toolCalls[].input` differed from the AI SDK JSON for
`1e2`, `3.0` or `-0.0`.

The least invasive fix: `parse_input_text`, which both the valid and the
invalid tool-call paths now use, walks the parsed value once and replaces
each finite, integral `f64` below 1e21 in magnitude with the integer JS
would write. Rust's `f64` `Display` gives the same shortest round-trip
digits as JS `Number::toString`, padded with zeros (`2 ** 60` is
`1152921504606847000`), and `-0` becomes `0`. Nothing else changes: no
`arbitrary_precision` feature, no custom number type, and no change to how
other JSON is read or written.

Remaining gaps, documented in the alignment doc:
- integral values from about 1.8e19 to 1e21 do not fit `i64`/`u64` and stay
  floats;
- integer literals above 2^53 stay exact, where JS rounds them.
Non-integral numbers and numbers of 1e21 or more already match JS
(`1.5`, `1e+300`).

Tests:
- core `integral_input_numbers_are_written_as_javascript_writes_them` pins
  valid and invalid calls and the `js_integer` edge cases;
- Python and Go e2e tests read integer tool input from a mocked response.

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

* docs(rfc): RFC-0035 shows the final repair wire and upstream's control flow

The RFC still showed the repair context and reply in the old snake_case,
externally tagged form. It now shows the JSON the exports write and read:
- the context is `{toolCall, error, inputSchema, tools, messages,
  instructions}`;
- `toolCall` carries `type: "tool-call"`, as the repair function's
  `LanguageModelV4ToolCall` does (provider 4.0.21
  language-model/v4/language-model-v4-tool-call.ts:7);
- `error` is tagged by `name` (`AI_InvalidToolInputError`);
- the reply is `{"type": "repaired", "toolCall": ...}`;
- a failed reply becomes `AI_ToolCallRepairError` with an `AI_Error` cause;
- the result patch rewrites `toolCalls[]` and `responseMessages`, keyed by
  the call's `toolCallId`.

It also takes the corrections from the RFC check (rfc-check patch 0002),
with line numbers moved to ai 7.0.127:
- `inputSchema` is a value; upstream passes a function of the tool name
  (generate-text/tool-call-repair-function.ts:31).
- A repaired call that still fails is not wrapped in
  `AI_ToolCallRepairError`: it is an invalid call carrying that parse's
  `AI_InvalidToolInputError` / `AI_NoSuchToolError`, with the original id,
  name and input. Upstream wraps only an error thrown by the repair
  function (generate-text/parse-tool-call.ts:90-96); a failed re-parse
  goes to the invalid-call branch (:103-134). `apply_repair_outcome`
  already behaves so.
- Swift's abort waiting for the hook differs from upstream, which races
  the repair function against the abort signal (:73-74, :137-173).

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

* test(node,python): decision usage omits unknown totals, never null

Ports the test half of li's fork PR #52 (cbfb76c9, b8fdda66).

`Usage` writes an unknown count by omitting `total`, as the AI SDK's
`LanguageModelV4Usage` types it `total: number | undefined` (provider
4.0.21 language-model/v4/language-model-v4-usage.ts:14, :39). The Node and
Python decision tests read `inputTokens` / `outputTokens` already, but
accepted an explicit `"total": null`:
- Node compares `total` with `undefined` instead of coalescing both sides
  to `null`;
- Python asserts that `total` is absent when the mocked count is missing
  or null.

The fork's other two changes are already in this branch: the Python
recording test reads `outcome.decisionResult` (4a1f3684), and the Flutter
typed-call helper no longer declares options no test passes (c81e2211).

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

* fix(core): keep recording schema 2 and read the recordings master writes

The migration series stays on recording schema 2, as master writes it;
the full schema 3 comes with the last layers (li, 2026-10-12).

- RECORDING_SCHEMA = 2. Schemas 1 and 2, what released versions write,
  load; 0 and 3 and above are refused with the existing clear error.
- Reading a master recording: its configuration snapshot (base_url,
  api_key_source, profile, provider_options) is ignored, provider_id
  defaults to empty, and a prompt this version cannot read (a system
  message with a part array) loads as empty.
- What cannot be replayed fails at replay time and names the cause:
  ProviderRecord::registry_id for registry rebuilds (aimux-providers,
  aimux-replay, aimux-web), InputRecord::unreadable_prompt for request
  replay and the mock matcher, master's call options, and decision
  capabilities that only master's provider.profile holds.
- New fixtures: two OpenAI chat recordings written by master a8e3a996's
  JsonlRecorder, and master's jev_systemone_live.jsonl byte for byte.
  The V4 Jev fixture goes back to schema 2.
- The S2-5 complete rule is unchanged.
- Docs: RFC-0023 revision line, alignment doc §9.1, decision API doc,
  CHANGELOG.

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

* chore(core): keep the removed key-source field name out of code

scripts/check_provider_boundaries.sh refuses the removed name in code
and comments. The schema doc describes master's configuration snapshot
in words, and the master-shaped unit test drops that one key; the
master fixture (excluded from the check) still carries it.

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

* docs(changelog): legacy vertex and bedrock keys are still read and written

The bullet said the legacy keys are neither read nor written; the code
reads them and writes metadata under both keys, as the upstream
packages do (amazon-bedrock-chat-language-model.ts:154-167,
google-language-model.ts:163-171), and tests pin that.

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

* docs(changelog): decision recordings carry no capability snapshot

The provider-factory bullet still said decision recordings keep the
capabilities under input; since decisions follow DecisionModelV4 they
carry none, as the DecisionModelV4 entry says.

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

* fix(go)!: Error.Data keeps a null API call error data apart from none

The AI SDK's `APICallError.data` is `unknown` (@ai-sdk/provider
errors/api-call-error.ts:18, :53): a provider error body of `null` is
data `null`, not missing data. The core keeps the two apart (#232 C34)
and aimux_error_provider_data returns NULL only for missing data, but Go
decoded the JSON into `any`, where both became nil.

`Error.Data` is now `json.RawMessage`: nil when the error carries no
data, `null` when the data was null, the raw JSON otherwise. A test
drives an OpenAI model against a server answering 400 with an object, a
`null` and a non-JSON body.

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

* docs: RFC-0035 names the tests that pin repair; RFC-0026 deltas carry text; Swift key order is a known gap

- RFC-0035 §6 cited contract-tests/fixtures/tool-call-repair.json and a
  `contract_fixture_matches_the_implementation` test; neither exists. The
  section now says there is no shared fixture yet and lists the core,
  C ABI and binding tests that pin each branch.
- RFC-0026's OpenAI-compatible output table reads `TextDelta { text }`
  and `ReasoningDelta { text }`, the stream parts' field since #229 B9.
- The alignment doc's §9.1 known gaps register that the Swift binding's
  typed provider options, provider metadata, provider references and
  `JSONValue.object` are Swift dictionaries and lose key order on a
  decode-encode round trip (#229 NP2-1); the NP2-1 note points there.

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

* refactor!: text results' usage, steps, tool outputs and Output follow the public JSON

Layer 7's types merged into layer 8 in snake_case; they now follow the
same rules as the rest of the public JSON.

- LanguageModelUsage is camelCase (types/usage.ts:10-67); an unknown
  count is omitted, the detail objects are always written.
- StepResult, its model, performance and response are camelCase
  (step-result.ts:138-278, language-model-response-metadata.ts:6-38).
- Typed tool results, tool errors, denied outputs and approval responses
  are camelCase; an approval response's toolCall and the results'
  toolResults[] carry their type tag (generate-text-result.ts:93,
  tool-result.ts:10-42, tool-approval-response-output.ts:7-35).
  TypeScript gains TaggedToolResult.
- Output is tagged by type with the AI SDK's output mode names
  (output.ts:68, :114, :241, :520, :657).

The bindings read and write the camelCase keys; Kotlin, Swift and Dart
keep a parsed JSON null output apart from no output. Pinned by
ai_sdk_json_test's steps_usage_and_tool_outputs_are_the_ai_sdk_json and
output_specifications_are_tagged, and by each binding's tests.

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

* docs(rfc): RFC-0036's stream example writes the public JSON

The aimux_call stream example shows a text-delta with its id and text
(TextStreamPart, stream-text-result.ts) and a finish part with camelCase
finishReason and totalUsage, as the bridge now emits them.

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

* refactor!: decisions follow the public JSON

Layer 7's decision types merged in snake_case; they now write the AI
SDK's JSON.

- DecisionCallOptions, DecisionRounding, DecisionUsage, DecisionResult
  (decision-model-v4-call-options.ts:7-14, decision-model-v4-result.ts:
  31-56), DecideOptions (decide.ts:53-61) and DecideResult
  (decision-result.ts:22-39) are camelCase. DecideUsage omits an unknown
  count instead of writing null (decision-result.ts:27-31).
- The model description is {specificationVersion, provider, modelId,
  supportedQuestionTypes} (decision-model-v4.ts:7-11), built once by
  decision_model_properties for the C ABI, Node and Python.
- What aimux parses inside a provider's namespace is camelCase too:
  images[].mediaType, OpenAI choiceValues and safetyIdentifier, candidate
  scoring's modelRevision, tokenizerRevision, promptFormatVersion and
  encoded.<id>.{promptTokenIds, labelTokenIds}, and the metadata's
  probabilitySource and latencyMs. Options a runtime receives as given
  keep the runtime's names.
- Decision replay matches on providerOptions and refuses recorded options
  with snake_case keys, as language replay does.
- The V4 Jev recording, the contract-test and provider fixtures convert
  their aimux keys; vendor wire bodies are unchanged.
- Python's decide takes the JSON keys as keyword arguments.

Pinned by ai_sdk_json_test::decisions_are_the_ai_sdk_json, the provider
decision tests (snake_case option names are rejected) and each binding's
decision test, which now also checks the camelCase usage.

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

* docs: fix two rustdoc links that fail cargo doc -D warnings

A redundant explicit link target in GenerateTextOptions::download and a
module-doc link to TungsteniteConnector that rustdoc resolves from the
crate root (the module also has an outer doc comment).

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

* docs: plain code spans for the private stream_transcription methods

rustdoc -D warnings rejects intra-doc links to private items.

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

* ci: speed up test builds, deduplicate caches, and use prebuilt cargo-ndk

Cargo.toml configures [profile.release] with fat LTO (lto = true),
codegen-units = 1, and opt-level = "z" so published FFI binaries stay
small. In CI, 11 test-only jobs (Java, Kotlin, Go, Python, Flutter,
Android example, contract tests, Swift) were running full fat-LTO release
builds from scratch even though they upload no release artifacts, while
writing separate 275 MB cache entries per job and exceeding GitHub's 10 GB
repository cache quota.

- Skip fat LTO and -Oz in test-only jobs via top-level CARGO_PROFILE_*
  overrides, while keeping Cargo.toml's full release profile (lto = true,
  codegen-units = 1, opt-level = "z") on node-binding and ffi-build so
  uploaded release artifacts keep their exact sizes.
- Strip debug symbols from dev/test profiles (CARGO_PROFILE_DEV_DEBUG=0,
  CARGO_PROFILE_TEST_DEBUG=0) to speed up linking the 162 integration
  test binaries.
- Build only the cdylib target (cargo rustc --crate-type cdylib) in jobs
  that only link the shared library, avoiding archiving a redundant
  staticlib.
- Enable cache-all-crates on Swatinem/rust-cache and share a single
  "ffi-test" cache key across the FFI test jobs to reduce cache churn.
- Replace `cargo install cargo-ndk` (source build) with
  taiki-e/install-action@v2 and enable Flutter/Gradle/npm/pip caches.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(flutter): typed stream fixtures are JSON maps; the chat mock is DeepSeek

The round-trip cases built empty nested maps as untyped literals, which
Dart infers as Map<dynamic, dynamic>, so the parsers' Map<String, dynamic>
casts failed. The stream test's mock model was Groq, which reads stream
usage from x_groq only (upstream groq-chat-language-model.ts:438), so the
finish part had no input tokens; the mock is DeepSeek again, as in [7/8].

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

* ci: keep uploaded artifacts unchanged and stop PR runs from evicting caches

Fixes on top of the previous commit:

- node-binding builds with `napi build` without `--release`, so its
  uploaded node-* artifacts are dev-profile builds. The workflow-level
  CARGO_PROFILE_DEV_DEBUG=0 stripped their debug info (Linux artifact
  55.3 MB -> 17.6 MB) and its CARGO_PROFILE_RELEASE_* overrides had no
  effect. Debug info is now dropped in rust-test only, where the test
  binaries are linked, and the dead overrides are gone.
- ffi-build goes back to `cargo build`, as release.yml does. Building
  only the cdylib changes the shipped library (x86_64 Linux .so
  9,446,848 -> 7,637,144 bytes, same 129 exports), so the ffi-*
  artifacts would no longer match the release build or earlier runs.
- rust-cache `cache-all-crates` keeps unused registry crates, it does
  not cache workspace crates; dropped. Caches are saved from master only
  (`save-if`); pull requests restore master's caches instead of each
  writing their own, which is what pushed the repository past 10 GB.
- The two node-binding legs on each runner OS (x64 and arm64 targets)
  shared one cache key: one leg failed to save and the other restored a
  cache built for the wrong target. The key now includes the target.
- CARGO_INCREMENTAL=0 is already set by rust-cache; dropped.

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

* test(swift): the plain-text result's content part is type-tagged

GenerateTextResult.content is the AI SDK content array, so a text part
is {"type":"text","text":...}; the test still read the old
externally tagged {"Text":{...}} shape. It now reads the part as the
tool-call test below it does.

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

* ci: fewer jobs per run so pull requests stop queueing for runners

With every stacked pull request running CI, runs wait far longer for a
runner than they spend building: each run had 6 macOS jobs against a
5-job macOS limit and 18 Linux jobs against a 20-job total limit, and
the macOS jobs of one run sat queued for 15 minutes.

- Java (JDK 8, 11, 17, 21) and Kotlin (JDK 17, 21) run in one job
  that builds the FFI library once and runs each JDK in turn: 6 jobs
  and 6 FFI builds become 1.
- The Go binding is tested in the ffi-build legs on Linux and macOS,
  against the static archive that job already builds: the 2 Go jobs
  and their FFI builds go away.
- The x86_64 macOS Node binding is cross-built on Apple Silicon and
  tested with x64 Node under Rosetta instead of on the Intel runner,
  where its build took 371 of the job's 465 seconds.
- scripts/build-ios-xcframework.sh builds both iOS slices in one cargo
  invocation, so they compile in parallel; the output is unchanged.

A run goes from 24 jobs (6 macOS) to 17 (5 macOS).

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

* docs(alignment): the first-chunk timer start is a registered difference

The first-chunk timer keeps starting with the operation, because providers
wait for the first SSE event inside do_stream; starting it with the response
stream would leave a stream that answers 200 and then sends nothing without
a bound. §5 now files it as a product difference, and §9.1 lists it with the
stream retries (streamRetries) among the known gaps.

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

* ci: restore the Flutter SDK cache everywhere, save it from master only

- subosito/flutter-action saves a 1.5-2.2 GB SDK cache per OS. A pull
  request can only restore caches saved on its own branch or on master,
  so with the action's cache on, every open pull request saved its own
  copy and the 10 GB budget filled up again. The action's `cache` input
  turns restore and save off together, so the two are split: a dry run
  of the action gives the key and path, actions/cache/restore runs on
  every branch, the action installs the SDK (a no-op when the restore
  hit), and actions/cache/save runs on master only, at the end of the
  job so the engine artifacts the build downloads are in the cache. The
  pub cache stays off, as on master. The Gradle cache already saves from
  the default branch only.
- bindings/node and bindings/python are their own Cargo workspaces and
  build into bindings/<name>/target, which rust-cache's default (the
  repository's target directory) never covered: the Node and Python jobs
  rebuilt aimux-core and aimux-providers on every run. `workspaces` now
  points rust-cache at each binding's directory.
- The ffi-build comment said the Go binding links the static archive;
  on macOS ld64 picks the .dylib next to it. Comment only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* ci: build test-only FFI libraries at opt-level 1, not 0

aimux_stream_text runs the whole stream future on the caller's thread
(ffi_block_on). With the test-only override at opt-level 0, one stream
through the C ABI needs about 530 KB of stack on the AI SDK branches
(about 300 KB on master), more than the 512 KB stack of a Swift
cooperative thread: the async Swift tests died with SIGBUS on #255 and
#259 once they carried this workflow. At opt-level 1 the same stream
needs about 110 KB; the shipping profile needs about 160 KB. The
library is also 40% smaller, and the build is still far from the
shipping profile's fat LTO with one codegen unit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* ci: pin cargo-ndk and keep pull requests from saving two more caches

cargo-ndk is installed through cargo-binstall, which took whatever was
newest; pin it to 4.1.2, the version CI has been installing.

The contract and Swift jobs were the last two that saved a Rust cache
on every pull request. Like the other jobs they now restore on pull
requests and save only on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* chore: delete the web console's generated ProviderName copy

tools/aimux-web/web/src/types/ProviderName.ts was the eighth copy written
by scripts/gen_provider_names.py, which this branch deletes. Nothing
imports it (it is an orphan on master too), and its header still claimed
the deleted generator. RFC-0036 §12.3 A2 requires every Rust, binding
and web copy and the generator to go.

The three quality-audit reports that cite `gen_provider_names.py --check`
as a current gate get a later-change note, as docs/internal/phase4-audit.md
already has.

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

* ci: type-check the web console against the generated types

The console imports the core types from bindings/node/src/types through
@aimux/types. gen_ts_types.py --check only compares the generated files,
so a renamed or removed Rust type regenerates cleanly and leaves a
dangling import that no pull request check catches. RFC-0036 §13.1 A4
asks for every import to be type-checked.

A new "Web console typecheck" job runs `npm ci && npm run typecheck`
(vue-tsc --noEmit) in tools/aimux-web/web. It runs when the shared code
changes (which includes tools/aimux-web and the Rust crates whose types
are generated) or when bindings/node changes, where the generated types
live; documentation-only changes skip it. "CI result" checks it.
Removing bindings/node/src/types/Recording.ts locally makes it fail on
the two files that import it.

RFC-0029 §9.1 gets a later-change note: the console no longer has its
own copies, and TS_RS_EXPORT_DIR no longer points at the web directory.

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

* docs: size and performance gates follow RFC-0043

#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

* docs: the alignment doc links back to RFC-0036 §12.2 and names the merged 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

* ci: run the AI SDK pin script tests

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

* docs: note the retired contract fixtures in the RFCs an…
eric8810 added a commit that referenced this pull request Oct 17, 2026
* fix(providers): the layer 6 transcription models refuse URL audio

Layer 5 added AudioInput::Url, which only `transcribe` downloads. The Azure
speech model and the Vertex Gemini transcription model return
AudioInput::url_error() for it, as the other models do.

Layer 5 also moved the shared `{ error: { message } }` handler out of
provider-utils. The Azure speech model gets its own handler with the same
mapping (azure-speech-transcription-model.ts:72-79 for the message).

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

* fix(core)!: file content parts are the AI SDK FilePart (#232 C35)

A file part has one tag, `file`, and its `data` is what the AI SDK's
`FilePart.data` is in JSON (provider-utils types/content-part.ts:62-102,
file-data.ts:44-48): a tagged `FileData`
(`{type: "data" | "url" | "reference" | "text"}`), a string (base64 or a
URL) or a bare `{provider: id}` reference. The aimux-only `file-base64`,
`file-url` and `file-reference` parts are removed. `reasoning-file` data
also takes a bare string (content-part.ts:164).

The prompt conversion follows `convertToLanguageModelV4FilePart`
(ai prompt/file-part-data.ts:89-141):
- a string is a URL when it parses as one, otherwise base64;
- a URL string that parsing changes keeps the text as `originalUrl`;
- a `data:` URL becomes inline data, and its media type wins over the
  part's `mediaType` (convert-to-language-model-prompt.ts:294-327);
- a `data:` URL inside `{type: "data"}` is rejected.
Upstream rejects that last case with InvalidDataContentError; aimux reports
AI_InvalidPromptError.

Response messages write generated files as bare base64 strings
(to-response-messages.ts:69-84).

The constructors `ContentPart::file`, `file_base64`, `file_url` and
`file_reference` stay and build the tagged form.

Bindings:
- Python, Java, Kotlin, Swift and Dart replace the three part classes with
  a typed `FilePartData` / `ReasoningFilePartData`.
- Node regenerates its types.
- Go documents the shape and gains an e2e test.

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

* test(anthropic): an unparsable stream chunk is an error part

Layer 5's raw-chunks test expected the chunk that does not parse to come
out as Err. Layer 6 emits it as StreamPart::Error and keeps streaming, as
anthropic-language-model.ts:1960-1963 enqueues `{ type: 'error' }`. The
test now expects the error part.

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

* fix(providers): Cohere, Open Responses and xAI Responses declare their supported URLs

Layer 5 made generate_text and stream_text download every prompt URL the
model's supported_urls() does not cover. These three layer 6 models kept
the empty default, so their image URLs (and xAI's PDF and text URLs) were
downloaded and sent inline, where their packages pass them through:

- Cohere chat: HTTP(S) images (cohere-chat-language-model.ts:51-53).
- Open Responses: HTTP(S) images (open-responses-language-model.ts:101-103).
- xAI Responses: HTTP(S) images, PDFs and text, `xaiResponsesSupportedUrls`
  (xai-responses-language-model.ts:142-149, :181).

Their converters already send these URLs as they are.

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

* fix(core)!: generated files are {base64, mediaType} (#232 C46)

A generated file is the AI SDK's `GeneratedFile`, whose JSON is
`{base64, mediaType, providerMetadata?}` (ai generate-text/generated-file.ts:10-32).
The text results' `files[]` held the provider's `{type: "data" | "url", ...}`
shape; they now hold `GeneratedFile`. The `file` and `reasoning-file` parts
of `content`, `reasoning` and the text stream are
`{type, file: GeneratedFile, providerMetadata?}`
(convert-language-model-content.ts:55-70, stream-language-model-call.ts:569-590):
the part keeps the provider metadata and the file has none, as upstream.

Types:
- `LanguageModelFile` is the provider-level file (`LanguageModelV4File`:
  `mediaType`, tagged `data`), used by `GenerateContent`, `StreamPart` and
  the provider crates.
- `GeneratedFile` is the text-level file; `base64()` and `bytes()` read it.
- `GeneratedFileOutput` replaces `ReasoningFileOutput` and also carries
  plain files.
- `StreamPart::map_reasoning_file` becomes `map_file` and maps both.

Upstream's stream file objects are `DefaultGeneratedFileWithType`, which
add `type: "file"` to the file's JSON; aimux does not write that field.

Bindings:
- Python, Java, Kotlin, Swift, Dart and Go gain a `GeneratedFile` type
  with a bytes accessor, and their file stream parts wrap it.
- Node regenerates its types.
- Go types `GenerateTextResult.Files` as `[]GeneratedFile`.

Tests pin the JSON to upstream:
- core `generated_files_are_base64_and_media_type`;
- providers `generated_file_is_base64_and_media_type_in_text_results`
  (Google image cassette);
- a test in each binding.

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

* test: restore the hand-written unit tests 48a92aaf deleted that still cover live code

48a92aaf removed hand-written tests along with the code it replaced. These
still exercise code that is live on layer 6, so they come back, verbatim
where they compile and adapted where layer 5/6 moved the API:

- aimux-core retry: prepared_retries_use_the_fixed_delay_and_backoff
- aimux-ffi exports smoke: provider_from_env_handle
- anthropic tool_name_mapping: unmapped_names_pass_through,
  function_tools_are_ignored
- bedrock event_stream: the six frame round-trip / CRC rejection tests
  (decode_messages backs decode_stream, which the Converse stream uses)
- google utils: smoke (GoogleJsonAccumulator, used by Vertex)
- openai-compatible chat: an_error_structure_decides_the_message_and_code
  and the lazy-base-URL test (ChatProfile/package fields added)
- moonshotai: the three top-level cached_tokens usage tests
- provider_error anthropic_stream_errors: both mid-stream error tests,
  adapted to upstream's behaviour (an error event is an error part and no
  finish follows)
- bedrock conformance: do_stream_returns_parts

The Bedrock conformance stream test only passed before because the old
parser ignored bytes it could not frame. The eight nova-lite stream
cassettes from rig store the base64 event stream under `response.body`,
so replay served base64 text. They now use `response.body_base64`, as
tests/common/replay.rs documents for non-UTF-8 bodies; the bytes are
unchanged.

The two OpenAI embedding cassette tests are not restored: their
cassettes were recorded with encoding_format "base64", while
@ai-sdk/openai sends "float" and accepts only number arrays.

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

* fix(codex): Codex models declare the OpenAI Responses supported URLs

Codex speaks the OpenAI Responses API through the shared Responses model
and converter, but its config kept the empty default. Since layer 5,
generate_text and stream_text download every prompt URL a model does not
declare, so Codex prompts had their image and PDF URLs downloaded and sent
inline. The OpenAI Responses model passes those URLs through
(openaiResponsesSupportedUrls, openai-responses-language-model.ts:209-212).

The Codex model config now takes the same patterns as the OpenAI
provider's responses config (HTTP(S) images and PDFs), and CodexModel
reports them on both channels.

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

* fix(huggingface): pin @ai-sdk/huggingface 2.0.62 and declare its supported URLs

@ai-sdk/huggingface was not pinned. 2.0.62 is the version whose
dependencies match our pins: @ai-sdk/openai-compatible 3.0.62,
@ai-sdk/provider-utils 5.0.53 and @ai-sdk/provider 4.0.21 (2.0.61 still
has openai-compatible 3.0.61, and 2.0.63 moves to 3.0.63 and 5.0.54).
VERSIONS.json carries the registry `dist.integrity` (sha512-FCjGyr...),
which equals the sha512 of the packed tarball the port was checked
against. The recorder's package.json, package-lock.json and PINNED_ONLY
list it too. The lock was regenerated with `npm install
--package-lock-only`, which adds only this entry and its nested
provider-utils 5.0.53.

aisdk_pins gains HUGGINGFACE (2.0.62, provider-utils 5.0.53), covered by
the existing drift tests, plus a test that its openai-compatible
dependency is the openai-compatible pin. The provider's transport is now
the package transport for that pin. Upstream `createHuggingFace` adds no
package part (huggingface-provider.ts:68-75), so requests still carry only
`ai-sdk-provider-utils/5.0.53`; a test fixes that.

The Responses model declares HTTP(S) image URLs, as upstream does
(huggingface-responses-language-model.ts:67-69). It used to keep the empty
default, so since layer 5 generate_text downloaded image URLs that the
converter already sends as `input_image` URLs.

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

* fix(core)!: timestamps are toISOString() strings (#232 C48)

Response timestamps are a `Date` upstream
(provider language-model-v4-response-metadata.ts:10; ai
types/language-model-response-metadata.ts:22 and the image, speech,
transcription, video and reranking metadata), and its JSON is
`toISOString()`: `2025-03-06T13:50:19.000Z`. aimux wrote each provider's
own string instead: chrono's `+00:00` with nanoseconds, Bedrock's raw HTTP
`Date` header, and Open Responses' Unix seconds as digits.

`aimux_core::Timestamp` replaces `Option<String>` on every response
timestamp. It holds a UTC time at millisecond precision in years 0000-9999,
as a `Date` does:
- serializing writes `toISOString()`;
- parsing reads any RFC 3339 date-time and rejects any other text, so a
  value read and written back is `new Date(text).toISOString()`;
- the TypeScript type stays `string`.

Providers build timestamps as upstream does:
- `Timestamp::now()`;
- `from_unix_seconds(created)` for `new Date(created * 1000)` (openai
  responses/openai-responses-language-model.ts:1509, open-responses
  open-responses-language-model.ts:579);
- `from_http_date` for Bedrock's `new Date(headers.date)`
  (amazon-bedrock-chat-language-model.ts:864-867).

The unused `util::rfc3339_now` helper is removed, and core gains a chrono
dependency (already in the lock file).

Tests:
- core `timestamps_are_iso_strings` (golden JSON and rejected forms);
- `timestamp::tests` (`toISOString`, `new Date(seconds * 1000)`, RFC 3339,
  HTTP dates);
- OpenAI and Mistral upstream e2e expectations move to the `.000Z` form;
- Go, Java, Kotlin, Python and Node each check a mocked `created` in the
  result's `response.timestamp`.

Binding types stay strings; their docs say how to parse one.

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

* chore(node): export the generated Timestamp type (#232 C48)

`scripts/gen_ts_types.py` lists `types/Timestamp.ts` (`string`) in
`types.ts`; the C48 commit added the file but not that line.

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

* fix(core)!: text and reasoning stream deltas carry text (#229 B9)

The AI SDK's `TextStreamPart` names the delta of a `text-delta` and a
`reasoning-delta` part `text`
(ai generate-text/stream-text-result.ts:435-440, :466-471). It copies the
field from the V4 part's `delta` (stream-language-model-call.ts:496-501).
`tool-input-delta` keeps `delta` (stream-text-result.ts:496-501).

aimux's `TextStreamPart::TextDelta` and `ReasoningDelta` now have a `text`
field. `into_text_stream_part` maps the provider-level `StreamPart`, which
keeps V4's `delta`, onto it.

Bindings:
- Go: `TextDeltaPayload.Text`.
- Java: `getText()` / `text(...)`.
- Kotlin, Swift and Dart: a `text` property or associated value.
- Python: stream parts read `text`.
- Node: regenerated types.
The web console, examples and docs read `part.text`.

Tests:
- core `text_stream_deltas_are_text` pins the three delta parts and rejects
  a `delta` key on a text delta.
- Each binding checks the wire keys or round-trips the parts. The Swift and
  Dart tests were edited without a toolchain to run them.

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

* fix: layer 7 code takes layers 2-6's types

The merge of [6/8] left layer 7's decision and transcription code on
the pre-merge shapes:

- indexmap is a workspace dependency since layer 3; the duplicate key
  in aimux-core is gone and the other crates take the workspace entry.
- WrapProvider forwards decision_model with the provider factory's
  Option<Result<..>> signature.
- CallOptions no longer carries call_id (layer 4): the decision
  language model's exchanges share the decision's recording context,
  which supplies the call id.
- Provider metadata and options are IndexMap namespaces (layer 3).
- The google, groq, mistral and xai transcription models refuse
  AudioInput::Url (layer 5) with AudioInput::url_error(), as every
  transcription model does; only transcribe downloads a URL.
- Tests: SharedProviderOptions literals, download: None in
  TranscriptionCallOptions, do_stream's Option (C21), and the replay
  tool's decision_model probe; the two test modules in
  transcription_model.rs are one.

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

* test(providers): the layer 7 transcription models refuse URL audio

google, groq, mistral and xai each get url_audio_is_refused_before_any_request:
an AudioInput::Url fails with the url error before any request is sent,
as layer 5 pins for the transcription models it already had.

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

* fix(core): decide runs its model call in the operation settings

Layer 4 moved max_retries and timeout out of CallOptions into the
operation's OperationSettings, which generate_text and stream_text
scope around their model calls. decide now scopes its model call the
same way, so a language model under DecisionLanguageModel (a router or
MoA reading the child retry count, a TraceLayer) sees the decision's
settings. A direct do_decide call has none, as a direct do_generate.

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

* refactor(core)!: provider namespace maps keep insertion order (#229 NP2-1)

The AI SDK types provider options, provider metadata and provider
references as JS objects (`@ai-sdk/provider@4.0.22`
shared-v4-provider-options.ts:24, shared-v4-provider-metadata.ts:24,
shared-v4-provider-reference.ts:20). JS objects keep their keys in
insertion order.

Rust: this is 4197573e, already on the lower layers, ported by hand onto
this layer.
- `SharedProviderOptions` and `SharedProviderMetadata` become one alias,
  `NamespaceMap = IndexMap<String, JsonObject>`.
- `SharedProviderReference` becomes `IndexMap<String, String>`.
- The inner objects already kept order through serde_json's
  `preserve_order`.
- Provider code and tests written on this layer that still built a
  `HashMap` now build an `IndexMap`.
- The Anthropic options-name rewrite uses `shift_remove`.

Bindings: the order must hold end to end.
- Node (`JSON.parse`), Python (`json.loads`, pydantic dicts), Java
  (Jackson `JsonNode`), Kotlin and Dart already keep it.
- Go maps sort their keys when marshaled, so:
  - `Tool.ProviderOptions` and `TranscriptionSessionOpts.ProviderOptions`
    become `json.RawMessage`;
  - `UploadFileResult.ProviderReference` becomes `ProviderReference` (raw
    JSON with `Get(provider)`).
- Java's `UploadFileResult` defaults to a `LinkedHashMap`.
- Swift's typed values are `Dictionary`, which has no order. The Swift API
  doc says so, and that the JSON strings keep the order.

The generated TypeScript is unchanged.

Tests:
- core `shared::tests` (outer and inner keys, out of alphabetical order);
- Node, Python, Go, Java and Kotlin send an invalid tool call whose
  `providerMetadata` keys are out of order through the native
  `applyToolCallRepair`, decode the result into their typed values and
  check the order;
- Go, Java and Kotlin also round-trip an upload `providerReference`;
- a Dart test decodes and re-encodes a tool call's metadata. It was
  written without a toolchain to run it.

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

* test(kotlin): the OpenAI decision test sends the fixture's options in order

V4 decision questions and choice criteria are maps, and the OpenAI
request lists them in the caller's order. The test re-serialized the
fixture's options through org.json's JSONObject, which does not keep key
order, so the request listed score before choice and failed the
comparison with the fixture. It now passes the options' raw text.

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

* docs(node): regenerate index.d.ts for applyToolCallRepair's doc

Layer 4 rewrote the Rust doc of applyToolCallRepair (it repairs the
call in the result's steps) without regenerating index.d.ts, so every
napi build rewrote the file. This is the napi build's output for the
merged source; only that doc comment changes.

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

* test(node): the reasoning replay test gives its tool call a result

"assistant without reasoning omits reasoning_content" sent an assistant
tool call with no tool result. Since 252371bf (#229 C38) the prompt is
checked as the AI SDK's convertToLanguageModelPrompt checks it, and that
prompt fails with MissingToolResultsError before any request. The
conversation now carries the tool message with the call's result, as
the other replay tests do; the assertion on the assistant message is
unchanged.

No other binding test sends a tool call without its result.

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

* test(bindings): the DeepSeek usage.raw tests read the step usage

Since d3fd8207 a result's `usage` is the usage summed over its steps,
as the AI SDK's addLanguageModelUsage, which carries no raw object
(generate-text.ts:1526-1544, types/usage.ts:122-155). The provider's raw
usage, with DeepSeek's prompt_cache_hit_tokens / _miss_tokens, is on
each step's `usage.raw`. The Node and Python cassette tests (RFC-0016
M10) read steps[0].usage.raw and check that the summed usage has none.
The other bindings read raw usage only from stream parts.

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

* docs(node): regenerate index.d.ts for applyToolCallRepair's doc

d3fd8207 rewrote the Rust doc of applyToolCallRepair (it repairs the
call in the result's steps) without regenerating index.d.ts, so every
napi build rewrote the file. This is the napi build's output; only that
doc comment changes.

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

* test(swift): the OpenAI decision test sends the fixture's options in order

The same order loss as the Kotlin test before efd10d65: the test
re-encoded the fixture's options with JSONSerialization, whose
dictionaries keep no key order, and compared the request as an
NSDictionary, whose arrays are ordered. V4 questions and choice
criteria are maps, and the OpenAI request lists them in the caller's
order. The test now passes the raw text of the fixture's options object.

Not compiled here (no Swift toolchain); the brace matcher was checked
against the fixture with an equivalent script.

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

* fix(core)!: standalone tool calls and sources carry their type tag (#229 NP2-7)

Outside `content` the AI SDK writes a tool call as `TypedToolCall`,
`{type: 'tool-call', ...}` (ai generate-text/tool-call.ts:8-14), and a source
as `Source`, a `LanguageModelV4Source` tagged
`{type: 'source', sourceType, ...}` (ai types/language-model.ts:92; provider
language-model-v4-source.ts:8, :36). This covers:
- the text results' `toolCalls` and `sources` (generate-text-result.ts:73,
  :78; stream-text-result.ts:173, :180);
- an approval request's `toolCall` (tool-approval-request-output.ts:20).

The repair function's `toolCall` is a tagged `LanguageModelV4ToolCall`
(language-model-v4-tool-call.ts:7). aimux wrote all of these untagged.

aimux's `ToolCall`, `RawToolCall` and `Source` stay untagged, because the
`content` and stream unions write their tag. New `wire` helpers write the
tag where a value stands alone:
- `TypeTag`;
- `typed` / `typed_vec` for fields;
- `Tagged` for a value serialized on its own.

The tag comes first. Reading accepts a value without the tag (JSON written
before it) and rejects a different one.

Tagged fields:
- `GenerateTextResult` and `StreamTextResultAggregated`: `toolCalls` and
  `sources`;
- `ToolApprovalRequestOutput.toolCall`;
- the repair context's `toolCall`, and the call `apply_tool_call_repair`
  returns through the C ABI, Node and Python.
`apply_tool_call_repair_to_result` writes the patched entry tagged. A
repair reply's call may carry the tag.

TypeScript types these fields as the new `TypedToolCall` and `TypedSource`
(generated, export only). Node's `ToolCallRepairContext.toolCall` is tagged.

Bindings: each one's standalone `ToolCall`, `RawToolCall` and `Source` now
writes the tag and checks it on read.
- Go: `MarshalJSON` / `UnmarshalJSON`.
- Java: a fixed `type` field plus a checking setter.
- Kotlin: an `@EncodeDefault` property checked in `init`.
- Swift: hand-written `Codable`.
- Dart: `toJson` / `fromJson`.
- Python: a `Literal` field with a default.

Tests:
- core `standalone_tool_calls_and_sources_are_tagged` (golden JSON, key
  order, content written once, untagged read, mistag rejected);
- FFI smoke test (context, repaired call, patched result);
- one test in each binding.
The Swift and Dart tests were written without a toolchain to run them.

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

* fix(core)!: integral tool input numbers are written as JavaScript writes them (#229)

The AI SDK parses a tool call's argument text with `JSON.parse`
(ai generate-text/parse-tool-call.ts:116, :256; provider-utils
parse-json.ts:97, secure-json-parse.ts:31). JS has one number type, so
`JSON.stringify(JSON.parse('{"n":1e2}'))` is `{"n":100}`. serde_json keeps
a number written with a fraction or exponent as an `f64` and writes it back
as `100.0`, so aimux's `toolCalls[].input` differed from the AI SDK JSON for
`1e2`, `3.0` or `-0.0`.

The least invasive fix: `parse_input_text`, which both the valid and the
invalid tool-call paths now use, walks the parsed value once and replaces
each finite, integral `f64` below 1e21 in magnitude with the integer JS
would write. Rust's `f64` `Display` gives the same shortest round-trip
digits as JS `Number::toString`, padded with zeros (`2 ** 60` is
`1152921504606847000`), and `-0` becomes `0`. Nothing else changes: no
`arbitrary_precision` feature, no custom number type, and no change to how
other JSON is read or written.

Remaining gaps, documented in the alignment doc:
- integral values from about 1.8e19 to 1e21 do not fit `i64`/`u64` and stay
  floats;
- integer literals above 2^53 stay exact, where JS rounds them.
Non-integral numbers and numbers of 1e21 or more already match JS
(`1.5`, `1e+300`).

Tests:
- core `integral_input_numbers_are_written_as_javascript_writes_them` pins
  valid and invalid calls and the `js_integer` edge cases;
- Python and Go e2e tests read integer tool input from a mocked response.

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

* docs(rfc): RFC-0035 shows the final repair wire and upstream's control flow

The RFC still showed the repair context and reply in the old snake_case,
externally tagged form. It now shows the JSON the exports write and read:
- the context is `{toolCall, error, inputSchema, tools, messages,
  instructions}`;
- `toolCall` carries `type: "tool-call"`, as the repair function's
  `LanguageModelV4ToolCall` does (provider 4.0.21
  language-model/v4/language-model-v4-tool-call.ts:7);
- `error` is tagged by `name` (`AI_InvalidToolInputError`);
- the reply is `{"type": "repaired", "toolCall": ...}`;
- a failed reply becomes `AI_ToolCallRepairError` with an `AI_Error` cause;
- the result patch rewrites `toolCalls[]` and `responseMessages`, keyed by
  the call's `toolCallId`.

It also takes the corrections from the RFC check (rfc-check patch 0002),
with line numbers moved to ai 7.0.127:
- `inputSchema` is a value; upstream passes a function of the tool name
  (generate-text/tool-call-repair-function.ts:31).
- A repaired call that still fails is not wrapped in
  `AI_ToolCallRepairError`: it is an invalid call carrying that parse's
  `AI_InvalidToolInputError` / `AI_NoSuchToolError`, with the original id,
  name and input. Upstream wraps only an error thrown by the repair
  function (generate-text/parse-tool-call.ts:90-96); a failed re-parse
  goes to the invalid-call branch (:103-134). `apply_repair_outcome`
  already behaves so.
- Swift's abort waiting for the hook differs from upstream, which races
  the repair function against the abort signal (:73-74, :137-173).

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

* test(node,python): decision usage omits unknown totals, never null

Ports the test half of li's fork PR #52 (cbfb76c9, b8fdda66).

`Usage` writes an unknown count by omitting `total`, as the AI SDK's
`LanguageModelV4Usage` types it `total: number | undefined` (provider
4.0.21 language-model/v4/language-model-v4-usage.ts:14, :39). The Node and
Python decision tests read `inputTokens` / `outputTokens` already, but
accepted an explicit `"total": null`:
- Node compares `total` with `undefined` instead of coalescing both sides
  to `null`;
- Python asserts that `total` is absent when the mocked count is missing
  or null.

The fork's other two changes are already in this branch: the Python
recording test reads `outcome.decisionResult` (4a1f3684), and the Flutter
typed-call helper no longer declares options no test passes (c81e2211).

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

* fix(core): keep recording schema 2 and read the recordings master writes

The migration series stays on recording schema 2, as master writes it;
the full schema 3 comes with the last layers (li, 2026-10-12).

- RECORDING_SCHEMA = 2. Schemas 1 and 2, what released versions write,
  load; 0 and 3 and above are refused with the existing clear error.
- Reading a master recording: its configuration snapshot (base_url,
  api_key_source, profile, provider_options) is ignored, provider_id
  defaults to empty, and a prompt this version cannot read (a system
  message with a part array) loads as empty.
- What cannot be replayed fails at replay time and names the cause:
  ProviderRecord::registry_id for registry rebuilds (aimux-providers,
  aimux-replay, aimux-web), InputRecord::unreadable_prompt for request
  replay and the mock matcher, master's call options, and decision
  capabilities that only master's provider.profile holds.
- New fixtures: two OpenAI chat recordings written by master a8e3a996's
  JsonlRecorder, and master's jev_systemone_live.jsonl byte for byte.
  The V4 Jev fixture goes back to schema 2.
- The S2-5 complete rule is unchanged.
- Docs: RFC-0023 revision line, alignment doc §9.1, decision API doc,
  CHANGELOG.

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

* chore(core): keep the removed key-source field name out of code

scripts/check_provider_boundaries.sh refuses the removed name in code
and comments. The schema doc describes master's configuration snapshot
in words, and the master-shaped unit test drops that one key; the
master fixture (excluded from the check) still carries it.

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

* docs(changelog): legacy vertex and bedrock keys are still read and written

The bullet said the legacy keys are neither read nor written; the code
reads them and writes metadata under both keys, as the upstream
packages do (amazon-bedrock-chat-language-model.ts:154-167,
google-language-model.ts:163-171), and tests pin that.

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

* docs(changelog): decision recordings carry no capability snapshot

The provider-factory bullet still said decision recordings keep the
capabilities under input; since decisions follow DecisionModelV4 they
carry none, as the DecisionModelV4 entry says.

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

* fix(go)!: Error.Data keeps a null API call error data apart from none

The AI SDK's `APICallError.data` is `unknown` (@ai-sdk/provider
errors/api-call-error.ts:18, :53): a provider error body of `null` is
data `null`, not missing data. The core keeps the two apart (#232 C34)
and aimux_error_provider_data returns NULL only for missing data, but Go
decoded the JSON into `any`, where both became nil.

`Error.Data` is now `json.RawMessage`: nil when the error carries no
data, `null` when the data was null, the raw JSON otherwise. A test
drives an OpenAI model against a server answering 400 with an object, a
`null` and a non-JSON body.

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

* docs: RFC-0035 names the tests that pin repair; RFC-0026 deltas carry text; Swift key order is a known gap

- RFC-0035 §6 cited contract-tests/fixtures/tool-call-repair.json and a
  `contract_fixture_matches_the_implementation` test; neither exists. The
  section now says there is no shared fixture yet and lists the core,
  C ABI and binding tests that pin each branch.
- RFC-0026's OpenAI-compatible output table reads `TextDelta { text }`
  and `ReasoningDelta { text }`, the stream parts' field since #229 B9.
- The alignment doc's §9.1 known gaps register that the Swift binding's
  typed provider options, provider metadata, provider references and
  `JSONValue.object` are Swift dictionaries and lose key order on a
  decode-encode round trip (#229 NP2-1); the NP2-1 note points there.

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

* refactor!: text results' usage, steps, tool outputs and Output follow the public JSON

Layer 7's types merged into layer 8 in snake_case; they now follow the
same rules as the rest of the public JSON.

- LanguageModelUsage is camelCase (types/usage.ts:10-67); an unknown
  count is omitted, the detail objects are always written.
- StepResult, its model, performance and response are camelCase
  (step-result.ts:138-278, language-model-response-metadata.ts:6-38).
- Typed tool results, tool errors, denied outputs and approval responses
  are camelCase; an approval response's toolCall and the results'
  toolResults[] carry their type tag (generate-text-result.ts:93,
  tool-result.ts:10-42, tool-approval-response-output.ts:7-35).
  TypeScript gains TaggedToolResult.
- Output is tagged by type with the AI SDK's output mode names
  (output.ts:68, :114, :241, :520, :657).

The bindings read and write the camelCase keys; Kotlin, Swift and Dart
keep a parsed JSON null output apart from no output. Pinned by
ai_sdk_json_test's steps_usage_and_tool_outputs_are_the_ai_sdk_json and
output_specifications_are_tagged, and by each binding's tests.

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

* docs(rfc): RFC-0036's stream example writes the public JSON

The aimux_call stream example shows a text-delta with its id and text
(TextStreamPart, stream-text-result.ts) and a finish part with camelCase
finishReason and totalUsage, as the bridge now emits them.

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

* refactor!: decisions follow the public JSON

Layer 7's decision types merged in snake_case; they now write the AI
SDK's JSON.

- DecisionCallOptions, DecisionRounding, DecisionUsage, DecisionResult
  (decision-model-v4-call-options.ts:7-14, decision-model-v4-result.ts:
  31-56), DecideOptions (decide.ts:53-61) and DecideResult
  (decision-result.ts:22-39) are camelCase. DecideUsage omits an unknown
  count instead of writing null (decision-result.ts:27-31).
- The model description is {specificationVersion, provider, modelId,
  supportedQuestionTypes} (decision-model-v4.ts:7-11), built once by
  decision_model_properties for the C ABI, Node and Python.
- What aimux parses inside a provider's namespace is camelCase too:
  images[].mediaType, OpenAI choiceValues and safetyIdentifier, candidate
  scoring's modelRevision, tokenizerRevision, promptFormatVersion and
  encoded.<id>.{promptTokenIds, labelTokenIds}, and the metadata's
  probabilitySource and latencyMs. Options a runtime receives as given
  keep the runtime's names.
- Decision replay matches on providerOptions and refuses recorded options
  with snake_case keys, as language replay does.
- The V4 Jev recording, the contract-test and provider fixtures convert
  their aimux keys; vendor wire bodies are unchanged.
- Python's decide takes the JSON keys as keyword arguments.

Pinned by ai_sdk_json_test::decisions_are_the_ai_sdk_json, the provider
decision tests (snake_case option names are rejected) and each binding's
decision test, which now also checks the camelCase usage.

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

* docs: fix two rustdoc links that fail cargo doc -D warnings

A redundant explicit link target in GenerateTextOptions::download and a
module-doc link to TungsteniteConnector that rustdoc resolves from the
crate root (the module also has an outer doc comment).

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

* docs: plain code spans for the private stream_transcription methods

rustdoc -D warnings rejects intra-doc links to private items.

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

* ci: speed up test builds, deduplicate caches, and use prebuilt cargo-ndk

Cargo.toml configures [profile.release] with fat LTO (lto = true),
codegen-units = 1, and opt-level = "z" so published FFI binaries stay
small. In CI, 11 test-only jobs (Java, Kotlin, Go, Python, Flutter,
Android example, contract tests, Swift) were running full fat-LTO release
builds from scratch even though they upload no release artifacts, while
writing separate 275 MB cache entries per job and exceeding GitHub's 10 GB
repository cache quota.

- Skip fat LTO and -Oz in test-only jobs via top-level CARGO_PROFILE_*
  overrides, while keeping Cargo.toml's full release profile (lto = true,
  codegen-units = 1, opt-level = "z") on node-binding and ffi-build so
  uploaded release artifacts keep their exact sizes.
- Strip debug symbols from dev/test profiles (CARGO_PROFILE_DEV_DEBUG=0,
  CARGO_PROFILE_TEST_DEBUG=0) to speed up linking the 162 integration
  test binaries.
- Build only the cdylib target (cargo rustc --crate-type cdylib) in jobs
  that only link the shared library, avoiding archiving a redundant
  staticlib.
- Enable cache-all-crates on Swatinem/rust-cache and share a single
  "ffi-test" cache key across the FFI test jobs to reduce cache churn.
- Replace `cargo install cargo-ndk` (source build) with
  taiki-e/install-action@v2 and enable Flutter/Gradle/npm/pip caches.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(flutter): typed stream fixtures are JSON maps; the chat mock is DeepSeek

The round-trip cases built empty nested maps as untyped literals, which
Dart infers as Map<dynamic, dynamic>, so the parsers' Map<String, dynamic>
casts failed. The stream test's mock model was Groq, which reads stream
usage from x_groq only (upstream groq-chat-language-model.ts:438), so the
finish part had no input tokens; the mock is DeepSeek again, as in [7/8].

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

* ci: keep uploaded artifacts unchanged and stop PR runs from evicting caches

Fixes on top of the previous commit:

- node-binding builds with `napi build` without `--release`, so its
  uploaded node-* artifacts are dev-profile builds. The workflow-level
  CARGO_PROFILE_DEV_DEBUG=0 stripped their debug info (Linux artifact
  55.3 MB -> 17.6 MB) and its CARGO_PROFILE_RELEASE_* overrides had no
  effect. Debug info is now dropped in rust-test only, where the test
  binaries are linked, and the dead overrides are gone.
- ffi-build goes back to `cargo build`, as release.yml does. Building
  only the cdylib changes the shipped library (x86_64 Linux .so
  9,446,848 -> 7,637,144 bytes, same 129 exports), so the ffi-*
  artifacts would no longer match the release build or earlier runs.
- rust-cache `cache-all-crates` keeps unused registry crates, it does
  not cache workspace crates; dropped. Caches are saved from master only
  (`save-if`); pull requests restore master's caches instead of each
  writing their own, which is what pushed the repository past 10 GB.
- The two node-binding legs on each runner OS (x64 and arm64 targets)
  shared one cache key: one leg failed to save and the other restored a
  cache built for the wrong target. The key now includes the target.
- CARGO_INCREMENTAL=0 is already set by rust-cache; dropped.

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

* test(swift): the plain-text result's content part is type-tagged

GenerateTextResult.content is the AI SDK content array, so a text part
is {"type":"text","text":...}; the test still read the old
externally tagged {"Text":{...}} shape. It now reads the part as the
tool-call test below it does.

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

* ci: fewer jobs per run so pull requests stop queueing for runners

With every stacked pull request running CI, runs wait far longer for a
runner than they spend building: each run had 6 macOS jobs against a
5-job macOS limit and 18 Linux jobs against a 20-job total limit, and
the macOS jobs of one run sat queued for 15 minutes.

- Java (JDK 8, 11, 17, 21) and Kotlin (JDK 17, 21) run in one job
  that builds the FFI library once and runs each JDK in turn: 6 jobs
  and 6 FFI builds become 1.
- The Go binding is tested in the ffi-build legs on Linux and macOS,
  against the static archive that job already builds: the 2 Go jobs
  and their FFI builds go away.
- The x86_64 macOS Node binding is cross-built on Apple Silicon and
  tested with x64 Node under Rosetta instead of on the Intel runner,
  where its build took 371 of the job's 465 seconds.
- scripts/build-ios-xcframework.sh builds both iOS slices in one cargo
  invocation, so they compile in parallel; the output is unchanged.

A run goes from 24 jobs (6 macOS) to 17 (5 macOS).

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

* docs(agents): update the differential probe for the new public JSON

The base now spells tool choice as `{"type": ...}`, has no
`CallOptions::max_retries`, and adds `user_agent_suffix` to the Open
Responses settings. Without this the probe silently falls back to a
default prompt for every cassette with a tool choice.

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

* docs(alignment): the first-chunk timer start is a registered difference

The first-chunk timer keeps starting with the operation, because providers
wait for the first SSE event inside do_stream; starting it with the response
stream would leave a stream that answers 200 and then sends nothing without
a bound. §5 now files it as a product difference, and §9.1 lists it with the
stream retries (streamRetries) among the known gaps.

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

* ci: restore the Flutter SDK cache everywhere, save it from master only

- subosito/flutter-action saves a 1.5-2.2 GB SDK cache per OS. A pull
  request can only restore caches saved on its own branch or on master,
  so with the action's cache on, every open pull request saved its own
  copy and the 10 GB budget filled up again. The action's `cache` input
  turns restore and save off together, so the two are split: a dry run
  of the action gives the key and path, actions/cache/restore runs on
  every branch, the action installs the SDK (a no-op when the restore
  hit), and actions/cache/save runs on master only, at the end of the
  job so the engine artifacts the build downloads are in the cache. The
  pub cache stays off, as on master. The Gradle cache already saves from
  the default branch only.
- bindings/node and bindings/python are their own Cargo workspaces and
  build into bindings/<name>/target, which rust-cache's default (the
  repository's target directory) never covered: the Node and Python jobs
  rebuilt aimux-core and aimux-providers on every run. `workspaces` now
  points rust-cache at each binding's directory.
- The ffi-build comment said the Go binding links the static archive;
  on macOS ld64 picks the .dylib next to it. Comment only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* ci: build test-only FFI libraries at opt-level 1, not 0

aimux_stream_text runs the whole stream future on the caller's thread
(ffi_block_on). With the test-only override at opt-level 0, one stream
through the C ABI needs about 530 KB of stack on the AI SDK branches
(about 300 KB on master), more than the 512 KB stack of a Swift
cooperative thread: the async Swift tests died with SIGBUS on #255 and
#259 once they carried this workflow. At opt-level 1 the same stream
needs about 110 KB; the shipping profile needs about 160 KB. The
library is also 40% smaller, and the build is still far from the
shipping profile's fat LTO with one codegen unit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* docs(agents): trim the native-Rust skills and profile to what stays true

The profile carried line numbers and commit hashes of one stack head, a
known-bug list and a candidate list that the stack has since worked
through, and a cross-build probe and gate script that CI never compiles
or runs. Keep only stable facts: upstream and module mapping, design-doc
sections, surfaces that must not change, oracles, checks, project
decisions (now including one-way layering and streams without a shared
tool-call tracker), traps and working rules.

The skills keep their process, discipline and rewrite table, shortened.
New rules from the provider-config and stream layers: removing closures
must not reverse module dependencies, a rewrite must not grow the code,
and an in-process differential is recorded as fixtures, not kept as a
copy of the old implementation.

The probe and gate script are dropped from the repository; PR
descriptions say where the probe lives and how to run it.

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

* docs(agents): drop the preset count from the profile

The registry has 282 rows, not 281, and the number changes whenever a
preset is added; the profile only keeps facts that stay true.

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

* ci: pin cargo-ndk and keep pull requests from saving two more caches

cargo-ndk is installed through cargo-binstall, which took whatever was
newest; pin it to 4.1.2, the version CI has been installing.

The contract and Swift jobs were the last two that saved a Rust cache
on every pull request. Like the other jobs they now restore on pull
requests and save only on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* chore: delete the web console's generated ProviderName copy

tools/aimux-web/web/src/types/ProviderName.ts was the eighth copy written
by scripts/gen_provider_names.py, which this branch deletes. Nothing
imports it (it is an orphan on master too), and its header still claimed
the deleted generator. RFC-0036 §12.3 A2 requires every Rust, binding
and web copy and the generator to go.

The three quality-audit reports that cite `gen_provider_names.py --check`
as a current gate get a later-change note, as docs/internal/phase4-audit.md
already has.

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

* ci: type-check the web console against the generated types

The console imports the core types from bindings/node/src/types through
@aimux/types. gen_ts_types.py --check only compares the generated files,
so a renamed or removed Rust type regenerates cleanly and leaves a
dangling import that no pull request check catches. RFC-0036 §13.1 A4
asks for every import to be type-checked.

A new "Web console typecheck" job runs `npm ci && npm run typecheck`
(vue-tsc --noEmit) in tools/aimux-web/web. It runs when the shared code
changes (which includes tools/aimux-web and the Rust crates whose types
are generated) or when bindings/node changes, where the generated types
live; documentation-only changes skip it. "CI result" checks it.
Removing bindings/node/src/types/Recording.ts locally makes it fail on
the two files that import it.

RFC-0029 §9.1 gets a later-change note: the console no longer has its
own copies, and TS_RS_EXPORT_DIR no longer points at the web directory.

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

* docs: size and performance gates follow RFC-0043

#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

* docs: the alignment doc links back to RFC-0036 §12.2 and names the merged 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

* ci: run the AI SDK pin script tests

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

* docs: note the retired contract fixtures in the RFCs and the changelog

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

* docs(web): note that cargo test rewrites the tracked node types

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

* test(openai): tool calls are flushed after an error frame

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

* refactor(provider-utils): bound the tracker's id suffix search

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

* test(core): restore the OpenAI output tool-argument tests

cc5446fa deleted the end-to-end tests of the OpenAI-compatible output
(fx4/p2 aimux-core/tests/tool_input_test.rs:401-804) together with the
V3 prompt tests around them. The converter unit tests in
openai_output.rs only feed hand-built parts to the converter, so three
contracts lost their only coverage: a repaired call shows in the
completion while the stream keeps the provider's name and deltas, a
repair that still fails keeps the original name and argument text on
both paths, and the stream's two-chunk shape when input deltas arrive.

They come back as aimux-core/tests/openai_output_tool_input_test.rs,
bodies unchanged; only the mock model moves to the [3/8] result types
(request / response on GenerateResult and StreamResult). One new test
completes the raw-text matrix for a known tool: truncated JSON, a JSON
string, non-JSON text, null and a wrong-typed object with whitespace,
each with and without input deltas, through both entry points.

The OpenAI output is an aimux extension (RFC-0035, RFC-0042 §5 items
7-8), so these tests pin aimux behaviour, not upstream behaviour.

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

* test(core): part metadata replays whole into providerOptions

Response messages copy a part's provider metadata into providerOptions
as one map (ai@7.0.127 to-response-messages.ts), and nothing pinned
that beyond a single known namespace. The new test feeds an unknown
namespace with nested nulls, an empty namespace and a known one through
the text path and the streamed reasoning path (metadata on the end
event) and requires the serialized map back unchanged, key order
included.

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

* test(ffi): restore the composite, frame-error and session-input tests

cc5446fa removed the FFI unit tests that need a mock language model
along with the V3 mocks themselves (fx4/p2 aimux-ffi/src/lib.rs
4178-4515 and 4837-4881). The entry points they cover are unchanged at
this layer, so the tests come back with only the mocks moved to the
current result types (request / response on GenerateResult and
StreamResult):

- a recoverable frame error is a data part on aimux_stream_text and is
  skipped on aimux_stream_text_as_openai, and both streams still end
  with on_done;
- aimux_router_new and aimux_moa_new build their models, reject bad
  JSON, a dead member or aggregator handle and a NULL array, treat an
  empty or null config as defaults, and allow zero MoA references;
- aimux_transcription_session_new rejects bad JSON, a non-transcription
  handle and a bad abort handle, and next_part rejects NULL out-params.

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

* fix(core): a stream that ends with Finish is traced as completed

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

* test(core): restore the TraceLayer suite

cc5446fa 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

* test(core): a traced call keeps the id of its recording

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

* test(google): restore the inlineData-to-file tests

cc5446fa 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

* test(core): restore the replay_with_model tests

cc5446fa 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

* test(core): pin an abort after a finished step and repeated tool-call ids

Upstream's stream-text flush (stream-text.ts:1725-1757) rejects only
when no step was recorded; an abort after a recorded step resolves with
that step and null total usage. consume() does the same, and now says
why next to the Abort arm. stream_text never yields an abort between
FinishStep and Finish, so the test builds that order by replacing the
result's stream.

A repeated tool-call id behaves as upstream on both paths: streaming
forwards both calls and resolves a later provider result to the last
one (stream-language-model-call.ts:432, :705-707, :764); generating
maps both content parts to the first (convert-language-model-content.ts
:74-84). Two tests pin that.

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

* docs(alignment): register recording_context and deferred provider results as known gaps

CallOptions still carries recording_context, which §3.1 excludes from
call options; it leaves with the OperationRuntime and attempt scope.
A deferred provider tool result is accepted when a provider tool of the
same name is passed, wider than upstream's supportsDeferredResults; the
flag comes with the tool definitions. Both now sit in the §9.1 list.

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

* refactor(stream_text): the chunk loop has no arm that cannot run

The loop over model chunks relied on its own order twice:
`StreamStart` was handled by an `if let ... continue` before the match,
so its match arm was `unreachable!("handled above")`; and the fallback
arm converted the remaining parts with `map_payloads(|_| unreachable!(..),
|_| unreachable!(..))`, valid only because the tool call and approval
arms came first.

`StreamPart::split_payload` now splits a part into the variants that
carry a payload (tool call, approval request, reasoning file) and the
rest, whose payload types are `Infallible`. The loop matches on that:
each payload has its own arm, and the fallback widens the rest with
`match never {}`. `StreamStart` is a normal arm; the step-opening
preamble skips it with `matches!`. A new payload-carrying variant now
fails to compile instead of panicking at run time. Behaviour is
unchanged.

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

* test(provider-utils): the default-transport test keeps 127.0.0.1 off its proxy

the_default_transport_can_be_replaced swaps the process-wide default
transport for one that sends everything except localhost through a fake
proxy. Tests running at the same time (download_blob, the post_to_api
timeout test) reach their mock servers on 127.0.0.1 through that default,
so with many test threads their requests landed in the fake proxy and
failed. 127.0.0.1 is now on the swapped transport's no_proxy list too.

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

* docs(web): a blank key at a custom base_url goes out as an empty key

The README and RFC-0029 said such a call goes out without a key. The
key is an explicit empty string, as with the AI SDK's `apiKey: ''`:
no stored or environment key is used, and providers that always send
their credential header (OpenAI's `Authorization: Bearer`) send it
empty.

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

* test(provider-utils): pin the redirect cap and re-signing on a same-origin hop

Two redirect rules had no test: an API call follows at most ten hops
and fails on the eleventh with the redirect's status, and a hop that
stays on the request's origin goes through the signing transport again
and is signed for its own URL, never with the first request's
signature (RFC-0040 §3.2). With the signing clock fixed, the two
signatures can differ only because the path does.

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

* docs(rfc): RFC-0040 §3.2 follows the implemented redirect rule

The merged RFC-0040 says credentialed requests do not follow redirects
by default. API requests follow them, as the AI SDK's postToApi does,
with credentials locked to the request's origin: a hop off it keeps
only User-Agent, signing and token decorators add nothing, a hop back
does not restore them, and the chain stops at ten. §3.2 gets a
revision note saying so and where the code and tests are, and the
alignment doc's §5 registers the difference from both the RFC text and
upstream fetch.

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

* test(providers): a preset's base-URL and optional-key variables

Nothing tested a registry row's base_url_env or its optional key
variable directly. Four serial tests use the ollama row: the base URL
variable is read per request, not when the provider is created; an
explicit base_url wins over it; an unusable value fails the request
with an InvalidArgument naming the variable and sends nothing; the
optional key goes out as Bearer only when set and non-empty. The
existing keyless test becomes serial and clears OLLAMA_API_KEY so it
cannot see a key one of them set.

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

* test(mistral): model_length is a length finish

The mapping follows @ai-sdk/mistral 4.0.54, which has no test for it.
Generate and stream both report Length and keep the raw reason.

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

* test(providers): non-object input of a past tool call

One assistant message carries six past tool calls (null, a string, an
array, a number, a boolean and an object) through each factory. OpenAI
chat sends "{}" for every non-object, Groq and DeepSeek send
JSON.stringify(input), and Anthropic wraps it as rawInvalidInput, as
their upstream converters do.

RFC-0042 §6.2 named only Bedrock for rawInvalidInput; Anthropic's
toAnthropicToolInput applies the same rule.

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

* fix(anthropic): toolset member calls go back as upstream sends them

A com…
eric8810 added a commit that referenced this pull request Oct 17, 2026
* test(flutter): typed stream fixtures are JSON maps; the chat mock is DeepSeek

The round-trip cases built empty nested maps as untyped literals, which
Dart infers as Map<dynamic, dynamic>, so the parsers' Map<String, dynamic>
casts failed. The stream test's mock model was Groq, which reads stream
usage from x_groq only (upstream groq-chat-language-model.ts:438), so the
finish part had no input tokens; the mock is DeepSeek again, as in [7/8].

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

* ci: keep uploaded artifacts unchanged and stop PR runs from evicting caches

Fixes on top of the previous commit:

- node-binding builds with `napi build` without `--release`, so its
  uploaded node-* artifacts are dev-profile builds. The workflow-level
  CARGO_PROFILE_DEV_DEBUG=0 stripped their debug info (Linux artifact
  55.3 MB -> 17.6 MB) and its CARGO_PROFILE_RELEASE_* overrides had no
  effect. Debug info is now dropped in rust-test only, where the test
  binaries are linked, and the dead overrides are gone.
- ffi-build goes back to `cargo build`, as release.yml does. Building
  only the cdylib changes the shipped library (x86_64 Linux .so
  9,446,848 -> 7,637,144 bytes, same 129 exports), so the ffi-*
  artifacts would no longer match the release build or earlier runs.
- rust-cache `cache-all-crates` keeps unused registry crates, it does
  not cache workspace crates; dropped. Caches are saved from master only
  (`save-if`); pull requests restore master's caches instead of each
  writing their own, which is what pushed the repository past 10 GB.
- The two node-binding legs on each runner OS (x64 and arm64 targets)
  shared one cache key: one leg failed to save and the other restored a
  cache built for the wrong target. The key now includes the target.
- CARGO_INCREMENTAL=0 is already set by rust-cache; dropped.

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

* test(swift): the plain-text result's content part is type-tagged

GenerateTextResult.content is the AI SDK content array, so a text part
is {"type":"text","text":...}; the test still read the old
externally tagged {"Text":{...}} shape. It now reads the part as the
tool-call test below it does.

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

* ci: fewer jobs per run so pull requests stop queueing for runners

With every stacked pull request running CI, runs wait far longer for a
runner than they spend building: each run had 6 macOS jobs against a
5-job macOS limit and 18 Linux jobs against a 20-job total limit, and
the macOS jobs of one run sat queued for 15 minutes.

- Java (JDK 8, 11, 17, 21) and Kotlin (JDK 17, 21) run in one job
  that builds the FFI library once and runs each JDK in turn: 6 jobs
  and 6 FFI builds become 1.
- The Go binding is tested in the ffi-build legs on Linux and macOS,
  against the static archive that job already builds: the 2 Go jobs
  and their FFI builds go away.
- The x86_64 macOS Node binding is cross-built on Apple Silicon and
  tested with x64 Node under Rosetta instead of on the Intel runner,
  where its build took 371 of the job's 465 seconds.
- scripts/build-ios-xcframework.sh builds both iOS slices in one cargo
  invocation, so they compile in parallel; the output is unchanged.

A run goes from 24 jobs (6 macOS) to 17 (5 macOS).

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

* docs(agents): update the differential probe for the new public JSON

The base now spells tool choice as `{"type": ...}`, has no
`CallOptions::max_retries`, and adds `user_agent_suffix` to the Open
Responses settings. Without this the probe silently falls back to a
default prompt for every cassette with a tool choice.

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

* docs(alignment): the first-chunk timer start is a registered difference

The first-chunk timer keeps starting with the operation, because providers
wait for the first SSE event inside do_stream; starting it with the response
stream would leave a stream that answers 200 and then sends nothing without
a bound. §5 now files it as a product difference, and §9.1 lists it with the
stream retries (streamRetries) among the known gaps.

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

* ci: restore the Flutter SDK cache everywhere, save it from master only

- subosito/flutter-action saves a 1.5-2.2 GB SDK cache per OS. A pull
  request can only restore caches saved on its own branch or on master,
  so with the action's cache on, every open pull request saved its own
  copy and the 10 GB budget filled up again. The action's `cache` input
  turns restore and save off together, so the two are split: a dry run
  of the action gives the key and path, actions/cache/restore runs on
  every branch, the action installs the SDK (a no-op when the restore
  hit), and actions/cache/save runs on master only, at the end of the
  job so the engine artifacts the build downloads are in the cache. The
  pub cache stays off, as on master. The Gradle cache already saves from
  the default branch only.
- bindings/node and bindings/python are their own Cargo workspaces and
  build into bindings/<name>/target, which rust-cache's default (the
  repository's target directory) never covered: the Node and Python jobs
  rebuilt aimux-core and aimux-providers on every run. `workspaces` now
  points rust-cache at each binding's directory.
- The ffi-build comment said the Go binding links the static archive;
  on macOS ld64 picks the .dylib next to it. Comment only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* ci: build test-only FFI libraries at opt-level 1, not 0

aimux_stream_text runs the whole stream future on the caller's thread
(ffi_block_on). With the test-only override at opt-level 0, one stream
through the C ABI needs about 530 KB of stack on the AI SDK branches
(about 300 KB on master), more than the 512 KB stack of a Swift
cooperative thread: the async Swift tests died with SIGBUS on #255 and
#259 once they carried this workflow. At opt-level 1 the same stream
needs about 110 KB; the shipping profile needs about 160 KB. The
library is also 40% smaller, and the build is still far from the
shipping profile's fat LTO with one codegen unit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* docs(agents): trim the native-Rust skills and profile to what stays true

The profile carried line numbers and commit hashes of one stack head, a
known-bug list and a candidate list that the stack has since worked
through, and a cross-build probe and gate script that CI never compiles
or runs. Keep only stable facts: upstream and module mapping, design-doc
sections, surfaces that must not change, oracles, checks, project
decisions (now including one-way layering and streams without a shared
tool-call tracker), traps and working rules.

The skills keep their process, discipline and rewrite table, shortened.
New rules from the provider-config and stream layers: removing closures
must not reverse module dependencies, a rewrite must not grow the code,
and an in-process differential is recorded as fixtures, not kept as a
copy of the old implementation.

The probe and gate script are dropped from the repository; PR
descriptions say where the probe lives and how to run it.

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

* docs(agents): drop the preset count from the profile

The registry has 282 rows, not 281, and the number changes whenever a
preset is added; the profile only keeps facts that stay true.

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

* ci: pin cargo-ndk and keep pull requests from saving two more caches

cargo-ndk is installed through cargo-binstall, which took whatever was
newest; pin it to 4.1.2, the version CI has been installing.

The contract and Swift jobs were the last two that saved a Rust cache
on every pull request. Like the other jobs they now restore on pull
requests and save only on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* feat(ops)!: add the aimux-ops dispatch layer

One typed dispatch for every op in RFC-0039 §3.2 plus decision.decide and decision.capabilities: an owner-isolated handle registry, op descriptors and a manifest, call/stream dispatch with a single end frame, the realtime transcription Operation with byte and item budgets, and the error envelope projected once. The retry-after parser moves to one pure helper in aimux-core.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(ffi)!: replace the C exports with the RFC-0039 primitives

aimux-ffi now exports exactly the fourteen primitives (call, stream, operation push/next, abort, drop, manifest, ABI version, error and string release, handle finalizer) over aimux-ops; the 130 provider-specific and field-getter exports, their headers and old-only tests are removed. aimux-codegen generates the header, control schemas and per-language op constants with a --check gate; C/C++ tests and CI jobs run against the generated header.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(node)!: move the node binding onto the ops layer

The node binding now goes through the RFC-0039 contract instead of provider-specific entry points.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(python)!: move the python binding onto the ops layer

The python binding now goes through the RFC-0039 contract instead of provider-specific entry points.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(go)!: move the go binding onto the ops layer

The go binding now goes through the RFC-0039 contract instead of provider-specific entry points.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(java)!: move the java binding onto the ops layer

The java binding now goes through the RFC-0039 contract instead of provider-specific entry points.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(kotlin)!: move the kotlin binding onto the ops layer

The kotlin binding now goes through the RFC-0039 contract instead of provider-specific entry points.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(swift)!: move the swift binding onto the ops layer

The swift binding now goes through the RFC-0039 contract instead of provider-specific entry points.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(flutter)!: move the flutter binding onto the ops layer

The flutter binding now goes through the RFC-0039 contract instead of provider-specific entry points.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ops): list the registered operations

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: delete the web console's generated ProviderName copy

tools/aimux-web/web/src/types/ProviderName.ts was the eighth copy written
by scripts/gen_provider_names.py, which this branch deletes. Nothing
imports it (it is an orphan on master too), and its header still claimed
the deleted generator. RFC-0036 §12.3 A2 requires every Rust, binding
and web copy and the generator to go.

The three quality-audit reports that cite `gen_provider_names.py --check`
as a current gate get a later-change note, as docs/internal/phase4-audit.md
already has.

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

* ci: type-check the web console against the generated types

The console imports the core types from bindings/node/src/types through
@aimux/types. gen_ts_types.py --check only compares the generated files,
so a renamed or removed Rust type regenerates cleanly and leaves a
dangling import that no pull request check catches. RFC-0036 §13.1 A4
asks for every import to be type-checked.

A new "Web console typecheck" job runs `npm ci && npm run typecheck`
(vue-tsc --noEmit) in tools/aimux-web/web. It runs when the shared code
changes (which includes tools/aimux-web and the Rust crates whose types
are generated) or when bindings/node changes, where the generated types
live; documentation-only changes skip it. "CI result" checks it.
Removing bindings/node/src/types/Recording.ts locally makes it fail on
the two files that import it.

RFC-0029 §9.1 gets a later-change note: the console no longer has its
own copies, and TS_RS_EXPORT_DIR no longer points at the web directory.

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

* refactor(ops): remove unused helpers and duplicated test scaffolding

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(ffi): trim the C ABI glue and generate the Flutter header from the manifest

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(go): remove code the ops layer made redundant

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(node): drop the legacy bench package and fix the stale trace assertion

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(flutter): generate the FFI bindings from the codegen header

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(kotlin): expect the abort as the cause of CancellationException

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(python): align the Responses fixture and remove the unused wrapper

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(java): remove the hand-written mock provider server

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: size and performance gates follow RFC-0043

#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

* docs: the alignment doc links back to RFC-0036 §12.2 and names the merged 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

* chore: satisfy the Rust 1.99 lints

Rust 1.99 deprecates AtomicU64::fetch_update and adds clippy::double_must_use, which rejects #[must_use] on functions returning an already must-use BoxStream.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ffi): include <cstring> in the C++ RAII test

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci(flutter): build the ABI test fixtures and run the real-library tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(python): make every GIL-release entry reach a blocking native wait

Video now uses inline Vertex bytes so core never downloads from the loopback fixture, recorder close no longer stops a dropped handle during teardown, and catalogue.fetch is skipped with its reason because it refuses non-public sources before any I/O.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(swift): qualify the iterator's Failure so Swift 6 compiles BoundedStream

Inside AsyncIterator, an unqualified Failure resolves to the AsyncIteratorProtocol associated type (any Error) under Swift 6.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(kotlin): assert on the root cause where coroutine stack-trace recovery copies exceptions

With assertions enabled, kotlinx-coroutines recovers stack traces by copying exceptions across suspension points, so identity holds only for the root cause; the dispatcher test compares threads instead of names, which debug mode suffixes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(node): import the worker module by file URL so the test runs on Windows

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(go): link the C library statically on Linux again

The ops-layer Go binding dropped -Wl,-Bstatic, so go test on Linux needed libaimux_ffi.so on the loader path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(node): commit the napi-generated declarations

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(flutter): check isolate responsiveness by ordering instead of tick counts

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: run the AI SDK pin script tests

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

* docs: note the retired contract fixtures in the RFCs and the changelog

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

* test(flutter): resume the fixture's native sleep after EINTR

On Linux the Dart VM signals its threads, which cut the 180 ms nanosleep short so the slow call returned before the caller's timer ran.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ops): add the RFC-0039 design, coverage ledger, conformance matrix and upgrade guide

The alignment doc gains one registered-difference line per RFC-0039 difference and gap, pointing at the design doc.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(web): note that cargo test rewrites the tracked node types

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

* test(openai): tool calls are flushed after an error frame

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

* refactor(provider-utils): bound the tracker's id suffix search

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

* test(core): restore the OpenAI output tool-argument tests

cc5446fa deleted the end-to-end tests of the OpenAI-compatible output
(fx4/p2 aimux-core/tests/tool_input_test.rs:401-804) together with the
V3 prompt tests around them. The converter unit tests in
openai_output.rs only feed hand-built parts to the converter, so three
contracts lost their only coverage: a repaired call shows in the
completion while the stream keeps the provider's name and deltas, a
repair that still fails keeps the original name and argument text on
both paths, and the stream's two-chunk shape when input deltas arrive.

They come back as aimux-core/tests/openai_output_tool_input_test.rs,
bodies unchanged; only the mock model moves to the [3/8] result types
(request / response on GenerateResult and StreamResult). One new test
completes the raw-text matrix for a known tool: truncated JSON, a JSON
string, non-JSON text, null and a wrong-typed object with whitespace,
each with and without input deltas, through both entry points.

The OpenAI output is an aimux extension (RFC-0035, RFC-0042 §5 items
7-8), so these tests pin aimux behaviour, not upstream behaviour.

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

* test(core): part metadata replays whole into providerOptions

Response messages copy a part's provider metadata into providerOptions
as one map (ai@7.0.127 to-response-messages.ts), and nothing pinned
that beyond a single known namespace. The new test feeds an unknown
namespace with nested nulls, an empty namespace and a known one through
the text path and the streamed reasoning path (metadata on the end
event) and requires the serialized map back unchanged, key order
included.

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

* test(ffi): restore the composite, frame-error and session-input tests

cc5446fa removed the FFI unit tests that need a mock language model
along with the V3 mocks themselves (fx4/p2 aimux-ffi/src/lib.rs
4178-4515 and 4837-4881). The entry points they cover are unchanged at
this layer, so the tests come back with only the mocks moved to the
current result types (request / response on GenerateResult and
StreamResult):

- a recoverable frame error is a data part on aimux_stream_text and is
  skipped on aimux_stream_text_as_openai, and both streams still end
  with on_done;
- aimux_router_new and aimux_moa_new build their models, reject bad
  JSON, a dead member or aggregator handle and a NULL array, treat an
  empty or null config as defaults, and allow zero MoA references;
- aimux_transcription_session_new rejects bad JSON, a non-transcription
  handle and a bad abort handle, and next_part rejects NULL out-params.

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

* fix(core): a stream that ends with Finish is traced as completed

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

* test(core): restore the TraceLayer suite

cc5446fa 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

* test(core): a traced call keeps the id of its recording

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

* test(google): restore the inlineData-to-file tests

cc5446fa 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

* test(core): restore the replay_with_model tests

cc5446fa 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

* test(core): pin an abort after a finished step and repeated tool-call ids

Upstream's stream-text flush (stream-text.ts:1725-1757) rejects only
when no step was recorded; an abort after a recorded step resolves with
that step and null total usage. consume() does the same, and now says
why next to the Abort arm. stream_text never yields an abort between
FinishStep and Finish, so the test builds that order by replacing the
result's stream.

A repeated tool-call id behaves as upstream on both paths: streaming
forwards both calls and resolves a later provider result to the last
one (stream-language-model-call.ts:432, :705-707, :764); generating
maps both content parts to the first (convert-language-model-content.ts
:74-84). Two tests pin that.

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

* docs(alignment): register recording_context and deferred provider results as known gaps

CallOptions still carries recording_context, which §3.1 excludes from
call options; it leaves with the OperationRuntime and attempt scope.
A deferred provider tool result is accepted when a provider tool of the
same name is passed, wider than upstream's supportsDeferredResults; the
flag comes with the tool definitions. Both now sit in the §9.1 list.

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

* refactor(stream_text): the chunk loop has no arm that cannot run

The loop over model chunks relied on its own order twice:
`StreamStart` was handled by an `if let ... continue` before the match,
so its match arm was `unreachable!("handled above")`; and the fallback
arm converted the remaining parts with `map_payloads(|_| unreachable!(..),
|_| unreachable!(..))`, valid only because the tool call and approval
arms came first.

`StreamPart::split_payload` now splits a part into the variants that
carry a payload (tool call, approval request, reasoning file) and the
rest, whose payload types are `Infallible`. The loop matches on that:
each payload has its own arm, and the fallback widens the rest with
`match never {}`. `StreamStart` is a normal arm; the step-opening
preamble skips it with `matches!`. A new payload-carrying variant now
fails to compile instead of panicking at run time. Behaviour is
unchanged.

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

* test(provider-utils): the default-transport test keeps 127.0.0.1 off its proxy

the_default_transport_can_be_replaced swaps the process-wide default
transport for one that sends everything except localhost through a fake
proxy. Tests running at the same time (download_blob, the post_to_api
timeout test) reach their mock servers on 127.0.0.1 through that default,
so with many test threads their requests landed in the fake proxy and
failed. 127.0.0.1 is now on the swapped transport's no_proxy list too.

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

* docs(web): a blank key at a custom base_url goes out as an empty key

The README and RFC-0029 said such a call goes out without a key. The
key is an explicit empty string, as with the AI SDK's `apiKey: ''`:
no stored or environment key is used, and providers that always send
their credential header (OpenAI's `Authorization: Bearer`) send it
empty.

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

* test(provider-utils): pin the redirect cap and re-signing on a same-origin hop

Two redirect rules had no test: an API call follows at most ten hops
and fails on the eleventh with the redirect's status, and a hop that
stays on the request's origin goes through the signing transport again
and is signed for its own URL, never with the first request's
signature (RFC-0040 §3.2). With the signing clock fixed, the two
signatures can differ only because the path does.

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

* docs(rfc): RFC-0040 §3.2 follows the implemented redirect rule

The merged RFC-0040 says credentialed requests do not follow redirects
by default. API requests follow them, as the AI SDK's postToApi does,
with credentials locked to the request's origin: a hop off it keeps
only User-Agent, signing and token decorators add nothing, a hop back
does not restore them, and the chain stops at ten. §3.2 gets a
revision note saying so and where the code and tests are, and the
alignment doc's §5 registers the difference from both the RFC text and
upstream fetch.

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

* test(providers): a preset's base-URL and optional-key variables

Nothing tested a registry row's base_url_env or its optional key
variable directly. Four serial tests use the ollama row: the base URL
variable is read per request, not when the provider is created; an
explicit base_url wins over it; an unusable value fails the request
with an InvalidArgument naming the variable and sends nothing; the
optional key goes out as Bearer only when set and non-empty. The
existing keyless test becomes serial and clears OLLAMA_API_KEY so it
cannot see a key one of them set.

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

* test(mistral): model_length is a length finish

The mapping follows @ai-sdk/mistral 4.0.54, which has no test for it.
Generate and stream both report Length and keep the raw reason.

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

* test(providers): non-object input of a past tool call

One assistant message carries six past tool calls (null, a string, an
array, a number, a boolean and an object) through each factory. OpenAI
chat sends "{}" for every non-object, Groq and DeepSeek send
JSON.stringify(input), and Anthropic wraps it as rawInvalidInput, as
their upstream converters do.

RFC-0042 §6.2 named only Bedrock for rawInvalidInput; Anthropic's
toAnthropicToolInput applies the same rule.

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

* fix(anthropic): toolset member calls go back as upstream sends them

A computer-toolset member call in the history was converted differently
from @ai-sdk/anthropic 4.0.71 (convert-to-anthropic-prompt.ts:984-1004):

- the `action` was removed from the input; upstream sends the input as
  it is, `action` included;
- a call without a string `action` was dropped with a warning, which left
  its tool_result without a tool_use; upstream warns and still sends the
  call, named after the toolset.

Both now follow upstream. The rest of the toolset path is aligned too:
the toolset name is read from `providerOptions.anthropic.toolsetName`
only (`getAnthropicToolsetName`), a tool_result carries `toolset_name`
before `content`, a member call's metadata is `{ toolsetName, caller }`
in that order, and a streamed member input that secureJsonParse would
refuse (malformed, or with a prototype key) is kept raw rather than
getting the `action` injected.

The computer toolset had no CHANGELOG line; it has one now. New tests in
anthropic_toolset_test.rs cover the request, both response paths and a
full round trip.

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

* test(preset): the registry checks run on any input

The registry schema checks lived in load_registry, which read the
embedded provider_registry.json, so they could only be exercised by
editing that file. parse_registry takes the JSON text instead; the
process-wide table passes the embedded file. New unit tests feed it
rows that break each rule (duplicate or malformed name, non-http base
URL, unknown auth, missing key variable, placeholders without params, a
derived param with env/default, an explicit null, an unknown field) and
one valid pair of rows.

The by-name shim in provider.rs now names the PR that removes it: #265,
where the C ABI and the bindings move to the ops table and stop calling
it.

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

* fix(anthropic): provider tool args are checked as upstream checks them

@ai-sdk/anthropic 4.0.71 prepareTools runs validateTypes on the args of
ten provider-defined tools: the computer toolset, the advisor,
text_editor_20250728 and the three web_fetch and three web_search
versions (anthropic-prepare-tools.ts:197-445). The Anthropic and Vertex
paths checked none of them and sent whatever came in; Bedrock checked
six, in a copy of its own.

The check now lives once, next to the tool definitions, and runs in
prepareTools' place for all three providers: args that do not match
fail the call with InvalidArgument before any request, and the other
tools still pass their args through unchecked. Bedrock keeps dropping
the three tools it does not support before the check, as upstream does.
prepare_tools and prepare_tools_with_provider return a Result.

The advisor regression test now passes the model the cassette recorded,
since an advisor without one is refused. New tests in
anthropic_provider_tool_args_test.rs and bedrock_upstream_e2e_test.rs.

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

* test(provider-utils): say how the botocore SigV4 pins were made

The pinned signatures match botocore's SigV4Auth only when the request
carries X-Amz-Content-SHA256 before signing; SigV4Auth adds that header
on its own for S3 alone. The test's doc comment now says so, so the
pins can be recomputed.

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

* docs(core): a tool result carries only output

The changelog still said a tool result reads the earlier result and
is_error fields; this layer removed that, so the entry now says a tool
result carries only the AI SDK output object. ToolResultPart's upstream
reference points at content-part.ts:218-241, and the reasoning parts
drop an #[allow(unused)] that suppressed nothing: their provider
metadata reaches the response messages and the stream aggregate.

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

* feat(anthropic): decision_model is upstream's evaluationModel

@ai-sdk/anthropic 4.0.71 defines evaluationModel as an
EvaluationLanguageModel over the Messages model, and the registry's
decision lookup falls back to it, so "anthropic:<id>" resolves upstream
while aimux returned NoSuchModel. decision_model now returns the same
DecisionLanguageModel adapter Google uses, with provider
"{name}.evaluation". The alignment doc's §3.1 names all three adapters.

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

* test(google): restore the thought-signature replay tests

[1/8] deleted the node test and the Rust request-body test for issue
#9: a Gemini thinking model rejects the next turn with HTTP 400 unless
the functionCall's thoughtSignature comes back beside it. What remained
checked only the parse direction. Three request-body tests bring the
replay direction back on the V4 prompt shape:

- providerOptions.google.thoughtSignature on a tool call is sent as a
  sibling of functionCall in the same part
  (convert-to-google-messages.ts:404-411);
- an unsigned call before Gemini 3 is sent without the field;
- on Gemini 3 an unsigned call gets skip_thought_signature_validator and
  a signed one keeps its signature (convert-to-google-messages.ts
  :540-552).

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

* test(providers): a failed decision is recorded and replays as the failure

"openai:<id>" with a refusal and "vllm:<id>" with a skipped question are
resolved through the registry under a RingRecorder. Each fails with the
API's message after one request, leaves one recording with outcome
status Error, no decision result and no key, and that recording replays
offline as the same failure rather than an answer.

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

* fix(core): request options reject unknown keys

[8/8] switched the request options to camelCase JSON but left them
lenient, so a key written the aimux 0.5 way (max_retries,
timeout.total_ms, fail_mode) silently fell back to the default instead
of failing. RFC-0042 §7.2 asks for unknown request fields to be
rejected.

GenerateTextOptions, TimeoutConfiguration, the options of the other
operations (embedding, speech, image, transcription, reranking, video
and its poll settings, search, file upload, decide), MoaConfig and the
C ABI's router and transcription-session configs now deny unknown
fields, so the error names the key. CallOptions stays lenient: a
recording keeps the call options and the operation settings in one
object, and replay reads both from it. Replay first drops the
null-valued keys of a recorded timeout, because aimux 0.5 wrote every
unset timeout key as null and such a key carries nothing.

The Node and Python bindings had three bugs of the same kind:
- their router config still read provider_name / model_id, so the JSON
  that names the router through the C ABI was ignored there. It is now
  camelCase and strict like the C ABI's, and so is their transcription
  session config, which still read input_audio_format;
- transcribe filled its placeholder as media_type, so any opts_json
  failed with "missing field `mediaType`". The code and its unit test
  now share one placeholder list;
- test_gil.py passed snake_case LIMITS, so its timeout guard was gone.

The bindings' Rust unit tests still used snake_case keys and never ran
in CI, because the crates are not workspace members. They are fixed,
and the node-binding and python-binding jobs now run cargo test --lib.
The Node test for a per-call body_overrides now expects the rejection,
and the Java SearchModel example uses maxResults.

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

* docs(core): replay's question types and the video status key

MockDecisionReplayModel reports every question type because a recording
holds no capability snapshot; decide rejects an unsupported type before
recording, so replay fails such a request with no matching recording
instead of answering it.

The idempotency key aimux mints goes on do_start only, while do_status
gets the caller's headers, as the AI SDK does.

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

* fix(core): a prompt part with an unknown type names it

Every tagged union already rejects an unknown `type` with "unknown
variant `x`, expected one of ...", as RFC-0042 §7.2 asks. The untagged
wrappers around them lost that error: ModelPrompt, UserContent and
AssistantContent tried each variant in turn and reported only "data did
not match any variant of untagged enum ModelPrompt", which is all a C,
Node or Python host saw for a prompt with a misspelled part type.

FilePartData had a worse case. As an untagged Tagged | String |
Reference, it read {"type": "bogus", ...} or {"type": "URL", ...} as a
provider reference whose provider is "type". The AI SDK never does
that: isProviderReference requires !('type' in data) and
isTaggedFileData takes the object as tagged data.

The four types, and ReasoningFilePartData, now deserialize by JSON
kind: a string is the text or string form, an array is read as the
parts or messages, and an object with a `type` key is read as tagged
file data, so each element keeps its own error. Serialization and the
generated TypeScript types are unchanged; only Deserialize is written
by hand.

New tests pin the "unknown variant" error for every tagged union, for
the wrappers, for file data inside a prompt, and at the C ABI.

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

* docs(core): cite upstream for the snake_case video frame type

VideoFrameType is the one snake_case literal pair left in the public
JSON, and it is upstream's own: @ai-sdk/provider@4.0.22
video-model-v4-frame-image.ts:9 declares 'first_frame' | 'last_frame',
and ai@7.0.127 generate-video.ts:814 compares against it. The doc
comment now says so instead of "Aligned with V4", and a test pins both
values and rejects the camelCase spelling. VideoFrameType.ts is
regenerated for the new comment.

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

* test(anthropic): compare usage JSON with what the AI SDK wrote

The usage shape (camelCase, both detail objects always present, unknown
counts absent, no raw on the total) was pinned only against hand-written
JSON. The recorded fixtures hold the AI SDK's own result.usage, which
in ai@7.0.127 is totalUsage (generate-text.ts:1797-1799), built by
addLanguageModelUsage (types/usage.ts:122-155).

assert_generate_result now sums the one step's usage the same way and
compares the whole JSON with the recorded value, so a drift in key
names, empty detail objects or raw shows up against upstream output.

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

* test(aimux-core): replace the in-test legacy json_repair port with recorded cases

The differential test kept a 430-line copy of the line-for-line port of
fix-json.ts / parse-partial-json.ts as its oracle. Record its verdict
instead: 740 inputs (one shortest per outcome/ending bucket out of
300 000 generated ones, plus hand-picked edges) with the fix_json output
and the PartialJson outcome, in fixtures/json_repair_cases.jsonl. The
upstream cases stay in util_test.rs. 14 hand mutations of json_repair.rs
are all caught.

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

* docs(core): say why json_repair stays public

Core's own paths never repair a final tool-call input (RFC-0042 §3.2).
The module stays public because the AI SDK exports parsePartialJson, for
callers that render streamed partial JSON.

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

* refactor(providers): keep vendor endpoints out of shared and openai_compatible

Making provider config plain data left the shared layers importing vendor
modules: shared's EndpointSource named the Vertex, Bedrock and Polly
endpoints and called google::supported_urls, and openai_compatible's
BaseUrl and fetch headers named the Vertex partner types and carried
Alibaba and Moonshot AI usage variants.

The shared layer now defines the extension points and the vendor modules
implement them on their own plain-data structs: ResolveEndpoint (with
HttpEndpoint for the common case) in shared, ResolveBaseUrl and
ResolveFetchHeaders in openai_compatible. The Gemini supported-URL patterns
are passed as a function of the base URL and model id. The Alibaba and
Moonshot AI usage converters move to preset.rs, which owns those registry
rows, and reach the chat model through the existing convert_usage hook, so
UsageDialect is gone.

ChatSettings::error_structure is a capturing Arc<dyn Fn> again, the same
shape as before and as convert_usage next to it.

check_provider_boundaries.sh gains a rule that fails when code in shared/
or openai_compatible/ names a crate module other than the shared layers,
the preset registry and runtime_decision.

Behaviour is unchanged.

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

* test(searxng): the instance URL comes from SEARXNG_URL per request

Covers the BaseUrl::Env path of the config-as-data change: set, the
request goes to that instance; unset, the request fails with LoadSetting
and nothing is sent. Also note in the changelog that PresetDescriptor can
no longer be built with a struct literal outside the crate.

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

* refactor(providers)!: remove the by-name provider shim the bindings no longer use

Bindings create providers through the ops layer, so aimux_providers::provider and its JSON options test have no callers left.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(provider-utils): PipeThrough is a Stream over one reused queue

pipe_through ran the transformer inside an async_stream generator and
moved the controller's queue out with std::mem::take after every call, so
each chunk allocated a fresh Vec. It now returns PipeThrough, a plain struct
that implements futures::Stream: it hands out queued parts from one
VecDeque that is kept for the whole stream, pulls the next input chunk only
when the queue is empty, and calls flush once the input ends unless the
transformer closed the stream. start runs when the stream is built, as the
WHATWG constructor does; nothing it emits changes.

TransformStreamController gains new, drain and is_closed so a transformer
can be driven and checked synchronously, without a stream.

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

* refactor(providers): chat streams share ChatToolCalls, no tracker type

StreamingToolCallTracker lived in aimux-provider-utils behind a builder,
a TypeValidation enum, a TrackerError type and a caller-owned out-vector
that every chat stream drained after each call. Its only users are the five
OpenAI-style chat streams, so it moves next to them as crate::stream's
ChatToolCalls: a Vec of in-progress calls plus the id generator, whose
delta method emits straight into the stream's controller and ends the
stream with InvalidResponseData itself. The vendor's type rule is a plain
invalid_type flag the stream computes from its own wire type (OpenAI:
'function' when present; Groq: 'function' required), so no rule enum is
left. The matching table, the 5.0.55 empty-arguments handling, the
dropped ambiguous deltas, id de-duplication and flush ordering are
unchanged, and so are the parts every stream emits.

The five chunk handlers stay separate Transformers: they differ in error
handling, the order of reasoning, text and tool parts, response-metadata
rules and finish metadata, so a shared one would be a set of flags. What
they do share now is the read-only StreamContext (warnings, raw-chunk
flag, URL, request body, response headers) with start and raw helpers,
and their accumulated state derives Default, so a constructor lists only
what the request decides.

The tracker's upstream test suite and the T3 differential fixture are
ported case for case to drive the streams' transform and flush directly:
untyped cases run on Mistral (optional index, injected ids), 'if-present'
on OpenAI, 'required' on Groq and extractMetadata on the
OpenAI-compatible thought signatures. The argument-state port moves with
its only caller.

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

* refactor(providers): vendor streams share StreamContext and Emit

Every vendor stream struct carried its own copy of the request context
(warnings, the raw-chunk flag, the URL, request body and response headers
an in-stream error reports, and for some the id generator) under
per-vendor names, and every constructor listed each accumulated field a
second time with its starting value. The context is now one StreamContext
field (with start and raw helpers, and the model's generate_id), and the
streams derive Default, so a constructor names only what the request
decides and ends with ..Default::default(). Where a starting value was not
a default (finish reason 'other', a first-chunk flag set to true) the
field becomes an Option or an inverted flag that the flush reads the same
way.

The rest of the repetition goes too: the parts emitted without provider
metadata (text, reasoning and tool-input start/delta/end) are written
through the Emit extension trait on the controller, one line each instead
of a five-line struct literal; Controller names
TransformStreamController<StreamPart>, so the transformer signatures fit
on one line; and FINISH_OTHER / FINISH_ERROR replace the finish-reason
literals without a raw reason, in the streams and the generate paths next
to them.

Google and Vertex keep separate streams. The emitted parts are unchanged.

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

* test(providers): drive chat and Anthropic stream transformers synchronously

Vendor streams are plain Transformer structs now, so their behaviour can be
pinned without HTTP or SSE framing. A small crate-internal helper,
stream::testing::run, calls start, transform per chunk and flush exactly as
pipe_through does (stopping once the controller is closed) and renders each
emitted item as a short line.

The OpenAI chat stream is covered for interleaved tool calls (deltas in
arrival order, end and tool-call parts in index order at flush), an error
frame (error part, then text-end and an Error finish), a flush that closes
open reasoning and text blocks, and a non-function tool type that errors the
stream without a flush. The Anthropic Messages stream is covered for
interleaved text and tool_use blocks ending at message_stop (later events are
not read), a mid-stream overloaded error, and a flush without message_stop,
which emits no finish part.

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

* chore(providers): let shared layers use the stream module in the boundary check

The vendor-reference rule for shared/ and openai_compatible/ predates the
crate-internal stream module that the chat streams now share; stream is a
shared layer, not a vendor.

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

* docs: say where the chat streams keep their tool-call state

RFC-0042 T2 and §4 placed the accumulation state inside provider-utils.
After the tracker removal in this layer it is a crate-private field of
the OpenAI-chat family's stream Transformers in aimux-providers/src/stream/.
Name that location, update the alignment doc rows that still said the
tracker moves into aimux-provider-utils, and register the structural
difference from @ai-sdk/provider-utils in §5.

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

* test(provider-utils): nothing is emitted after a transform stream ended

Every vendor stream relies on the controller dropping what is emitted
after terminate() or error(); cover it directly, and name the AI SDK /
WHATWG counterpart of enqueue_error, error and terminate in their docs.

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

* fix(ops): terminate and drop an operation safely after its runtime shut down

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(ops): error envelopes never carry a missing or invalid secret

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(rfc-0039): record the accepted D8 and schema-2 order, the new amendments and documented semantics

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ops)!: one schema per control object, poll mode for operation.next, digest in the manifest

The manifest's control schemas are the only definition; codegen copies them
instead of composing contradicting ones. operation.next is a poll op, the
descriptor field is sourceSymbolInventory (RFC-0039 §2.2), and the manifest
carries its own digest so bindings can pin it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(codegen): write the Dart constants under lib/ so package imports resolve

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(bindings): pin opsVersion and manifest digest exactly, name ops by constant, reclaim forgotten Java handles

Go, Java, Kotlin, Swift and Flutter require the exact generated OPS_VERSION
and the manifest's digest. Facades name ops through the generated constants.
Java handles a caller never closed are reclaimed through aimux_handle_finalizer
from a phantom-reference queue. Java bounds the callback frame copy like the
other bindings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(node,python): replay the shared vectors, reject a full push queue, pin the manifest exactly

Node and Python replay all eleven shared operation scenarios through their
real dispatch path in their normal test commands. Node's operation.push
rejects with ResourceLimit when the queue is full, as Python and C do. Node
reads ABI, ops version and digest from its generated constants, now under
src/generated so the compiled package keeps its layout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: drop steps that claimed shared-vector checks, one iOS symbol list, add ASan and Linux ARM64 jobs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(rfc-0039): every binding now pins the manifest digest

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ops,node): one handle-id rule shared by DTO decoding, the registry and Node

Registry and Node parsed handle ids by hand and rejected leading zeros, unlike
HandleId and the manifest schema. All three now use HandleId's FromStr.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(rfc-0039): the series' last PR removes schema-2 and older recording reads

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(core): fix_json steps through UTF-16 code units like upstream

A character outside the BMP is two code units upstream and one char here,
so "[1😀" was repaired to "[1]" where upstream gives "[1😀]". Stepping
such a character twice reproduces upstream's two steps: neither half is a
JSON token, and a mark on either keeps the whole character.

Against the same 1,000,000 inputs run through ai@7.0.127 fix-json.ts, the
63 fix_json differences are gone; fix_json now matches upstream on all of
them.

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

* test(dataforseo): the login comes from DATAFORSEO_LOGIN per request

* feat(core): the recording context has an operation scope

RecordingContext::scope runs a future with a recording context current,
RecordingContext::scope_stream keeps it current on every poll of a stream,
and RecordingContext::current reads it, the same way OperationSettings
carries the retry count, timeout and session id (operation.rs). A None
scope hides an outer context.

This is the mechanism the model call options' recording_context moves to:
the alignment document's call options carry no recording context (§3.1),
and the transport reads its diagnostic scope from the operation (§3.5).

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

* docs(contributing): say what .agents/ holds and in which language

* refactor(core)!: the recording context is an operation value, not a call option

CallOptions and DecisionCallOptions lose recording_context; the V4 call
options carry no recording context (@ai-sdk/provider@4.0.21
language-model-v4-call-options.ts:8-138), and the alignment document's
call options carry none either (§3.1). This was the last item of that kind
in its known-gaps list.

generate_text, stream_text and decide run the model call in
RecordingContext::scope; stream_text's pump task polls the provider stream
through RecordingContext::scope_stream, since a spawned task does not
inherit the scope. The router and MoA scope each child step in the child
context they used to put on the child's options (step_context), so every
exchange keeps its call id, step label and attempt numbers.
HttpRequest::new, the Anthropic request builder and the trace layer read
RecordingContext::current(). The JSON is unchanged: the field was never
serialized.

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

* fix(core)!: a deferred tool result needs a provider tool that supports it

A provider tool result whose call is not in the response is accepted only
for a provider tool of that name that sets supportsDeferredResults, as in
ai@7.0.127 convert-language-model-content.ts:96-103; before, any provider
tool of that name was enough.

ProviderTool gains the AI SDK tool's optional supportsDeferredResults
(provider-utils@5.0.55 src/types/tool.ts:316-326), which upstream's tool
factories set: Anthropic web_search, web_fetch, code_execution 20250825 and
20260120, tool_search and advisor, and openai.programmatic_tool_calling. It
is omitted from the JSON when absent and is not sent to the provider. The
alignment document no longer lists the wider rule as a gap.

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

* docs(core): the replay key and input record no longer name a recording_context field

CallOptions has no recording_context since the recording context became an
operation scope; the comments on canonical_call_key, call_options_match and
InputRecord.options list the runtime fields that remain.

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

* build(core): ts-rs exports run only from the generator

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

* fix(provider-utils): the tool call tracker generates ids as upstream

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

* feat(bindings): typed provider tools carry supportsDeferredResults

The AI SDK's provider tool has an optional supportsDeferredResults flag
(provider-utils@5.0.55 src/types/tool.ts:316-326). The typed ProviderTool
in Java, Kotlin, Swift, Python and Flutter gains the same optional field,
written as "supportsDeferredResults" and omitted from JSON when absent.
Go has no typed ProviderTool, and the Node type is generated from the
core. A round-trip test in each binding covers the field absent, true and
false.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anth…
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