From edfd5e3736128b05f65108a5c2c83271b9a1f891 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Fri, 12 Jun 2026 15:01:58 +0800 Subject: [PATCH] fix(apicompat): default tool strict to false --- .../chatcompletions_responses_test.go | 85 +++++++++++++++++++ .../apicompat/chatcompletions_to_responses.go | 12 ++- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/backend/internal/pkg/apicompat/chatcompletions_responses_test.go b/backend/internal/pkg/apicompat/chatcompletions_responses_test.go index c12715e112..795a73938e 100644 --- a/backend/internal/pkg/apicompat/chatcompletions_responses_test.go +++ b/backend/internal/pkg/apicompat/chatcompletions_responses_test.go @@ -113,6 +113,91 @@ func TestChatCompletionsToResponses_ToolCalls(t *testing.T) { assert.Equal(t, "ping", resp.Tools[0].Name) } +func TestChatCompletionsToResponses_ToolStrict(t *testing.T) { + strictTrue := true + strictFalse := false + tests := []struct { + name string + strict *bool + want bool + }{ + {name: "defaults omitted strict to false", want: false}, + {name: "preserves explicit true", strict: &strictTrue, want: true}, + {name: "preserves explicit false", strict: &strictFalse, want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := &ChatCompletionsRequest{ + Model: "gpt-4o", + Messages: []ChatMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}}, + Tools: []ChatTool{{ + Type: "function", + Function: &ChatFunction{ + Name: "lookup", + Strict: tt.strict, + }, + }}, + } + + resp, err := ChatCompletionsToResponses(req) + require.NoError(t, err) + require.Len(t, resp.Tools, 1) + require.NotNil(t, resp.Tools[0].Strict) + assert.Equal(t, tt.want, *resp.Tools[0].Strict) + + payload, err := json.Marshal(resp) + require.NoError(t, err) + + var serialized struct { + Tools []map[string]json.RawMessage `json:"tools"` + } + require.NoError(t, json.Unmarshal(payload, &serialized)) + require.Len(t, serialized.Tools, 1) + strictJSON, ok := serialized.Tools[0]["strict"] + require.True(t, ok, "strict must be present in the Responses payload") + assert.JSONEq(t, string(mustMarshalJSON(t, tt.want)), string(strictJSON)) + }) + } +} + +func TestChatCompletionsToResponses_LegacyFunctionDefaultsStrictFalse(t *testing.T) { + req := &ChatCompletionsRequest{ + Model: "gpt-4o", + Messages: []ChatMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}}, + Functions: []ChatFunction{{ + Name: "lookup", + }}, + } + + resp, err := ChatCompletionsToResponses(req) + require.NoError(t, err) + require.Len(t, resp.Tools, 1) + require.NotNil(t, resp.Tools[0].Strict) + assert.False(t, *resp.Tools[0].Strict) + + payload, err := json.Marshal(resp) + require.NoError(t, err) + assert.Contains(t, string(payload), `"strict":false`) +} + +func TestResponsesTool_StrictFalseIsSerialized(t *testing.T) { + strict := false + payload, err := json.Marshal(ResponsesTool{ + Type: "function", + Strict: &strict, + }) + require.NoError(t, err) + assert.JSONEq(t, `{"type":"function","strict":false}`, string(payload)) +} + +func mustMarshalJSON(t *testing.T, value any) []byte { + t.Helper() + data, err := json.Marshal(value) + require.NoError(t, err) + return data +} + func TestChatCompletionsToResponses_MaxTokens(t *testing.T) { t.Run("max_tokens", func(t *testing.T) { maxTokens := 100 diff --git a/backend/internal/pkg/apicompat/chatcompletions_to_responses.go b/backend/internal/pkg/apicompat/chatcompletions_to_responses.go index 463bdd0d15..7cbb4f5f20 100644 --- a/backend/internal/pkg/apicompat/chatcompletions_to_responses.go +++ b/backend/internal/pkg/apicompat/chatcompletions_to_responses.go @@ -419,7 +419,7 @@ func convertChatToolsToResponses(tools []ChatTool, functions []ChatFunction) []R Name: t.Function.Name, Description: t.Function.Description, Parameters: t.Function.Parameters, - Strict: t.Function.Strict, + Strict: defaultStrictFalse(t.Function.Strict), } out = append(out, rt) } @@ -431,7 +431,7 @@ func convertChatToolsToResponses(tools []ChatTool, functions []ChatFunction) []R Name: f.Name, Description: f.Description, Parameters: f.Parameters, - Strict: f.Strict, + Strict: defaultStrictFalse(f.Strict), } out = append(out, rt) } @@ -439,6 +439,14 @@ func convertChatToolsToResponses(tools []ChatTool, functions []ChatFunction) []R return out } +func defaultStrictFalse(src *bool) *bool { + if src == nil { + value := false + return &value + } + return src +} + // convertChatFunctionCallToToolChoice maps the legacy function_call field to a // Responses API tool_choice value. //