Repository navigation
[6/8] fix: vendors follow the upstream packages - #254
Merged
Merged
Conversation
6 of 12 tasks
10 of 12 tasks
fixtures/aisdk/VERSIONS.json lists the seven AI SDK packages that the
upstream comparison tests are pinned to: ai 7.0.127, @ai-sdk/provider
4.0.21, @ai-sdk/provider-utils 5.0.53, @ai-sdk/anthropic 4.0.71,
@ai-sdk/google 4.0.87, @ai-sdk/openai 4.0.83 and
@ai-sdk/openai-compatible 3.0.62.
Every entry has one shape, {"version": "<x.y.z>", "integrity":
"sha512-<base64>"}, where integrity is the sha512 SRI of the npm tarball
(the registry's dist.integrity). A bare version string is not a valid
entry. The file used to arrive later in the stack, together with the
provider factories, as bare version strings; it comes first now so that
every later change can be checked against the pinned upstream sources.
Ported from fork commit 9d7508c.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
scripts/fetch-aisdk.sh downloads every package pinned in
fixtures/aisdk/VERSIONS.json with npm pack and unpacks it into
target/aisdk/<package>@<version>/, so the upstream sources that the
comparison tests follow can be read locally at exactly the pinned
versions.
- The whole VERSIONS.json is checked before anything is downloaded:
every entry must be {"version", "integrity": "sha512-..."}, with a
plain npm package name (it becomes a path) and an exact version.
- Each tarball's sha512 must equal the pinned integrity, and its
package.json must name the pinned package and version.
- A package is unpacked into a work directory under target/aisdk and
renamed into place only after its checks pass, so a failure never
leaves a partial tree. A rerun replaces each tree with identical
content.
AISDK_VERSIONS and AISDK_DEST override the input file and the
destination. scripts/fetch-aisdk.test.mjs (node --test) runs the script
against an npm registry served from the test process, with a temporary
npm cache and empty npm config files, so it needs no network.
Ported from fork commit 19db928. Unlike the fork, a bare version string
or a missing integrity is an error instead of a fallback to npm view,
because VERSIONS.json has a single format.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
scripts/fetch-aisdk.sh now verifies every package before it places any. Phase 1 fetches, checks and unpacks all packages into a staging directory; phase 2 moves the old trees aside, places the new ones and puts the old ones back if a move fails. A network error, a missing version or an integrity or version mismatch in any package leaves target/aisdk exactly as it was. The new test "a failing package leaves every destination as it was" fails on the per-package version. scripts/aisdk-fixtures/versions.mjs is what the fixture recorder uses to read and rewrite fixtures/aisdk/VERSIONS.json: - readVersions / checkVersions accept only the object form, with the same name, version and sha512 checks as fetch-aisdk.sh. - versionsDocument keeps the pinned integrity when the version is unchanged, takes it from package-lock.json when the lock pins the same version, and fails when neither has it or when the two disagree. It never writes an entry without integrity. versions.test.mjs (node --test) covers this and checks the committed VERSIONS.json. Its lockfile comparison is skipped until scripts/aisdk-fixtures/package-lock.json exists. Ported from fork commits ed1564b and 1ce4eb6. Unlike the fork, versionsDocument fails instead of writing a version-only entry, because VERSIONS.json has a single format. The recorder (record.mjs) is not part of this change; it switches to versionsDocument when it is added. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
… 5.0.55 does The tracker followed @ai-sdk/provider-utils 5.0.53, which merges a second parallel call into the first when the stream reuses or omits labels: a named delta with an unseen id and empty arguments resolved to the matching call even after that call's structured arguments were complete, and an unlabeled continuation with several open calls was dropped as ambiguous. 5.0.55 (5094ebd) adds `hasEmptyArguments` (src/streaming-tool-call-tracker.ts:131, :140-141), passes `name != null && hasEmptyArguments` as the start evidence on the unseen-id path (:275-285), resolves the unlabeled path through `resolveMatchingToolCall` (:305-308), and lets that function pick the only candidate without complete structured arguments (:324-354). The Rust tracker now does the same. The three test cases added upstream in 5.0.55 (src/streaming-tool-call-tracker.test.ts:248-302, :304-341, :343-381 at tag @ai-sdk/provider-utils@5.0.55) are ported; the first and third fail on the previous tracker. fixtures/aisdk/VERSIONS.json pins provider-utils 5.0.55 with its registry integrity; the other entries are the object-form pins of the fork's VERSIONS.json. The user-agent version string is left alone. The alignment doc's #185 row records the new baseline. Closes #228 B10. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The AI SDK V4 interfaces carry `specificationVersion: 'v4'` on `ProviderV4` and on every model interface, so the traits keep it too (issue #230, B4): - `Provider` and the model traits (language, embedding, image, transcription, speech, reranking, video, search, files, skills, decision) have a default `specification_version()` returning "v4"; `&P` forwards to the provider it points to. Wrappers keep the default, as the upstream wrappers report 'v4' themselves. - check_provider_boundaries.sh no longer forbids the method. - The architecture alignment doc and the CHANGELOG no longer describe it as removed. - The Anthropic and Google files tests check it again, and a new test checks providers from `create_provider` and from a registry, with their language models. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…private Re-applies fork PR #41 on this layer. The `streaming_tool_call_tracker` and `streaming_tool_call_argument_state` modules are private; the crate root re-exports `StreamingToolCallTracker`, `StreamingToolCallDelta`, `TypeValidation` and `TrackerError`. `@ai-sdk/provider-utils@5.0.55` exports only the tracker and its delta and options types from `src/index.ts:115-119`; the argument state is internal. So `StreamingToolCallArgumentState` and `starts_with_structured_value` are no longer re-exported (the fork hid them with `#[doc(hidden)]` instead), and their port of `streaming-tool-call-argument-state.test.ts` moves into the module. The tracker's correlation table moves from the now private module docs onto the type. The unreleased CHANGELOG entry says so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
… SDK tracker Re-applies fork PRs #44 and #47 on this layer: twelve delta sequences (unattributable and ambiguous deltas, blank names, typeValidation 'if-present' / 'required' / 'none', interleaved index and id reuse) with what the pinned upstream StreamingToolCallTracker emits for each, per delta, on error and on flush. The test drives aimux's tracker the same way and compares. scripts/gen-t3-tracker-fixture.mjs no longer hardcodes 5.0.53: it reads the "@ai-sdk/provider-utils" pin from fixtures/aisdk/VERSIONS.json, refuses any other installed version, and records the version and the sha256 of src/streaming-tool-call-tracker.ts in `source`. The fixture holds the 5.0.55 results. It was not regenerated by running the script here (upstream code is not executed in this environment); the 5.0.53 recording was updated by reading the 5.0.55 source. Of the twelve cases only two reach a path 5.0.55 changed (resolveMatchingToolCall with several candidates and no explicit start, or the unseen-id path with a named empty fragment). `ambiguous_delta_across_open_calls_is_dropped` is unchanged: both open calls are incomplete, so it is still ambiguous (streaming-tool-call-tracker.ts:305-308, :349-351). `index_reuse_with_new_id` changes: the index-only continuation "2}" now goes to call_b, the only call at index 0 whose structured arguments are incomplete (:291-298, :345-347), so call_b ends as {"b":2} instead of {"b":. Running the script against a node_modules holding 5.0.55 should reproduce the file byte for byte. The typeValidation per case follows the upstream callers: @ai-sdk/openai@4.0.83 openai-chat-language-model.ts:541 ('if-present'), @ai-sdk/groq@4.0.52 groq-chat-language-model.ts:393 ('required'), @ai-sdk/openai-compatible@3.0.62 openai-compatible-chat-language-model.ts:555 (default 'none'). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
When a tool call is invalid, Core still fills `input` with the parsed argument text when that text is valid JSON, as the AI SDK's catch-all does (ai@7.0.127 src/generate-text/parse-tool-call.ts:112-133). That back-fill used plain serde_json, while the main path rejects a `__proto__` key or a `constructor.prototype` (contains_forbidden_prototype). Upstream uses `safeParseJSON` for the back-fill too (:116-117), which goes through `secureJsonParse` (@ai-sdk/provider-utils@5.0.53 src/secure-json-parse.ts:49-67), so such text stays the verbatim string. The back-fill now applies the same check. Test: invalid_call_input_is_not_back_filled_with_forbidden_prototype_keys covers a known tool (rejected by the main path) and an unknown tool (NoSuchTool) with both forbidden shapes; it fails without the change. Closes #229 C48. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The comment claimed alignment with `LanguageModelV4CallOptions.timeout`, but V4 call options have no timeout (@ai-sdk/provider 4.0.21 language-model-v4-call-options.ts:8-138). The type is the object form of the AI SDK's `TimeoutConfiguration` (ai 7.0.127 src/prompt/request-options.ts:13-22), a request option of generateText / streamText (`RequestOptions.timeout`, :126). Note the upstream keys aimux does not carry (`toolMs`, `tools`, the bare-number `totalMs` form). Closes #230 B20. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Replace the integration-branch atomic cutover in the alignment document (header, section 0.4 Q3, D31, the section 0.7 ABI row, section 9.1 and appendix I1) with layer-by-layer merges into master. Each layer is CI-green with all its consumers updated, and nothing is released from master until every layer is merged and the P2 gates pass; fixes for released versions ship from a maintenance branch. The new C ABI, codegen, the full recording schema 3 and the binding migration come last, before the release. Until then the existing C constructors keep their signatures and call the new factories, and ts-rs stays as the interim generator (section 0.5 and the Part 2 generator note). The Part 2 implementation order moves codegen, the new C ABI and the full recording schema to the end. ROADMAP defers the ops protocol schema and L0 passthrough until after the migration and stops fixing C1 at nine exports. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Remove designs from the alignment document that were never implemented and are no longer planned: - LanguageModelRef, ResolvedModel and RegistryModelReference. Model references follow upstream: an operation takes a string model ID, resolved through a registry, or a model instance; named methods come from the registration's extensions. The recording target keeps the string ID, and live replay asks for an explicit target when there is none. - Registered sidecar metadata and its propagation through resolution, scope and wrap. Trace metadata comes from an explicit audit family or an injected descriptor index, and catalogue joins use explicit package/model links. - The SessionInferer trait and registration API. Session inference keeps the existing opt-in prefix implementation in aimux. - Session affinity middleware. Callers pass session headers per request through headers, as RFC-0019 describes. Section 1.2 rulings and the appendix entry that pointed at these designs now state what aimux does instead. The size and performance gates (S1-S3, P, the CLI budget) and the capability matrix move out of the alignment document to ROADMAP as work after the migration. Their thresholds wait for the rewrite of #208 (RFC-0038); until then each migration layer's PR lists artifact sizes before and after. Section 9.1 gains a known-gaps list of the agreed designs the migration layers do not implement, one line each. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
This layer added `ToolCallNotFoundForApproval` and dropped the variant-set test instead of updating it. Restore the exhaustive match and the wire-tag list for the current 17 variants, add the new variant's golden payload, and pin its message against the AI SDK's ToolCallNotFoundForApprovalError (ai 7.0.127 src/error/tool-call-not-found-for-approval-error.ts:22). Closes #232 N31-7.1-1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
This layer deleted the 17 router tests that the tool-call layer still had (origin/v4/2-tool-calls-follow-the-ai-sdk:aimux-core/src/router.rs). Bring them back on the current result types (`GenerateResult.request` / `.response`, `StreamResult.request` / `.response`): routing strategies, fallback on error, no fallback for streams, child retries without rerouting, a single composite run when every child fails, and step-labelled child recording contexts. Router is an aimux extension with no AI SDK counterpart, so these pin aimux behaviour (RFC-0021) rather than upstream behaviour. Closes #232 B23 (router half). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The AI SDK wraps only an error thrown by the repair function in
ToolCallRepairError (ai@7.0.127 src/generate-text/parse-tool-call.ts:90-96).
A repaired call is parsed with doParseToolCall (:103-106); when that throws,
the outer catch builds the invalid call from the original call and carries
the repaired call's own InvalidToolInputError / NoSuchToolError (:112-133).
Core wrapped that second failure in ToolCallRepair { original_error, cause }.
It now returns the original call, invalid, with the replacement's error, for
the closure path and for a host's `repaired` reply alike. This is the
parse/repair change the layer plan takes from layer D2 (fb0f79e).
Because that error carries the replacement's argument text, the
OpenAI-compatible output and the host-repair reconstruction no longer take
the error's text blindly: `original_tool_call_text` uses it only when it
back-fills to the call's own `input`, and falls back to `input` otherwise.
Tests: an_invalid_repair_is_not_repaired_again now expects the unwrapped
InvalidToolInput; a_repaired_reply_that_fails_validation_is_not_wrapped
covers the host path; invalid_repair_keeps_original_arguments_with_original_name
still shows the original arguments in both OpenAI outputs.
RFC-0035 takes the hunks from the doc corrections: the `repaired` row and
the control-flow sentence say the failure is not wrapped, and the
pre-evaluated `input_schema` and Swift's delayed abort are registered as
differences (line references checked against ai 7.0.127). Binding doc
comments that promised a ToolCallRepair error here are corrected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Re-applies fork PR #14 (RFC-0042 §3.2 vectors), keeping only the rows whose result equals ai@7.0.127 parseToolCall: - a provider-executed dynamic call with no tool set is parsed by parseProviderExecutedDynamicToolCall (src/generate-text/parse-tool-call.ts:199-224) with safeParseJSON (@ai-sdk/provider-utils@5.0.55 src/parse-json.ts:89); - a JSON string stays a string, scalars and null parse, a repeated key keeps its last value, and ` 1e2 ` is 100 (compared by value, since serde_json keeps it as a float); - unparseable text is an invalid call whose error and input keep the verbatim text (:112-133). Dropped: the row expecting empty text to be invalid. Upstream parses blank text as {} (:202-205). The fork's ignored/pending vectors are gone. Also keeps duplicate_key_text_survives_in_the_error: the error's toolInput is the original text (:258-263). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The retry wrapper's two texts put `getErrorMessage(error)` after "Last error:" (@ai-sdk/provider-utils@5.0.53 retry-with-exponential-backoff.ts:97, :103, :138; @ai-sdk/provider@4.0.21 errors/get-error-message.ts). For an API failure that is `APICallError.message`, which never carries the HTTP status. Aimux composed the text from `AiMuxError`'s Display, giving "Last error: API call error: HTTP 429: ...". The text now uses the attempt's own message: `ApiCallError.message` for an API failure, the variant's message otherwise. JavaScript's `Error.toString()` also prefixes the class name (`AI_APICallError: `); Aimux errors have no class name, so only the message part is reproduced. Issue item N31-6.4-1. Tests: retry::tests::retry_error_messages_match_the_ai_sdk, aimux-ffi retry error test now pins the exact text. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Eight `max_retries` comments said `None` meant "provider default" or "the model default", the per-provider retry design the alignment document withdraws (docs/aisdk-architecture-alignment.md D14). They now name the AI SDK's `maxRetries` and its default of 2 (ai@7.0.127 util/prepare-retries.ts), and say that a model's `retry_config()` can still set another count until the layer that removes model-level retry settings lands. Issue item N4.5-1 (code comments). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The AI SDK types provider options, provider metadata and provider references as JS objects (`@ai-sdk/provider@4.0.21` shared-v4-provider-options.ts:24, shared-v4-provider-metadata.ts:24, shared-v4-provider-reference.ts:20), which keep their keys in insertion order. `SharedProviderOptions` and `SharedProviderMetadata` become one alias, `NamespaceMap = IndexMap<String, JsonObject>`, and `SharedProviderReference` becomes `IndexMap<String, String>`, so a value serializes the way it was built or parsed. The inner objects already kept order through serde_json's `preserve_order`. The JSON shape and the generated TypeScript are unchanged; Rust callers that built these maps from a `HashMap` now build an `IndexMap` (`NamespaceMap::new()` / `::from`). `VideoOperationStatus` allows clippy's large-variant lint: the larger map pushes `VideoResult` over the threshold, and boxing would only add an allocation per poll. The alignment document's Part 2 sketch and inventory row now describe the same aliases (#229 NP2-1). Tests: shared::tests::provider_options_keep_insertion_order_through_serde, shared::tests::provider_reference_keeps_insertion_order_through_serde. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Upstream keeps poll pacing on the operation: `generateVideo` takes
`poll: { intervalMs, timeoutMs }` with defaults 5000 / 600000
(ai@7.0.127 generate-video/generate-video.ts:55-80, :575-576), and
`VideoModelV4` has no poll settings (@ai-sdk/provider@4.0.21
video-model/v4/video-model-v4.ts). `VideoModel::poll_config()` and
`VideoPollConfig` are removed; the poll loop reads `VideoCallOptions::poll`
and falls back to the upstream defaults. RunwayML's config-level
`poll_interval` / `timeout` go with it (there is no @ai-sdk/runwayml
package; the per-call `poll` option replaces them).
Each status check, retries included, now races the time left in the poll
budget as upstream does (generate-video.ts:585-622): when the budget runs out
the pending request is dropped and the call fails with the polling timeout
text "Video generation timed out after {timeoutMs}ms." (:579-581). Before,
only the gap between checks was bounded, so a hanging status GET or a long
retry delay ran past the poll timeout.
Issue items: #230 C19, #229 U5(a).
Tests: video_model::tests::a_hanging_status_check_is_bounded_by_the_poll_timeout,
status_retries_race_the_remaining_poll_timeout,
poll_pacing_defaults_to_the_ai_sdk_values.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Rust's f64 parser accepts `inf`, `Infinity` and `NaN`, and `as i64` saturates infinity, so `retry-after-ms: Infinity` produced an i64::MAX hint. The core retry loop dropped it through its window check, but the public hint (FFI `aimux_error_retry_ms`, the Node and Python `retry_ms`) reported it. A non-finite value, including seconds that overflow once converted to milliseconds, is now no hint, as upstream never uses it either (ai@7.0.127 util/retry-with-exponential-backoff.ts:47-55 keeps a delay only when `0 <= ms && (ms < 60000 || ms < exponentialBackoffDelay)`). Carries the master fix 13fe482 for #228 N2 to this layer. Test: error::tests::non_finite_retry_after_is_no_hint. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Closes #229 C39. `CallOptions.tool_choice` is now `LanguageModelV4ToolChoice`, the object union of @ai-sdk/provider@4.0.21 src/language-model/v4/language-model-v4-tool-choice.ts:1-5 ({type:'auto'|'none'|'required'} | {type:'tool', toolName}). The user-level `ToolChoice` (ai@7.0.127 src/types/language-model.ts:102-106) stays on `GenerateTextOptions`. `prepare_tool_choice` is the AI SDK's prepareToolChoice (ai@7.0.127 src/prompt/prepare-tool-choice.ts:4-15): no choice is {type:'auto'}, a string becomes {type:<string>}, a tool choice keeps its toolName. `into_call_options` always sends the prepared choice, as generateText / streamText do on every step (src/generate-text/generate-text.ts:953-955, :1048; stream-text.ts:2382-2384), so a call without tools now carries {type:'auto'} too. Every provider except open-responses already drops the choice when there are no tools; open-responses forwards `toolChoice?.type` as upstream does (@ai-sdk/open-responses@2.0.58 src/responses/open-responses-language-model.ts:310, :379-380). Providers match on `LanguageModelV4ToolChoice`. Replay maps the recorded V4 choice back to the user-level one when it re-runs a call; recordings now store the object form, so ExactMatcher no longer matches recordings made before this change (CHANGELOG). Tests: tool_choice_test (v4_tool_choice_is_an_object_union, v4_tool_choice_rejects_the_user_level_string_form, prepare_tool_choice_follows_the_ai_sdk, a_call_without_a_choice_carries_auto_even_without_tools), open_responses_test tool_choice_is_sent_without_tools. Generated TS: LanguageModelV4ToolChoice.ts, CallOptions.ts, ToolChoice.ts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The AI SDK's `ReasoningPart` is `{ type, text, providerOptions? }`
(`@ai-sdk/provider-utils@5.0.53` types/content-part.ts:107), and
`toResponseMessages` replays reasoning as
`{ type: 'reasoning', text, providerOptions: part.providerMetadata }`
(`ai@7.0.127` generate-text/to-response-messages.ts:62). A provider's
signature stays in its own namespace.
`ContentPart::Reasoning.signature` is removed, with
`extract_reasoning_signature`, which copied `anthropic` / `bedrock` /
`amazonBedrock` signatures out of the metadata into it. The prompt
conversion already ignored the field, and the providers read the signature
from `provider_options`, so nothing that reaches a provider changes. Input
JSON that still carries `signature` is accepted and the key is ignored
(#229 B8).
Test: response_messages::tests::reasoning_signature_is_replayed_only_in_provider_options.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Closes the tool part of #229 NP2-2. `LanguageModelV4ProviderTool.id` is `${string}.${string}` (@ai-sdk/provider@4.0.21 src/language-model/v4/language-model-v4-provider-tool.ts:17), so `ProviderTool.id` is now `types::NamespacedKind`: a string newtype whose constructor and deserializer reject text without a `.`. Like the template literal type, it admits empty segments ("x.", ".y"). The same type is meant for the custom content `kind` (language-model-v4-custom-content.ts:13, language-model-v4-prompt.ts:138), which is the rest of NP2-2 and belongs to the messages and content layer. The JSON stays a plain string; the generated TS type is `NamespacedKind = \`${string}.${string}\``. Tests: tool_wire_test (a_provider_tool_id_needs_a_namespace, a_namespaced_kind_is_any_text_with_a_dot). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Closes #229 NP2-6. `LanguageModelV4ToolResult.result` is `NonNullable<JSONValue>` (@ai-sdk/provider@4.0.21 src/language-model/v4/language-model-v4-tool-result.ts:23), so `tool::ToolResult.result` rejects JSON `null` on deserialize. Every other JSON value, falsy ones included, is kept. The generated TS type stays `JsonValue` (the doc comment states the rule). Test: tool_wire_test a_tool_result_is_never_null. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The tool-layer part of #229 C41, which is documentation and one test; the code for the rest lands with the core content and stream layers. Alignment doc, core output section: - the parsed tool-call example has no `dynamic` key: a static call omits it (ai@7.0.127 src/generate-text/parse-tool-call.ts:279-288); test a_static_call_serializes_without_dynamic pins that; - TypedToolResult / TypedToolError carry `dynamic?: false` when static and `dynamic: true` when dynamic (src/generate-text/tool-result.ts:20, :35; tool-error.ts:16, :30); - a deferred provider tool result (input undefined, convert-language-model-content.ts:105-138) is allowed only for a provider tool with `supportsDeferredResults`; otherwise "Tool call … not found" (:96-103). Not done here: the core `tool-error`, `tool-output-denied` and `tool-approval-response` parts belong to the core ContentPart / TextStreamPart (C42, C43); `title` and `toolMetadata` come from the user-level tool definition (@ai-sdk/provider-utils src/types/tool.ts), which aimux does not have yet; deferred results go with NP2-3. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Closes the MoA part of #232 B23: the 18 tests in moa.rs that layer 1 had (fan-out and aggregation, per-field usage, best-effort and fail-fast references, child retries without replaying the fan-out, aggregator stream setup retry, stream error relay, non-text part forwarding, reference tool stripping, a router as a reference) were dropped by the tool-call refactor. They are back, adapted to this layer's types: - reference tool stripping now clears the optional `LanguageModelV4ToolChoice` instead of resetting it to Auto; - the aggregator's streamed tool call is a provider-layer `StreamPart::ToolCall(RawToolCall)` whose input is the raw text, and is checked to pass through unchanged. The router tests are the other half of B23 and belong to the messages layer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in the CHANGELOG wording from [4/8]: only generate_text checks a tool result without its call; stream_text passes it on, as streamText does. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in the CHANGELOG wording from [4/8]: only generate_text checks a tool result without its call; stream_text passes it on, as streamText does. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #266 (a blank key on a custom base URL is sent as an empty key, as the AI SDK does for apiKey: ''). Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #266 (a blank key on a custom base URL is sent as an empty key, as the AI SDK does for apiKey: ''). Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #266 (a blank key on a custom base URL is sent as an empty key, as the AI SDK does for apiKey: ''). Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #266 (a blank key on a custom base URL is sent as an empty key, as the AI SDK does for apiKey: ''). Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #266 (a blank key on a custom base URL is sent as an empty key, as the AI SDK does for apiKey: ''). Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #266 (a blank key on a custom base URL is sent as an empty key, as the AI SDK does for apiKey: ''). Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #266 (a blank key on a custom base URL is sent as an empty key, as the AI SDK does for apiKey: ''). Resolution: rfc/0029-web-console.md and tools/aimux-web/README.md keep this layer's text, which already says what #266 says and adds which providers cannot take a custom base_url and where Replay sends. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #266 (a blank key on a custom base URL is sent as an empty key, as the AI SDK does for apiKey: ''). Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Contributor
Author
Generated by Claude Code |
The fetch test pointed npm's user and global config at the same empty
file. npm refuses that ("double-loading config ... as global, previously
loaded as user") and exits before any request, so on CI every test that
downloads failed with "npm pack failed". It passed in the cloud only
because the environment's NPM_CONFIG_USERCONFIG overrode the test's own
setting.
The test now writes two files, and drops inherited npm_config_* variables
in any case before setting its own, so it runs the same everywhere.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in 6ca248e: the fetch test gives npm separate user and global config files, so the AI SDK pin scripts step in the generator drift job passes on CI. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in 6ca248e: the fetch test gives npm separate user and global config files, so the AI SDK pin scripts step in the generator drift job passes on CI. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in 6ca248e: the fetch test gives npm separate user and global config files, so the AI SDK pin scripts step in the generator drift job passes on CI. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in 6ca248e: the fetch test gives npm separate user and global config files, so the AI SDK pin scripts step in the generator drift job passes on CI. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in 6ca248e: the fetch test gives npm separate user and global config files, so the AI SDK pin scripts step in the generator drift job passes on CI. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…ings #252 took max_retries, timeout and session_id off CallOptions and made them operation settings, but RFC-0015, RFC-0016, RFC-0017, RFC-0023, RFC-0024, RFC-0029 and RFC-0041 still named them as CallOptions fields. Each of those RFCs gets a later-change line in its header: the three are set on GenerateTextOptions, generate_text and stream_text put them in OperationSettings, and code below the model interface reads them with OperationSettings::current(), as the AI SDK keeps maxRetries and timeout on generateText and not in LanguageModelV4CallOptions; recordings still write them under the old keys. The passages that describe the current shape are rewritten (RFC-0015 §5 and §8, RFC-0017 §2.4, the RFC-0023 §1 table, RFC-0024 §1.2 and §3.1, RFC-0029 §5.4, the RFC-0041 §10.2 row); dated implementation records keep the field where it was then. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in 38b6cde: the RFCs that named max_retries, timeout and session_id as CallOptions fields now say they are operation settings. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in 38b6cde: the RFCs that named max_retries, timeout and session_id as CallOptions fields now say they are operation settings. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one commit) through [1/8], whose master merge eric made (2a0b02b). The tree is unchanged: this branch already had all three. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one commit) through [2/8]. The tree is unchanged: this branch already had all three. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one commit) through [3/8]. The tree is unchanged: this branch already had all three. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one commit) through [4/8]. The tree is unchanged: this branch already had all three. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one commit) through [5/8]. The tree is unchanged: this branch already had all three. Resolution: no conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
eric8810
changed the base branch from
v4/5-providers-are-ai-sdk-factories
to
master
October 17, 2026 14:03
master 带入下方各层的 squash;冲突为 squash 不对称,取本分支版本 (层分支内容 = master 同内容 + 本层改写),删除型按本层意图 git rm。
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by li
Summary
Before: after the move to factories, a number of vendor behaviours still differ from the pinned upstream packages, some upstream vendor features are missing, and several upstream-ported tests were deleted. User agents carry hard-coded versions. Azure's token decorator adds a bearer token on a cross-origin redirect hop. Anthropic on AWS defaults to
us-east-1and signs only when told to. Stream error statuses are invented in some places and missed in others. openai-compatible has nosupportsMultiPartToolContent, Vertex has no MaaS or xAI sub-provider,openai_outputreads logprobs only underopenai, and OpenAI image edits fetch URL files without the download guard. The #90 endpoint regression test and a set of hand-written unit tests are gone.After: factories, request settings, base URLs, empty keys and provider names follow upstream; vendor option schemas live in each package; the missing vendor features are ported. Each package sends its own user agent, with its version and the provider-utils version from its own package.json, both read from the pins. Stream error events become error parts with upstream's statuses, and API requests follow cross-origin redirects without credentials, Azure's token included. Anthropic on AWS reads its region, workspace id and auth as upstream does, and the AWS signers follow aws4fetch. Vertex gains the MaaS and xAI sub-providers, openai-compatible gains
supportsMultiPartToolContent, OpenAI and Azure expose their provider tools astools, and OpenAI image edits download URL files through the guard. Models declare the URLs their package passes through, so those are not downloaded. The upstream-ported model tests are kept, and the deleted hand-written tests that still cover live code are restored.Type of change
Checklist
cargo fmt --all -- --checkpassescargo clippy --workspace --all-targets -- -D warningspassescargo test --workspacepasses (cassette playback — no network/keys)README.md,docs/,rfc/,bindings/README.md)Notes for reviewers
@ai-sdk/openai4.0.83,openai-compatible3.0.62,anthropic4.0.71,anthropic-aws2.0.63,amazon-bedrock5.0.100,azure4.0.84,google4.0.87,google-vertex5.0.98,open-responses2.0.58,huggingface2.0.62,mistral4.0.54,cohere4.0.52,xai5.0.12,groq4.0.52 anddeepseek3.0.56 (groq and deepseek first ported in [5/8] refactor!: providers are AI SDK factories #253, adjusted here), each with the provider-utils its package.json names;@ai-sdk/provider4.0.21,provider-utils5.0.55 andai7.0.127 as pinned since [1/8] chore: CI on every PR and test fixture cleanup #249/[2/8] refactor!: tool calls follow the AI SDK #250. These are the 18 pins infixtures/aisdk/VERSIONS.json.toolson the provider instance.function_callfinish reason to tool-calls.cached_tokens.@ai-sdk/openairequests floats; re-recording them would bring them back.AWS_REGION, else the credentials' region, with nous-east-1default. The workspace id is required (setting orANTHROPIC_AWS_WORKSPACE_ID). Withoutauth, a non-emptyANTHROPIC_AWS_API_KEYmeansx-api-key, else SigV4 from the AWS environment variables.SigV4Fetch::newtakes a requiredSigV4Signing.downloadBlob, and Google Files uploads send no provider-utils part.@ai-sdk/openaidoes; openai-compatible chat and completion stream errors carry no inferred status; Anthropic maps 8 error types, and an unparsable chunk or error event is an error part after which the stream goes on, with no finish beforemessage_stop.openai_outputwrites logprobs in the Chat Completions shape, and OpenAI chat logprobs metadata dropsbytes.vertexandbedrockoption keys are read, and metadata is written back under both keys, as upstream does.OpenAIProvider.tools,AzureOpenAIProvider.tools,OpenResponsesProviderSettings.user_agent_suffixandHttpRequest.omit_provider_utils_user_agent; exhaustive struct literals need them. Newaimux_provider_utils::download_blob.fixtures/aisdk/VERSIONS.jsongrows from 7 to 18 packages: the 8 vendor packages,anthropic-aws,open-responsesandhuggingface. The recorder lock nests each package's own provider-utils.registry_fixed_base_urls_are_correct(Architecture audit: R3 remaining — structured AiMuxError (R1+R2 done in #93) #90), and 19 hand-written unit tests that still cover live code (retry, the FFIprovider_from_env, Bedrock event-stream checksums and conformance stream, Anthropic tool-name mapping and stream errors, the Google JSON accumulator, openai-compatible chat, Moonshot usage). The Bedrock stream cassettes now store their event stream asbody_base64.cargo test --workspacepass, as doscripts/check_provider_boundaries.sh,scripts/gen_ts_types.py --check,scripts/gen_providers_doc.py --checkand the pin script tests. The Go, Java and Kotlin suites pass locally.@ai-sdk/anthropic4.0.71 sends them: the input keepsaction, and a call without one is sent under the toolset name with a warning instead of being dropped. The ten provider tools that upstreamprepareToolsvalidates (computer toolset, advisor,text_editor_20250728, threeweb_fetch, threeweb_search) are validated by one check shared by Anthropic, Vertex and Bedrock; a mismatch fails the call before any request.anthropic::prepare_tools::{prepare_tools, prepare_tools_with_provider}return aResult.openai::tools::programmatic_tool_calling()setssupportsDeferredResults, as@ai-sdk/openai4.0.83 does (src/tool/programmatic-tool-calling.ts:55).ffi-x86_64-unknown-linux-gnuffi-aarch64-apple-darwinffi-x86_64-pc-windows-msvcnode-x86_64-unknown-linux-gnunode-aarch64-unknown-linux-gnunode-aarch64-apple-darwinnode-x86_64-apple-darwinnode-x86_64-pc-windows-msvcnode-aarch64-pc-windows-msvc