Skip to content

[10/13] refactor: tool-call repair is a trait - #258

Merged
eric8810 merged 397 commits into
masterfrom
v4/10-tool-call-repair-is-a-trait
Oct 17, 2026
Merged

eric8810 merged 397 commits into
masterfrom
v4/10-tool-call-repair-is-a-trait

Conversation

@cunninghamcard-bit

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

Copy link
Copy Markdown
Contributor

Requested by li

Summary

Before: the tool-call repair hook (the Rust form of the AI SDK's experimental_repairToolCall) is a boxed closure, ToolCallRepair(Arc<dyn Fn(ToolCallRepairContext) -> Pin<Box<dyn Future<...>>>>). It can only be written as a closure.

After: the hook is a trait, RepairToolCall (async_trait, like LanguageModel), and ToolCallRepair holds an Arc<dyn RepairToolCall>. A struct can implement it. Closures still work through a blanket impl, so ToolCallRepair::new(closure) is unchanged. ToolCallRepair::from(hook) is new. This follows how Rust libraries such as rig express hooks (rig 9e7cb60, crates/rig-agent/src/agent/hook.rs).

Type of change

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

Refactor, no public break.

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; [9/13] ([9/13] refactor!: rewrite json_repair in idiomatic Rust #257) is merged, and this is the next PR of the Rust-adaptation series.
  • Direction: structure only. The repair behaviour is unchanged: called at most once, only when a tool set was given; the reply is re-parsed and re-validated; None keeps the original error; Err becomes ToolCallRepair { original_error, cause }.
  • Not touched: public JSON (repair_tool_call stays #[serde(skip)]), generated TS types, the C ABI and every binding. They have their own repair entry points and only use the error variant.
  • The alignment doc's D25 only says to keep tool-call repair; it does not fix its shape.

Verification

Check Result
cargo test --workspace at [8/8] (65ca6be) 170 suites: 2877 passed, 0 failed, 7 ignored
cargo test --workspace with this change 171 suites: 2882 passed, 0 failed, 7 ignored (2880 on [9/13] plus the 2 new tests). The only new tests are the RepairToolCall doc example and repair_hook_can_be_a_trait_implementation
Existing repair tests unchanged apart from one import line
The tests catch a broken hook making the holder ignore the hook fails 3 of 10 tool_input_test tests; reverted
Gate fmt, clippy, rustdoc -D warnings, gen_ts_types.py --check, gen_providers_doc.py --check, check_provider_boundaries.sh all pass. Locally, clippy 1.97 also flags #[must_use] on FetchResponse::bytes_stream in aimux-provider-utils/src/fetch.rs, which predates this series and passes CI's toolchain

claude added 27 commits October 13, 2026 09:02
google-vertex's Node MaaS and xAI wrappers pass no headers to
createOpenAICompatible. Their fetch spreads the resolved provider headers,
then `Authorization`, over the request's headers
(google-vertex-maas-provider-node.ts:34-58,
google-vertex-xai-provider-node.ts:33-55). So a provider header and the
bearer token win over a per-call header of the same name.

The openai-compatible config gains `fetch_headers`, layered after the
per-call headers in its single header merge. The partner sub-providers
put their headers and token there, so auth errors keep their types and
the headers still follow the credentialed-origin rule on redirects.
This also fixes the node wrapper line numbers cited in the C15 test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…d replay (#229 NP2-5)

The Part 2 acceptance list asks for core-level tests of the stream and
result types. For the stream and results:

- `TextStreamPart` has exactly the AI SDK's 26 part types, in the union's
  order (stream-text-result.ts:581-607), read from the exported TypeScript
  type so a new variant fails the test;
- interleaved text and reasoning segments keep the position of their start
  part while deltas update them by id (stream-text.ts:1540-1600), and the
  response message replays them in that order;
- a preliminary tool result reaches the stream but not the step content
  (stream-text.ts:1648-1650);
- the same model output gives the same response messages whether it was
  generated or streamed.

finish-step and abort are covered by the existing framing and abort tests.
The content-part and message variant counts belong to the content layer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…nt event

The JSONL recorder's bounded queue drops an event when it is full or the
writer is gone, and both recorders mark a call inconsistent on a
duplicate attempt or an exchange update that matches no exchange. Both
only counted or remembered it: try_finalize, write_ready_all and the
ring's finalize and flush still wrote the call with complete = true,
which the alignment doc's completion barrier (§4.3) forbids.

send_or_drop now records the call id of the event it drops, shared with
the writer, and the writer writes a call that lost an event or is marked
inconsistent with complete = false, whether its last event or a flush
writes it. The ring recorder does the same for inconsistent calls. The
recording schema number is unchanged.

Tests drive the writer with a full queue and with duplicate / unmatched
events, for both write paths and for the ring, and check that an intact
call next to them stays complete.

Fixes #232 S2-5.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
TranscriptionModelV4.doStream is optional (@ai-sdk/provider@4.0.22
src/transcription-model/v4/transcription-model-v4.ts:44-46), and
experimental_streamTranscribe rejects a model without it before any I/O
(ai@7.0.127 src/transcribe/stream-transcribe.ts:125-138). aimux had a default
method that failed with UnsupportedFunctionality, so a missing method could
only be found by calling it.

- TranscriptionModel::do_stream returns Option<BoxFuture<..>>, None by
  default, as the alignment doc (§3.1, D22) specifies.
- stream_transcribe reports None as UnsupportedFunctionality with the
  upstream message.
- OpenAI, Azure, Google, Vertex, xAI and ElevenLabs return Some; per-model
  checks (a model ID that cannot stream) stay inside the future, as in the
  upstream doStream bodies. Groq, Mistral and the other batch-only models
  return None.
- Tests: transcription_model::tests::a_model_without_do_stream_cannot_stream,
  groq_transcription_test::the_model_has_no_streaming_method.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
createAnthropicStreamError sets `data: 'data' in error ? error.data :
error` (anthropic-language-model.ts:91). aimux built its own
`{type: "error", error: {type, message}}` instead. The error-chunk
schema's `code` and `data` are now read (anthropic-api.ts:1450-1459).
A present `data` is the error's data, even when it is null. Without one,
the data is the error as the schema parses it: type, message and any of
code, statusCode and isRetryable that the frame carries, with unknown
keys dropped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
aimux-replay's live decision replay accepted only Jev recordings and
built a registry holding Jev alone, while language recordings were
rebuilt from default_providers(). A decision recorded with OpenAI or a
runtime preset could only be replayed with --mock.

Both paths now rebuild from one registry, replay_registry(): every
built-in provider with default settings, the recorded one created again
through create_provider() with --api-key when given, and Jev (outside
the built-in set) when the recording names it, with --api-key or
TYPESAFE_API_KEY as before.

The test records an OpenAI decision against a local server, replays it
through run_replay with another key, and checks the second request
reaches /v1/decisions with that key; a second test checks the registry
resolves ollama and Jev decision models and reports an unknown name.

Fixes #232 B17.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…U3, U5)

- streamRetries: upstream streamText can re-call the model after a mid-stream
  error (ai 7.0.127 stream-text.ts:374, :1948-1953, :2495-2665); aimux does
  not implement it yet and behaves as upstream does when it is unset.
- additionalRetryableError (util/retry-with-exponential-backoff.ts:70-88,
  generate-image.ts:181-182) is listed as a difference.
- first_chunk_ms is armed before do_stream, upstream arms it when the
  response stream starts (stream-text.ts:2228-2243, :2671); changing it
  needs do_stream to return on headers, which is undecided.
- Video: the poll settings belong to the operation and the poll timeout
  races every status check (generate-video.ts:543-549, :586-625); a
  caller's idempotency key goes to start and status alike (:520-536); the
  per-batch key derivation is a product difference (:338-372).
- The provider default retry, composite zero retry and model retry_config
  paragraphs say the alignment document withdraws them.
- Fix the generate-video.ts line numbers cited in video_model.rs (they
  pointed at 7.0.122 lines; commit c5200fd's message has the same stale
  numbers).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
A custom provider lost the fallback provider's model discovery, so a
registry entry built with custom_provider could no longer list models.
Discovery is an aimux extension; it now follows upstream's files/skills
forwarding (ai@7.0.127 src/registry/custom-provider.ts:268-283): the
configured CustomProviderOptions::discovery, else the fallback's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
to_chat_completion put the provider's logprobs metadata straight into
choices[].logprobs: a token list for chat and a list of lists for
Responses. The Chat Completions wire format has an object there,
`{ "content": [{ token, logprob, bytes, top_logprobs }], "refusal": null }`.

The OpenAI chat model keeps only `choice.logprobs.content`
(openai-chat-language-model.ts:459-461), so the converter wraps it.
Responses lists (openai-responses-language-model.ts:1062-1080) are joined
in output order. A token without `bytes` gets null, which the wire format
allows. DeepSeek's object (`{content, reasoning_content}`,
deepseek-chat-language-model.ts:497) is passed through as it is.

round_trip_with_logprobs sends an OpenAI chat completion with logprobs
through generate_text and the converter and gets the same object back.
The C17 Azure test now expects the wire shape.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
… gaps

The §9.1 known-gaps list gains two entries.

Bedrock Mantle is still a registry preset (OpenAI-compatible chat).
Upstream createBedrockMantle is a full provider with chat and responses
models and its own auth.

The AWS SigV4 fetches also sign GET requests (list_models). Upstream
signs only POST requests with a body. The model list is aimux-only and
needs the signature, so this difference is kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The trace scope key isolates by provider name and model id; the V4 model
carries no base URL, so every aimux-web call of one provider shared one
scope and two endpoints matched each other's LCP history.

- TraceLayer::with_scope_name names the scope instead of the model's
  provider(); records still report provider(). The layer's comment says
  same-name endpoints are not isolated unless the host names each one.
- aimux-web names the scope "<provider> @ <base_url>" when the call's
  base_url is not the provider's default.
- RFC-0015 revision line: the ScopeKey formula in §6 describes master.

Fixes #232 B18.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The V4 layer writes schema 3 with an identity-only ProviderRecord and
replays live from the host registry (decision recordings for any
provider). A call that lost an event or was marked inconsistent is
written with complete: false, and the sensitive headers include
x-prodia-key and *subscription-key. Until the V4 layers merge, the RFC
body still describes master.

Refs #232.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
SpeechResult gains usage: Option<JsonObject>, as SpeechModelV4Result's
usage?: JSONObject (@ai-sdk/provider@4.0.22
src/speech-model/v4/speech-model-v4-result.ts:23-27). Google speech
passes its usageMetadata through unchanged and leaves usage unset when
the response has none or null (@ai-sdk/google@4.0.87
src/google-speech-model.ts:356-358, google-speech-api.ts:35). The other
vendors report none. Telemetry of speech usage is not part of this
change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Supported question types are a property of the model, as upstream
DecisionModelV4.supportedQuestionTypes (@ai-sdk/provider@4.0.22
src/decision-model/v4/decision-model-v4.ts:11), so the recording input
no longer stores input.decision_capabilities and replay no longer reads
it. The live recording fixture drops the field; the replay mock accepts
every question type and size and takes rounding and the probability
source from the recorded result. Recording schema stays 3.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…ader rules

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Upstream `createOpenResponses` wraps its headers in
`withUserAgentSuffix(.., options.userAgentSuffix ??
"ai-sdk-open-responses/" + VERSION)` (open-responses-provider.ts:114-127),
and postJsonToApi then appends the provider-utils part of the
provider-utils 2.0.58 depends on. aimux sent no package part.

`@ai-sdk/open-responses` 2.0.58 is now pinned: VERSIONS.json carries the
registry `dist.integrity` (sha512-VF48lc...), which equals the sha512 of
the registry tarball, whose contents equal the extracted sources the
port was checked against. The recorder's package.json, package-lock.json
(regenerated with `npm install --package-lock-only`, which only adds this
entry) and PINNED_ONLY list it too. aisdk_pins gains OPEN_RESPONSES
(2.0.58, provider-utils 5.0.53, its package.json dependency), covered by
the existing drift tests.

The provider sends `ai-sdk-open-responses/2.0.58` through the package
transport, and `OpenResponsesProviderSettings::user_agent_suffix` replaces
the package part as `userAgentSuffix` does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Upstream parses `choices[].logprobs.content` with a zod schema of
`{token, logprob, top_logprobs: [{token, logprob}]}` (openai-chat-api.ts
:71-87, chunk :159-175), which strips `bytes` and any other key, and sets
`providerMetadata.openai.logprobs` only when `content` is not null
(openai-chat-language-model.ts:459-461 generate, :613-615 stream). aimux
copied the raw list, `bytes` included, and set `logprobs: null` for a
null `content`.

Both paths now go through `logprobs_content`, which keeps exactly the
schema's keys. `to_chat_completion` therefore returns `"bytes": null` for
an OpenAI chat result, and `round_trip_with_logprobs` expects that.
DeepSeek's own schema keeps `bytes` and is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…e checks, namespaced kinds

Merges fx3/p3 (layer 3 at ed0e436, which already contains layers 1 and 2).

Conflicts resolved keeping both sides:
- Step content is layer 3's core ContentPart: StepResult.content and the
  text result accessors use ContentPart, tool_results return
  TypedToolResult, files come from the File { file, .. } part, and reasoning
  converts to AssistantContentPart (no signature).
- response_messages.rs is layer 3's builder (tool message sorted by tool
  call order, tool_error), plus this layer's is_deep_equal_data approval
  filter and its tests, typed_tool_result and approval_response for the
  stream path. The C47 tool-message sorting is not added a second time.
- generate_text keeps this layer's step pipeline and gains layer 3's tool
  choice check (generate-text.ts:1155-1176) and the invalid-call tool error
  after the content, built with this layer's invalid_tool_call_error.
- stream_text checks the tool choice on the finish chunk: the step finishes
  with an error finish reason (raw reason kept) and an error part follows
  (stream-language-model-call.ts:644-687). A consumed stream records tool
  results, tool errors and approval responses where they arrive
  (stream-text.ts:1529-1538, :1648-1650).
- TextStreamPart::Custom.kind is a NamespacedKind, as StreamPart::Custom.
- apply_tool_call_repair_to_result works on the typed steps and also
  rewrites or drops the invalid call's tool error and its tool-message
  result.
- The restored RFC-0026 OpenAI output, MoA and router tests are ported to
  TextStreamPart, ResponseInfo and the text() accessor; the AiMuxError
  variant-set test includes NoOutputGenerated; layer 3's content and
  tool-choice tests read the step content and the FinishStep / Finish parts.
- FFI, Node and Python error mappings keep NoOutputGenerated and add
  ToolChoiceViolation to the response-data code / class.
- MIGRATION-NS.md stays deleted; generated TS regenerated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
google-vertex's Node MaaS and xAI wrappers spread the resolved provider
headers over `init.headers` in their fetch, after postToApi built the
user-agent (google-vertex-maas-provider-node.ts:43-49,
google-vertex-xai-provider-node.ts:41-47; provider-utils
post-to-api.ts:100-104). A provider-level `user-agent` therefore leaves
nothing of the package part or the `ai-sdk-provider-utils` part. aimux
layered it over the per-call headers but the helper layer still appended
`ai-sdk-provider-utils/5.0.51`.

`CompatModelConfig::request_headers` now also returns the fetch headers'
`user-agent`, and `with_transport` sends it through a per-request
transport that sets it as the whole header after the helper layer. The
headers are still resolved once, at the config level, so a header or
token error keeps its type. `transport_user_agent` leaves a header with
no provider-utils part and no suffix exactly as it is, so the package
transport after it does not touch the caller's value.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
… replay (U5, K1, K7, C45, C47)

- §3.5: object generation is the deprecated wrapper over `output`
  (generate-object.ts:120, stream-object.ts:190); the separate object spans
  are withdrawn.
- §4.5 / §9.3: async jobs follow generateVideo's start/status model, the
  caller's idempotency key and the poll budget (generate-video.ts:520-536,
  :576-625); doGenerate-only models are retried whole as upstream does
  (generate-image.ts:206, transcribe.ts:243).
- §5: per-batch idempotency keys (product difference, :338-372), stream
  retries and the first-chunk timeout's start (not aligned yet,
  stream-text.ts:374, :2228-2243).
