Skip to content

[v4/p2-tool-calls] refactor!: tool calls follow the AI SDK - #236

Closed
cunninghamcard-bit wants to merge 8 commits into
v4/1-ci-on-every-pr-and-test-fixture-cleanupfrom
v4/p2-tool-calls
Closed

cunninghamcard-bit wants to merge 8 commits into
v4/1-ci-on-every-pr-and-test-fixture-cleanupfrom
v4/p2-tool-calls

Conversation

@cunninghamcard-bit

Copy link
Copy Markdown
Contributor

Requested by li

Summary

Before: the streaming tool-call tracker lives in aimux-stream as a port of provider-utils 5.0.51. Tool types differ from upstream: tool_choice is required, tool examples and provider-tool args are untyped, and tool calls carry a separate thought signature.

After: the tracker lives in aimux-provider-utils next to the vendors that use it, aligned with the current AI SDK tracker. Tool types match the pinned upstream types field for field. tool_choice is optional; vendors omit an unset choice and the call layer defaults it to auto when tools are given, as upstream prepare-tool-choice does. The thought signature travels in provider metadata and provider options under the vendor namespace.

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

Status: Draft, opened early so the series layout can be reviewed. The items under "Planned in follow-up pushes" land on this branch before it is marked ready. cargo test --workspace has not been run on this branch yet.


🤖 Generated with Claude Code

https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa

🤖 Generated with Claude Code

claude and others added 8 commits October 12, 2026 09:42
No crate in the workspace referenced the tracker; providers accumulate
streamed tool-call deltas themselves. Drops the module, its 787-line test
file and the re-exported companion types. Breaking for aimux-stream's
public Rust API; noted in CHANGELOG under Unreleased.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EYDYDWYcPsDmjBjFeuDVe
(cherry picked from commit fe8758f)
Re-adds the tracker as a port of the current
@ai-sdk/provider-utils streaming-tool-call-tracker.ts instead of the
older index-only version removed in the previous commit.

- Correlate deltas by wire id, index and function name; drop ambiguous ones
- Unique tool-call ids with bounded suffixes; blank generator output falls
  back to "tool-call"
- Ignore unmatched blank function names; retain blank-name continuations
- Flush in index order only when every call has an index
- New StreamingToolCallArgumentState / starts_with_structured_value port
- process_delta and flush return the emitted parts instead of buffering

Tests are ported case for case from the upstream suites (45 tracker cases,
9 argument-state cases). No caller in the workspace uses the tracker yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EYDYDWYcPsDmjBjFeuDVe
(cherry picked from commit 6035201)
…nd wire the OpenAI stream to it

The AI SDK keeps StreamingToolCallTracker in @ai-sdk/provider-utils, where
every provider that speaks the OpenAI chat-completions wire format imports
it. aimux had the tracker in aimux-stream (the eventsource-parser role),
with its own ToolCallStreamPart event type and no callers, while
openai/model.rs kept an index-only accumulator. This commit puts the
pieces where the AI SDK has them.

aimux-stream
- StreamingToolCallTracker, StreamingToolCallArgumentState,
  starts_with_structured_value and ToolCallStreamPart move out; the crate
  is SSE decoding only. serde_json becomes a dev-dependency (test
  fixture); the `tool-calls` keyword is dropped.

aimux-provider-utils
- streaming_tool_call_tracker.rs / streaming_tool_call_argument_state.rs
  land here, logic unchanged. The tracker emits aimux_core::StreamPart
  ToolInputStart / ToolInputDelta / ToolInputEnd / ToolCall directly, so
  there is no second event type (RFC-0036: ToolCallStreamPart must not be a
  second public protocol). The generic metadata parameter becomes
  serde_json::Value / ProviderMetadata; TrackerError converts into
  AiMuxError::InvalidResponseData. The ported upstream tests (45 + 9)
  move with it, projecting StreamPart back to the upstream event shape.

