Repository navigation
[13/13] docs: add the native-Rust porting skills - #261
Merged
Merged
Conversation
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
… order V4 decision questions and choice criteria are maps, and the OpenAI request lists them in the caller's order. The test re-serialized the fixture's options through org.json's JSONObject, which does not keep key order, so the request listed score before choice and failed the comparison with the fixture. It now passes the options' raw text. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Layer 4 rewrote the Rust doc of applyToolCallRepair (it repairs the call in the result's steps) without regenerating index.d.ts, so every napi build rewrote the file. This is the napi build's output for the merged source; only that doc comment changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
"assistant without reasoning omits reasoning_content" sent an assistant tool call with no tool result. Since 252371b (#229 C38) the prompt is checked as the AI SDK's convertToLanguageModelPrompt checks it, and that prompt fails with MissingToolResultsError before any request. The conversation now carries the tool message with the call's result, as the other replay tests do; the assertion on the assistant message is unchanged. No other binding test sends a tool call without its result. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
… a result fx3/p3 at 35711cb: the Node reasoning_replay test sends its tool call with the tool result, as layer 3's prompt check requires. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Since d3fd820 a result's `usage` is the usage summed over its steps, as the AI SDK's addLanguageModelUsage, which carries no raw object (generate-text.ts:1526-1544, types/usage.ts:122-155). The provider's raw usage, with DeepSeek's prompt_cache_hit_tokens / _miss_tokens, is on each step's `usage.raw`. The Node and Python cassette tests (RFC-0016 M10) read steps[0].usage.raw and check that the summed usage has none. The other bindings read raw usage only from stream parts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
d3fd820 rewrote the Rust doc of applyToolCallRepair (it repairs the call in the result's steps) without regenerating index.d.ts, so every napi build rewrote the file. This is the napi build's output; only that doc comment changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
fx3/p4 at 4ce9aa0, with fx3/p3 at 35711cb: - the Node reasoning_replay test sends its tool call with the tool result (layer 3's prompt check); - the Node and Python DeepSeek tests read the provider's raw usage from steps[0].usage.raw, since the summed usage carries none (layer 4); - bindings/node/index.d.ts is the napi output for applyToolCallRepair's layer 4 doc. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
fx3/p5 at cfe290a, which merges fx3/p4 at 4ce9aa0: - the Node reasoning_replay test sends its tool call with the tool result (layer 3's prompt check); - the Node and Python DeepSeek tests read the provider's raw usage from steps[0].usage.raw, since the summed usage carries none (layer 4); - bindings/node/index.d.ts is the napi output for applyToolCallRepair's layer 4 doc. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
fx3/p6 at 8bef2c9, which carries fx3/p4 at 4ce9aa0 and fx3/p3 at 35711cb: - the Node reasoning_replay test sends its tool call with the tool result (layer 3's prompt check); - the Node and Python DeepSeek tests read the provider's raw usage from steps[0].usage.raw, since the summed usage carries none (layer 4); - bindings/node/index.d.ts gets layer 4's regeneration, the same hunk as 6aa327d, so it merges without a conflict. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…order The same order loss as the Kotlin test before efd10d6: the test re-encoded the fixture's options with JSONSerialization, whose dictionaries keep no key order, and compared the request as an NSDictionary, whose arrays are ordered. V4 questions and choice criteria are maps, and the OpenAI request lists them in the caller's order. The test now passes the raw text of the fixture's options object. Not compiled here (no Swift toolchain); the brace matcher was checked against the fixture with an equivalent script. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
NP2-7) Outside `content` the AI SDK writes a tool call as `TypedToolCall`, `{type: 'tool-call', ...}` (ai generate-text/tool-call.ts:8-14), and a source as `Source`, a `LanguageModelV4Source` tagged `{type: 'source', sourceType, ...}` (ai types/language-model.ts:92; provider language-model-v4-source.ts:8, :36). This covers: - the text results' `toolCalls` and `sources` (generate-text-result.ts:73, :78; stream-text-result.ts:173, :180); - an approval request's `toolCall` (tool-approval-request-output.ts:20). The repair function's `toolCall` is a tagged `LanguageModelV4ToolCall` (language-model-v4-tool-call.ts:7). aimux wrote all of these untagged. aimux's `ToolCall`, `RawToolCall` and `Source` stay untagged, because the `content` and stream unions write their tag. New `wire` helpers write the tag where a value stands alone: - `TypeTag`; - `typed` / `typed_vec` for fields; - `Tagged` for a value serialized on its own. The tag comes first. Reading accepts a value without the tag (JSON written before it) and rejects a different one. Tagged fields: - `GenerateTextResult` and `StreamTextResultAggregated`: `toolCalls` and `sources`; - `ToolApprovalRequestOutput.toolCall`; - the repair context's `toolCall`, and the call `apply_tool_call_repair` returns through the C ABI, Node and Python. `apply_tool_call_repair_to_result` writes the patched entry tagged. A repair reply's call may carry the tag. TypeScript types these fields as the new `TypedToolCall` and `TypedSource` (generated, export only). Node's `ToolCallRepairContext.toolCall` is tagged. Bindings: each one's standalone `ToolCall`, `RawToolCall` and `Source` now writes the tag and checks it on read. - Go: `MarshalJSON` / `UnmarshalJSON`. - Java: a fixed `type` field plus a checking setter. - Kotlin: an `@EncodeDefault` property checked in `init`. - Swift: hand-written `Codable`. - Dart: `toJson` / `fromJson`. - Python: a `Literal` field with a default. Tests: - core `standalone_tool_calls_and_sources_are_tagged` (golden JSON, key order, content written once, untagged read, mistag rejected); - FFI smoke test (context, repaired call, patched result); - one test in each binding. The Swift and Dart tests were written without a toolchain to run them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…tes them (#229) The AI SDK parses a tool call's argument text with `JSON.parse` (ai generate-text/parse-tool-call.ts:116, :256; provider-utils parse-json.ts:97, secure-json-parse.ts:31). JS has one number type, so `JSON.stringify(JSON.parse('{"n":1e2}'))` is `{"n":100}`. serde_json keeps a number written with a fraction or exponent as an `f64` and writes it back as `100.0`, so aimux's `toolCalls[].input` differed from the AI SDK JSON for `1e2`, `3.0` or `-0.0`. The least invasive fix: `parse_input_text`, which both the valid and the invalid tool-call paths now use, walks the parsed value once and replaces each finite, integral `f64` below 1e21 in magnitude with the integer JS would write. Rust's `f64` `Display` gives the same shortest round-trip digits as JS `Number::toString`, padded with zeros (`2 ** 60` is `1152921504606847000`), and `-0` becomes `0`. Nothing else changes: no `arbitrary_precision` feature, no custom number type, and no change to how other JSON is read or written. Remaining gaps, documented in the alignment doc: - integral values from about 1.8e19 to 1e21 do not fit `i64`/`u64` and stay floats; - integer literals above 2^53 stay exact, where JS rounds them. Non-integral numbers and numbers of 1e21 or more already match JS (`1.5`, `1e+300`). Tests: - core `integral_input_numbers_are_written_as_javascript_writes_them` pins valid and invalid calls and the `js_integer` edge cases; - Python and Go e2e tests read integer tool input from a mocked response. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…l flow
The RFC still showed the repair context and reply in the old snake_case,
externally tagged form. It now shows the JSON the exports write and read:
- the context is `{toolCall, error, inputSchema, tools, messages,
instructions}`;
- `toolCall` carries `type: "tool-call"`, as the repair function's
`LanguageModelV4ToolCall` does (provider 4.0.21
language-model/v4/language-model-v4-tool-call.ts:7);
- `error` is tagged by `name` (`AI_InvalidToolInputError`);
- the reply is `{"type": "repaired", "toolCall": ...}`;
- a failed reply becomes `AI_ToolCallRepairError` with an `AI_Error` cause;
- the result patch rewrites `toolCalls[]` and `responseMessages`, keyed by
the call's `toolCallId`.
It also takes the corrections from the RFC check (rfc-check patch 0002),
with line numbers moved to ai 7.0.127:
- `inputSchema` is a value; upstream passes a function of the tool name
(generate-text/tool-call-repair-function.ts:31).
- A repaired call that still fails is not wrapped in
`AI_ToolCallRepairError`: it is an invalid call carrying that parse's
`AI_InvalidToolInputError` / `AI_NoSuchToolError`, with the original id,
name and input. Upstream wraps only an error thrown by the repair
function (generate-text/parse-tool-call.ts:90-96); a failed re-parse
goes to the invalid-call branch (:103-134). `apply_repair_outcome`
already behaves so.
- Swift's abort waiting for the hook differs from upstream, which races
the repair function against the abort signal (:73-74, :137-173).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Ports the test half of li's fork PR #52 (cbfb76c, b8fdda6). `Usage` writes an unknown count by omitting `total`, as the AI SDK's `LanguageModelV4Usage` types it `total: number | undefined` (provider 4.0.21 language-model/v4/language-model-v4-usage.ts:14, :39). The Node and Python decision tests read `inputTokens` / `outputTokens` already, but accepted an explicit `"total": null`: - Node compares `total` with `undefined` instead of coalescing both sides to `null`; - Python asserts that `total` is absent when the mocked count is missing or null. The fork's other two changes are already in this branch: the Python recording test reads `outcome.decisionResult` (4a1f368), and the Flutter typed-call helper no longer declares options no test passes (c81e221). 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
…3-add-the-native-rust-porting-skills 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 |
eric8810
approved these changes
Oct 17, 2026
eric8810
left a comment
Contributor
There was a problem hiding this comment.
复核通过,撤回修改请求。逐项核实:PROFILE 不再含 preset 计数(只留不随代码漂移的稳定事实——比改数字更对)✓;CONTRIBUTING.md 补 .agents/ 目录规则(中文、只写不过期的事实、与所描述代码同 PR 修改)✓。批准。措辞如需调整我方后续直接改。
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
…3-add-the-native-rust-porting-skills 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/12-vendor-streams-are-transformer-structs
to
master
October 17, 2026 15:31
15 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by li
Summary
Before: the method behind the Rust-native series (#257–#260) existed only outside the repository: how to rewrite TS-shaped Rust without changing behaviour, how to prove that, and which design-doc decisions to leave alone.
After: it is in
.agents/, for coding agents and people, in 219 lines:ts-rust-portports, fixes or checks a module against its pinned AI SDK upstream;rust-native-rewritedoes a behaviour-preserving native rewrite, proven by an old-vs-new differential;native-rust/PROFILE.mdholds the repository facts both read.Type of change
Checklist
cargo fmt --all -- --checkpassescargo clippy --workspace --all-targets -- -D warningspassescargo test --workspacepasses (cassette playback — no network/keys)README.md,docs/,rfc/,bindings/README.md)Notes for reviewers
.agents/, plus a paragraph inCONTRIBUTING.mdsaying what.agents/holds and that it is written in Chinese like the recent RFCs. CI does not build or run anything here.json_repair_cases.jsonl), not as a copy of the old implementation..agents/skills/ts-rust-port/SKILL.md.agents/skills/rust-native-rewrite/SKILL.md.agents/native-rust/PROFILE.md.agents/native-rust/README.mdVerification
.agents/and oneCONTRIBUTING.mdparagraph