Skip to content

feat(provider-utils)!: align StreamingToolCallTracker with the AI SDK and move it next to its users - #204

Draft
cunninghamcard-bit wants to merge 5 commits into
masterfrom
stream/align-ai-sdk
Draft

cunninghamcard-bit wants to merge 5 commits into
masterfrom
stream/align-ai-sdk

Conversation

@cunninghamcard-bit

@cunninghamcard-bit cunninghamcard-bit commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Now tracker only, based on master; the SSE work moved to its own PR (see #213). Kept as draft until the direction below is confirmed against #185 / ROADMAP.

  • StreamingToolCallTracker is rewritten to match @ai-sdk/provider-utils (5.0.51) and moved from aimux-stream to aimux-provider-utils, where upstream keeps it. It emits StreamPart directly (ToolInputStart / ToolInputDelta / ToolInputEnd / ToolCall). Deltas are correlated by wire id, index and function name, ambiguous deltas are dropped, ids are de-duplicated with bounded suffixes, blank function names are ignored, and flush orders calls by index only when every call has one. Adds StreamingToolCallArgumentState / starts_with_structured_value.
  • aimux-providers OpenAI chat (openai/model.rs, which serves every registry-backed provider) drives the tracker instead of its private ToolCallAccumulator; DeltaToolCall.index becomes Option<usize> to match the wire. Same wiring as @ai-sdk/openai's chat model.
  • aimux-stream no longer exports the tracker or ToolCallStreamPart.

Direction vs. #185 / ROADMAP

#185 and ROADMAP planned "delete the tracker (pure deletion), then introduce ToolInput{Raw,Parsed}". This PR takes the other route and the reasons are:

  1. The AI SDK keeps a tracker in @ai-sdk/provider-utils and its OpenAI chat model drives it. aimux's openai/model.rs had its own ToolCallAccumulator doing the same job with index-only correlation, which mishandles reused indices across parallel calls and id-less continuations. Deleting the unused aimux-stream copy would have left that duplicate in place.
  2. The Raw/Parsed split that Tool-call input boundary: type the raw/parsed distinction, drop the unused StreamingToolCallTracker #185 wanted is covered by the message-protocol design in docs: full-chain AI SDK alignment design and impact map #200 Part II (V4 tool-call input is the raw string; core's parse_tool_call produces the parsed value). The tracker is orthogonal to it.

ROADMAP's #185 line is updated in #200 (§0.7 table + a note on the line). If the maintainers prefer the deletion route, this PR is closed and only the SSE PR goes in.

Review notes

  • CHANGELOG describes only the final state (tracker in aimux-provider-utils, no ToolCallStreamPart); the intermediate return-ToolCallStreamPart API from earlier commits is gone from the entry.
  • Tests are ported, not written. streaming_tool_call_tracker_test.rs and the argument-state test are case-for-case ports of the upstream tests at the pinned tag; the tracker test projects StreamPart down to the four fields upstream asserts on.
  • Parity was checked by running upstream: the same delta sequences were fed to the JS tracker and the Rust one (identical output for the cases tried). Some odd-looking behaviours are upstream's own (an id-less continuation with a reused index and name is appended to the existing call) and are kept on purpose.
  • The default tool-call id generator is the constant tool-call (as before), not a random id.

Checklist

  • cargo fmt --all -- --check
  • cargo clippy -p aimux-stream -p aimux-provider-utils -p aimux-providers --all-targets -- -D warnings
  • cargo doc with RUSTDOCFLAGS=-D warnings
  • cargo test -p aimux-stream -p aimux-provider-utils -p aimux-providers
  • CI on this head

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS

@eric8810

eric8810 commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

这里需要先与 #185 / ROADMAP 对齐 tracker 的方向:原计划是删除 tracker,再引入 ToolInput{Raw,Parsed};当前最终 diff 是重写、移到 provider-utils,并接入 OpenAI chat stream。两条路线不相同,请明确选择及取舍,再更新相关 issue / roadmap。

SSE 大事件和解析行为修复可以与 tracker 的架构选择分开讨论;如果希望先落可独立验证的修复,可以考虑拆分。

另有两点需要补齐:当前 head 861f26e 没有 CI checks(工作流只匹配 master base);CHANGELOG 还同时描述返回 ToolCallStreamPart 的中间 API 和最终删除该类型的 API,请统一成最终状态。建议先保持 draft,等方向和当前候选提交的验证都明确后再转 ready。

claude and others added 5 commits October 3, 2026 04:49
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_015EYDYDWYcPsDmjBjFeuDVe
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_015EYDYDWYcPsDmjBjFeuDVe
…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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
…-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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
@cunninghamcard-bit
cunninghamcard-bit changed the base branch from chore/deletion-batch to master October 3, 2026 16:37
@cunninghamcard-bit cunninghamcard-bit changed the title feat(stream)!: align SseStream and StreamingToolCallTracker with the AI SDK feat(provider-utils)!: align StreamingToolCallTracker with the AI SDK and move it next to its users Oct 3, 2026
@cunninghamcard-bit

Copy link
Copy Markdown
Contributor Author

已拆分:SSE 部分单独成 PR(见 https://github.com/arcships/aimux/pull/213),本 PR 只剩 tracker,base 改为 master,保持 draft。

方向:选"对齐上游并移入 provider-utils",不走 #185 的"纯删"。两点理由写在描述里:上游在 @ai-sdk/provider-utils 保留 tracker 并由 OpenAI chat model 驱动,而 aimux 的 openai/model.rs 有一份私有 ToolCallAccumulator 只按 index 关联,并行调用复用 index、无 id 的续片都会出错,删掉 aimux-stream 那份未用的拷贝并不解决它;#185 想要的 Raw/Parsed 区分由 #200 第二部分的四层消息协议承担,与 tracker 正交。ROADMAP 的 #185 行在 #200 里更新。如果维护者仍倾向删除路线,关掉本 PR、只合 SSE 即可。

RFC-0042(#212)的 T2 接受"累积器放 provider-utils"这个归属,T3 要求歧义增量报 InvalidResponseData 而不是像上游那样丢弃——本 PR 现在是上游行为(丢弃)。T3 定下来之前本 PR 不动这一点;若接受 T3,我在这里改成报错并补差分测试,差异登记进允许差异表。T2 里"输出类型最终换成 V4 StreamPart"属于 #200 的类型切换,不在本 PR 范围。

CHANGELOG 改成只描述最终状态(tracker 在 provider-utils、直接产出 StreamPart,没有 ToolCallStreamPart)。


Generated by Claude Code

This branch has not been deployed

No deployments
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.

3 participants