Skip to content

[12/13] refactor!: vendor streams are Transformer structs - #260

Merged
eric8810 merged 425 commits into
masterfrom
v4/12-vendor-streams-are-transformer-structs
Oct 17, 2026
Merged

eric8810 merged 425 commits into
masterfrom
v4/12-vendor-streams-are-transformer-structs

Conversation

@cunninghamcard-bit

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

Copy link
Copy Markdown
Contributor

Requested by li

Summary

Before: every vendor's do_stream is an async_stream generator. Its parser state lives in a dozen local variables inside one long closure. The five OpenAI-style chat streams hand their tool_calls[i] fragments to StreamingToolCallTracker, a public builder-configured object in aimux-provider-utils ported from @ai-sdk/provider-utils.

After:

  • Each language-model do_stream is a named struct implementing Transformer (start / transform / flush, the shape of the AI SDK's TransformStream), driven by pipe_through. That covers OpenAI chat and Responses, OpenAI-compatible chat and completion, Groq, DeepSeek, Mistral, Anthropic, Bedrock, Cohere, Google, Vertex, xAI, Hugging Face and Open Responses.
  • pipe_through returns PipeThrough, which implements futures::Stream directly over one reused VecDeque: no async_stream! generator and no new Vec per chunk.
  • The request context every stream needs (warnings, raw-chunk flag, URL, request body, response headers, id generator) is one StreamContext; accumulated state is #[derive(Default)], so constructors no longer list every field twice.
  • StreamingToolCallTracker (and StreamingToolCallDelta, TypeValidation, TrackerError) is gone. The chat streams keep their in-progress tool calls as a field (ChatToolCalls, crate-private in aimux-providers/src/stream/), with each vendor's type rule passed as data. The matching rules are the upstream ones and are unchanged.
  • Transformers can be driven synchronously in tests (TransformStreamController::new / drain).

This re-derives the earlier "vendor streams are Transformer structs" layer of the RFC-0036 stack onto the migrated code.

Type of change

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

Breaking for the Rust API of aimux-provider-utils only: the tracker types are removed. No JSON, C ABI or binding changes.

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; [11/13] ([11/13] refactor: provider config is plain data #259) is merged, and this is the next PR of the Rust-adaptation series.
  • Direction: structure only. Every emitted stream part and its order, errors, raw chunks, finish reasons, usage and provider metadata stay the same.
  • Where a stream stops: where [8/8] refactor!: public JSON equals the AI SDK JSON #256 stops (after an Anthropic message_stop, or a Google, Vertex, xAI or Hugging Face error event).
  • Not merged: the five chat streams still have their own Transformer: their error handling, part order, metadata and finish rules differ. They share the tool-call table and the context. Google and Vertex are also still separate; merging protocol families is a different direction for a later PR.
  • Tests moved, not dropped: the 46 tracker cases and the six unattributable-delta fixture cases now drive the real vendor chat streams (aimux-providers/src/stream/chat_tool_calls_tests.rs). New synchronous tests cover the OpenAI chat and Anthropic transformers (interleaved tool calls, error frames, flush).
  • Departure from the AI SDK's structure: upstream provider-utils has the tracker as its own module; here it is part of the chat stream. Behaviour follows upstream. Registered in docs/aisdk-architecture-alignment.md §5, and the merged RFC-0042 (T2, §4) now names this location instead of provider-utils.
  • PipeThrough calls start when created rather than on the first poll; no transformer depends on that.
  • Size: this layer is +6,800 / −5,116 (docs +6 / −5), of which tests +360 / −273 (it was +6,558 / −4,525 before the revision).

Verification

Check Result
cargo test --workspace 3207 passed, 0 failed, 7 ignored (the ts-rs export tests run only under the ts-export feature since [1/8]). New: 3 PipeThrough/controller tests (one checks that nothing is emitted after terminate() or error()) and 7 transformer tests; the tracker cases moved as above
Upstream fixture replays unchanged
Old vs new, stream part by stream part #258 against this PR (so #259 and this PR together): 9,212 do_stream sequences (289,720 parts), each run four ways (whole body, with raw chunks, in 7-byte chunks, cut in half followed by a transport error), plus 20,173 requests and 10,961 generate results and errors. 0 differ once the process-wide source-N / id-N counters are normalised
Mutation check trimming the arguments of each tool-input delta in ChatToolCalls made 728 sequences differ, only on part.delta, only in the chat family (OpenAI, compatible, DeepSeek, Groq, Mistral, and presets that use the compatible chat model)
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:31
claude added 28 commits October 13, 2026 13:25
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
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
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
…r 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
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
…cover live code

48a92aa 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
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
…orted 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
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
`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
…settings, factories)

fx3/p6 at 3f389ba: layer 6 after its rounds 3 and 4, and the merge of
layer 5, which carries layers 2 to 4.

Conflicts resolved:
- provider-utils lib.rs: layer 7's decision_language_model module and
  layer 6's download_blob; the download_guard re-export goes, as in
  layer 5.
- decision_support.rs: layer 7's provider_metadata and response_info,
  built on layer 3's IndexMap SharedProviderMetadata.
- jev.rs: layer 6's jev_failed_response_handler and layer 7's rounding
  doc.
- openai/mod.rs imports: layer 7's DecisionLanguageModel, without the
  load_optional_setting import layer 5 dropped.
- Recorder pins: package.json pins @ai-sdk/provider 4.0.22 (layer 7) and
  provider-utils 5.0.55 (layer 2). package-lock.json was regenerated from
  layer 6's lock with npm install --package-lock-only --ignore-scripts:
  the only change is the top-level provider moving to 4.0.22, with each
  vendor keeping its own nested copy. VERSIONS.json merged cleanly (18
  entries).
- CHANGELOG: both sides' Added bullets; layer 6's Fixed section.
- Generated TS types: layer 6's, regenerated in a follow-up commit.

Follow-up commits fix the code that does not compile against the merged
layers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
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
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
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
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
…P2-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 4197573, 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
claude added 13 commits October 16, 2026 11:18
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
…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
…streams-are-transformer-structs

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
@cunninghamcard-bit cunninghamcard-bit changed the title [12/13] refactor: vendor streams are Transformer structs [12/13] refactor!: vendor streams are Transformer structs Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

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

你提到 我们的处理 位置
(我们自己的改动)StreamingToolCallTracker 和各个流的结构 tracker 整个删除(RFC-0042 T2):OpenAI chat 一族五个流把进行中的工具调用作为 Transformer 的字段(crate 内部的 ChatToolCalls),关联行为仍是 provider-utils 5.0.55 基线,上游 tracker 的 46 个用例改为驱动真实的 chat 流;pipe_through 返回直接实现 futures::Stream 的 PipeThrough,不再用 async_stream;各流共用一个 StreamContext;RFC-0042 T2/§4 位置描述和对齐文档 §5 差异登记随这一层修改 aimux-providers 的 chat 流、stream 模块,rfc/0042,docs/aisdk-architecture-alignment.md
应有测试钉住流结束后不再输出任何东西 已补单测:terminate() 或 error() 之后,enqueue、enqueue_error、error 都被丢弃 stream 模块单测
enqueue_error、error、terminate 的文档应对照 AI SDK 和 WHATWG 三处文档都加了对照:enqueue_error 对应 AI SDK 里 enqueue 一个 type 为 error 的部件后继续;error 对应 WHATWG controller.error(e);terminate 对应 controller.terminate() 三个方法的文档
重新推导时的停止点 不复活旧层的行为,停止点跟随 #256 无代码改动

Generated by Claude Code

eric8810 and others added 12 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
…streams-are-transformer-structs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
@eric8810
eric8810 changed the base branch from v4/11-provider-config-is-plain-data to master October 17, 2026 15:15
master 带入下方各层的 squash;冲突与自动合并漂移收敛到本层树;
provider-utils 的 4 个 tracker 文件按本层意图删除(累积逻辑已移入
vendor stream 的 Transformer 结构)。零差异校验随下。
@eric8810
eric8810 merged commit 4647377 into master Oct 17, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants