Skip to content

[8/8] refactor!: public JSON equals the AI SDK JSON - #256

Merged
eric8810 merged 376 commits into
masterfrom
v4/8-public-json-equals-the-ai-sdk-json
Oct 17, 2026
Merged

eric8810 merged 376 commits into
masterfrom
v4/8-public-json-equals-the-ai-sdk-json

Conversation

@cunninghamcard-bit

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

Copy link
Copy Markdown
Contributor

Requested by li

Summary

Before: the public JSON of results, stream parts, errors, recordings and options uses aimux's own snake_case shapes, and errors are tagged aimux's way. Bytes are arrays of numbers, an explicit null and a missing value are the same, file parts use aimux's file-base64 / file-url / file-reference tags, generated files are a tagged data-or-URL union, and timestamps are any string. Text and reasoning deltas carry delta in the JSON, and standalone tool calls and sources carry no type. A tool input of 1e2 comes back as 100.0. Go and Java reorder provider namespace keys.

After: it is the AI SDK's JSON: camelCase fields, type-tagged unions, and errors tagged by the AI SDK error name (AI_APICallError, ...). Bytes are base64 strings, decision images included, read with atob's forgiving decode. Where upstream's field is unknown (response and request bodies, APICallError.data, inputSchemaInput), null stays apart from a missing value. A file part is upstream's FilePart with the one tag file, generated files are {base64, mediaType}, and timestamps are toISOString() strings. Text and reasoning deltas carry text, and standalone tool calls and sources carry their type. Integral tool-input numbers are written as JavaScript writes them. The call-layer tool result carries upstream's structured output, and Size and AspectRatio serialize as upstream's template strings. The seven bindings follow: they keep their byte-valued APIs and encode at the JSON boundary, and they keep provider namespace order.

