fix(apicompat): default tool strict to false

This commit is contained in:
wucm667
2026-06-12 15:01:58 +08:00
parent e34ad2b194
commit edfd5e3736
2 changed files with 95 additions and 2 deletions
@@ -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
@@ -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.
//