- Part 2: the result convenience fields are deprecated upstream
  (generate-text-result.ts:52-160, stream-text-result.ts:148-330); the
  response-message table gains the empty-reasoning, invalid-input,
  inputSchemaInput, tool-message order and tool-error encoding rows
  (to-response-messages.ts:43, :93-94, :122, :133-146, :203, :220-265); the
  deferred-result paragraph records aimux's provider-tool approximation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Upstream `downloadBlob` calls `fetchUntrustedUrl({url, abortSignal})`
with no headers (download-blob.ts:106-142, fetch-untrusted-url.ts:46-86),
so nothing adds a user-agent; only `getFromApi` and `postToApi` append
`ai-sdk-provider-utils/<v>` (get-from-api.ts:77-90). The OpenAI-compatible
image model downloads URL files with `downloadBlob(file.url)`
(openai-compatible-image-model.ts:241-244). aimux routed that download
through `get_from_api`, which sent `ai-sdk-provider-utils/5.0.53`.

aimux-provider-utils gains `download_blob`: a guarded GET with no caller
headers and no provider-utils part (an internal flag on the prepared
request skips the append). The OpenAI-compatible image model uses it.
Validated `get_from_api` downloads (Luma, Replicate, fal, Recraft, BFL,
Gladia; upstream `getFromApi({validateUrl: true})`) keep the part.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Upstream reads a URL image file or mask with `downloadBlob(file.url,
{abortSignal})` (openai-image-model.ts:166, :357), which goes through
`fetchUntrustedUrl` (provider-utils download-blob.ts:106-142): the target
is validated, redirects are checked hop by hop, and no headers are sent.
aimux fetched it with a plain `get_from_api` on the default transport, so
a private-network URL was fetched and `ai-sdk-provider-utils/5.0.53` was
sent.

