Parse and validate tool inputs at the Core boundary - #165
Draft
cunninghamcard-bit wants to merge 9 commits into
Draft
Parse and validate tool inputs at the Core boundary#165cunninghamcard-bit wants to merge 9 commits into
cunninghamcard-bit wants to merge 9 commits into
Conversation
cunninghamcard-bit
force-pushed
the
codex/tool-input-parse-repair
branch
6 times, most recently
from
August 24, 2026 05:49
17420c6 to
c4d46a7
Compare
cunninghamcard-bit
force-pushed
the
codex/tool-input-parse-repair
branch
2 times, most recently
from
August 25, 2026 01:27
c61092d to
cdb2497
Compare
cunninghamcard-bit
marked this pull request as ready for review
August 25, 2026 01:42
Providers deliver the model's raw tool-argument text; Core parses it as exact JSON and validates it against the function tool's JSON Schema, following the AI SDK parseToolCall contract. Invalid calls are returned as tool calls marked invalid with a typed error (NoSuchTool / InvalidToolInput / ToolCallRepair) instead of silently passing raw strings through. The optional repair_tool_call callback gets one attempt and its replacement is revalidated from scratch. Provider-executed dynamic calls bypass the tool-set lookup, as in the AI SDK. The C ABI reports the new variants under the existing Tool error code; Node types carry them fully.
The golden variant-set test only asserted the wire tags of the variants it already knew, so an added variant sailed past a test named "exactly thirteen". Extend the list to the three tool-contract variants (the dead legacy catch-all Tool is gone), snapshot their exact JSON payloads, and add a compile-time exhaustive match, so any future addition fails the build of this test instead of relying on the runtime assertion.
cunninghamcard-bit
force-pushed
the
codex/tool-input-parse-repair
branch
2 times, most recently
from
August 25, 2026 01:49
cd589d5 to
716d40a
Compare
CI's clippy is newer than the workspace MSRV allows us to follow: as_chunks::<N>() is stable only since Rust 1.88 while the MSRV is 1.85, so trace/hash.rs and openai/embedding.rs keep chunks_exact behind unknown_lints; ws.rs's tungstenite error is consumed by its single caller, so boxing it buys nothing. Same attributes and wording as arcships#163/arcships#164, so whichever lands second is a no-op in these files.
cunninghamcard-bit
force-pushed
the
codex/tool-input-parse-repair
branch
from
August 25, 2026 01:49
716d40a to
deddae6
Compare
cunninghamcard-bit
marked this pull request as draft
August 25, 2026 03:50
… end Port of the follow-up round built in the stacked worktree, rebased onto the master-based branch: - generate: assemble response messages in stream order (text/reasoning segment flushing, provider_executed on replayed tool calls), matching toResponseMessages; response_tool_call_input replaces the narrower response_message_input (JS 'typeof x === "object"' also admits arrays). - openai outputs: buffer raw tool arguments across repair so invalid streams replay verbatim. - anthropic (direct + vertex): collapse bash/text-editor code-execution variants into the caller's single code_execution tool; the wire name survives in the normalized input 'type'. Google/xAI/HuggingFace conversions aligned the same way. - bindings: thought_signature + provider_executed + arbitrary-JSON provider_metadata on ToolCall across Go/Java/Kotlin/Swift/Flutter/ Node/Python; wire-format fixtures extended. Deliberately not ported (coupled to the retry PR's machinery, restored when that branch rebases onto this one): timeout/abort racing of the repair callback and its six tests, recoverable stream-frame handling, anthropic_stream_error.
Mirror the AI SDK's file split: to-response-messages logic moves out of
generate.rs into response_messages.rs as a ResponseMessageBuilder that owns
all assembly state (parts, text/reasoning buffers, provider options, the
reasoning aggregate). Both the streaming consume loop and the non-streaming
content loop shrink to per-part method calls; the twenty-odd inline flush
sequences collapse into the builder's flush discipline.
Also aggregate the remaining provider duplication: the streamed tool-input
finalizer (empty input -> "{}" + code-execution wire-name re-wrap) that was
inlined in both the Anthropic and Vertex-Anthropic stream loops becomes
anthropic::stream::finalize_streamed_tool_input. raw_tool_input moves next
to the rest of the parse contract in tool.rs.
tool.rs mixed the tool type definitions with the parse contract. Match the AI SDK's split: parse_tool_call.rs now owns RawToolCall, the repair callback and its context, parse_tool_call, JSON parsing with the prototype-pollution guard, schema validation, and raw_tool_input; tool.rs keeps only the types (Tool/FunctionTool/ProviderTool/ToolCall/ToolResult/ToolChoice). No compatibility re-exports: importers move to aimux_core::parse_tool_call.
GenerateContent::ToolCall.input was a serde_json::Value that only ever carried the provider's raw argument text wrapped in Value::String — the contract lived in a doc comment, so nothing stopped a provider from parsing the arguments itself and handing Core a structured value. Typing the field as String makes 'providers never parse tool input' a compile error to violate, and drops the unwrap dance on the Core side. The wire format is unchanged: Value::String(text) and String serialize identically, so no binding or fixture moves. StreamPart::ToolCall.input stays a Value: unlike GenerateContent it is dual-use — providers emit raw text, Core replaces it in-flight with the parsed value, and the user-facing stream sees the parsed one. Separating those two directions means splitting the stream-part type the way the AI SDK splits LanguageModelV2StreamPart from TextStreamPart, which is its own PR.
…rse-repair # Conflicts: # aimux-provider-utils/src/ws.rs
- anthropic/stream.rs: clippy 1.98's manual_unwrap_or fires on the wire-name match. Its suggested `unwrap_or` is not equivalent (an unrecognized Some(name) must collapse to code_execution, not pass through), so express the same rule with filter + unwrap_or. - kotlin Types.kt: the merge added providerMetadata to ToolCall twice, which kotlinx.serialization rejects as a duplicate serial name.
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
Move tool-call input parsing and validation to the Core boundary, following the AI SDK
parseToolCall/repairToolCallcontract.generate_textandstream_textparse exact JSON and validate it against the selected function tool's JSON Schema.invalid: trueand a typed error instead of being dropped or silently passing an unparsed string through.repair_tool_callcallback gets one attempt; its replacement is parsed and validated again from scratch.Behavior
NoSuchTool, including the available tool names when a tool set was supplied.InvalidToolInput, preserving the original argument text and parsed value when available.ToolCallRepair, preserving both the original validation error and the repair failure.do_streamcontinues to expose raw serialized arguments; the user-facingstream_textresult exposes parsed input after the complete tool call is assembled.This is a breaking behavior change for callers that relied on invalid provider arguments being accepted as ordinary tool input.
Public contract and bindings
AiMuxErrorto 16 wire variants. The C ABI maps all three to the existing tool-error code.provider_metadata,invalid, anderrorthrough the top-level and streaming tool-call surfaces in Node, Python, Go, Java, Kotlin, Swift, and Flutter.repair_tool_callis serde-skipped and cannot cross the JSON/FFI boundary, so only Rust callers can install the callback. Other bindings still receive the invalid call and typed failure as data and can implement an application-level repair loop. This limitation is recorded indocs/api/gaps.md.Scope
This PR is based directly on
masterand is independent of #164. It contains no operation-retry, timeout-pipeline, or recoverable-stream changes.Verification
cargo fmt --all -- --checkaimux-coreandaimux-providerstest suites