fix(openai): never run a tool call cut off mid-stream #18
No reviewers
Labels
No labels
area/engine
clarity/clear
enhancement
security
size/M
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
basepeak-ai/gptscript!18
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/incomplete-tool-calls"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
Production, tenant allard, thread
t16bmc4(2026-10-16 08:25Z): the model router ended an HTTP 200 stream with an in-stream error (server_error: The model stopped sending output mid-stream) while the model was partway through aworkspace_writecall. The stream reader takes in-stream errors only from a line that begins withdata: {"error":. A chunk that haschoicesand a top-levelerrorwithfinish_reason: "error"is parsed as an ordinary chunk. After it comes a clean[DONE], socallreturned the partial message as a finished completion. The engine then ranworkspace_writewith the unterminated arguments{"filename": "Bestandsregister_Dokumentensammlung.md"(0 bytes written) and saved the call into the chat state. From then on the provider rejected every request on that thread (invalid_request_error: The request was rejected as malformed), because the history held arguments it could not parse.Change
pkg/openai/client.go:checkComplete: a completion is incomplete when its stream ends withfinish_reason: "error", or when any tool call has non-empty arguments that are not valid JSON (this also catches a dropped connection). Empty arguments are still accepted, since models send them for tools with no parameters. A user cancel skips the check.callretries an incomplete completion up to 3 times; this is safe because nothing from it has run. If every attempt is incomplete it returnsErrIncompleteCompletion. The tool call is never handed to the engine, never executed and never cached. The non-streaming path runs the same check, and a response with no choices now returns an error instead of panicking onChoices[0].dropInvalidToolCalls(called fromtoMessages) is a second line of defence. Before history is sent, it removes every tool call whose arguments are invalid JSON, the tool result that answers it, and any assistant message left empty. It works on copies and leaves the engine's state alone. Threads that already carry such a call, liket16bmc4, recover on their next message.Verification
pkg/openai/incomplete_test.go. Its SSE fixtures replay the incident: an errored stream, a dropped stream, and a truncated stream that succeeds on retry. Further fixtures cover a complete call and an empty-arguments call, plus a poisoned-history fixture for the sanitizer.go test -race ./pkg/openai/passes.go test ./pkg/...passes exceptpkg/testsTestMCPLoad, which hangs to the 10-minute timeout in the MCP loader on this machine. That test has nothing to do with this change.golangci-lint run ./pkg/openai/...reports 0 issues (with the Go 1.26 toolchain CI uses). Run over the whole repo, the installed linter (v2.13.2, newer than the v2.9.0 pinned here) reports 32 issues. The same 32 are reported onmain, and none of them are in this change.a191d5514a4f45a84f97View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.