`image_file_to_bytes` now calls `download_blob(url, abort_signal)`. A
non-public target is refused before any request, as upstream; a failed
download is a status-code error. CHANGELOG has a Breaking (security)
bullet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Upstream `GoogleFiles.uploadFile` starts the resumable upload and sends
the bytes with the provider's plain `fetch`, not `postToApi`
(google-files.ts:84-92, :118-126), so neither request gets the
`ai-sdk-provider-utils/<v>` part that postToApi appends
(post-to-api.ts:100-104). The init request carries the provider headers,
whose user-agent is `ai-sdk-google/4.0.87` (google-provider.ts:258-269);
the byte upload carries only its two `X-Goog-Upload-*` headers, so no
user-agent. aimux sent the part on both.

`HttpRequest` gains a public `omit_provider_utils_user_agent` flag, which
sends the user-agent as given (or none). The Google upload sets it on both
requests (the upload stays validated, deliberately stricter than
upstream), and `download_blob` now sets it too instead of its private
prepared-request tweak. The poll stays a `getFromApi` and keeps the part.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…n settings (#230 C7)

`CallOptions` loses `max_retries`, `timeout` and `session_id`. The V4 call
options have none of them (@ai-sdk/provider language-model-v4-call-options.ts:8-138);
`maxRetries` and `timeout` are request options of generateText /
streamText (ai prompt/request-options.ts:104-127), reported on the
operation's start event (generate-text-events.ts:54-62).

`GenerateTextOptions` keeps all three, so the JSON bindings are unchanged.
`generate_text` and `stream_text` run their model calls inside
`operation::OperationSettings::scope`, a task-local scope. The router and
MoA take the child retry count from `OperationSettings::current()`, the
trace layer takes the session id from it, and the recorder writes the
three settings into `input.options` under the keys they had, so replay
restores them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings [6/8] (and the [5/8] and [1/8] work it merged) ahead of [7/8],
so [8/8] can start while [7/8] is finished. Conflicts: ApiCallError
keeps [8/8]'s optional status_code serialization with [6/8]'s doc text;
xAI keeps [8/8]'s realtime web_socket with [6/8]'s package fetch;
ApiCallError.ts keeps [8/8]'s shape (regenerated later in this layer).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…ings

Merges fx3/p4 (layer 4 at 822def8, which contains layers 1 to 3). Layer 5
already had layer 1; layers 2 and 3 arrive through layer 4.

Conflicts resolved keeping both intents:
- Retry: layer 5 deleted RetryConfig and retry_config(), so the core
  operations, the router and MoA call prepare_retries(max_retries, abort)
  with the operation's count (OperationSettings, layer 4's C7). The MoA
  test's retry_config() assertion is dropped.
