Skip to content

fix(auth): ignore id-only tool-call scaffolds in empty completion check - #189

Open
warelik wants to merge 2 commits into
kaitranntt:mainfrom
warelik:fix/toolcall-id-only-buffering
Open

fix(auth): ignore id-only tool-call scaffolds in empty completion check#189
warelik wants to merge 2 commits into
kaitranntt:mainfrom
warelik:fix/toolcall-id-only-buffering

Conversation

@warelik

@warelik warelik commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Problem

  1. ID-Only Tool Call Scaffolds Suppress Failover: OpenAI and Responses API streams can emit early tool-call deltas or output items containing only an id or call_id without a function name or arguments. Previously, isMeaningfulToolCall and hasMeaningfulResponsesCallItem treated any non-empty ID as meaningful output (hasMeaningfulOutput() = true). When an upstream stream encountered an error after this scaffold, readStreamBootstrap treated the stream as having successfully started (bootstrapErr == nil), suppressing model failover and returning an unusable empty tool call to the client.
  2. Responses API action Payloads Misclassified as Empty: Responses API tool call items (such as web_search_call and computer_call) provide their payload in the action field instead of arguments or input. When ID-only matching was removed, these calls were incorrectly classified as empty completions and discarded.

Fix

  • sdk/cliproxy/auth/empty_completion.go:271: In isMeaningfulToolCall, removed the check on call.ID so ID-only scaffolds without function name or arguments are not marked meaningful.
  • sdk/cliproxy/auth/empty_completion.go:384: Added Action json.RawMessage to openAIResponseOutputItem.
  • sdk/cliproxy/auth/empty_completion.go:772: In hasMeaningfulResponsesCallItem, removed item.ID / item.CallID checks and added nonEmptyJSONPayload(item.Action) to correctly recognize web_search_call and computer_call output items as meaningful.

Tests

  • TestToolCallIDOnlyRegression: Verifies that ID-only tool call fragments in both OpenAI and Responses API formats are treated as empty (allowing upstream failover), while fragments with function names, arguments, or action payloads are recognized as meaningful.
  • TestEmptyCompletionPredicate: Extensively tests payload predicates across OpenAI, Responses API, Gemini, and Claude formats for ID-only vs meaningful tool calls and actions.

Reverse bite-check

Reverting sdk/cliproxy/auth/empty_completion.go to the pre-fix state reproduces the failure where ID-only tool calls are misclassified as meaningful:

=== RUN   TestToolCallIDOnlyRegression
=== RUN   TestToolCallIDOnlyRegression/openai_tool_call_id_only_is_empty_and_not_meaningful
    empty_completion_test.go:3278: StreamBootstrapDetector.Observe() = true for id-only tool_call, want false
=== RUN   TestToolCallIDOnlyRegression/openai_tool_call_with_name_is_meaningful_and_forwards
=== RUN   TestToolCallIDOnlyRegression/openai_tool_call_with_name_and_empty_object_arguments_is_meaningful_and_forwards
=== RUN   TestToolCallIDOnlyRegression/responses_api_function_call_id_only_is_empty_and_not_meaningful
    empty_completion_test.go:3331: StreamBootstrapDetector.Observe() = true for Responses function_call id only, want false
=== RUN   TestToolCallIDOnlyRegression/responses_api_function_call_with_name_is_meaningful_and_forwards
=== RUN   TestToolCallIDOnlyRegression/responses_api_function_call_with_name_and_empty_object_arguments_is_meaningful_and_forwards
--- FAIL: TestToolCallIDOnlyRegression (0.00s)
    --- FAIL: TestToolCallIDOnlyRegression/openai_tool_call_id_only_is_empty_and_not_meaningful (0.00s)
    --- PASS: TestToolCallIDOnlyRegression/openai_tool_call_with_name_is_meaningful_and_forwards (0.00s)
    --- PASS: TestToolCallIDOnlyRegression/openai_tool_call_with_name_and_empty_object_arguments_is_meaningful_and_forwards (0.00s)
    --- FAIL: TestToolCallIDOnlyRegression/responses_api_function_call_id_only_is_empty_and_not_meaningful (0.00s)
    --- PASS: TestToolCallIDOnlyRegression/responses_api_function_call_with_name_is_meaningful_and_forwards (0.00s)
    --- PASS: TestToolCallIDOnlyRegression/responses_api_function_call_with_name_and_empty_object_arguments_is_meaningful_and_forwards (0.00s)