The by-name provider entries (aimux_register_providers and the binding calls to it) stay until the bindings migrate to the new C ABI.

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, with PR 1 ([1/8] chore: CI on every PR and test fixture cleanup #249) to PR 7 ([7/8] feat: the remaining upstream model kinds #255) merged in; last of the eight, next is PR 9 ([9/13] refactor!: rewrite json_repair in idiomatic Rust #257).
  • Supersedes: [v4/p8-public-json] refactor!: public JSON equals the AI SDK JSON #242, which GitHub closed when its branch was renamed to match the title.
  • Web wire types: refactor(web): generate the web wire types with ts-rs #247 is on master, so it is not part of this diff.
  • Replaces: Draft refactor(core)!: public JSON equals the AI SDK JSON, one provider construction path #225, except the removal of the by-name provider entries, which moves to the final binding migration.
  • Types from PR 7 on the public JSON: LanguageModelUsage is camelCase and omits an unknown count instead of writing null; StepResult and its response are camelCase; typed tool results, tool-output-denied and tool-approval-response are type-tagged; Output is tagged with the AI SDK output mode names. The decision types (DecisionCallOptions, DecisionResult, DecideOptions, DecideResult, the model description) follow decision-model-v4-*.ts, decide.ts and decision-result.ts. Keys aimux parses inside a provider namespace are camelCase; options a runtime receives as given (Ollama keep_alive, vLLM chat_template_kwargs) keep their names.
  • Recordings: the schema stays 2. The envelope is written camelCase and master's snake_case keys are still read, so master_recording_test passes unchanged. Replaying a master recording whose call options are snake_case fails with JsonParse naming the keys, instead of silently dropping them.
  • Go errors: Error.Data is json.RawMessage, so no data, null and a value stay apart, as upstream's APICallError.data (unknown).
  • Provider config JSON: reads and writes baseUrl, and base_url is still accepted. The removed retry and body settings are rejected in either spelling. Node retry tests pass maxRetries as a call option, so zero disables retries.
  • Aligns to: @ai-sdk/provider-utils 5.0.53 uint8-utils, types/content-part; ai 7.0.127 generate-text/ (generated-file, tool-call, stream-text-result, parse-tool-call), prompt/file-part-data; @ai-sdk/provider 4.0.21 / 4.0.22 shared V4 types, results and errors.
  • Closes: V4 alignment: public JSON, tools and lost tests #232 C33, C33a, C34, C35, C46, C48; V4 alignment: core types, stream parts, results and call pipeline vs ai 7.x #229 B9 (the JSON; the Rust types are in PR 4), NP2-1 (the bindings; the Rust types are in PR 3), NP2-7.
  • Fork fixes included: 1.15: binding CI green (flutter analyze, decision usage shape, recording key) cunninghamcard-bit/aimux#52: the Node and Python decision tests expect a missing usage count to have no total, never null. Its recording and Flutter commits were already on this branch.
  • Not in this PR:
    • Swift was changed without a local toolchain; CI builds and tests it.
    • Integral numbers between about 1.8e19 and 1e21 stay floats, and integer literals above 2^53 stay exact where JavaScript rounds them. Closing that needs a workspace-wide number type; the alignment doc records it.
    • Swift dictionaries have no order, so only the JSON strings keep namespace order there (alignment doc §9.1).
    • A generated file in the stream has no type: "file" field, which upstream's file object adds.
    • Upstream's GenerateVideoResult (video / videos, responses[]) differs from aimux's VideoResult (videos, one response). The JSON rules hold; the result shape stays aimux's own.
  • Behaviour changes (breaking): every wire change has a CHANGELOG Breaking entry. In particular:
    • An array of numbers where bytes are expected is rejected.
    • A stored result or recording whose timestamp is not RFC 3339 no longer parses.
    • The file-base64, file-url and file-reference parts are removed. A data: URL inside {type: "data"} is an invalid prompt.
    • The Rust types GeneratedFile (base64(), bytes()), LanguageModelFile and Timestamp are new; ReasoningFileOutput becomes GeneratedFileOutput.
    • Go: Tool.ProviderOptions and TranscriptionSessionOpts.ProviderOptions are json.RawMessage, and UploadFileResult.ProviderReference is a ProviderReference.
    • The existing C exports are kept.
  • Checks: fmt, clippy, cargo doc with -D warnings and cargo test --workspace (3170 passed, 6 ignored; the ts-rs export tests now run under scripts/gen_ts_types.py --check instead) pass, as do scripts/check_provider_boundaries.sh, scripts/gen_ts_types.py --check, scripts/gen_providers_doc.py --check and the pin script tests. Node (ava, 129), Python (pytest, 111), Go, Java (108), Kotlin (76) and Flutter (132, flutter analyze clean) pass locally.
  • Merged RFC / docs: RFC-0035 shows the final repair wire (context, reply and patch) and upstream's control flow, with Swift's delayed abort registered as a difference; the alignment doc's tool-call section (integral numbers); the API docs of all seven languages for bytes, timestamps, file parts and stream deltas; docs/api/decision.md in camelCase; RFC-0035 §6 names the tests that pin each repair branch; RFC-0026's converter table and RFC-0036's stream example carry text; revision lines in RFC-0023, RFC-0037 and RFC-0038; the web console and README read part.text; CHANGELOG.
  • Draft RFC: RFC-0039: ops、FFI 与八种绑定契约 #209 (errors discriminated by name).
  • Review follow-up: StreamPartPayload gains a File variant here, since File carries the same generic file payload as ReasoningFile in this layer.
  • Review follow-up: MoA failMode and the recording operation use kebab-case values, as the AI SDK spells multi-word literals: "best-effort" / "fail-fast" and "language-model" / "decision" (breaking for failMode; recordings do not change, since language-model is never written).
  • Review follow-up: the Go binding's Tool carries SupportsDeferredResults (supportsDeferredResults, omitted when unset), since Go gains provider tools in this layer; the other bindings got the flag in [4/8].
  • Size: release artifacts before and after (CI artifacts, zip size):
CI artifact (zip) master a8e3a99 [7/8] 22db086 this PR ca181dd change vs [7/8] change vs master
ffi-x86_64-unknown-linux-gnu 3.77 MB 5.11 MB 5.14 MB +26.9 KiB (+0.54%) +1341.7 KiB (+36.49%)
ffi-aarch64-apple-darwin 3.12 MB 4.24 MB 4.25 MB +10.1 KiB (+0.24%) +1105.1 KiB (+36.29%)
ffi-x86_64-pc-windows-msvc 3.03 MB 4.15 MB 4.16 MB +14.1 KiB (+0.35%) +1111.2 KiB (+37.61%)
node-x86_64-unknown-linux-gnu 55.30 MB 66.56 MB 67.13 MB +563.8 KiB (+0.87%) +11559.0 KiB (+21.41%)
node-aarch64-unknown-linux-gnu 53.25 MB 63.95 MB 64.47 MB +502.4 KiB (+0.80%) +10949.8 KiB (+21.06%)
node-aarch64-apple-darwin 15.06 MB 19.80 MB 19.93 MB +121.7 KiB (+0.63%) +4752.7 KiB (+32.31%)
node-x86_64-apple-darwin 13.60 MB 18.02 MB 18.11 MB +88.3 KiB (+0.50%) +4405.1 KiB (+33.17%)
node-x86_64-pc-windows-msvc 10.57 MB 14.33 MB 14.39 MB +58.1 KiB (+0.42%) +3729.5 KiB (+36.14%)
node-aarch64-pc-windows-msvc 9.87 MB 13.47 MB 13.54 MB +61.3 KiB (+0.47%) +3575.7 KiB (+37.08%)

claude added 19 commits October 13, 2026 08:16
The AI SDK puts its provider tools on the provider: openai.tools is
openaiTools (openai-provider.ts:387) and azure.tools is azureOpenaiTools,
the code interpreter, file search, image generation and web search subset
(azure-openai-provider.ts:434, azure-openai-tools.ts). Give OpenAIProvider
and AzureOpenAIProvider a public tools member over the existing
openai::tools factories.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
openai-responses-language-model.ts:1660-1663 enqueues a raw part for
every chunk, before the parts derived from it, when includeRawChunks is
set. The Responses stream already does; nothing pinned it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Regenerated with scripts/gen_providers_doc.py after the Vertex MaaS and
xAI sub-providers were added.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
is_sensitive_key matched only cookie, key, authorization, api-key and
*token names, so X-Prodia-Key (sent by the Prodia provider) and Azure's
Ocp-Apim-Subscription-Key would be written in clear text into
recordings (and logs, which share the policy) whenever recording was
on. Both names are now sensitive, the second as any name ending in
subscription-key, separator-insensitive. A recording test sends them
through the HTTP layer and checks that the stored exchange holds
[REDACTED] and that neither secret appears in the JSONL.

Carries master's d38a9d3 (#228 S2-1) to the V4 code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The status_code field doc changed with the stream-error status fix
(97e131d); this is scripts/gen_ts_types.py's output for it. The
script's normalization also drops the file's old trailing spaces.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings [6/8]'s vendor alignment (user-agents from the pinned versions,
SigV4 per upstream, Vertex MaaS and xAI, Azure token honours omitted
credentials, the restored registry base URL test) together with what
[6/8] merged from [5/8] and [1/8]. No conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The Dart NativeFinalizer called aimux_drop_handle(uint64_t) through a
void (*)(void *) pointer. On 32-bit ARM (armeabi-v7a, which the release
ships) a uint64_t argument does not travel like a pointer, so the drop
received a garbage high word and a GC'd Model or ProviderHandle leaked
its handle.