aimux-providers
- openai/model.rs replaces its HashMap<usize, ToolCallAccumulator> with the
  tracker: tool_calls deltas are correlated by wire id, index and function
  name instead of index alone; id-less continuations follow their call;
  indices reused across parallel calls stay distinct; ambiguous deltas are
  dropped; a call without a wire id gets a generated `tool-call` /
  `tool-call-N` id instead of an empty string; a new call without a
  function name ends the stream with InvalidResponseData (AI SDK
  behaviour) instead of starting a call with an empty name.
- DeltaToolCall.index is Option<usize> (the AI SDK schema is
  `index: z.number().nullish()`).

Every registry-backed provider goes through openai/model.rs, so this is
the one plug point until S4-2 folds the mistral and xai chat copies into
the same implementation.

Verification: cargo test -p aimux-stream -p aimux-provider-utils; cargo test
-p aimux-providers (129 suites, 3041 passed, 0 failed, including the
cassette replays); cargo clippy -p aimux-stream -p aimux-provider-utils
-p aimux-providers --all-targets -- -D warnings; cargo fmt --all -- --check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
(cherry picked from commit df4d09f)
…-state port

Carried over from the SSE test port commit so the two split branches
leave the file identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
(cherry picked from commit 7bc200e)
Describes only the final API: the tracker lives in aimux-provider-utils
and emits StreamPart directly; the intermediate ToolCallStreamPart-
returning API from earlier commits is not mentioned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
(cherry picked from commit 264ade3)
… Rust

Same behaviour as the upstream port (checked differentially against the
previous implementation on ~1.1M random deltas), with Rust structure:

- Correlate by linear scan over per-call wire ids and indices instead of
  id/index hash maps re-sorted on every delta; no per-delta allocations
  besides the emitted parts.
- `finish(self)` consumes the tracker, so the never-true-before-flush
  `has_finished` state is gone and post-flush use cannot compile.
- `StreamingToolCallDelta<'a>` borrows from the provider wire type and
  carries `provider_metadata`, replacing `StreamingToolCallFunction`, the
  untyped `extra` bag, the test-only builders and the two boxed metadata
  hooks.
- `process` / `finish` append to a caller-owned buffer.
- Drop the unreachable `TrackerError::IdExhausted`.
- The argument state scans bytes and models completion as its own state.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a11e83-bf7b-75bb-900b-34851e6f12ee
(cherry picked from commit b0fd923)
…es and docs

With the tracker in aimux-provider-utils, aimux-stream only decodes SSE:
serde_json is needed by its tests alone, and the tool-calls keyword no
longer applies.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
One tool struct per layer, field for field with the pinned upstream types:
- CallOptions.tool_choice is optional. Vendors omit an unset choice, as
  their upstream packages do; generate_text and stream_text default it to
  auto when tools are given, as upstream prepare-tool-choice does.
- FunctionTool.input_examples is a list of { input: object } and
  ProviderTool.args is a JSON object.
- Tool calls carry no separate thought signature. The value travels in
  provider metadata on output and provider options on input, under the
  vendor namespace, where each upstream vendor package reads and writes it.
- Call sites pass the tool structs instead of their field lists.

The fifteen tool-choice tests ported from upstream (OpenAI, Groq, Mistral,
Bedrock) are kept and adapted; hand-written tests that pinned the old
shapes are deleted. CHANGELOG and docs/api/gaps.md record the wire-format
changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
@cunninghamcard-bit cunninghamcard-bit self-assigned this Oct 12, 2026
@cunninghamcard-bit cunninghamcard-bit changed the title [V4 2/8] refactor!: tool calls follow the AI SDK [v4/p2-tool-calls] refactor!: tool calls follow the AI SDK Oct 12, 2026
@cunninghamcard-bit
cunninghamcard-bit deleted the v4/p2-tool-calls branch October 12, 2026 10:43
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