fix(ffi): preserve detailed errors across the C ABI (issue #17) - #19
Merged
Conversation
Problem A: aimux_generate_text / aimux_stream_text dropped the serde_json::Error from prompt_json parsing (only "invalid prompt_json" remained); aimux_provider_new also swallowed config_json parse errors. Report them like the adjacent opts_json branch already did. Problem B: all 20 language-model constructors (and the multimodal ones for null arguments) reduced every AiMuxError to a bare 0 handle. Add a thread-local error slot + aimux_last_error() returning the error JSON envelope (read-and-clear, caller frees with aimux_free_string). Every u64 constructor clears the slot on entry, records the envelope on failure, and stays empty on success. Bindings: - Go: takeLastError() + LockOSThread around constructor+read in newModel / wrapHandle / ProviderWithBase / newMultimodalHandle - Java: JNA aimux_last_error() (Pointer + aimux_free_string) and constructorFailure() used by all 21 model factories - C: example.c prints the detailed error on constructor failure Tests: error_detail_test.rs covers serde detail, UnknownProvider/Json/ InvalidArgument envelopes, clear-on-success, last-write-wins, read-once and TLS isolation. cargo test -p aimux-ffi: 15 passed.
cargo fmt: wrap record_error_msg calls at 100 cols. Java: use fully-qualified java.util.regex.Matcher (no import needed).
cunninghamcard-bit
added a commit
to cunninghamcard-bit/aimux
that referenced
this pull request
Aug 3, 2026
…e envelope design Port the applicable cases from PR arcships#19's error_detail_test.rs (serde detail in prompt errors for generate/stream, UnknownProvider / Json / InvalidArgument envelopes, azure required-arg) and align the envelope contents with that PR: null / invalid-UTF-8 arguments now report error_type "InvalidArgument" (was "Other") and config_json parse failures report "Json", so the two designs emit identical error envelopes. The last_error TLS-semantics tests (clear-on-success, last-write-wins, read-once, thread-local isolation) have no counterpart here — each call returns its own self-contained result. Replaced with envelope-semantics tests: success shape, per-call error independence, and an 8-thread concurrent-constructor check that needs no thread pinning. cargo test -p aimux-ffi --release: 14 passed (10 new + 4 smoke); Go binding go test ./... passes against the rebuilt staticlib. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cunninghamcard-bit
added a commit
to cunninghamcard-bit/aimux
that referenced
this pull request
Aug 3, 2026
…sue arcships#17) Constructors returned a bare u64 with 0 on any failure, discarding the AiMuxError already computed underneath (unknown provider, bad config, missing env var, invalid model id). Instead of adding a side-channel retrieval symbol, widen the return channel itself: every aimux_*_new / aimux_provider_new / aimux_provider_from_env now returns the JSON envelope the rest of the ABI already uses: {"handle":<u64>} on success {"error":"...","error_type":"...","status_code":..} on failure Success and failure arrive in the same return value of the same call, so there is nothing to correlate across calls and no thread-affinity requirement for any binding runtime. Also fixes Problem A of arcships#17: aimux_generate_text / aimux_stream_text preserve the serde_json detail for invalid prompt_json (matching the adjacent opts_json branch), and aimux_provider_new preserves it for invalid config_json. Error types align with PR arcships#19's envelopes: InvalidArgument for null/invalid-UTF-8 args, Json for config parse failures. Tests: error_detail_test.rs ports the applicable regression cases from PR arcships#19 (serde detail for generate/stream, UnknownProvider / Json / InvalidArgument envelopes); the last_error TLS-semantics cases have no counterpart here and are replaced with envelope-semantics tests (per-call error independence, 8-thread concurrent constructors, no pinning). native_constructors_test.rs rewritten to parse the envelope. cargo test -p aimux-ffi --release: 14 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cunninghamcard-bit
added a commit
to cunninghamcard-bit/aimux
that referenced
this pull request
Aug 3, 2026
…imux_last_error (issue arcships#17) Follow-up simplification of arcships#19: instead of a bare u64 handle plus the aimux_last_error() thread-local side channel, every constructor returns the JSON envelope the rest of the ABI already uses: {"handle":<u64>} on success {"error":"...","error_type":"...","status_code":..} on failure Success and failure arrive in the same return value of the same call, so there is no second call to correlate — the entire last_error mechanism (LAST_ERROR TLS slot, begin_constructor, record_error/record_error_msg, the aimux_last_error symbol and its thread-affinity contract) is removed. No binding runtime needs thread pinning. Error envelopes are unchanged from arcships#19 (InvalidArgument for null / invalid-UTF-8 args, Json for config parse failures, UnknownProvider with the available-provider list), and Problem A's serde detail for prompt_json/config_json is preserved. error_detail_test.rs: the envelope-relevant cases from arcships#19 are kept and read the error from the constructor result; the TLS-semantics cases (clear-on-success, last-write-wins, read-once, thread-local isolation) have no counterpart — replaced with envelope-semantics tests (per-call error independence, 8-thread concurrent constructors, no pinning). cargo test -p aimux-ffi --release: 14 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cunninghamcard-bit
added a commit
to cunninghamcard-bit/aimux
that referenced
this pull request
Aug 3, 2026
parseHandleJSON reads the envelope, frees the C string, and surfaces the detailed error; a degenerate envelope with neither error nor handle is rejected. The takeLastError/callConstructor dispatch machinery and runtime.LockOSThread pinning from arcships#19 are removed — constructors are plain direct C calls, since the result is self-contained. Also removes stale StreamTextContext doc references (no such API); the per-call timeout (RFC-0016 H3) is the only interruption mechanism. go vet + go test ./... pass with a real cgo link; E2E against a live OpenAI-compatible endpoint verifies unknown-provider detail, prompt_json serde detail, real generate, and real streaming. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cunninghamcard-bit
added a commit
to cunninghamcard-bit/aimux
that referenced
this pull request
Aug 3, 2026
All JNA constructor declarations return Pointer; AimuxResult.extractHandle frees exactly once (try/finally), null-checks, and throws with the detailed engine error. The aimux_last_error declaration and constructorFailure helper from arcships#19 are removed. Static-only change; CI to verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cunninghamcard-bit
added a commit
to cunninghamcard-bit/aimux
that referenced
this pull request
Aug 3, 2026
example.c / example.cpp parse the envelope and print the detailed engine error on constructor failure (replacing arcships#19's aimux_last_error read in example.c); example.cpp includes <stdexcept> for its throw sites. Both compile against the new header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cunninghamcard-bit
added a commit
to cunninghamcard-bit/aimux
that referenced
this pull request
Aug 3, 2026
…imux_last_error (issue arcships#17) Follow-up simplification of arcships#19: instead of a bare u64 handle plus the aimux_last_error() thread-local side channel, every constructor returns the JSON envelope the rest of the ABI already uses: {"handle":<u64>} on success {"error":"...","error_type":"...","status_code":..} on failure Success and failure arrive in the same return value of the same call, so there is no second call to correlate — the entire last_error mechanism (LAST_ERROR TLS slot, begin_constructor, record_error/record_error_msg, the aimux_last_error symbol and its thread-affinity contract) is removed. No binding runtime needs thread pinning. Error envelopes are unchanged from arcships#19 (InvalidArgument for null / invalid-UTF-8 args, Json for config parse failures, UnknownProvider with the available-provider list), and Problem A's serde detail for prompt_json/config_json is preserved. error_detail_test.rs: the envelope-relevant cases from arcships#19 are kept and read the error from the constructor result; the TLS-semantics cases (clear-on-success, last-write-wins, read-once, thread-local isolation) have no counterpart — replaced with envelope-semantics tests (per-call error independence, 8-thread concurrent constructors, no pinning). cargo test -p aimux-ffi --release: 14 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cunninghamcard-bit
added a commit
to cunninghamcard-bit/aimux
that referenced
this pull request
Aug 3, 2026
parseHandleJSON reads the envelope, frees the C string, and surfaces the detailed error; a degenerate envelope with neither error nor handle is rejected. The takeLastError/callConstructor dispatch machinery and runtime.LockOSThread pinning from arcships#19 are removed — constructors are plain direct C calls, since the result is self-contained. Also removes stale StreamTextContext doc references (no such API); the per-call timeout (RFC-0016 H3) is the only interruption mechanism. go vet + go test ./... pass with a real cgo link; E2E against a live OpenAI-compatible endpoint verifies unknown-provider detail, prompt_json serde detail, real generate, and real streaming. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cunninghamcard-bit
added a commit
to cunninghamcard-bit/aimux
that referenced
this pull request
Aug 3, 2026
All JNA constructor declarations return Pointer; AimuxResult.extractHandle frees exactly once (try/finally), null-checks, and throws with the detailed engine error. The aimux_last_error declaration and constructorFailure helper from arcships#19 are removed. Static-only change; CI to verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cunninghamcard-bit
added a commit
to cunninghamcard-bit/aimux
that referenced
this pull request
Aug 3, 2026
example.c / example.cpp parse the envelope and print the detailed engine error on constructor failure (replacing arcships#19's aimux_last_error read in example.c); example.cpp includes <stdexcept> for its throw sites. Both compile against the new header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eric8810
pushed a commit
that referenced
this pull request
Aug 3, 2026
…imux_last_error (issue #17) (#21) * fix(ffi): constructors return JSON handle/error envelope, replacing aimux_last_error (issue #17) Follow-up simplification of #19: instead of a bare u64 handle plus the aimux_last_error() thread-local side channel, every constructor returns the JSON envelope the rest of the ABI already uses: {"handle":<u64>} on success {"error":"...","error_type":"...","status_code":..} on failure Success and failure arrive in the same return value of the same call, so there is no second call to correlate — the entire last_error mechanism (LAST_ERROR TLS slot, begin_constructor, record_error/record_error_msg, the aimux_last_error symbol and its thread-affinity contract) is removed. No binding runtime needs thread pinning. Error envelopes are unchanged from #19 (InvalidArgument for null / invalid-UTF-8 args, Json for config parse failures, UnknownProvider with the available-provider list), and Problem A's serde detail for prompt_json/config_json is preserved. error_detail_test.rs: the envelope-relevant cases from #19 are kept and read the error from the constructor result; the TLS-semantics cases (clear-on-success, last-write-wins, read-once, thread-local isolation) have no counterpart — replaced with envelope-semantics tests (per-call error independence, 8-thread concurrent constructors, no pinning). cargo test -p aimux-ffi --release: 14 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(go): consume JSON handle/error envelope from constructors parseHandleJSON reads the envelope, frees the C string, and surfaces the detailed error; a degenerate envelope with neither error nor handle is rejected. The takeLastError/callConstructor dispatch machinery and runtime.LockOSThread pinning from #19 are removed — constructors are plain direct C calls, since the result is self-contained. Also removes stale StreamTextContext doc references (no such API); the per-call timeout (RFC-0016 H3) is the only interruption mechanism. go vet + go test ./... pass with a real cgo link; E2E against a live OpenAI-compatible endpoint verifies unknown-provider detail, prompt_json serde detail, real generate, and real streaming. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(java): consume JSON handle/error envelope from constructors All JNA constructor declarations return Pointer; AimuxResult.extractHandle frees exactly once (try/finally), null-checks, and throws with the detailed engine error. The aimux_last_error declaration and constructorFailure helper from #19 are removed. Static-only change; CI to verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(kotlin): consume JSON handle/error envelope from constructors Same shape as the Java binding: Pointer? externs, extractHandle with free-in-finally, detailed errors surfaced. Static-only change; CI to verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(swift): consume JSON handle/error envelope from constructors extractHandle copies the string, frees once, then throws AimuxError with the detailed message on the error envelope. swift build passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(flutter): consume JSON handle/error envelope from constructors Constructor typedefs return Pointer<Utf8>; _extractHandle frees once and throws StateError with the detailed message. Static-only change; CI to verify (no dart SDK in the authoring environment). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(c): consume JSON handle/error envelope from constructors example.c / example.cpp parse the envelope and print the detailed engine error on constructor failure (replacing #19's aimux_last_error read in example.c); example.cpp includes <stdexcept> for its throw sites. Both compile against the new header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
eric8810
added a commit
that referenced
this pull request
Aug 3, 2026
* fix(ffi): preserve detailed errors across the C ABI (issue #17) Problem A: aimux_generate_text / aimux_stream_text dropped the serde_json::Error from prompt_json parsing (only "invalid prompt_json" remained); aimux_provider_new also swallowed config_json parse errors. Report them like the adjacent opts_json branch already did. Problem B: all 20 language-model constructors (and the multimodal ones for null arguments) reduced every AiMuxError to a bare 0 handle. Add a thread-local error slot + aimux_last_error() returning the error JSON envelope (read-and-clear, caller frees with aimux_free_string). Every u64 constructor clears the slot on entry, records the envelope on failure, and stays empty on success. Bindings: - Go: takeLastError() + LockOSThread around constructor+read in newModel / wrapHandle / ProviderWithBase / newMultimodalHandle - Java: JNA aimux_last_error() (Pointer + aimux_free_string) and constructorFailure() used by all 21 model factories - C: example.c prints the detailed error on constructor failure Tests: error_detail_test.rs covers serde detail, UnknownProvider/Json/ InvalidArgument envelopes, clear-on-success, last-write-wins, read-once and TLS isolation. cargo test -p aimux-ffi: 15 passed. * style(ffi): cargo fmt + fix Java Matcher reference (CI #19) cargo fmt: wrap record_error_msg calls at 100 cols. Java: use fully-qualified java.util.regex.Matcher (no import needed). * feat(logging): unified tracing observability (RFC-0014) - logging.rs: init_logging (Once-guarded, never overrides host subscriber) + lazy env auto-init (AIMUX_LOG / AIMUX_LOG_LEVEL) + body redaction (AIMUX_LOG_BODY=1, 4KB truncation, auth-field masking) - http.rs: http_request span (method/host/attempt) + request/response debug events (URL without query, header count only) + retry warn / failed error events + ObservedByteStream (ttfb/chunks/duration) - generate.rs: generate span + generate_end event (provider/model/modality, ok/duration/finish_reason) - FFI: aimux_init_logging C ABI export + header declaration; providers re-export init_logging - Tests: unit (idempotency, filter precedence via event capture, redaction) + wiremock integration (retry chain, summaries, body redaction, stream events); workspace suite green * fix(logging): collapse nested ifs (clippy collapsible_if, deny warnings) * fix(logging): disable ANSI in test capture subscribers fmt() auto-detects ANSI support; on CI (GitHub Actions) it emits color codes that split field rendering (status=429 -> status^[[2m=^[[0m429), breaking literal contains() assertions. with_ansi(false) makes captured output deterministic across platforms. * docs(rfc-0014): correct stale provider count (172 -> 250 registry entries) * docs(rfc-0014): drop hardcoded provider count (172/250 -> count-agnostic) * feat(logging): expose init_logging entry in all 8 bindings (RFC-0014) Python: aimux.init_logging(level); Node: initLogging(level); Go: InitLogging(level) via cgo; Java: Aimux.initLogging(level) via JNA; Kotlin: initLogging(level); Swift: Aimux.initLogging(level:); Flutter/Dart: initLogging(level) via dart:ffi. C/Rust entries existed. All idempotent, no-op when host subscriber present, AIMUX_LOG* env takes precedence. Docs: per-language entry table in docs/api/rust.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #17 — the C ABI layer (
aimux-ffi, shared by Go/Java/Kotlin/Swift/Flutter/C) dropped detailed errors in two places.Problem A —
prompt_json/config_jsonparse errors lostaimux_generate_text/aimux_stream_textdiscarded theserde_json::Errorfromprompt_json(callers only saw"invalid prompt_json");aimux_provider_newalso swallowedconfig_jsonparse errors (Err(_) => return 0). Both now report the serde detail exactly like the adjacentopts_jsonbranch already did.Problem B — all constructors reduced
AiMuxErrorto a bare020 language-model constructors (and every multimodal constructor for null args) dropped the full
AiMuxError(e.g.UnknownProviderwith the available-provider list, missing env var, bad config). Solution without breaking the ABI (55 symbols untouched, 1 added):aimux_last_error(): read-and-clear, returns the existing error JSON envelope{"error","error_type","status_code"}, caller frees withaimux_free_stringu64constructor clears the slot on entry, records the envelope on failure, stays empty on success; null/invalid-UTF-8 args recordInvalidArgumentBindings
takeLastError()helper;runtime.LockOSThread()around constructor+read innewModel,wrapHandle,ProviderWithBase,newMultimodalHandle(constructors are pure config building, so pinning is cheap)aimux_last_error()declared asPointer(released viaaimux_free_string);constructorFailure()extracts theerrorfield for all 21 model factoriesexample.cprints the detailed error on constructor failureKotlin/Swift/Flutter call the same symbols and can adopt
aimux_last_error()the same way (thread-affinity note in the header docs); not changed in this PR.Verification
cargo test -p aimux-ffi: 15 passed (11 new regression tests inerror_detail_test.rs+ 4 existing smoke tests)cargo check --workspace: cleanUnknownProvider/Json/InvalidArgumentenvelopes, clear-on-success, last-write-wins, read-once, TLS isolationNotes
aimux_last_error()is added;0remains the documented failure sentinelaimux_last_error()semantics (destructive read, same OS thread as the failed constructor, caller-owned pointer) are documented inaimux-ffi.h