Skip to content

[13/13] docs: add the native-Rust porting skills - #261

Merged
eric8810 merged 438 commits into
masterfrom
v4/13-add-the-native-rust-porting-skills
Oct 17, 2026
Merged

eric8810 merged 438 commits into
masterfrom
v4/13-add-the-native-rust-porting-skills

Conversation

@cunninghamcard-bit

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

Copy link
Copy Markdown
Contributor

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-port ports, fixes or checks a module against its pinned AI SDK upstream;
  • rust-native-rewrite does a behaviour-preserving native rewrite, proven by an old-vs-new differential;
  • native-rust/PROFILE.md holds the repository facts both read.

Type of change

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

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; [12/13] ([12/13] refactor!: vendor streams are Transformer structs #260) is merged. The last PR of the Rust-adaptation series.
  • Direction: four new files under .agents/, plus a paragraph in CONTRIBUTING.md saying what .agents/ holds and that it is written in Chinese like the recent RFCs. CI does not build or run anything here.
  • Only facts that stay true. The profile has no line numbers, commit hashes, bug lists or candidate lists (those go stale as soon as the series merges; they belong in issues). It keeps the upstream and module mapping, the design-doc sections to read, the surfaces that must not change, the oracles, the CI checks, the project decisions (including one-way layering and streams without a shared tool-call tracker), traps and working rules.
  • Rules learned in this series: removing closures must not reverse module dependencies; a rewrite must not grow the code with boilerplate; an in-process differential is committed as recorded results (like [9/13] refactor!: rewrite json_repair in idiomatic Rust #257's json_repair_cases.jsonl), not as a copy of the old implementation.
  • Not committed: the cross-build probe and a local gate script. Code that CI does not compile would rot; the skills describe how to build the probe, and PR descriptions say how it was run.
  • The skills and the profile are written in Chinese; the README and this description are in English.
Path What it is
.agents/skills/ts-rust-port/SKILL.md Port a module, fix one disagreement with the pinned upstream (test first), or check against upstream
.agents/skills/rust-native-rewrite/SKILL.md Native rewrite without behaviour change: discipline, process, rewrite table, trace scan
.agents/native-rust/PROFILE.md Repository facts both skills read
.agents/native-rust/README.md Index

Verification

Check Result
Code touched none: 4 new files under .agents/ and one CONTRIBUTING.md paragraph
CI runs as usual; nothing here is built, so the result matches #260

claude added 30 commits October 13, 2026 15:03
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
claude added 11 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
…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

Copy link
Copy Markdown
Contributor Author

谢谢跟进。

你提到 我们的处理 位置
PROFILE 里写的 281 个 preset 不对(registry 有 282 行),改成 282 或删掉数字都可以 删掉了数字,只说 preset 来自 provider_registry.json 的运行时 descriptor 表。这个数每加一个 preset 就变,profile 只留不会过期的事 .agents/native-rust/PROFILE.md
.agents/ 是 docs/ 和 rfc/ 之外的第三类目录,建议在 RFC-0036 或 CONTRIBUTING 里加一句规则,留给维护者定 在 CONTRIBUTING.md 补了一段,目录树也加了 .agents/:放给编码代理和维护者用的 skills 和共用的 PROFILE,用中文写(同近期 RFC),只写不会过期的事,和所描述的代码同一个 PR 改。措辞要调整直接说 CONTRIBUTING.md

Generated by Claude Code

@eric8810 eric8810 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复核通过,撤回修改请求。逐项核实:PROFILE 不再含 preset 计数(只留不随代码漂移的稳定事实——比改数字更对)✓;CONTRIBUTING.md 补 .agents/ 目录规则(中文、只写不过期的事实、与所描述代码同 PR 修改)✓。批准。措辞如需调整我方后续直接改。

eric8810 and others added 13 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
…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
eric8810 changed the base branch from v4/12-vendor-streams-are-transformer-structs to master October 17, 2026 15:31
@eric8810
eric8810 merged commit 97830b5 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