Guard streamed tool terminals and retain Usage #46

Merged
weichen merged 9 commits from feature/fast-slow-u3-r04-stream-guard into main 2026-10-19 09:20:09 +00:00
2 changed files with 22 additions and 0 deletions
Showing only changes of commit 6e972335c9 - Show all commits
+4
View File
@@ -195,6 +195,10 @@ func emitParsed(ctx context.Context, r io.Reader, out chan<- Event) {
// A finish reason alone does not prove the tool arguments arrived intact.
// Preserve returned usage when rejecting a partial terminal, so the host
// can charge this call and stop rather than treating it as a free retry.
if finished == "tool_calls" && len(order) == 0 {
sendTerminalFailure("tool call finish reason without a tool call")
return
}
if len(order) > 0 {
problem := ""
switch {
+18
View File
@@ -93,6 +93,24 @@ func TestParseSSEToolCallIncrements(t *testing.T) {
}
}
func TestParseSSEToolFinishWithoutCallFails(t *testing.T) {
body := strings.Join([]string{
`data: {"choices":[{"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":12,"completion_tokens":5,"total_tokens":17}}`,
`data: [DONE]`,
"",
}, "\n")
var end Event
for ev := range ParseSSE(context.Background(), strings.NewReader(body)) {
if ev.Kind == KindMessageEnd {
end = ev
}
}
if end.Err == nil || end.Message.StopReason != StopError || end.Usage.TotalTokens != 17 ||
!end.StreamComplete || len(end.Message.ToolCalls) != 0 {
t.Fatalf("empty tool finish must fail and retain Usage: %#v", end)
}
}
func TestParseSSELengthFailsUnfinishedToolCall(t *testing.T) {
body := strings.Join([]string{
`data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"read_file","arguments":"{\"path\":"}}]}}]}`,