- CallOptions has neither layer 4's max_retries / timeout / session_id nor
  layer 5's body_overrides. Replay restores the three operation settings
  from the record; InputRecord's doc names neither call_id nor
  body_overrides.
- trace/layer.rs keeps layer 5's with_scope_name and layer 4's session id
  from the operation scope.
- VideoModel has neither poll_config() (layer 4's C19) nor retry_config()
  (layer 5). RunwayML keeps layer 5's factory settings and loses its poll
  constants; its tests pass poll per call (fast_poll).
- AiMuxError has both LoadApiKey / LoadSetting and MissingToolResults /
  ToolChoiceViolation; the FFI, Node and Python mappings carry all four.
- Provider code keeps layer 5's factories and exchange helpers, with layer
  3's IndexMap namespace maps (anthropic and google files) and layer 2's
  LanguageModelV4ToolChoice imports. Layer 5's removals (groq tools and
  chat option parsing in openai/convert.rs, the xAI chat converter) stay
  removed.
- provider-utils keeps layer 5's sigv4_fetch and drops the retry module;
  the tracker's argument state stays private as layer 2 made it.
- Layer 5 removed the hand-synced web types; the files layers 2 to 4 added
  there are not kept, and the console imports TextStreamPart from
  @aimux/types.
- Bindings and docs: layer 4's result and stream shapes with layer 5's
  RequestInfo / ResponseInfo (Swift), deepseek mocks (Node, Go) and the
  removed body_overrides. CHANGELOG: layer 4's "All bindings" section goes
  before "Added".
