Skip to content

[9/13] refactor!: rewrite json_repair in idiomatic Rust - #257

Merged
eric8810 merged 388 commits into
masterfrom
v4/9-rewrite-json-repair-in-idiomatic-rust
Oct 17, 2026
Merged

eric8810 merged 388 commits into
masterfrom
v4/9-rewrite-json-repair-in-idiomatic-rust

Conversation

@cunninghamcard-bit

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

Copy link
Copy Markdown
Contributor

Requested by li

Summary

Before: json_repair (the partial-JSON repair used for streamed tool inputs and generate_object) is a line-for-line port of the AI SDK's fix-json.ts and parse-partial-json.ts. It copies the input into a Vec<char>, tracks positions with an isize and a -1 sentinel, keeps a flat state stack with .expect("never empty"), and returns a { value: Option<Value>, state } pair where which fields are set depends on the state.

After: the same behaviour, written in Rust shape. It scans the borrowed &str, keeps a stack of open values (Vec<Frame>, empty stack = done, no panic path), and returns PartialJson::{Parsed(Value), Repaired(Value), Failed}. Upstream's undefined-input state is the caller's Option (text.map(PartialJson::parse)).

This is the first of the Rust-adaptation PRs stacked on the AI SDK migration. Each one keeps behaviour identical to the PR below it and proves it.

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; the migration series [1/8]–[8/8] ([1/8] chore: CI on every PR and test fixture cleanup #249–[8/8] refactor!: public JSON equals the AI SDK JSON #256) is merged, and this is the first PR after it.
  • Direction: structure only. No output changes for any input.
  • Breaking (Rust API only): parse_partial_json, ParsePartialJsonResult and ParsePartialJsonState are replaced by PartialJson::parse. No deprecated aliases, per the alignment doc's one-shot rule (D31). The only callers were the four upstream cases in aimux-core/tests/util_test.rs. No JSON, C ABI or binding changes. CHANGELOG updated.
  • No second implementation in the repository. The rewrite was proven against the previous port in-process; the repository keeps the result of that comparison as data, not the old code: aimux-core/tests/fixtures/json_repair_cases.jsonl holds 740 inputs with the old port's fix_json output and parse outcome (one shortest input per outcome / ending bucket out of 300,000 generated ones, plus hand-picked edges), checked by json_repair_cases_test.rs. The upstream cases stay in util_test.rs.

Verification

Check Result
Old vs new (in-process, during the rewrite) 1,000,000 generated inputs, 0 differences
Recorded cases 740 cases pass
The recorded cases catch changes 14 hand mutations of json_repair.rs (number start, literal tails, unicode escapes, array/object separators, outcome kind): all 14 caught
Upstream cases aimux-core/tests/util_test.rs: 51 cases (47 fix-json, 4 parse-partial-json) pass
Against upstream itself the same 1,000,000 inputs through ai@7.0.127 fix-json.ts and provider-utils@5.0.53 secure-json-parse.ts under node 22 (runner not committed): fix_json matches upstream on all of them. The previous port differed on 63, all a character outside the BMP right after a number or literal in an array; the scanner now steps through UTF-16 code units as upstream does, so "[1😀" repairs to "[1😀]" on both sides
Gate fmt, clippy, rustdoc -D warnings, gen_ts_types.py --check, gen_providers_doc.py --check, check_provider_boundaries.sh; CI on this branch

@cunninghamcard-bit
cunninghamcard-bit marked this pull request as ready for review October 13, 2026 03:53
claude added 28 commits October 13, 2026 08:51
…V4 release

@ai-sdk/provider 4.0.22 renamed the experimental EvaluationModelV4 to
DecisionModelV4 (src/decision-model/v4/decision-model-v4.ts:6-17) and
EvaluationUnsupportedQuestionTypeError to DecisionUnsupportedQuestionTypeError.
The decision model follows that release, so the baseline moves to it.

- VERSIONS.json pins 4.0.22 with its integrity.
- The recorder's package.json and package-lock.json name the same version
  (regenerated with npm install --package-lock-only --ignore-scripts; the
  pinned vendor packages keep their own 4.0.21 copies).
- aisdk_provider_pin_test fails when the pin drifts from 4.0.22.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…lts (#229 C41 stream and results, fork #27 part)

As streamText's model call does (ai stream-language-model-call.ts:760-792),
a provider's tool result in the stream becomes a `ToolResult` part holding a
`TypedToolResult` (`tool_call_id`, `tool_name`, `input` of the call it
answers, `output`, `provider_executed: true`, the chunk's `dynamic`,
`provider_metadata`), or a `ToolError` part holding a `TypedToolError` when
the provider marks it as an error. A result whose call is not in the stream
(a deferred result) has no `input`. Before, the stream forwarded the
provider's `ToolResult` with `result` and `is_error`.

`TypedToolResult` and `TypedToolError` follow ai generate-text/
tool-result.ts:10-42 and tool-error.ts:6-36 (the user-level tool result's
`providerExecuted` of fork #27); `toolMetadata` and `title` are absent, as
aimux has no user-level tool definitions. Upstream drops the provider's
`preliminary` flag at this point; aimux keeps it, so a preliminary result
stays out of the step content as on the generated path and as the
alignment doc's rule 6 asks.

A consumed stream keeps the results and the provider's error results in
the step content. The OpenAI chat output renders a provider error result as
`[tool error: name]` text, as the generated path does. The bindings decode
the new `ToolResult` shape and `ToolError` (Go keeps raw payloads and needs
no change).

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

As streamText's model call does (ai stream-language-model-call.ts:709-725),
a client tool call that could not be parsed is followed in the stream by a
`ToolError` part with the call's input, its error message
(`getErrorMessage`) and `dynamic: true`. An invalid provider-executed call
gets none. The error message is the error's display text, as layer 3 uses
for the generated path's tool error.

This branch's step content has no core tool error part, so a consumed
stream does not keep it; the core content parts of layer 3 do.

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

Upstream the `ai-sdk-provider-utils/<v>` part comes from the
provider-utils the calling package imports (provider-utils
post-to-api.ts:100-104, get-from-api.ts:77-81). npm installs each
package's exact dependency, so the version follows the package's
package.json: 5.0.51 for azure, amazon-bedrock, cohere, deepseek, groq,
mistral, xai and google-vertex, and 5.0.53 for openai,
openai-compatible, anthropic, anthropic-aws and google. google-vertex
runs MaaS and xAI on its own openai-compatible 3.0.59.

shared::aisdk_pins is now a per-package table (name, version,
provider_utils), and each entry cites its package.json. Every pinned
package's transport goes through shared::package_fetch, which rewrites
the provider-utils part the helper layer adds. A per-call user-agent
therefore keeps the package's part, as upstream does.

The provider-utils crate's default part is no longer tied to the
VERSIONS.json provider-utils pin, which follows the port. The drift tests
check each package's version against VERSIONS.json and its provider-utils
against the dependency in the recorder lock, including google-vertex's
nested packages. package_user_agent_test pins the full user-agent of
every package as literals.

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

`TextStreamPart` gains `ToolOutputDenied` (ai generate-text/
tool-output-denied.ts:7-15) and `ToolApprovalResponse` (the
`ToolApprovalResponseOutput` of tool-approval-response-output.ts:7-35), in
upstream's order (stream-text-result.ts:581-607), which completes the AI
SDK's 26 part types. `tool-output-denied` is a stream part only, not a core
content part (content-part.ts:12-33).

aimux's stream does not produce them yet. Upstream answers approvals and
reports denied tools when it executes tools (execute-tools-from-stream.ts:
170-210), which aimux does not, and reports the approvals denied in the
prompt's last tool message, provider-executed ones included, before the
first model call (stream-text.ts:2030-2090); that needs the prompt's
approval collection (collect-tool-approvals.ts), which is not ported. The
bindings decode both parts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
/api/calls and /api/cache-probe resolved the key from the request spec,
then the Settings key store, then the provider's env var, and passed
the request's base_url to the provider unchanged. A client could point
base_url at its own host and receive the console's key.

When the base URL differs from the provider's own default, only a key
carried in the request (plaintext, loopback binds only) is used: the
Settings key and the env var are not read, env: references are
refused, and a call without a key goes out with an empty key instead of
the provider's env fallback. Every built-in provider is reachable from
the console on this layer, and an empty key does not stop the ones that
read other credentials from the environment (AWS SigV4 credentials, the
Vertex access token, the Codex sign-in, the DataForSEO login), so those
refuse a custom base_url. A base_url equal to the provider's default
keeps the normal key order.

aimux-providers gains provider_default_base_url(), the endpoint
create_provider()'s factory calls when given no base URL (the package's
DEFAULT_BASE_URL, now crate-visible, or the registry row's URL; None
when the URL is built from settings).

Replay rebuilds the provider from the recording's identity with its
default base URL (recordings store no URL on this layer), so it never
calls a custom base URL. The test sends calls with a custom base_url to
a local server and checks that neither the stored keys nor the env keys
arrive, that env: and a Bedrock call are refused before reaching it,
and that a key carried in the request does arrive. README, RFC-0029
§5.5 and the alignment doc's §0.6 note say the same.

Carries master's bf0d19e (#228 B12) to the V4 code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
An `{"error": ...}` chunk on the chat stream is passed on as it is
upstream (openai-compatible-chat-language-model.ts:588-595). aimux
took a numeric `code` in 400..=599 as the status, which made a code
like 429 retryable. The error now keeps the code as provider_code with
no status, as the completion path already did since N3. Presets and
the Vertex MaaS and xAI sub-providers share this chat model.

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

The master branch's chunk-timer matrix (tests/stream_timeout_matrix_test.rs)
and the stream pipeline tests in generate.rs were deleted on the way to V4
without replacement. They come back as `generate::pipeline_tests`, adjusted
to the AI SDK's full stream:

- the chunk timer is armed and reset only by output chunks
  (stream-text.ts:182-225, :2842-2857), and a consumer's delay does not
  count against it (§13-17, -18);
- dropping the stream leaves no armed timer behind (§13-21b);
- the first event a provider peeks inside do_stream is kept, the first-chunk
  budget measures its arrival rather than the first poll, and it bounds a
  do_stream that never returns (§8.3);
- the model's finish ends the stream while the connection stays open, a
  provider error part does not hide the finish, a malformed frame is
  reported and the stream goes on, a transport error ends it;
- a retryable first error (429) is retried before the stream is returned,
  a non-retryable one is returned once (stream-text.ts:2409-2420).

The pinned AI SDK package ships no stream-text tests, so these keep
aimux's own cases with the upstream source cited. The first-chunk tests
follow the current design (timer armed at operation start, U3) and change
if that does.

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

谢谢评审。你看的是旧 head 540fef5,这一层已随整套栈更新,下面以更新后的为准。

你提到 我们的处理 位置
和上游 ai@7.0.127 跑 100 万条输入,fix_json 有 63 处不同,都和 BMP 以外的字符有关;可维持登记,是否按 UTF-16 码元对齐另定 直接对齐:扫描改成和上游一样按 UTF-16 码元走,BMP 以外的字符算两步,"[1😀" 也修成 "[1😀]"。同样 100 万条重跑,fix_json 与上游完全一致,模块说明不再登记这个差异 aimux-core/src/json_repair.rs(fix_json 与 doctest)
core 自己的路径已不调用这个模块,模块说明应写清为什么保留、给谁用 模块说明补了一段:最终工具调用输入按原样解析,不做 partial-JSON 修复(RFC-0042 §3.2);保留公开是因为 ai 导出了 parsePartialJson,给需要展示流式半截 JSON 的调用方用 aimux-core/src/json_repair.rs 模块说明
(我们自己的改动)测试里整份拷贝的旧实现 mod legacy 已删除。新旧差分只在改写期间跑,入库的是结果:740 条固定样本(30 万随机输入按结果种类 × 结尾字符类每桶取最短一条,加手挑边界),14 个手工变异全部被抓到。上游实测 runner 不入库,CI 不跑的脚本会过期;固定样本加上游 51 例足够在升级基线时复验 aimux-core/tests/fixtures/json_repair_cases.jsonl

Generated by Claude Code

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