From 5b1166a5bf6211df3b8a76585dfc34f709666d47 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Fri, 18 Sep 2026 17:55:08 +0530 Subject: [PATCH] Normalize legacy function_call finish reason on the Chat path Some OpenAI-compatible endpoints still emit the deprecated "function_call" finish reason. The Chat path forwarded the raw string verbatim, so callers saw "function_call" where the Python and .NET clients report "tool_calls". Normalize "function_call" to "tool_calls" on both the streaming and non-streaming paths, matching the Python client; other reasons pass through unchanged. --- provider/openaiprovider/chat.go | 15 +++++++++++++-- provider/openaiprovider/chat_test.go | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/provider/openaiprovider/chat.go b/provider/openaiprovider/chat.go index 1479a0bb..30386aba 100644 --- a/provider/openaiprovider/chat.go +++ b/provider/openaiprovider/chat.go @@ -149,7 +149,7 @@ func (a *chatClient) run(ctx context.Context, messages []*message.Message, optio if choice.Message.Refusal != "" { contents = append(contents, &message.ErrorContent{Message: choice.Message.Refusal, ErrorCode: "Refusal"}) } - finishReason = choice.FinishReason + finishReason = normalizeChatFinishReason(choice.FinishReason) } if resp.JSON.Usage.Valid() { contents = addUsage(contents, resp.Usage) @@ -209,7 +209,7 @@ func (a *chatClient) run(ctx context.Context, messages []*message.Message, optio } var finishReason string if len(chunk.Choices) > 0 { - finishReason = chunk.Choices[0].FinishReason + finishReason = normalizeChatFinishReason(chunk.Choices[0].FinishReason) } resp := &agent.ResponseUpdate{ Contents: contents, @@ -230,6 +230,17 @@ func (a *chatClient) run(ctx context.Context, messages []*message.Message, optio } } +// normalizeChatFinishReason maps the deprecated "function_call" finish reason, +// still emitted by some OpenAI-compatible endpoints, onto the canonical +// "tool_calls", matching the Python client. Other reasons pass through +// unchanged. +func normalizeChatFinishReason(reason string) string { + if reason == "function_call" { + return "tool_calls" + } + return reason +} + func mapRole(r string) message.Role { switch r { case "user": diff --git a/provider/openaiprovider/chat_test.go b/provider/openaiprovider/chat_test.go index 20447b8c..f5504894 100644 --- a/provider/openaiprovider/chat_test.go +++ b/provider/openaiprovider/chat_test.go @@ -428,6 +428,25 @@ func TestChatBasicRequestResponse_NonStreaming(t *testing.T) { } } +// Some OpenAI-compatible endpoints still emit the deprecated "function_call" +// finish reason; it must be normalized to the canonical "tool_calls", matching +// the Python client. +func TestChatLegacyFunctionCallFinishReasonNormalized(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, `{"id":"chatcmpl-fc","object":"chat.completion","created":1727888631,"model":"gpt-4o-mini","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"function_call"}]}`) + })) + defer server.Close() + + resp, err := newTestClient(server).RunText(t.Context(), "hi").Collect() + if err != nil { + t.Fatalf("error = %v", err) + } + if resp.FinishReason != "tool_calls" { + t.Errorf("FinishReason = %q, want %q", resp.FinishReason, "tool_calls") + } +} + func TestChatURLCitationAnnotations_NonStreaming(t *testing.T) { const input = ` {