- fixtures/aisdk/VERSIONS.json takes layer 2's provider-utils 5.0.55 entry.

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
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
claude added 16 commits October 16, 2026 11:15
…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
The fetch test pointed npm's user and global config at the same empty
file. npm refuses that ("double-loading config ... as global, previously
loaded as user") and exits before any request, so on CI every test that
downloads failed with "npm pack failed". It passed in the cloud only
because the environment's NPM_CONFIG_USERCONFIG overrode the test's own
setting.

The test now writes two files, and drops inherited npm_config_* variables
in any case before setting its own, so it runs the same everywhere.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in 6ca248e: the fetch test gives npm separate user and global config files, so the AI SDK pin scripts step in the generator drift job passes on CI.

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in 6ca248e: the fetch test gives npm separate user and global config files, so the AI SDK pin scripts step in the generator drift job passes on CI.

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in 6ca248e: the fetch test gives npm separate user and global config files, so the AI SDK pin scripts step in the generator drift job passes on CI.

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in 6ca248e: the fetch test gives npm separate user and global config files, so the AI SDK pin scripts step in the generator drift job passes on CI.

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
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
…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

谢谢评审。这一层已随整套栈更新,只是合入了新的下层,本层代码没有变。

你提到 我们的处理 位置
Fn 闭包的 blanket impl 可能让调用方没法再为自己的类型手写 RepairToolCall 不会:Fn 是 #[fundamental] trait,调用方的结构体不实现 Fn,coherence 允许 impl RepairToolCall for MyType。trait 文档里的 RenameTool doctest 就是这种写法,CI 会编译它,所以没有另加说明 RepairToolCall 的 trait 文档(RenameTool doctest)
PR 说明里的变异验证(让 holder 忽略 hook,10 个测试里 3 个失败)是否该写成测试入库 不入库:现有 10 个修复行为测试已覆盖这条路径,变异只是证明这些测试有效,结果写在 PR 说明里 PR 说明

Generated by Claude Code

eric8810 and others added 10 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
@eric8810
eric8810 changed the base branch from v4/9-rewrite-json-repair-in-idiomatic-rust to master October 17, 2026 14:54
master 带入下方各层的 squash;全部冲突与自动合并差异收敛到本层树
(层树为作者维护、CI 全绿的真相;squash 内容已包含于层树),零差异。
@eric8810
eric8810 merged commit f7b5050 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