Skip to content

feat!: every error keeps its cause - #280

Closed
cunninghamcard-bit wants to merge 2 commits into
masterfrom
feat/every-error-keeps-its-cause
Closed

cunninghamcard-bit wants to merge 2 commits into
masterfrom
feat/every-error-keeps-its-cause

Conversation

@cunninghamcard-bit

Copy link
Copy Markdown
Contributor

Requested by li · project thread

Summary

Before: an error kept only one line of text. A refused connection read error sending request for url (http://…?key=…): the OS reason was gone and the query (which may hold an API key) was in the message. Parse, validation, URL, base64 and download failures were format!ed into strings, InvalidToolInput.cause was a string, and no binding exposed a cause.

After: every error whose AI SDK class takes cause has one, and it is what source() returns. anyhow/{:#} prints the whole chain down to Connection refused (os error 111); the JSON payload carries the same chain as cause, and each binding exposes it natively (Node error.cause, Python __cause__, Java/Kotlin getCause(), Go Unwrap(), Swift/Flutter cause, C++ aimux::NativeError::json()). Transport errors drop the URL. Messages are unchanged.

How: a cause is either a nested AiMuxError (kept whole, like the AI SDK nesting a JSONParseError in an APICallError) or a SourceError, a cloneable, serializable { message, cause? } snapshot of a foreign error's source() chain. AiMuxError stays on thiserror with #[source] on each cause; Retry is transparent and RetryError::source() is the last attempt (lastError), ApiCallError::source() its cause. The string variants JsonParse, NoOutputGenerated, InvalidResponseData, InvalidArgument, InvalidPrompt and Other become { message, cause } with AiMuxError::json_parse(..)-style constructors, and every site that had the underlying error now passes it. FetchError carries a SourceError built from reqwest::Error::without_url().

Registered in docs/ai-sdk-request-pipeline.md §3 (replacing "cause 本轮不加入"): messages still include the underlying text, because the C ABI message getter only sees the message; ops binding/protocol/recording errors take no cause, their reason is one level and already in the message and payload. docs/error-model.md gains the per-language table.

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

  • The bulk of the diff is mechanical: tuple constructors to the new constructors, Variant(msg) patterns to Variant { message, .. }. The real changes are aimux-core/src/error.rs, aimux-provider-utils/src/{fetch,handle_fetch_error,post_to_api,response_handler,http,ws}.rs, aimux-core/src/{output,parse_tool_call}.rs and the binding error modules.
  • Shared vectors contract-tests/ops/errors/aimux-api-call.json (a transport chain) and aimux-invalid-response-data.json (a nested AI_JSONParseError) now carry a cause; aimux-invalid-tool-input.json has its cause as an object.
  • New tests: the Rust source() chain and wire round trip with and without a cause, a real refused connection (chain kept, URL gone), serde and retry sources, and a cause test per binding (Node, Python, Java, Go, Swift, Flutter, C++).
  • [15/15] feat!: generate binding DTOs from the Rust types #268 regenerates binding types too; whichever merges second regenerates them.

Generated by Claude Code

Every error whose AI SDK class takes `cause` now has one, and it is what
`source()` returns: a nested `AiMuxError`, or a `SourceError` snapshot of a
foreign error's `source()` chain. The JSON payload carries the same chain
and each binding exposes it natively. Transport errors drop the URL.

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

Copy link
Copy Markdown
Contributor Author

这个原型太大,按 RFC-0047(#286)拆成了三个叠起来的 PR,内容都搬过去了,这里关闭:

步骤 PR 内容
1/3 #289 ErrorCause / SourceError 核心类型,网络层把底层错误作为 ApiCallError.cause
2/3 #290 六个只有消息的变体带上 cause
3/3 #293 ops 信封四个域带 cause,各语言绑定接成原生 cause

Generated by Claude Code

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