Skip to content

refactor(core)!: public JSON equals the AI SDK JSON, one provider construction path - #225

Closed
cunninghamcard-bit wants to merge 25 commits into
rfc-0036/vendor-alignmentfrom
rfc-0036/wire-format
Closed

cunninghamcard-bit wants to merge 25 commits into
rfc-0036/vendor-alignmentfrom
rfc-0036/wire-format

Conversation

@cunninghamcard-bit

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

Copy link
Copy Markdown
Contributor

What

Makes aimux's public JSON equal the AI SDK's JSON, leaves one construction path for providers, and gives the call-layer tool result upstream's structured output. All seven language bindings follow. Base rfc-0036/vendor-alignment (C); follows C and is the last of four (A to D). It is a breaking change for every binding. New PR, with no earlier PR to replace.

Layers

Branch PR Content Diff size
rfc-0036/wire-format none (new) camelCase and type-tagged public JSON, omitted optionals, structured tool result output, by-name provider entries deleted, bindings migrated 318 files, 8212 insertions(+), 10321 deletions(-)

Before and after

Area Before After Upstream reference (pinned)
Field names snake_case (tool_call_id, finish_reason, provider_metadata, ...) camelCase on every public type in aimux-core, with upstream's exact name where it is not the camelCase of the Rust name provider/src/**, ai/src/** type definitions
Unions externally tagged ({"TextDelta":{...}}) or snake_case tags (tool_call) tagged by type (or upstream's key: role, sourceType) with upstream's strings (text-delta, tool-call, tool-result, file-base64, ...); aimux-only unions use kebab-case language-model-v4-stream-part.ts, language-model-v4-content.ts, ai/src/prompt/message.ts, ...
Optional fields serialized as null omitted; the generated TypeScript is field?: T upstream T | undefined fields
Image size, aspect ratio {width, height} objects "1024x1024", "16:9" strings image-model-v4-call-options.ts, video-model-v4-call-options.ts
Call-layer tool result free JSON result plus isError, preliminary, dynamic toolCallId, toolName, output (text, json, execution-denied, error-text, error-json, content), providerOptions; conversion to the provider prompt is an identity mapping; a repair function's parse failure is rethrown as is ai/src/prompt/message.ts (ToolResultPart), convert-to-language-model-prompt.ts, parse-tool-call.ts
By-name provider entries aimux_providers::provider::{provider, provider_handle, register_provider, load_providers_from_json, ...} and C aimux_register_providers beside the factories deleted; FFI, Node and Python go through create_provider(name, PresetSettings) and keep their public function names; config_json rejects unknown keys per-package create<Vendor> factories, ai/src/registry/provider-registry.ts
Aimux's own formats snake_case the same rule for recordings, traces, the error JSON and the web wire mirror; aimux-replay and aimux-cli read the new shape none (aimux only)
NoSuchProvider message No such provider: x (available providers: …), upstream's text No such provider: x. Did you mean 'y'? (available providers: …): up to three close names from available_providers (case-insensitive, - read as _, edit distance ≤ 2 and ≤ a third of the name). An aimux extension of the upstream message; fields, error JSON and ABI unchanged, and callers do not parse messages (docs/error-model.md rule 5). No ported test asserts the full text ai/src/registry/no-such-provider-error.ts (no suggestion)

Vendor request and response types under aimux-providers/src/*/ are unchanged: they keep the vendor's wire format. ChatCompletion* in openai_output.rs keeps OpenAI's snake_case because it is OpenAI's response shape.

Bindings

Node, Python, Go, Java, Kotlin, Swift and Flutter decode and encode the new JSON, drop registerProviders and document the camelCase config_json keys. aimux-ffi.h documents the new keys and that the stream callbacks receive TextStreamPart. The OpenAI constructors return the Responses model, as upstream's openai(id) does, so binding tests whose mock servers speak chat completions now build a chat preset at the mock URL; tests that expected a missing API key at construction now expect it on the first call.

Binding Result How
Node 77 passed, 0 failed (exhaustive cassette replay 809/809) ava against a debug napi build
Python 84 passed, 0 failed (exhaustive cassette replay included) pytest in a uv venv, maturin develop
Go 135 passed, 0 failed go test ./... against the debug static library
Java 95 passed, 0 failed gradle clean test, Java 17
Kotlin 63 passed, 0 failed gradle test, Java 17
Flutter 114 passed, 0 failed dart test (Flutter 3.47 SDK) with the debug dylib
Swift 54 passed, 0 failed library and unchanged test files compiled with swiftc and run through a local XCTest stand-in that is not in the repository

Tests

  • Deleted: aimux-providers/tests/presets_test.rs and bindings/node/__test__/provider_config.test.ts (hand-written tests of the removed by-name registry); hand-written cases in tool_choice_test.rs and tool_input_test.rs without an upstream counterpart; binding tests that only pinned registerProviders.
  • Adapted: ported, cassette-replay and *_upstream_e2e_test.rs expectations to the new JSON; the inline error-JSON golden strings in error_value_golden_test.rs now hold the name-tagged shape (AI SDK error names such as AI_APICallError), and the test checks them against the serializer; binding tests to the new names.
  • Cassettes, fixtures/aisdk and the four replay_fixtures (vendor wire format) are unchanged.

Not in this pull request

  • ProxyConfig (aimux-provider-utils) and the provider token JSON (aimux-providers) keep snake_case keys; they are outside aimux-core, which this rule covers.
  • AiMuxError stays an untyped JSON value in the Swift, Kotlin and Flutter bindings, and transcription stream parts stay JSON strings there.
  • Video aspectRatio does not accept upstream's 'adaptive' value.
  • The binding constructors do not expose the model families added in C.

Relation to the maintainer's design

Reference order: the pinned AI SDK source first; where it has no counterpart (C ABI, bindings, recording, code generation), the maintainer's RFCs 0039 (ops and bindings, #209), 0040 (provider construction and auth, #210), 0041 (replay governance, #211) and 0042 (V4 type boundaries, #212), which are Drafts that refine the #200 design doc docs/aisdk-architecture-alignment.md; that design doc only where those RFCs defer to it. Where an RFC restates upstream behaviour differently from the pinned source, upstream wins and the RFC gets an amendment request. The stack targets rfc-0036/integration; RFC-0039 §11 and RFC-0040 §7.2 require the whole closure to switch to master at once, so items marked "before master" must land on integration first. "Branch E" is the follow-up branch stacked on D, in progress.

Upstream over RFC text

  • Repaired tool call that still fails validation. RFC-0035 §3.2 wraps the failure as ToolCallRepair { original_error, cause } and states this matches AI SDK parseToolCall step by step; RFC-0042 §5 (item 3) repeats it. The pinned ai/src/generate-text/parse-tool-call.ts (ai 7.0.122) wraps only an exception thrown by the repair function: the re-parse of the repaired call sits outside that try, so its error reaches the outer catch, which returns an invalid tool call carrying that parse error unwrapped, with the original call's input and toolName. This PR follows upstream (aimux-core/src/parse_tool_call.rs, apply_repair_outcome). Upstream's parse-tool-call.test.ts has no case for it; the evidence is the source. Request: amend RFC-0035 §3.2.

Implemented in branch E, where the AI SDK has no counterpart

  • Error envelope. The error object stays the AI SDK one, tagged by name with the class properties (e.g. {"name":"AI_APICallError","url":…,"requestBodyValues":{},"statusCode":500,"isRetryable":true}, see aimux-core/tests/error_value_golden_test.rs). E adds RFC-0039 §6.1's envelope around it, { kind, code, message, error, retryable, retryAfterMs }, and reduces the 23 aimux_error_* getters to code, message and json.
  • C ABI handle model. RFC-0039: one shared dispatch for the C adapter, Node and Python (§0.1, §2.1); handle kinds and the op × handle matrix (§3); the aimux_call / aimux_stream primitives with ABI version and manifest query (§4.1); named convenience functions such as the docs: full-chain AI SDK alignment design and impact map #200 design doc §6's aimux_provider_model(provider, method, model_id) generated from the same op table (§0.2, §4.1). This PR still has 127 exports including the per-vendor constructors; it removes only aimux_register_providers. The old exports are deleted in one commit at the end of E.
  • Code generation. RFC-0042 §7 and RFC-0039 §0.1.7 and §10: descriptor + manifest is the only generation source, the C header and every language's DTOs and op constants are generated, and ts-rs / scripts/gen_* retire in the same switch. This PR keeps ts-rs (189 #[ts(export)] in aimux-core) and hand-updates the Go, Java, Kotlin, Swift and Flutter types (71 files, +4,973 / −6,816). E builds the descriptors, op table, manifest.json with --check, and the generated C header; the per-language DTO generators that replace the hand-written types, and the retirement of ts-rs, are needed before master.
  • Kotlin on the Java artifact. RFC-0039 §9.4 (Draft): Kotlin depends on the same-version Java artifact and drops its own JNA layer and native loader. This PR keeps both and updates Types.kt alongside Types.java. E moves Kotlin onto Java after Java uses the new ABI.

Partial or not checked

Agrees

  • JSON naming: camelCase fields, kebab-case type tags, absent distinct from null (RFC-0042 §1; RFC-0041 §4.1 for the recording JSON).
  • Unknown request-config keys are rejected (RFC-0042 §7.2).

Verification

Gate on the head 415e37e1: cargo fmt --check, cargo clippy --workspace --all-targets -D warnings, cargo doc with -D warnings, tests of core, provider-utils, providers, ffi, web, replay and cli: 2,780 passed, 0 failed, 6 ignored (14 fewer than C; this PR deletes hand-written tests); Node and Python bindings cargo check; provider boundary script; gen_ts_types.py --check; gen_providers_doc.py --check. Cassettes and fixtures/aisdk unchanged. Diff against the base: 324 files changed, 8223 insertions(+), 10330 deletions(-). After the restack onto the current master, only this head was tested; the intermediate layers were checked with fmt and clippy only. Binding suites: see the table above.

Restacked head 117df5ce. Restack (2026-10-09): merged master (#213, SSE) and the rewritten #204 tracker down the stack by merge, no history rewrite. The merge into this layer was conflict-free. 117df5ce then drives the OpenAI/Azure chat tracker with @ai-sdk/openai's options: type validated if present, ids from generate_id, DeltaToolCall.index required. Checked on that head: cargo fmt --check, cargo clippy --workspace --all-targets -D warnings, cargo test --workspace (153 test binaries, 0 failed). cargo doc, binding suites and the generator --check scripts were not rerun.

Commit 2a5f7d68 (the NoSuchProvider suggestion, on top of 8f5265c6): aimux-core error:: tests (13), the aimux-providers default_providers tests (2, one checks real misspellings such as opeanai, gorq, google-vertex against the 326 built-in names), and cargo clippy -D warnings for both crates. The full gate above was not rerun.

Not verified

None of the binding suites run in CI for stacked branches. Swift ran through the stand-in above because SwiftPM crashes at startup on the test machine's Command Line Tools and XCTest is absent there, so swift test was not run.

🤖 Generated with Claude Code

cunninghamcard-bit and others added 17 commits October 7, 2026 01:16
…o through create_provider

What: aimux_providers::provider (provider, provider_handle,
register_provider, is_external_provider, load_providers_from_json,
ExternalProviderEntry, ProviderProfile, ProviderOptions and the JSON
overlay registration) is deleted. The ffi exports aimux_provider_new,
aimux_provider_from_env and aimux_provider_handle_new build their
provider through create_provider with PresetSettings; aimux_register_providers
is removed from the C ABI and header. The Node and Python by-name
functions keep their names and parameters and use the same path; their
registerProviders / register_providers entries are removed. Unknown keys
in the by-name config are now rejected. Deletes presets_test.rs and the
Node provider_config test, which pinned the old registry.

Why: one path for provider construction, the per-package factories plus
create_provider and the registry; upstream has no by-name overlay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
… output

What: ContentPart::ToolResult has tool_call_id, tool_name, output
(ToolResultOutput: text, json, execution-denied, error-text, error-json,
content) and provider_options; the free JSON result and the is_error,
preliminary and dynamic flags are gone, and the conversion to the
provider prompt copies the output unchanged. Response messages build
the output from the generate-level tool result. The web wire type, its
TypeScript consumer, the Node and Python test suites and the Python
wrapper follow. A repaired tool call that still fails parsing reports
that parse error directly, as upstream does; only a failing repair
function is wrapped. tool_input_test and tool_choice_test are now ports
of parse-tool-call.test.ts and prepare-tool-choice.test.ts; their cases
without an upstream counterpart are deleted.

Why: upstream's ModelMessage tool result uses the same output union as
the provider layer; one shape for one thing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
Every public type in aimux-core serializes camelCase fields, unions carry
upstream's type tags, and optional fields are omitted instead of null.
The generated TypeScript types, the web wire mirror and the FFI, CLI and
replay readers follow. Vendor request/response types are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	bindings/node/src/types/ContentPart.ts
#	bindings/node/src/types/EvaluationCallOptions.ts
#	bindings/node/src/types/EvaluationResult.ts
#	tools/aimux-web/web/src/types/generated/WireContentPart.ts
Image size and video resolution are "WxH", aspect ratios "W:H", as the
upstream call options type them; they were {width, height} objects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…C ABI

Node, Python, Go, Java, Kotlin, Swift and Flutter decode and encode the
new public JSON (camelCase fields, upstream type tags, absent optionals,
structured tool-result output). aimux_register_providers and every
register-providers wrapper are removed. aimux-ffi.h documents the
camelCase config keys and that stream callbacks receive TextStreamPart.

Tests whose mock servers speak chat completions use a chat preset, since
the OpenAI constructor now returns the Responses model as upstream's
openai(id) does. Missing API keys surface on the first call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cunninghamcard-bit

cunninghamcard-bit commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

公共 JSON 与绑定不应继续固化统一审批契约(核对 head 415e37e1)。

本轮确认的范围调整:aimux 不承担审批运行时,也不定义统一的审批公共类型/语义;审批相关的定义、解释和处理由上层负责。这是对 #200 §0.4 原先“保留审批 variant”范围的收窄,不是否认 AI SDK 中存在这些类型。

本 PR 将继承的审批类型发布为 tool-approval-request / tool-approval-response 的 wire variant,并更新 Node、Python、Java、Kotlin、Swift、Flutter 等绑定及文档。例如 ToolApprovalResponsePart.ts;错误序列化还新增了 AI_ToolCallNotFoundForApprovalError 的映射。

请在 #219/#220 调整完成后,同步移除专用审批 DTO、联合分支、公开导出、专用错误映射,以及声明这些类型的文档和仅固定该审批契约的测试;由原始类型源修改后再生成/同步绑定,不只手删 TypeScript 文件。保留已有通用 JSON/原始数据接口的合同测试即可;本次不要求新增审批专用数据承载、无损往返能力或对应验收门槛。

cunninghamcard-bit and others added 4 commits October 8, 2026 17:24
A misspelled provider name now reads
`No such provider: opeanai. Did you mean 'openai'? (available providers: …)`.
The suggestion comes from the error's own `available_providers`: names
compare case-insensitively with `-` read as `_`, a name is close at an
optimal-string-alignment distance of at most 2 and at most a third of the
longer name, and at most three are listed, nearest first.

This extends the upstream message: the pinned
`ai/src/registry/no-such-provider-error.ts` text has no suggestion. The
fields, the error JSON and the ABI are unchanged, and callers do not parse
the message (docs/error-model.md, rule 5). No ported upstream test asserts
the full message text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…options

Type validation 'if-present', the shared generate_id, and a required
tool-call index, as in openai-chat-language-model.ts and openai-chat-api.ts.
Servers that omit the index use openai_compatible.

Amp-Thread-ID: https://ampcode.com/threads/T-01a11e83-bf7b-75bb-900b-34851e6f12ee
Co-authored-by: Amp <amp@ampcode.com>
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.

2 participants