A REPRODUCIBLE DEBUGGING NOTE · 26 OCT 2026

Two valid streams.
One missing tool call.

An “OpenAI-compatible” gateway can return valid SSE, valid function arguments and the right finish reason, while quietly losing one of two parallel tool calls. Validating the downstream response alone cannot tell you that a call disappeared.

两份流记录各自通过检查,也可能在网关转发后少了一次工具调用。这里用合成样例演示如何发现差异,不需要密钥,也不会调用模型。

Run the dropped-call example ↗

The smallest useful reproduction

The upstream capture contains two interleaved function calls. The gateway capture contains just the first. Both end with finish_reason: "tool_calls" and a dispatched [DONE] marker. Each call’s concatenated arguments are valid JSON.

Upstream

2 calls · passes alone
weather({"city":"Taipei"})
clock({"zone":"UTC"})

Gateway

1 call · passes alone
weather({"city":"Taipei"})

These are decoded calls, not raw SSE. The interactive example contains the complete captures. All values are synthetic; this is not a reproduction of a customer deployment.

A comparison of both sides reports TOOL_CALLS_DROPPED and TOOL_CALL_SET_CHANGED. That is evidence of a difference in these paired fixtures, even though neither individual capture has a malformed call.

Compare meaning, not rewritten IDs

A translator may give a call a different ID or index without changing its meaning. Comparing raw JSON would flag that harmless rewrite. Stream Check compares function names and decoded arguments per response choice, ignoring call-ID/index remapping and JSON object key order. It preserves duplicate calls when counting; two identical invocations are still two invocations.

Argument numbers use JavaScript’s numeric semantics. An unsafe integer produces a warning and makes comparison inconclusive rather than silently declaring equal rounded numbers.

Before you blame the gateway

  1. Capture both sides of the same request. Two independent model generations may legitimately choose different calls.
  2. Keep the complete SSE response bodies. A missing event delimiter, finish reason or terminal marker means the capture may be incomplete.
  3. Check each capture first. If either has errors or warnings, this checker reports an inconclusive comparison. It does not turn a bad capture into proof of gateway loss.

A text capture cannot measure buffering, distinguish a client timeout from a proxy timeout, or prove that a function was actually executed. Verify those separately in the client and runtime.

What to inspect in a translator

Keep a separate accumulator for each choice and tool index. Preserve index: 0 instead of omitting a zero-valued field. Append argument fragments in order; parse them after reconstruction. Later fragments may omit IDs, names and types, so a filter must not discard them just because those identity fields are absent. Also check paths that forward text and tool deltas together.

The official function-calling guide explains fragment association and first-fragment identity fields. The SSE standard defines dispatch boundaries. An error should be checked against the actual client contract, not a claim that every fragment must repeat every field.

Run it locally

git clone https://github.com/LIghtJUNction/Inspiration.git
cd Inspiration
node bin/stream-check.mjs upstream.sse --compare gateway.sse --json

No package installation or model request is needed. The browser uses the same analyzer as the CLI. An exported structural report omits captured text, function names, argument values, IDs and headers, while retaining codes, positions and counts. Review that metadata before sharing it.

For a related real-world problem report, Sourcebot #1105 distinguishes unfinished streams from malformed function arguments. This synthetic example does not establish the cause of that issue or claim to fix its deployment.

FREE TOOL · OPEN SOURCE

Useful in a real investigation?

Try the paired example, or support continued development.

Try the example ↗

Voluntary support ↗ · No purchase or service commitment.