FAIL
FAIL	github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth	0.457s
FAIL

Verification

TMPDIR=/Users/warelik/.cache/gotmp GOCACHE=/Users/warelik/.cache/gocache go build ./...
TMPDIR=/Users/warelik/.cache/gotmp GOCACHE=/Users/warelik/.cache/gocache go vet ./sdk/cliproxy/auth/...
gofmt -l sdk/cliproxy/auth/empty_completion.go sdk/cliproxy/auth/empty_completion_test.go
TMPDIR=/Users/warelik/.cache/gotmp GOCACHE=/Users/warelik/.cache/gocache go test -v -run "TestToolCallIDOnlyRegression" ./sdk/cliproxy/auth/...

Output:

=== RUN   TestToolCallIDOnlyRegression
=== RUN   TestToolCallIDOnlyRegression/openai_tool_call_id_only_is_empty_and_not_meaningful
=== RUN   TestToolCallIDOnlyRegression/openai_tool_call_with_name_is_meaningful_and_forwards
=== RUN   TestToolCallIDOnlyRegression/openai_tool_call_with_name_and_empty_object_arguments_is_meaningful_and_forwards
=== RUN   TestToolCallIDOnlyRegression/responses_api_function_call_id_only_is_empty_and_not_meaningful
=== RUN   TestToolCallIDOnlyRegression/responses_api_function_call_with_name_is_meaningful_and_forwards
=== RUN   TestToolCallIDOnlyRegression/responses_api_function_call_with_name_and_empty_object_arguments_is_meaningful_and_forwards
--- PASS: TestToolCallIDOnlyRegression (0.00s)
    --- PASS: TestToolCallIDOnlyRegression/openai_tool_call_id_only_is_empty_and_not_meaningful (0.00s)
    --- PASS: TestToolCallIDOnlyRegression/openai_tool_call_with_name_is_meaningful_and_forwards (0.00s)
    --- PASS: TestToolCallIDOnlyRegression/openai_tool_call_with_name_and_empty_object_arguments_is_meaningful_and_forwards (0.00s)
    --- PASS: TestToolCallIDOnlyRegression/responses_api_function_call_id_only_is_empty_and_not_meaningful (0.00s)
    --- PASS: TestToolCallIDOnlyRegression/responses_api_function_call_with_name_is_meaningful_and_forwards (0.00s)
    --- PASS: TestToolCallIDOnlyRegression/responses_api_function_call_with_name_and_empty_object_arguments_is_meaningful_and_forwards (0.00s)
PASS
ok  	github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth	0.442s

Tooling note

The jbcontext semantic-search CLI is installed but non-functional in this environment
(jbcontext search fails with the OS keychain is not accessible). The equivalent
review passes were performed with the repository's own tooling and manual inspection
instead; this is disclosed for transparency about how the change was reviewed.

W ARELIK added 2 commits August 20, 2026 12:33
An OpenAI-compatible stream or Responses-API stream can emit a tool_calls or function_call delta carrying only an id / call_id without a function name or arguments. Previously, isMeaningfulToolCall and hasMeaningfulResponsesCallItem accepted these ID-only scaffolds as meaningful content (setting acc.hasToolCalls = true).

In readStreamBootstrap (sdk/cliproxy/auth/conductor_stream.go), when an upstream chunk carries an error but bootstrap.hasMeaningfulOutput() is true, the error is suppressed, appended to the buffer, and readStreamBootstrap returns nil error. executeStreamWithModelPool then sees bootstrapErr == nil and assumes the stream started successfully, permanently disabling failover for that request and delivering an unusable partial tool call to the client.

Fix the defect at sdk/cliproxy/auth/empty_completion.go:274-276 by removing the call.ID check in isMeaningfulToolCall, and at sdk/cliproxy/auth/empty_completion.go:776-783 by dropping the item.ID and item.CallID disjuncts in hasMeaningfulResponsesCallItem. This ensures that tool call deltas require a name, arguments, input, or result before being marked meaningful.

This aligns with the Claude branch in the same file (sdk/cliproxy/auth/empty_completion.go:822), which already requires both ID and Name (strings.TrimSpace(b.ID) != "" && strings.TrimSpace(b.Name) != "").

Mirrors upstream fix on router-for-me/CLIProxyAPI PR #4881.
Responses-API tool calls (e.g. web_search_call, computer_call) carry payload in action rather than arguments/input. Include nonEmptyJSONPayload check for item.Action so valid completions are not discarded as empty.
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.

1 participant