Repository navigation
[5/8] refactor!: providers are AI SDK factories - #253
Merged
Merged
Conversation
Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a0eb91-5e79-775e-975e-b85e2ac49ab6 (cherry picked from commit 7f68ac3)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> (cherry picked from commit 758d0bc)
The returned impl Iterator is already must_use, so clippy's double_must_use rejected it. (provider.rs part of commit 8397424) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…e removal - scripts/gen_providers_doc.py: the `provider_name` module no longer exists; the docstrings and the section-header guard now name only `provider`. `--check` output is unchanged. - docs/internal/phase4-audit.md: add a follow-up note (same style as RFC-0020) so the historical audit no longer reads as if the generated ProviderName types and their drift tests still exist. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS (cherry picked from commit dfcd54f)
The A2 line said Node would keep a generated string-literal union for completion. It does not: overlay-registered provider names can never be part of a compile-time union, so keeping one would keep the second name path this PR removes. Record the actual A2 scope. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS (cherry picked from commit 3e2d9fc)
Pull requests into a branch other than master got no CI, so a stacked PR could not show it was green against the PR below it. The pull_request trigger now has no branch filter. A new push to a PR cancels that PR's run still in progress; pushes to master are never cancelled. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…that pin Rust-only behaviour The V4 migration that follows replaces these tests with tests ported from the AI SDK. They are deleted first, on their own, so the PRs after this one show only code changes. This commit removes only test files (Rust, the seven bindings, the shared contract fixtures and their Node runner) and the CI steps that ran the contract fixtures. No library code changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Amp-Thread-ID: https://ampcode.com/threads/T-01a0eb91-5e79-775e-975e-b85e2ac49ab6 Co-authored-by: Amp <amp@ampcode.com> (cherry picked from commit dc1cd14)
…ia alias - Wire*.ts were hand-maintained mirrors of wire.rs and had already drifted (WireContentPart lacked provider_executed). Derive TS on the wire structs and export them to web/src/types/generated/ through gen_ts_types.py, which now handles two targets and checks both in CI. - Core types resolve through the @aimux/types tsconfig alias instead of five-level relative paths into bindings/node. - gen_ts_types.py sync no longer rewrites files that differ only by ts-rs end-of-line spaces. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> (cherry picked from commit 69c1351)
No crate in the workspace referenced the tracker; providers accumulate streamed tool-call deltas themselves. Drops the module, its 787-line test file and the re-exported companion types. Breaking for aimux-stream's public Rust API; noted in CHANGELOG under Unreleased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015EYDYDWYcPsDmjBjFeuDVe (cherry picked from commit fe8758f)
Re-adds the tracker as a port of the current @ai-sdk/provider-utils streaming-tool-call-tracker.ts instead of the older index-only version removed in the previous commit. - Correlate deltas by wire id, index and function name; drop ambiguous ones - Unique tool-call ids with bounded suffixes; blank generator output falls back to "tool-call" - Ignore unmatched blank function names; retain blank-name continuations - Flush in index order only when every call has an index - New StreamingToolCallArgumentState / starts_with_structured_value port - process_delta and flush return the emitted parts instead of buffering Tests are ported case for case from the upstream suites (45 tracker cases, 9 argument-state cases). No caller in the workspace uses the tracker yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015EYDYDWYcPsDmjBjFeuDVe (cherry picked from commit 6035201)
…nd wire the OpenAI stream to it The AI SDK keeps StreamingToolCallTracker in @ai-sdk/provider-utils, where every provider that speaks the OpenAI chat-completions wire format imports it. aimux had the tracker in aimux-stream (the eventsource-parser role), with its own ToolCallStreamPart event type and no callers, while openai/model.rs kept an index-only accumulator. This commit puts the pieces where the AI SDK has them. aimux-stream - StreamingToolCallTracker, StreamingToolCallArgumentState, starts_with_structured_value and ToolCallStreamPart move out; the crate is SSE decoding only. serde_json becomes a dev-dependency (test fixture); the `tool-calls` keyword is dropped. aimux-provider-utils - streaming_tool_call_tracker.rs / streaming_tool_call_argument_state.rs land here, logic unchanged. The tracker emits aimux_core::StreamPart ToolInputStart / ToolInputDelta / ToolInputEnd / ToolCall directly, so there is no second event type (RFC-0036: ToolCallStreamPart must not be a second public protocol). The generic metadata parameter becomes serde_json::Value / ProviderMetadata; TrackerError converts into AiMuxError::InvalidResponseData. The ported upstream tests (45 + 9) move with it, projecting StreamPart back to the upstream event shape. aimux-providers - openai/model.rs replaces its HashMap<usize, ToolCallAccumulator> with the tracker: tool_calls deltas are correlated by wire id, index and function name instead of index alone; id-less continuations follow their call; indices reused across parallel calls stay distinct; ambiguous deltas are dropped; a call without a wire id gets a generated `tool-call` / `tool-call-N` id instead of an empty string; a new call without a function name ends the stream with InvalidResponseData (AI SDK behaviour) instead of starting a call with an empty name. - DeltaToolCall.index is Option<usize> (the AI SDK schema is `index: z.number().nullish()`). Every registry-backed provider goes through openai/model.rs, so this is the one plug point until S4-2 folds the mistral and xai chat copies into the same implementation. Verification: cargo test -p aimux-stream -p aimux-provider-utils; cargo test -p aimux-providers (129 suites, 3041 passed, 0 failed, including the cassette replays); cargo clippy -p aimux-stream -p aimux-provider-utils -p aimux-providers --all-targets -- -D warnings; cargo fmt --all -- --check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS (cherry picked from commit df4d09f)
…-state port Carried over from the SSE test port commit so the two split branches leave the file identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS (cherry picked from commit 7bc200e)
Describes only the final API: the tracker lives in aimux-provider-utils and emits StreamPart directly; the intermediate ToolCallStreamPart- returning API from earlier commits is not mentioned. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS (cherry picked from commit 264ade3)
… Rust Same behaviour as the upstream port (checked differentially against the previous implementation on ~1.1M random deltas), with Rust structure: - Correlate by linear scan over per-call wire ids and indices instead of id/index hash maps re-sorted on every delta; no per-delta allocations besides the emitted parts. - `finish(self)` consumes the tracker, so the never-true-before-flush `has_finished` state is gone and post-flush use cannot compile. - `StreamingToolCallDelta<'a>` borrows from the provider wire type and carries `provider_metadata`, replacing `StreamingToolCallFunction`, the untyped `extra` bag, the test-only builders and the two boxed metadata hooks. - `process` / `finish` append to a caller-owned buffer. - Drop the unreachable `TrackerError::IdExhausted`. - The argument state scans bytes and models completion as its own state. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a11e83-bf7b-75bb-900b-34851e6f12ee (cherry picked from commit b0fd923)
…es and docs With the tracker in aimux-provider-utils, aimux-stream only decodes SSE: serde_json is needed by its tests alone, and the tool-calls keyword no longer applies. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
- Drop the redundant #[must_use] on provider_names(); the returned impl Iterator is already must_use, so clippy::double_must_use failed CI. - Gate the wire types' ts-rs export tests behind a ts-export feature that only gen_ts_types.py enables. A plain cargo test previously wrote Wire*.ts into bindings/node/src/types via the workspace-wide TS_RS_EXPORT_DIR, which made gen_ts_types.py --check fail on stale files. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> (cherry picked from commit 8397424)
…ow the AI SDK V4 types - Content: one source, file and reasoning type per layer. A source is a url source or a document source; file citations and grounding documents map to document sources. The missing upstream content variants and usage fields are added. - Provider options and provider metadata have one typed shape. Vendor packages build and read them; a provider option that is not an object is an error, unknown keys no longer leak, and option parse errors surface for Cohere, Google and Mistral. Bedrock reads amazonBedrock before bedrock. - The provider prompt is modelled by role with one file part. Part tags use upstream's hyphenated names; references, files and tool output convert as upstream does, and file parts keep upstream's media-type and text-data rules. - The #226 decision adapters and Jev follow the typed options, metadata and split usage types. - Hand-written tests that built provider prompts by hand or pinned Rust-only behaviour are deleted; ported upstream tests are restored. FFI, bindings and their generated types follow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…ion as upstream does - Provider generate and stream results carry request and response information, as the upstream V4 result types do; raw is the provider result itself. - The response metadata stream part is absorbed into the finish step, and request and response are carried onto the call-layer results. - A tool-call repair applied to a result also updates the result's content. CHANGELOG records the result fields as breaking; the migration notes describe tool-call metadata on StreamingToolCallDelta. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
One tool struct per layer, field for field with the pinned upstream types:
- CallOptions.tool_choice is optional. Vendors omit an unset choice, as
their upstream packages do; generate_text and stream_text default it to
auto when tools are given, as upstream prepare-tool-choice does.
- FunctionTool.input_examples is a list of { input: object } and
ProviderTool.args is a JSON object.
- Tool calls carry no separate thought signature. The value travels in
provider metadata on output and provider options on input, under the
vendor namespace, where each upstream vendor package reads and writes it.
- Call sites pass the tool structs instead of their field lists.
The fifteen tool-choice tests ported from upstream (OpenAI, Groq, Mistral,
Bedrock) are kept and adapted; hand-written tests that pinned the old
shapes are deleted. CHANGELOG and docs/api/gaps.md record the wire-format
changes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…5-providers-are-ai-sdk-factories # Conflicts: # CHANGELOG.md # scripts/local-ci.sh
…est layer One change, without a transition layer: the old provider interface and its configs are removed in the same commit that adds the new ones. Request layer (aimux-provider-utils): - Injectable fetch, resolvable settings (values or functions evaluated per request) and the AI SDK request helpers. API keys and settings load per request, with LoadApiKey and LoadSetting errors. Provider interface: - Provider takes the AI SDK ProviderV4 shape, with a provider registry. Vendor packages, each a factory as in its upstream package: - openai-compatible, with the presets read from the registry table at run time. - The OpenAI family: openai, azure, codex, huggingface, xai. - groq and deepseek. - The Anthropic and Google families: anthropic, anthropic_aws, google, vertex, bedrock, aws_polly, open_responses. - The remaining vendors: mistral, cohere, voyage, elevenlabs, jev and 28 single-modality vendors. Consumers: - FFI, the bindings, the CLI, the web console and replay build providers by name through create_provider. Recordings keep only the provider identity. Retries are call options; provider configs reject the removed keys, and a missing key or an unfilled preset parameter fails at call time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The returned boxed stream is already must_use, so clippy 1.99 (double_must_use) rejects the attribute under -D warnings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Since messages follow the AI SDK V4 types, a reasoning part keeps its signature in provider_options.anthropic.signature, as the Node test already does. The Go round-trip test still sent the removed top-level `signature`, so no thinking block reached the wire. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…uctor The content field was added to StreamTextResultAggregated and to its generated fromJson, but not to the constructor, so lib/types.g.dart did not compile and every test file importing the types failed to load. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
This was referenced Oct 12, 2026
…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
…results-and-streams-follow-the-ai-sdk
…ned code The results layer adds AIMUX_E_NO_OUTPUT_GENERATED = 18 (the AI SDK's NoOutputGeneratedError) and every binding maps it, but the Go, Java, Kotlin and Flutter tests still used 18 as the first value outside the enum. They now check that 18 resolves to NoOutputGenerated and that 19 is rejected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #247 (with #246) through the layer below: ts-rs exports run only under the ts-export feature, and the generated ProviderName types are gone. Resolution: the aimux-core files keep this layer's code, and every export in them, including the ones this layer adds (ToolChoice, the tool and stream-part types), is gated as #[cfg_attr(feature = "ts-export", ts(export))]; CHANGELOG keeps both sides. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #247 (with #246) through the layer below: ts-rs exports run only under the ts-export feature, and the generated ProviderName types are gone. Resolution: the aimux-core files keep this layer's code, and every export in them, including the ones this layer adds to the message, content, result and stream types, is gated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #247 (with #246) through the layer below: ts-rs exports run only under the ts-export feature, and the generated ProviderName types are gone. Resolution: the aimux-core files keep this layer's code with every export gated, including the result, step and output types this layer adds. The web console's copies of the core types, which this layer had updated, stay deleted as #247 deletes them; engine.ts and client.ts import TextStreamPart and the other core types from @aimux/types and the wire types from @/types/generated, and the web typecheck passes. docs/api/reference.md keeps this layer's rows without the ProviderName row; CHANGELOG keeps both sides. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #247 (with #246) through the layer below: ts-rs exports run only under the ts-export feature, and the generated ProviderName types are gone. Resolution: core files keep this layer's code with every export gated, including the ones this layer adds; model_id.rs stays deleted (this layer removes ModelId); aimux-providers lib.rs and provider.rs keep this layer's version, which already exposes provider_names() from default_providers; gen_ts_types.py gains the export-gate check on top of this layer's targets; reference.md keeps this layer's ProviderOptions row; CHANGELOG keeps both sides. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The entry for supportsDeferredResults read as if every tool result without its call were checked. Only generate_text checks it, as upstream's generateText does (ai@7.0.127 convert-language-model-content.ts:96-103); stream_text, like streamText, passes the result on without its input (stream-language-model-call.ts:760-798), which stream_tool_results_and_errors_follow_the_ai_sdk already pins. 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
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
…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 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
eric8810
changed the base branch from
v4/4-results-and-streams-follow-the-ai-sdk
to
master
October 17, 2026 13:44
master 带入下方各层的 squash;冲突为 squash 不对称,取本分支版本 (层分支内容 = master 同内容 + 本层改写),删除型按本层意图 git rm。
…r the master merge The master merge (c0e6ec4) took master's side in two test files, so the Rust core job failed to compile: elevenlabs_speech_test.rs lost the HashMap import its helper still uses, and openrouter_test.rs compared the tool-call input as a string, while on this layer it is a JSON value. Both files are back to this layer's version; the tree then equals the layer before the merge. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
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: each vendor is a config struct behind aimux's own provider trait, settings are read once at construction (an invalid env base URL panics), and consumers build providers through per-vendor constructors. There is one global proxy,
supported_urlsis synchronous, there is nowrap_provider, and URLs in prompts, audio input and video results are passed on as they are. The moved code still has the #228 problems that #248 fixes on master: secrets inDebugoutput, URL userinfo in logs, unredacted Prodia and Azure key headers in recordings, the Flutter finalizer signature, the Python GIL, and aimux-web sending stored keys to anybase_url. A recording that lost an event is still marked complete, aimux-replay replays decision recordings live only for Jev, and the realtime WebSocket tests are gone.After: providers take the AI SDK
ProviderV4shape with a provider registry, and every vendor is a factory as in its upstream package; providers and models reportspecification_version()"v4". The request layer has injectable fetch, a proxy per transport, and settings resolved per request, so an invalid env base URL fails each call instead of panicking. FFI, the bindings, the CLI, the web console and replay build providers by name throughcreate_provider.supported_urlsis async, andwrap_providerwraps a whole provider.generate_text,stream_text,transcribeandgenerate_videodownload the URLs a model cannot fetch, through a guarded download the caller can replace, as the AI SDK operations do. The master fixes are carried onto the new code. Recordings keep only the provider identity, stay on schema 2, load what master wrote, and are marked incomplete after a lost or inconsistent event. aimux-replay replays any provider's decision recordings live, and each custom aimux-web endpoint gets its own trace scope.This is one PR on purpose: splitting it would need a temporary adapter between the old and new provider interfaces that the next PR deletes again. The commit message and the sections below go vendor by vendor so it can be reviewed in parts.
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
aimux-provider-utils); provider interface and registry (aimux-core); operation downloads (aimux-core/src/download.rsand the three operations); openai-compatible and the preset table; the OpenAI family (openai, azure, codex, huggingface, xai); groq and deepseek; the Anthropic and Google families (anthropic, anthropic_aws, google, vertex, bedrock, aws_polly, open_responses); the remaining vendors; recording and replay; consumers (FFI, bindings, CLI, web, replay).data:URL in a prompt is sent as it is, and downloads get no media-type sniffing; both are registered as product choices (alignment doc §5).aimux_init_proxykeeps its C signature; the new C ABI can replace it with a transport handle.generate_videodownloads URL videos by default and returnsVideoData::Binary, so every binding'sgeneratereturns bytes. A Vertexgs://result needs an injected download, because the guarded default fetches only public HTTP(S) URLs.generate_textandstream_textdownload prompt URLs that the model'ssupported_urls()does not cover and send them inline;transcribeacceptsAudioInput::Url.ProviderRecordholds identity only.complete: false.OpenAIProvider::language_model(id)(andopenai:<id>) returns the Responses model, as@ai-sdk/openaidoes;.chat(id)gives Chat Completions.base_urlonly a key sent in the request is used, andenv:is refused;env:VARmust name the provider's key variable. Providers with ambient credentials (Bedrock, Vertex, Codex, DataForSEO, Polly, Anthropic on AWS) refuse a custombase_url.ModelId, thedownload_guardre-export, the seven#[deprecated]wrappers,create_standard_json_error_response_handler,RetryConfig/retry_config(),body_overrides, and the Rustinit_proxy. EachReqwestFetch/TungsteniteConnectortakes aProxyConfig;aimux_init_proxyand the Node and Pythoninit_proxyreplace the default transport.LanguageModel::supported_urlsand the middleware override are async.aimux_drop_handle_ptr(void *), which the Flutter finalizer uses. Every blocking Python call releases the GIL.cargo test --workspacepass, as doscripts/check_provider_boundaries.sh,scripts/gen_ts_types.py --checkand the pin script tests. The Go, Java, Kotlin and Python suites pass locally; the Flutter finalizer test runs in CI.specification_version, headers, default transport, video polling, operation downloads, downloaded media types), §9.1 (recording schema), D13 and the part 2 namespace wording; revision lines on RFC-0002, RFC-0006, RFC-0015, RFC-0016, RFC-0017, RFC-0018, RFC-0020 and RFC-0023; RFC-0029 §5.5; README;docs/api(C, decision, API, Rust, Node, Python, Go); the request-pipeline doc; CHANGELOG.parse_registry(json), with a unit test per rule; the by-name shim inprovider.rsnames its removal PR, [14/14] feat!: ops layer, C ABI and bindings #265.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-msvcCargo.lock gains no crate in this PR, so the growth is the PR's own code: every vendor is rebuilt as an AI SDK factory on the shared request layer. In
aimux-core,aimux-providersandaimux-provider-utils, Rust grows by about 15,300 lines net: about 6,400 undersrc/(in-file unit test modules included) and about 8,900 undertests/.