Repository navigation
[12/13] refactor!: vendor streams are Transformer structs - #260
Merged
Merged
Conversation
cunninghamcard-bit
marked this pull request as ready for review
October 13, 2026 07:31
5 of 12 tasks
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
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
Contributor
Author
|
谢谢评审。这一层已随整套栈更新,改动较大,请以更新后的为准。
Generated by Claude Code |
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
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 结构)。零差异校验随下。
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: every vendor's
do_streamis anasync_streamgenerator. Its parser state lives in a dozen local variables inside one long closure. The five OpenAI-style chat streams hand theirtool_calls[i]fragments toStreamingToolCallTracker, a public builder-configured object inaimux-provider-utilsported from@ai-sdk/provider-utils.After:
do_streamis a named struct implementingTransformer(start/transform/flush, the shape of the AI SDK'sTransformStream), driven bypipe_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_throughreturnsPipeThrough, which implementsfutures::Streamdirectly over one reusedVecDeque: noasync_stream!generator and no newVecper chunk.StreamContext; accumulated state is#[derive(Default)], so constructors no longer list every field twice.StreamingToolCallTracker(andStreamingToolCallDelta,TypeValidation,TrackerError) is gone. The chat streams keep their in-progress tool calls as a field (ChatToolCalls, crate-private inaimux-providers/src/stream/), with each vendor's type rule passed as data. The matching rules are the upstream ones and are unchanged.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
Breaking for the Rust API of
aimux-provider-utilsonly: the tracker types are removed. No JSON, C ABI or binding changes.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
message_stop, or a Google, Vertex, xAI or Hugging Face error event).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.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).docs/aisdk-architecture-alignment.md§5, and the merged RFC-0042 (T2, §4) now names this location instead of provider-utils.PipeThroughcallsstartwhen created rather than on the first poll; no transformer depends on that.Verification
cargo test --workspacets-exportfeature since [1/8]). New: 3PipeThrough/controller tests (one checks that nothing is emitted afterterminate()orerror()) and 7 transformer tests; the tracker cases moved as abovedo_streamsequences (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-widesource-N/id-Ncounters are normalisedChatToolCallsmade 728 sequences differ, only onpart.delta, only in the chat family (OpenAI, compatible, DeepSeek, Groq, Mistral, and presets that use the compatible chat model)-D warnings,gen_ts_types.py --check,gen_providers_doc.py --check,check_provider_boundaries.shpass on stable 1.99