aimux-ffi now exports aimux_drop_handle_ptr(void *), which takes the
handle as a pointer-sized value and drops it (NULL is a no-op), and the
Flutter finalizer uses it. Every existing export is unchanged. The C
header and docs/api/c.md list it; the export count is 130. Tests: the
Rust unit test and the export smoke test release handles through it,
and a Flutter test checks it releases the handle its address names.

Carries master's 3d57895 (#228 S2-3) to the V4 code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
The binding's block_on calls for generate_text, generate_object,
consume_stream_text, generate_text_as_openai, ProviderHandle.list_models,
get_model_specs and the eight multimodal calls held the GIL while they
waited on the network, so every other Python thread stopped for the
whole request. They now run inside allow_threads, as decide() and the
stream iterator already did, and so do the recording flushes, which
wait on the writer thread. Results and errors are unchanged.

test_gil.py serves the mock API from a thread of the test process: it
can only answer while the call releases the GIL. Without the fix both
calls fail with their 10 s total timeout. The chat case uses deepseek()
(chat completions), since openai() builds a Responses model here.

Carries master's 478522d (#228 C30) to the V4 code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…t differs (fork #18, #12)

The response messages' tool approval request part carries
`input_schema_input` only when it is not deep-equal to the call's input
under the AI SDK's `isDeepEqualData` (ai to-response-messages.ts:133-146,
util/is-deep-equal-data.ts:8-48): numbers compare as JavaScript doubles,
objects by key set in any order, arrays element by element, and `[]` is not
`{}`. The builder wrote `None` unconditionally.

`ToolApprovalRequestOutput` holds the pre-validation input Rust-side only
(upstream keeps it on the tool call, tool-call.ts:64-86); it is not part of
the type's JSON. aimux's parse validates without transforming the input, so
text generation leaves it unset.

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

