You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Baseline: master @ 5e78b54 (PR #164 branch @ 03880e0). Every item below went through an adversarial verification pass: line counts were re-measured, dependents were grepped, and anything that would be lost is written next to the item. Items marked revised are directions that hold, but whose original proposal missed a dependency or a behaviour; the revised form is what is written here.
Each item is its own PR against master, with no overlap with #164. We will go through them one at a time in this issue; tick the box and link the PR when it lands.
Ground rules
Cassettes are not deleted. The 2,799 recordings are the byte-level replay gate for every PR here, and later PRs lean on them harder, not less.
Docs are reorganized, not deleted. Regroup by audience, archive history, keep only what users read on the public surface.
One direction, one PR. Providers first, then bindings; the highest-confidence deletions land first.
Ledger
Net code lines only. Cassettes and doc moves are not counted. "Build" is what has to be written in order to delete, and is already subtracted from the net.
Track
Scope
Delete
Build
PRs
Status
A · Mechanical
dead scripts, unused deps, generated enum, type copies, example scaffolding, inventory data
duplicated preludes, abort written six times, spec doc
−1,500
0
1
code read
Total
−52,000 to −54,000
+8,800
25
net ≈ −44,000 lines of code, plus 42,599 lines of inventory data moved to archive/
A good share of the C and D "delete" column is rewrite; the residual is counted under "Build".
A · Mechanical cleanup
Five independent PRs. None touches provider logic. Can start today.
A1 — dead scripts and deps (−1,644, verified). Delete 15 dead scripts: fix_profiles{,2..5}.rs, generate_all_providers.py, update_lib_rs.py, list_wrong_providers.py, revert_wrong_inventory.py, update_inventory_status.py, gen_provider_registry.py, gen_vertex_maas_providers.py, generate_thin_wrapper_cassettes.py, migrate_openai_compatible_*.py. Delete the scripts/fix_tool workspace member (hello-world). Delete the unused root Cargo.toml deps schemars, proc-macro2, syn, quote. Note: gen_provider_registry.py reads openai_compat_registry.rs, which was deleted in the same commit that created it, so it never ran. The hints at provider.rs:4 and provider.rs:88 must change to "edit the JSON directly".
A2 — generated ProviderName enum (−3,483, revised). Delete the generated enum and its language copies; the registry is keyed by string. aimux-web needs a 4-line provider_names() iterator. The generator's output table misses a 9th static copy, tools/aimux-web/web/src/types/ProviderName.ts (262 lines); delete it too. gen_provider_names.py --check is the first step of the contract-tests job in ci.yml; remove those two lines with the script. Node autocomplete can come from a string-literal union emitted by gen_ts_types.py (~260 lines), no gate needed.
A3 — archive provider-inventory/ (42,599 lines archived, verified). Move the 4 files to archive/; mark rfc/0004 historical. No code reads it; provider_registry.json is the declared single source of truth.
A4 — aimux-web type copies (−3,018, revised). Delete the 140 hand-copied ts-rs type files in tools/aimux-web/web/src/types/ (138 are byte-identical with bindings/node/src/types; HttpRecord.ts and JsonValue.ts have already drifted). Point tsconfig paths at the generated set, or add the directory to the sync targets of gen_ts_types.py. Keep the 7 web-only Wire*.ts. All 16 references are import type, no runtime behaviour.
Rejected after verification: per-modality cargo features (media / audio / rerank / search) delete nothing, only add a CI axis, and the search one breaks bindings/node, which is its own workspace with default features. Deleting the anya2a model catalogue in catalogue.rs is a product decision, not cleanup; all four bindings expose it.
B · Providers → protocol registry
Backbone: rfc/0032-provider-protocol-registry.md. It folds the 17 LanguageModel implementations into 7 protocols, adds protocol / auth / models[] to registry rows, retires 33 wrappers, and merges the responses family.
Corrections to write back into RFC-0032
Step 5 depends on step 1; they are not parallel. Today provider_handle() only ever returns the chat-completions OpenAIProvider, Provider::name() hardcodes "openai", and the responses provider_options key hardcodes "openai". Merging open_responses / HF / xai responses steps on all three, so the RFC's claim that "the first four steps have no dependency inversion" does not hold for step 5.
The responses request builder does not apply body_overrides. Only the chat builder does (openai/convert.rs:1500). Codex's store: false cannot ride in on a flag until that gap is filled.
xai chat and mistral chat are not pure forks. xai: flat error body {"error":"msg"} on HTTP 200, zero-valued usage when absent, non-inclusive cache tokens. mistral: content arrays, tool_choice: any, model_length finish reason. These become new profile hooks, and openai's ChatCompletionResponse fields have no serde(default), so switching paths naively fails to parse. Merged savings are 500 to 1,700 lines depending on whether convert.rs folds in, not the fixed 650 the RFC estimates.
Collapsing vertex_ai_* into one row loses eu/us regional host derivation. Registry placeholders only support static substitution like {project}; {location} needs three host shapes derived from global / eu / us / other. bedrock_mantle loses AWS_REGION the same way.
Profiles must deserialize from JSON.OpenAICompatProfile.stream_usage_key: Option<&'static str> cannot live in a registry row. Switch to an owned-string serde struct first (pi-ai's compat shape), then fold xai / mistral differences in; otherwise every new flag needs a mapping table in provider.rs.
Deleting mistral/model.rs and xai/model.rs breaks the workspace.aimux-ffi/src/lib.rs (1190–1242), bindings/node/src/lib.rs (1127, 1165), bindings/python/src/lib.rs (497, 518), tools/aimux-web/src/model_builder.rs and tools/aimux-cli/src/probe/provider.rs call MistralProvider::model() / XAIProvider::model() directly. So the C-track forwarding shim (C2) and those five call sites switching to provider() must land before B4 / B5; RFC step 4 ("bindings last") cannot be last.
A registry row is not 7 lines. The existing 251 rows are flat 5-key objects; with protocol, nested auth{kind,env} and params{} a row is ~11 lines, so 77 new entries cost ~850 lines. The responses family's 4,557 lines share only 95 to 190 lines with the target implementation; the parameterized rewrite is ~1,000 lines, not 600.
PRs
B1 — protocol enum and from_resolved (+350, pure addition, RFC-0032 §3). Protocol enum (6–7 arms), AuthKind, params, one XxxConfig::from_resolved per protocol; provider("anthropic", …) works; ProviderRecord gains protocol so replay rebuilds by protocol. All cassettes green. In the same PR make Provider::name() return the configured name (1 line), otherwise after B2 openrouter reports itself as "openai".
B2 — auth kinds and 33 wrappers → rows (−4,300 / +850, revised). auth.kind = none | bearer_env; OpenAIConfig.api_key may be empty and then no Authorization header is sent (separate commit with a test). 33 wrappers become registry rows: delete the files, delegate_list_models!, and thin_wrapper_config_test.rs. Loses the "env var holds a URL" convention (OLLAMA_BASE_URL etc.); add a base_url_env field to get it back. Vertex {location} host derivation must land first, and params expansion must run before base_url_has_placeholder() rejects {. The replay.rs allowlist only drops 20 names; keep "openrouter". 44 pub types disappear: Rust API break.
B3 — serde compat flags plus CI grep (+200, both reviewers required it). Compat flags become a serde struct. Add a CI grep: no provider-name literals inside openai/, anthropic/, google/, bedrock/, cohere/ (today openai/ already has 14 "groq"). pi-ai has no such rule and drifted to 25 flags plus 31 provider === branches and 19 baseUrl sniffs.
B4 — mistral chat over openai::model (−530 to −1,160 / +150, revised). mistral chat uses openai's execute_* with 4 profile hooks; delete mistral/{model,convert,types}.rs. Prerequisite: C2 shim landed and the ffi / node / python / aimux-web / aimux-cli call sites switched to provider(). "Keep convert.rs" does not hold: execute_* calls openai's build_request_body internally, so mistral's builder becomes dead code. Either fold content arrays and tool_choice: any into openai as flags, or delete only model.rs. 76 tests see reasoning ids change from rc-{nanos} to reasoning-0. 86 cassettes byte-compared.
B5 — xai chat over openai::model (−490 to −1,700 / +250, revised). Same shape: response_handlers, citations → Source, search_parameters, error body inside HTTP 200, optional fields get serde(default). Same prerequisite as B4. xai/responses/convert.rs imports two helpers from xai::convert that openai lacks (remove_additional_properties_false, supports_reasoning_effort, 21 lines); move them into openai/ first, otherwise B5 must wait for B6's xai step. 86 tests pin text-{chunk_id} / xai-source-N ids and zero-valued usage and will all change; 62 cassettes byte-compared.
B6 — responses family (−3,500 / +1,000, revised). Fix three prerequisites first: build_headers ignores config.headers, provider_options_name() must read config, the builder must apply body_overrides. Then in order: azure shell (non-breaking, −203) → open_responses.rs becomes a registry row (−1,396) → HF (−1,264) → codex subscription loop (−259) → xai responses: delete only the stream loop, keep the provider-tool adapter (−400). HF is not a subset of open_responses: {"huggingface":{"itemId"}} metadata, response.created → ResponseMetadata, mcp_call items, and base64 media-type sniffing must all move into the shared implementation, otherwise 22 behaviours are lost. xai's reasoning_text.delta and response.done need two arms in the shared reducer. 360 tests redirected.
B7 — vertex over shared cores (−870 / +250, revised). anthropic_model.rs goes through anthropic_*_core (as anthropic_aws already does), with a failed_response_handler parameter added to the core; vertex/model.rs goes through google's execute_* seam. google's core hardcodes the "google" metadata namespace in 8 places and vertex uses "googleVertex": parameterize. After an in-stream {type:error} vertex currently does not emit Finish; the core does. Behaviour change.
B8 — small duplicates (−170, revised). 6 identical build_header_list copies (the one in openai/image.rs deliberately omits Content-Type; keep it), AWS credential loading dedup, 3 copies of the "JSON error inside a 2xx" guard lifted into provider-utils. Provider name in error text becomes a parameter.
B9 (optional) — single-modality provider shells (−940 / +150, revised). 30 image / speech / transcription / search providers each repeat XxxConfig { api_key, base_url } plus an XxxProvider shell, 1,811 lines. "The registry can already express this" was rejected: searxng has no key, only SEARXNG_URL, and today's resolve_key errors on an empty env_var. Wait for B2's auth: none and base_url_env, and expect to recover about half.
C · FFI
Of three designs, review chose "design from the C ABI inward": one spec_json constructs any model, one call(op) and one stream(op) carry every operation, errors are a JSON string. Today 109 exports (115 on the branch); target 9. Old and new exports coexist for one minor version so the 7 binding migrations do not have to share a release.
C1 — add the 9 exports and dispatch.rs (+2,200, verified). op → aimux-core call, JSON in and out, coexisting with the old exports. aimux_call(0, "configure", …) carries logging, recording, sessions, proxy, and external provider registration. Budget: lib.rs rewrite ~1,150 (858 lines of helpers between exports are still needed by the 9 new ones), dispatch.rs 300, header plus tests 720. The op strings need an exhaustive table test (every op × every handle kind) and must be exported as constants in aimux.h and every binding.
C2 — 40 per-provider constructors become one-line forwards to model(spec) (−682 / +120, revised). Symbols unchanged. Keep the deterministic FFI error when api_key is NULL (Java / Kotlin / Swift tests use it as their "real C ABI error" sample). Bedrock key/secret/region and vertex project/location go through params. This step lets the providers track delete types while bindings are still on the old ABI. Same PR must rewrite exports_smoke_test.rs (1,102 lines, references the 40 symbols 29 times). aimux-ffi.h is hand-written with no cbindgen and no drift gate; the 40 declarations are removed by hand.
C3 — 16 aimux_error_* field accessors → aimux_error_json (−766 / +40, revised). Keep code / message for C callers. The original missed retry_ms: it is computed by retry_after_hint() (including HTTP-date parsing) and is not in the serde JSON, so the envelope needs a derived retry_after_ms. The branch adds 6 Retry accessors; the envelope needs the Retry variant.
C4 — delete the old exports, old header, 2,073 lines of old FFI tests (−7,000, revised). The trace / session / recording families are not deleted; they become ops. Deleting aimux_trace_* and aimux_session_* outright was rejected: Go and Flutter docs present them as the only entry point for the five pure-C-ABI languages. As ops they stay reachable from C. next_part's out_state stays as is, not wrapped in the JSON envelope (one extra allocation and parse on the busy-poll path).
Two review concerns checked and dismissed: one-shot transcription and file upload as base64 inside JSON do not add a copy, because both exports are already base64 C strings today (lib.rs:2126, lib.rs:2381); no 10th byte-carrying export is needed, and the streaming path keeps raw bytes via aimux_session_push. Composite models take child handles as integers in the spec; construction must Arc-clone the children like aimux_router_new does today, with a test that dropping the child handles after construction still generates.
D · Bindings
Seven languages each hand-write per-provider constructors, per-field error decoding, type mirrors, and stream pumps. After the 9-symbol ABI each binding is four objects: Model.new(spec), model.call(op, request), model.stream(op, request, abort?), and AimuxError (subclass chosen from the error JSON tag). Type mirrors stay as an optional typed layer and are not in this round.
Language
Facade today
After
Path
Status
Go
2,965
450
cgo unchanged; delete 44 New* and per-field error decoding
measured
Java
3,450
600
JNA declarations 91 → 9; one Model class; AimuxException picks 13 subclasses from the JSON tag. The 100 Builder inner classes (1,594 lines) can go, but only after making the private all-args constructors public or moving to Java 17 records. A NativeHandle base class for the nine modality classes saves 192 more lines. The 40 requireNonNull calls in factories are a documented NPE contract; keep them
Builder revised
Kotlin
4,744
300
Today a second complete JVM binding (own JNA interface, facade, exception hierarchy) with no gradle dependency on Java. "Depend on aimux-java wholesale" was rejected: Errors.kt's sealed AimuxException is a documented exhaustive-when contract and Java 8 has no sealed classes (Java uses 13 nested subclasses). Revised: the JNA interface, Model facade and multimodal facades (~1,700 lines) depend on the Java artifact; Kotlin keeps its sealed exception hierarchy (350 lines) as a mapping layer over the Java exceptions, plus coroutine / Flow sugar. CI needs a kotlin → java artifact dependency edge
91 dart:ffi lookups → 9. Interim: ffigen from the header (−458 hand-written declarations, pre-authorized by RFC-0001 §227, all 91 symbols have prototypes). Two pieces stay hand-written: the openAimuxLibrary loader (iOS DynamicLibrary.process() path) and dropHandlePtr (reinterprets aimux_drop_handle as a NativeFinalizer; ffigen only emits a private pointer field)
revised
Node
3,514
1,200
keep napi-rs (a ctypes-style call would block the event loop), but depend on aimux_ffi::dispatch as an rlib and share op / spec / error JSON with the C path. aimux-ffi's crate-type already includes rlib. Node currently bypasses aimux-ffi entirely, so this is a new dependency edge, and whether napi's tokio bridge conflicts with the FFI global runtime's re-entrancy guard is unmeasured. The "shared binding core for the three Rust glue crates" shrank from 3,400 to 1,500 lines on verification: only prompt / options parsing, spec, and dispatch can be shared; Node's structured error classes need Env to build JS exceptions, and AimuxResult plus the stream pump stay on the napi side; FFI has its own runtime and ffi_block_on
revised
Python
2,611
400
design C proposes replacing the pyo3 crate with stdlib ctypes. Both reviewers require a prototype first: streaming needs a helper thread plus queue, exceptions inside ctypes callbacks are swallowed, and the wheel must bundle libaimux_ffi per platform. If the prototype fails, fall back to a thin pyo3 layer over dispatch
prototype first
D1 — Kotlin over the Java artifact (~−1,700, revised as above). Depends only on the Java artifact, not the new ABI; can go first.
D2 — Go (after C1)
D3 — Java (after C1)
D4 — Swift (after C1)
D5 — Flutter (ffigen interim can go before C1; 9-symbol rewrite after)
D8 — generate the type mirrors (0 lines removed, hand maintenance goes to zero). aimux-core's serde types are mechanically exported once (ts-rs → bindings/node/src/types, guarded by --check), then hand-mirrored in Java, Flutter, Swift, Kotlin, Go and Python, plus the ungenerated aimux-web copy: 18,944 lines, ~10% of Rust plus binding source. Every file header says which ts output it was copied from; this is why Align the request pipeline with the AI SDK #164's error-type change touches 8 languages. Cannot delete first and fill in later: Go's 8 multimodal entry points only accept typed *XxxCallOptions with no JSON-string path, and Python's wrapper.py (1,221 lines) is all-or-nothing. Emit a JSON Schema from the same #[derive(TS)], generate per language, and extend the gen_ts_types.py --check gate to all six outputs. Last PR of the track.
E1 (−1,500, one PR on the branch before merge; existing CI covers it)
docs/ai-sdk-request-pipeline.md (1,037 lines): keep §1–§3 and §14 (~115 lines), move to rfc/0031. §4–§10 restate rustdoc; §11–§13 are a completed migration checklist. −920
The eight modality generate_* wrappers and generate_text / stream_text repeat the same prelude (timeout, prepare_retries, RecordingContext, session, span); extract one run_operation. −150
Abort handling is written six times (retry loop, retry::delay, timeout::run_until, sleep_or_abort, send_one_request, two body readers). Every entry point is inside timeout::run_until, and dropping the future cancels; keep one, the rest become plain awaits. −120
Two identical body-read loops merged; DEFAULT_MAX_DOWNLOAD_SIZE = 2 GiB read into a Vec is no limit at all. −50
Not a deletion, but noted: fal / luma / BFL / revai / gladia still do submit+poll inside one do_generate, so core retry re-submits a billed job (the same bug fixed for video); is_retryable() and the inline predicate in retry.rs are written twice.
Docs reorganization (no deletion)
Three layers by audience. Move with git mv, fix links with one grep pass.
api/ keeps only reference.md and the generated providers.md; the 9 per-language guides move into bindings/<lang>/README.md (today only flutter / go / python have one) so each binding maintains its own docs.
api/gaps.md is an issue, not a doc: file it as an issue, then delete.
PERF-RESULTS.md, aimux-vs-aisdk-node.md and bindings/node/bench go under docs/benchmarks/.
rfc/ gets an index
New rfc/README.md: one table of number, title, status (implemented / superseded / draft); move the RFC list out of the root README.
docs/ai-sdk-request-pipeline.md → rfc/0031-*.md (with the E1 trim); update the 0032 draft with the corrections above and commit it.
Two number collisions (0005 ×2, 0027 ×2): do not renumber, mark them in the index, avoid breaking links.
archive/ for history
docs/internal/ (105 files), docs/quality-audit/ (34), docs/plan/ (17), provider-inventory/ move to a root archive/ with one README ("no longer maintained, see git history").
docs/internal/cache-tracing/prototype is the ancestor copy of aimux-core/src/trace; keep only its README when archiving.
quality-audit/round4/ holds a 119k-line clippy log and an lcov file; both are build artifacts, the one suggested exception to "no deletion" (git history keeps them).
Language. Public guides are mixed: provider-config-manual, session-affinity-guide and error-model are Chinese, api/*.md is English, RFCs are mostly Chinese. Suggested rule for CONTRIBUTING.md: user-facing docs/ and bindings/*/README in English, rfc/ stays Chinese. This round only moves files, no translation.
Comparison with pi-ai
pi-ai is 23,720 lines, half of it the 10 protocol implementations in src/api/. Its 40 providers average 24 lines; 27 are exactly 14–15 lines of createProvider({ id, baseUrl, auth, models, api }). The model list is generated from models.dev. Its 30 test suites hit real keys across a provider × capability matrix. aimux already has half of this: the 251-row registry is pi-ai's providers/, missing only the protocol column.
Adopt
Protocol implementation is code, protocol selection is data (RFC-0032 already says this).
Per-model overlay models[]: per-model quirks like deepseek-reasoner are 3–6 lines of JSON, not Rust.
Compat flags as a deserializable struct, plus the CI grep forbidding provider names inside protocol directories. pi-ai skipped this and drifted to 31 provider === branches.
Table-driven conformance test: one (provider, protocol, cassette dir) table replays all 32 cassette directories, replacing 15 forks' individual constructor tests.
In-band stream termination (a NULL callback after the Finish / Error event); delete every pump's "return error but never call on_done" branch.
Do not adopt
The models.dev catalogue: only 140 of the 251 registry names are in it, and it is a 4.4 MB runtime fetch; RFC-0032 §6 already rules it out.
Cross-provider message transforms (transform-messages.ts): new behaviour, out of scope.
OAuth login flows: pi-ai wrote 2,664 lines for 7 vendors; aimux keeps "string key or env".
Honest expectation: the 6 Rust protocol implementations total ~16,000 lines, more than pi-ai's 10, because pi-ai hands transport and types to each vendor's official SDK. The bulk of the reduction is not in the protocol implementations but around them: the 33 wrappers, 5 responses copies, 40 FFI constructors and 7 binding facades.
Sequencing
A1–A5: any time, in any order.
B1 (pure addition) and C2 (constructor forwards) are prerequisites for every provider deletion. B4 / B5 also need the five direct call sites of MistralProvider::model() / XAIProvider::model() switched to provider().
B2 → B3 → B4 / B5 → B6 (after its three prerequisites) → B7 → B8 → B9.
C1 → C3 → C4, with C4 only after every binding has migrated.
D1 Kotlin first (Java artifact only). D2–D6 after C1, one PR each. D7 Python after the ctypes prototype. D8 type-mirror generation last.
Baseline:
master@ 5e78b54 (PR #164 branch @ 03880e0). Every item below went through an adversarial verification pass: line counts were re-measured, dependents were grepped, and anything that would be lost is written next to the item. Items marked revised are directions that hold, but whose original proposal missed a dependency or a behaviour; the revised form is what is written here.Each item is its own PR against
master, with no overlap with #164. We will go through them one at a time in this issue; tick the box and link the PR when it lands.Ground rules
Ledger
Net code lines only. Cassettes and doc moves are not counted. "Build" is what has to be written in order to delete, and is already subtracted from the net.
archive/A good share of the C and D "delete" column is rewrite; the residual is counted under "Build".
A · Mechanical cleanup
Five independent PRs. None touches provider logic. Can start today.
fix_profiles{,2..5}.rs,generate_all_providers.py,update_lib_rs.py,list_wrong_providers.py,revert_wrong_inventory.py,update_inventory_status.py,gen_provider_registry.py,gen_vertex_maas_providers.py,generate_thin_wrapper_cassettes.py,migrate_openai_compatible_*.py. Delete thescripts/fix_toolworkspace member (hello-world). Delete the unused rootCargo.tomldepsschemars,proc-macro2,syn,quote. Note:gen_provider_registry.pyreadsopenai_compat_registry.rs, which was deleted in the same commit that created it, so it never ran. The hints atprovider.rs:4andprovider.rs:88must change to "edit the JSON directly".ProviderNameenum (−3,483, revised). Delete the generated enum and its language copies; the registry is keyed by string.aimux-webneeds a 4-lineprovider_names()iterator. The generator's output table misses a 9th static copy,tools/aimux-web/web/src/types/ProviderName.ts(262 lines); delete it too.gen_provider_names.py --checkis the first step of thecontract-testsjob inci.yml; remove those two lines with the script. Node autocomplete can come from a string-literal union emitted bygen_ts_types.py(~260 lines), no gate needed.provider-inventory/(42,599 lines archived, verified). Move the 4 files toarchive/; markrfc/0004historical. No code reads it;provider_registry.jsonis the declared single source of truth.aimux-webtype copies (−3,018, revised). Delete the 140 hand-copied ts-rs type files intools/aimux-web/web/src/types/(138 are byte-identical withbindings/node/src/types;HttpRecord.tsandJsonValue.tshave already drifted). Pointtsconfigpaths at the generated set, or add the directory to the sync targets ofgen_ts_types.py. Keep the 7 web-onlyWire*.ts. All 16 references areimport type, no runtime behaviour.flutter createscaffolding underbindings/flutter/example/(the macOS half of the two pbxproj files,MainMenu.xib,win32_window.cpp, CMakeLists). The original proposal deleted all five platforms and was rejected: theflutter-example-buildCI job runsnm -gon the iOS simulator build looking foraimux_openai_new, which is the regression gate for xcframework force-linking (iOS: CocoaPods fails to link vendored aimux_ffi.xcframework ('Framework aimux_ffi not found') #25 / fix(flutter): stage xcframework slice via script_phase — iOS CocoaPods link (issue #25) #26). iOS and Android stay.Rejected after verification: per-modality cargo features (
media/audio/rerank/search) delete nothing, only add a CI axis, and thesearchone breaksbindings/node, which is its own workspace with default features. Deleting the anya2a model catalogue incatalogue.rsis a product decision, not cleanup; all four bindings expose it.B · Providers → protocol registry
Backbone:
rfc/0032-provider-protocol-registry.md. It folds the 17LanguageModelimplementations into 7 protocols, addsprotocol/auth/models[]to registry rows, retires 33 wrappers, and merges the responses family.Corrections to write back into RFC-0032
provider_handle()only ever returns the chat-completionsOpenAIProvider,Provider::name()hardcodes"openai", and the responsesprovider_optionskey hardcodes"openai". Merging open_responses / HF / xai responses steps on all three, so the RFC's claim that "the first four steps have no dependency inversion" does not hold for step 5.body_overrides. Only the chat builder does (openai/convert.rs:1500). Codex'sstore: falsecannot ride in on a flag until that gap is filled.{"error":"msg"}on HTTP 200, zero-valued usage when absent, non-inclusive cache tokens. mistral: content arrays,tool_choice: any,model_lengthfinish reason. These become new profile hooks, and openai'sChatCompletionResponsefields have noserde(default), so switching paths naively fails to parse. Merged savings are 500 to 1,700 lines depending on whetherconvert.rsfolds in, not the fixed 650 the RFC estimates.vertex_ai_*into one row loses eu/us regional host derivation. Registry placeholders only support static substitution like{project};{location}needs three host shapes derived from global / eu / us / other.bedrock_mantlelosesAWS_REGIONthe same way.OpenAICompatProfile.stream_usage_key: Option<&'static str>cannot live in a registry row. Switch to an owned-string serde struct first (pi-ai's compat shape), then fold xai / mistral differences in; otherwise every new flag needs a mapping table inprovider.rs.mistral/model.rsandxai/model.rsbreaks the workspace.aimux-ffi/src/lib.rs(1190–1242),bindings/node/src/lib.rs(1127, 1165),bindings/python/src/lib.rs(497, 518),tools/aimux-web/src/model_builder.rsandtools/aimux-cli/src/probe/provider.rscallMistralProvider::model()/XAIProvider::model()directly. So the C-track forwarding shim (C2) and those five call sites switching toprovider()must land before B4 / B5; RFC step 4 ("bindings last") cannot be last.protocol, nestedauth{kind,env}andparams{}a row is ~11 lines, so 77 new entries cost ~850 lines. The responses family's 4,557 lines share only 95 to 190 lines with the target implementation; the parameterized rewrite is ~1,000 lines, not 600.PRs
from_resolved(+350, pure addition, RFC-0032 §3).Protocolenum (6–7 arms),AuthKind,params, oneXxxConfig::from_resolvedper protocol;provider("anthropic", …)works;ProviderRecordgainsprotocolso replay rebuilds by protocol. All cassettes green. In the same PR makeProvider::name()return the configured name (1 line), otherwise after B2 openrouter reports itself as"openai".auth.kind = none | bearer_env;OpenAIConfig.api_keymay be empty and then noAuthorizationheader is sent (separate commit with a test). 33 wrappers become registry rows: delete the files,delegate_list_models!, andthin_wrapper_config_test.rs. Loses the "env var holds a URL" convention (OLLAMA_BASE_URLetc.); add abase_url_envfield to get it back. Vertex{location}host derivation must land first, andparamsexpansion must run beforebase_url_has_placeholder()rejects{. Thereplay.rsallowlist only drops 20 names; keep"openrouter". 44pubtypes disappear: Rust API break.openai/,anthropic/,google/,bedrock/,cohere/(todayopenai/already has 14"groq"). pi-ai has no such rule and drifted to 25 flags plus 31provider ===branches and 19baseUrlsniffs.openai::model(−530 to −1,160 / +150, revised). mistral chat uses openai'sexecute_*with 4 profile hooks; deletemistral/{model,convert,types}.rs. Prerequisite: C2 shim landed and the ffi / node / python / aimux-web / aimux-cli call sites switched toprovider(). "Keepconvert.rs" does not hold:execute_*calls openai'sbuild_request_bodyinternally, so mistral's builder becomes dead code. Either fold content arrays andtool_choice: anyinto openai as flags, or delete onlymodel.rs. 76 tests see reasoning ids change fromrc-{nanos}toreasoning-0. 86 cassettes byte-compared.openai::model(−490 to −1,700 / +250, revised). Same shape:response_handlers, citations →Source,search_parameters, error body inside HTTP 200, optional fields getserde(default). Same prerequisite as B4.xai/responses/convert.rsimports two helpers fromxai::convertthat openai lacks (remove_additional_properties_false,supports_reasoning_effort, 21 lines); move them intoopenai/first, otherwise B5 must wait for B6's xai step. 86 tests pintext-{chunk_id}/xai-source-Nids and zero-valued usage and will all change; 62 cassettes byte-compared.build_headersignoresconfig.headers,provider_options_name()must read config, the builder must applybody_overrides. Then in order: azure shell (non-breaking, −203) →open_responses.rsbecomes a registry row (−1,396) → HF (−1,264) → codex subscription loop (−259) → xai responses: delete only the stream loop, keep the provider-tool adapter (−400). HF is not a subset of open_responses:{"huggingface":{"itemId"}}metadata,response.created→ResponseMetadata,mcp_callitems, and base64 media-type sniffing must all move into the shared implementation, otherwise 22 behaviours are lost. xai'sreasoning_text.deltaandresponse.doneneed two arms in the shared reducer. 360 tests redirected.anthropic_model.rsgoes throughanthropic_*_core(asanthropic_awsalready does), with afailed_response_handlerparameter added to the core;vertex/model.rsgoes through google'sexecute_*seam. google's core hardcodes the"google"metadata namespace in 8 places and vertex uses"googleVertex": parameterize. After an in-stream{type:error}vertex currently does not emitFinish; the core does. Behaviour change.build_header_listcopies (the one inopenai/image.rsdeliberately omitsContent-Type; keep it), AWS credential loading dedup, 3 copies of the "JSON error inside a 2xx" guard lifted into provider-utils. Provider name in error text becomes a parameter.XxxConfig { api_key, base_url }plus anXxxProvidershell, 1,811 lines. "The registry can already express this" was rejected: searxng has no key, onlySEARXNG_URL, and today'sresolve_keyerrors on an emptyenv_var. Wait for B2'sauth: noneandbase_url_env, and expect to recover about half.C · FFI
Of three designs, review chose "design from the C ABI inward": one
spec_jsonconstructs any model, onecall(op)and onestream(op)carry every operation, errors are a JSON string. Today 109 exports (115 on the branch); target 9. Old and new exports coexist for one minor version so the 7 binding migrations do not have to share a release.dispatch.rs(+2,200, verified). op → aimux-core call, JSON in and out, coexisting with the old exports.aimux_call(0, "configure", …)carries logging, recording, sessions, proxy, and external provider registration. Budget:lib.rsrewrite ~1,150 (858 lines of helpers between exports are still needed by the 9 new ones),dispatch.rs300, header plus tests 720. The op strings need an exhaustive table test (every op × every handle kind) and must be exported as constants inaimux.hand every binding.model(spec)(−682 / +120, revised). Symbols unchanged. Keep the deterministic FFI error whenapi_keyis NULL (Java / Kotlin / Swift tests use it as their "real C ABI error" sample). Bedrock key/secret/region and vertex project/location go throughparams. This step lets the providers track delete types while bindings are still on the old ABI. Same PR must rewriteexports_smoke_test.rs(1,102 lines, references the 40 symbols 29 times).aimux-ffi.his hand-written with no cbindgen and no drift gate; the 40 declarations are removed by hand.aimux_error_*field accessors →aimux_error_json(−766 / +40, revised). Keepcode/messagefor C callers. The original missedretry_ms: it is computed byretry_after_hint()(including HTTP-date parsing) and is not in the serde JSON, so the envelope needs a derivedretry_after_ms. The branch adds 6 Retry accessors; the envelope needs the Retry variant.aimux_trace_*andaimux_session_*outright was rejected: Go and Flutter docs present them as the only entry point for the five pure-C-ABI languages. As ops they stay reachable from C.next_part'sout_statestays as is, not wrapped in the JSON envelope (one extra allocation and parse on the busy-poll path).Two review concerns checked and dismissed: one-shot transcription and file upload as base64 inside JSON do not add a copy, because both exports are already base64 C strings today (
lib.rs:2126,lib.rs:2381); no 10th byte-carrying export is needed, and the streaming path keeps raw bytes viaaimux_session_push. Composite models take child handles as integers in the spec; construction mustArc-clone the children likeaimux_router_newdoes today, with a test that dropping the child handles after construction still generates.D · Bindings
Seven languages each hand-write per-provider constructors, per-field error decoding, type mirrors, and stream pumps. After the 9-symbol ABI each binding is four objects:
Model.new(spec),model.call(op, request),model.stream(op, request, abort?), andAimuxError(subclass chosen from the error JSON tag). Type mirrors stay as an optional typed layer and are not in this round.New*and per-field error decodingModelclass;AimuxExceptionpicks 13 subclasses from the JSON tag. The 100 Builder inner classes (1,594 lines) can go, but only after making the private all-args constructors public or moving to Java 17 records. ANativeHandlebase class for the nine modality classes saves 192 more lines. The 40requireNonNullcalls in factories are a documented NPE contract; keep themErrors.kt's sealedAimuxExceptionis a documented exhaustive-whencontract and Java 8 has no sealed classes (Java uses 13 nested subclasses). Revised: the JNA interface,Modelfacade and multimodal facades (~1,700 lines) depend on the Java artifact; Kotlin keeps its sealed exception hierarchy (350 lines) as a mapping layer over the Java exceptions, plus coroutine / Flow sugar. CI needs a kotlin → java artifact dependency edgefromCgetters;AsyncThrowingStreamwrapsaimux_streamdart:ffilookups → 9. Interim: ffigen from the header (−458 hand-written declarations, pre-authorized by RFC-0001 §227, all 91 symbols have prototypes). Two pieces stay hand-written: theopenAimuxLibraryloader (iOSDynamicLibrary.process()path) anddropHandlePtr(reinterpretsaimux_drop_handleas aNativeFinalizer; ffigen only emits a private pointer field)aimux_ffi::dispatchas an rlib and share op / spec / error JSON with the C path.aimux-ffi's crate-type already includes rlib. Node currently bypasses aimux-ffi entirely, so this is a new dependency edge, and whether napi's tokio bridge conflicts with the FFI global runtime's re-entrancy guard is unmeasured. The "shared binding core for the three Rust glue crates" shrank from 3,400 to 1,500 lines on verification: only prompt / options parsing, spec, and dispatch can be shared; Node's structured error classes needEnvto build JS exceptions, andAimuxResultplus the stream pump stay on the napi side; FFI has its own runtime andffi_block_onlibaimux_ffiper platform. If the prototype fails, fall back to a thin pyo3 layer over dispatchbindings/node/src/types, guarded by--check), then hand-mirrored in Java, Flutter, Swift, Kotlin, Go and Python, plus the ungenerated aimux-web copy: 18,944 lines, ~10% of Rust plus binding source. Every file header says which ts output it was copied from; this is why Align the request pipeline with the AI SDK #164's error-type change touches 8 languages. Cannot delete first and fill in later: Go's 8 multimodal entry points only accept typed*XxxCallOptionswith no JSON-string path, and Python'swrapper.py(1,221 lines) is all-or-nothing. Emit a JSON Schema from the same#[derive(TS)], generate per language, and extend thegen_ts_types.py --checkgate to all six outputs. Last PR of the track.Each binding PR runs only its own CI job.
E · PR #164 internal
docs/ai-sdk-request-pipeline.md(1,037 lines): keep §1–§3 and §14 (~115 lines), move torfc/0031. §4–§10 restate rustdoc; §11–§13 are a completed migration checklist. −920generate_*wrappers andgenerate_text/stream_textrepeat the same prelude (timeout,prepare_retries,RecordingContext, session, span); extract onerun_operation. −150retry::delay,timeout::run_until,sleep_or_abort,send_one_request, two body readers). Every entry point is insidetimeout::run_until, and dropping the future cancels; keep one, the rest become plain awaits. −120StreamTextResult::text()equalsconsume().map(|a| a.text). −45PreparedRetriesisRetryConfigin different units; the genericretry_with_exponential_backoffhas one production caller. −70get_from_api.rs25 lines,handle_fetch_error.rs21,retry.rs3-line re-export, …); fold intohttp.rs. −90, −7 filesDEFAULT_MAX_DOWNLOAD_SIZE = 2 GiBread into aVecis no limit at all. −50do_generate, so core retry re-submits a billed job (the same bug fixed for video);is_retryable()and the inline predicate inretry.rsare written twice.Docs reorganization (no deletion)
Three layers by audience. Move with
git mv, fix links with one grep pass.docs/keeps only what users readREADME.mdindex;guides/holds session-affinity, provider-config, codex-subscription, error-model.api/keeps onlyreference.mdand the generatedproviders.md; the 9 per-language guides move intobindings/<lang>/README.md(today only flutter / go / python have one) so each binding maintains its own docs.api/gaps.mdis an issue, not a doc: file it as an issue, then delete.PERF-RESULTS.md,aimux-vs-aisdk-node.mdandbindings/node/benchgo underdocs/benchmarks/.rfc/gets an indexrfc/README.md: one table of number, title, status (implemented / superseded / draft); move the RFC list out of the root README.docs/ai-sdk-request-pipeline.md→rfc/0031-*.md(with the E1 trim); update the 0032 draft with the corrections above and commit it.archive/for historydocs/internal/(105 files),docs/quality-audit/(34),docs/plan/(17),provider-inventory/move to a rootarchive/with one README ("no longer maintained, see git history").docs/internal/cache-tracing/prototypeis the ancestor copy ofaimux-core/src/trace; keep only its README when archiving.quality-audit/round4/holds a 119k-line clippy log and an lcov file; both are build artifacts, the one suggested exception to "no deletion" (git history keeps them).Language. Public guides are mixed: provider-config-manual, session-affinity-guide and error-model are Chinese,
api/*.mdis English, RFCs are mostly Chinese. Suggested rule for CONTRIBUTING.md: user-facingdocs/andbindings/*/READMEin English,rfc/stays Chinese. This round only moves files, no translation.Comparison with pi-ai
pi-ai is 23,720 lines, half of it the 10 protocol implementations in
src/api/. Its 40 providers average 24 lines; 27 are exactly 14–15 lines ofcreateProvider({ id, baseUrl, auth, models, api }). The model list is generated from models.dev. Its 30 test suites hit real keys across a provider × capability matrix. aimux already has half of this: the 251-row registry is pi-ai'sproviders/, missing only theprotocolcolumn.Adopt
models[]: per-model quirks like deepseek-reasoner are 3–6 lines of JSON, not Rust.provider ===branches.(provider, protocol, cassette dir)table replays all 32 cassette directories, replacing 15 forks' individual constructor tests.Finish/Errorevent); delete every pump's "return error but never call on_done" branch.Do not adopt
transform-messages.ts): new behaviour, out of scope.Honest expectation: the 6 Rust protocol implementations total ~16,000 lines, more than pi-ai's 10, because pi-ai hands transport and types to each vendor's official SDK. The bulk of the reduction is not in the protocol implementations but around them: the 33 wrappers, 5 responses copies, 40 FFI constructors and 7 binding facades.
Sequencing
MistralProvider::model()/XAIProvider::model()switched toprovider().