diff --git a/backend/internal/pkg/apicompat/responses_to_chatcompletions_tool_name_test.go b/backend/internal/pkg/apicompat/responses_to_chatcompletions_tool_name_test.go new file mode 100644 index 0000000000..c30ca97389 --- /dev/null +++ b/backend/internal/pkg/apicompat/responses_to_chatcompletions_tool_name_test.go @@ -0,0 +1,44 @@ +package apicompat + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestResponsesEventToChatChunks_ArgumentsDeltaOmitsEmptyName pins that +// streamed arguments-only deltas never re-emit the tool name as an empty +// string. OpenAI-compatible clients accumulate the name from the first +// tool-call delta; a trailing "name":"" would overwrite the accumulated name +// and break tool dispatch ("unknown tool"). +func TestResponsesEventToChatChunks_ArgumentsDeltaOmitsEmptyName(t *testing.T) { + state := NewResponsesEventToChatState() + + added := ResponsesEventToChatChunks(&ResponsesStreamEvent{ + Type: "response.output_item.added", + OutputIndex: 0, + Item: &ResponsesOutput{ + Type: "function_call", + CallID: "call_a", + Name: "exec", + }, + }, state) + require.Len(t, added, 1) + first, err := json.Marshal(added[0]) + require.NoError(t, err) + require.Contains(t, string(first), `"name":"exec"`) + + for _, fragment := range []string{`{"cmd":"ls"}`, `{"cmd":"ls","flags":"-la"}`} { + deltas := ResponsesEventToChatChunks(&ResponsesStreamEvent{ + Type: "response.function_call_arguments.delta", + OutputIndex: 0, + Delta: fragment, + }, state) + require.Len(t, deltas, 1) + raw, err := json.Marshal(deltas[0]) + require.NoError(t, err) + require.NotContains(t, string(raw), `"name"`, "arguments delta must not carry a name field") + require.Contains(t, string(raw), `"arguments"`) + } +} diff --git a/backend/internal/pkg/apicompat/types.go b/backend/internal/pkg/apicompat/types.go index bcc8cd481e..9f2e8388e7 100644 --- a/backend/internal/pkg/apicompat/types.go +++ b/backend/internal/pkg/apicompat/types.go @@ -714,7 +714,9 @@ type ChatToolCall struct { // ChatFunctionCall contains the function name and arguments. type ChatFunctionCall struct { - Name string `json:"name"` + // Empty name is omitted so streamed arguments-only deltas never overwrite + // the tool name a client accumulated from the first delta. + Name string `json:"name,omitempty"` Arguments string `json:"arguments"` }