As convertLanguageModelContent does (ai convert-language-model-content.ts:
87-104), `generate_text` fails with `Tool call {id} not found.` when the
model returns a provider tool result whose call is not in the response,
unless the result is a deferred one. Before, such a result passed through
silently.

Upstream accepts a deferred result only for a provider tool whose
definition sets `supportsDeferredResults`. aimux's tool definitions are the
V4 tool set and carry no such flag, so a result is accepted as deferred when
a provider tool of that name was passed; whether the tool definition should
carry the flag is left open. The stream passes such results through, as
upstream's stream does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
A request's `api_key: "env:VAR"` read any environment variable of the
console process. Together with a caller-supplied base_url, a client of
a non-loopback console could send any server variable to a host of its
choice. resolve_api_key now accepts only the variable the provider's key
is registered under and refuses every other name before reading it.
This covers the calls, cache-probe and replay paths, which all resolve
keys through it.

aimux-providers gains provider_api_key_env(), the variable
create_provider()'s factory reads a key from when none is passed: the
vendor package's own constant (now crate-visible, so the table cannot
drift from what the factory reads) or the registry row's env_var. The
console does not keep its own copy of the provider table.

Carries master's d5a10bd (#228 B13) to the V4 code. The tests use
registry rows (xpersona, the_grid_ai, siliconflow) whose variables they
set and restore, since this layer's console builds providers through
create_provider and has no overlay rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…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
Brings in #266 (a blank key on a custom base URL is sent as an empty
key, as the AI SDK does for apiKey: '').

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 #266 (a blank key on a custom base URL is sent as an empty
key, as the AI SDK does for apiKey: '').

Resolution: rfc/0029-web-console.md and tools/aimux-web/README.md keep
this layer's text, which already says what #266 says and adds which
providers cannot take a custom base_url and where Replay sends.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in #266 (a blank key on a custom base URL is sent as an empty
key, as the AI SDK does for apiKey: '').

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 #266 (a blank key on a custom base URL is sent as an empty
key, as the AI SDK does for apiKey: '').

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 #266 (a blank key on a custom base URL is sent as an empty
key, as the AI SDK does for apiKey: '').

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
eric 的意见 我们的处理 位置
实况调用的 options 未知字段被静默忽略 GenerateTextOptions、TimeoutConfiguration、各模态 options、DecideOptions、MoaConfig、router 和转写会话配置都加了 deny_unknown_fields,旧的 max_retries 会报错并点名;旧录制回放不受影响。上游 TimeoutConfiguration 里的 toolMs 和 tools 也会报错,因为 aimux 不执行工具。顺带修了 Node/Python 三处同类问题,绑定 crate 的单元测试加进 CI(f275cdfd) aimux-core/src/options.rs、aimux-core/tests/ai_sdk_json_test.rs
LanguageModelUsage 的两个 detail 对象总是写出 和上游一致:这两个对象在 ai 7.0.127 里是必填字段,空的会写成 {}。加了一条直接拿上游录制 fixture 比对 usage 的断言(586235f9) aimux-providers/tests/anthropic_factory_test.rs
顶层未知 type 要给出明确的 decode 错误 带 tag 的联合类型本来就报 unknown variant,补了逐类型测试。核对时修了两处:untagged 的 ModelPrompt、UserContent、AssistantContent 吞掉了内层错误;FilePartData 会把带 type 的对象当成 provider reference(d365388a) aimux-core/tests/ai_sdk_json_test.rs
三处 snake/下划线残留 first_frame/last_frame 就是上游的字面量,补了出处和测试(72e4fb9f)。另外两处改成 kebab-case:MoA 的 failMode 是 "best-effort"/"fail-fast",录制的 operation 是 "language-model"/"decision"(c6382f14)。录制输出不变,因为 language-model 从不写出;failMode 这个键是本系列新加的 aimux-core/src/moa.rs、aimux-core/src/recording.rs

Generated by Claude Code

claude and others added 21 commits October 16, 2026 11:18
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
…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
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
@eric8810
eric8810 changed the base branch from v4/7-remaining-upstream-model-kinds to master October 17, 2026 14:28
master 带入下方各层的 squash;冲突与自动合并漂移全部收敛到本层树:
ModelTest.kt 取层版本;OpenAiResponsesFixtures.java/.kt 与
ReasoningFileOutput.ts 为本层已删文件(fixtures 已内联、类型已并),
按层意图 git rm。零差异校验随下。
@eric8810
eric8810 merged commit 9ad5131 into master Oct 17, 2026
20 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