From 8510736a1fe78a302abdb8c32e43973194cad2fd Mon Sep 17 00:00:00 2001 From: Jiale Lin Date: Sun, 19 Jul 2026 16:49:22 -0700 Subject: [PATCH] feat(openai): support object-type FunctionCallBehavior for forcing a specific function The OpenAI API allows function_call to be either a string ("none", "auto") or an object ({"name": "my_function"}) to force the model to call a particular function. Previously, FunctionCallBehavior was typed as a string and could not represent the object form. Changes: - Change ChatRequest.FunctionCallBehavior from FunctionCallBehavior (string) to any in openaiclient, enabling both string and object forms - Change CallOptions.FunctionCallBehavior from FunctionCallBehavior to any in the llms package for the same reason - Add FunctionCallBehaviorFunctionObject struct with Name field - Add WithFunctionCallBehaviorFunction(name string) CallOption that sets function_call to {"name": ""} in the OpenAI request - Remove now-unnecessary type cast in openaillm.go - Add unit tests for the new option and JSON serialization of all forms Existing string-valued behavior ("none", "auto", unset) is unchanged. The deprecated function_call field is superseded by tool_choice in the modern OpenAI API; users should prefer WithToolChoice for new code. Closes #254 --- llms/openai/internal/openaiclient/chat.go | 4 +- .../openai/internal/openaiclient/chat_test.go | 52 +++++++++++++++++++ llms/openai/openaillm.go | 2 +- llms/options.go | 25 +++++++-- llms/options_test.go | 28 ++++++++++ 5 files changed, 105 insertions(+), 6 deletions(-) diff --git a/llms/openai/internal/openaiclient/chat.go b/llms/openai/internal/openaiclient/chat.go index 991c236ab..0067bfd5f 100644 --- a/llms/openai/internal/openaiclient/chat.go +++ b/llms/openai/internal/openaiclient/chat.go @@ -79,7 +79,9 @@ type ChatRequest struct { // Deprecated: use Tools instead. Functions []FunctionDefinition `json:"functions,omitempty"` // Deprecated: use ToolChoice instead. - FunctionCallBehavior FunctionCallBehavior `json:"function_call,omitempty"` + // FunctionCallBehavior can be a string ("none", "auto") or an object {"name": "my_function"} + // to force the model to call a specific function. + FunctionCallBehavior any `json:"function_call,omitempty"` // Metadata allows you to specify additional information that will be passed to the model. Metadata map[string]any `json:"metadata,omitempty"` diff --git a/llms/openai/internal/openaiclient/chat_test.go b/llms/openai/internal/openaiclient/chat_test.go index 0a26725b2..581103bad 100644 --- a/llms/openai/internal/openaiclient/chat_test.go +++ b/llms/openai/internal/openaiclient/chat_test.go @@ -106,6 +106,58 @@ func TestChatMessage_MarshalUnmarshal(t *testing.T) { require.Equal(t, msg, msg2) } +func TestChatRequest_FunctionCallBehavior_Marshal(t *testing.T) { + t.Parallel() + + t.Run("string none", func(t *testing.T) { + req := ChatRequest{ + Model: "gpt-3.5-turbo", + FunctionCallBehavior: FunctionCallBehaviorNone, + } + b, err := json.Marshal(req) + require.NoError(t, err) + var m map[string]json.RawMessage + require.NoError(t, json.Unmarshal(b, &m)) + assert.Equal(t, `"none"`, string(m["function_call"])) + }) + + t.Run("string auto", func(t *testing.T) { + req := ChatRequest{ + Model: "gpt-3.5-turbo", + FunctionCallBehavior: FunctionCallBehaviorAuto, + } + b, err := json.Marshal(req) + require.NoError(t, err) + var m map[string]json.RawMessage + require.NoError(t, json.Unmarshal(b, &m)) + assert.Equal(t, `"auto"`, string(m["function_call"])) + }) + + t.Run("object form forces specific function", func(t *testing.T) { + req := ChatRequest{ + Model: "gpt-3.5-turbo", + FunctionCallBehavior: struct { + Name string `json:"name"` + }{Name: "my_function"}, + } + b, err := json.Marshal(req) + require.NoError(t, err) + var m map[string]json.RawMessage + require.NoError(t, json.Unmarshal(b, &m)) + assert.Equal(t, `{"name":"my_function"}`, string(m["function_call"])) + }) + + t.Run("unset is omitted", func(t *testing.T) { + req := ChatRequest{Model: "gpt-3.5-turbo"} + b, err := json.Marshal(req) + require.NoError(t, err) + var m map[string]json.RawMessage + require.NoError(t, json.Unmarshal(b, &m)) + _, present := m["function_call"] + assert.False(t, present, "function_call should be omitted when unset") + }) +} + func TestChatMessage_MarshalUnmarshal_WithReasoning(t *testing.T) { t.Parallel() msg := ChatMessage{ diff --git a/llms/openai/openaillm.go b/llms/openai/openaillm.go index 84690072a..ae5b1c1cc 100644 --- a/llms/openai/openaillm.go +++ b/llms/openai/openaillm.go @@ -290,7 +290,7 @@ func (o *LLM) GenerateContent(ctx context.Context, messages []llms.MessageConten }(), ToolChoice: opts.ToolChoice, - FunctionCallBehavior: openaiclient.FunctionCallBehavior(opts.FunctionCallBehavior), + FunctionCallBehavior: opts.FunctionCallBehavior, Seed: opts.Seed, Metadata: apiMetadata, WebSearchOptions: webSearchOptionsFromCallOptions(opts.WebSearchOptions), diff --git a/llms/options.go b/llms/options.go index 87b35359c..268c2f8e9 100644 --- a/llms/options.go +++ b/llms/options.go @@ -55,11 +55,10 @@ type CallOptions struct { // Deprecated: Use Tools instead. Functions []FunctionDefinition `json:"functions,omitempty"` // FunctionCallBehavior is the behavior to use when calling functions. - // - // If a specific function should be invoked, use the format: - // `{"name": "my_function"}` + // It can be a FunctionCallBehavior string ("none", "auto") or a + // FunctionCallBehaviorFunctionObject to force calling a specific function. // Deprecated: Use ToolChoice instead. - FunctionCallBehavior FunctionCallBehavior `json:"function_call,omitempty"` + FunctionCallBehavior any `json:"function_call,omitempty"` // Metadata is a map of metadata to include in the request. // The meaning of this field is specific to the backend in use. @@ -153,6 +152,13 @@ const ( FunctionCallBehaviorAuto FunctionCallBehavior = "auto" ) +// FunctionCallBehaviorFunctionObject forces the model to call the named function. +// Use with WithFunctionCallBehaviorFunction. +// Deprecated: Use ToolChoice with a ToolChoice object instead. +type FunctionCallBehaviorFunctionObject struct { + Name string `json:"name"` +} + // WithModel specifies which model name to use. func WithModel(model string) CallOption { return func(o *CallOptions) { @@ -274,6 +280,8 @@ func WithPresencePenalty(presencePenalty float64) CallOption { } // WithFunctionCallBehavior will add an option to set the behavior to use when calling functions. +// Accepts FunctionCallBehaviorNone or FunctionCallBehaviorAuto. +// To force a specific function, use WithFunctionCallBehaviorFunction instead. // Deprecated: Use WithToolChoice instead. func WithFunctionCallBehavior(behavior FunctionCallBehavior) CallOption { return func(o *CallOptions) { @@ -281,6 +289,15 @@ func WithFunctionCallBehavior(behavior FunctionCallBehavior) CallOption { } } +// WithFunctionCallBehaviorFunction forces the model to call the named function. +// This sets function_call to {"name": ""} in the OpenAI API request. +// Deprecated: Use WithToolChoice with a ToolChoice object instead. +func WithFunctionCallBehaviorFunction(name string) CallOption { + return func(o *CallOptions) { + o.FunctionCallBehavior = FunctionCallBehaviorFunctionObject{Name: name} + } +} + // WithFunctions will add an option to set the functions to include in the request. // Deprecated: Use WithTools instead. func WithFunctions(functions []FunctionDefinition) CallOption { diff --git a/llms/options_test.go b/llms/options_test.go index 86cc88fcb..9fee6aa7c 100644 --- a/llms/options_test.go +++ b/llms/options_test.go @@ -408,6 +408,34 @@ func TestDeprecatedFunctionOptions(t *testing.T) { }) } +func TestWithFunctionCallBehaviorFunction(t *testing.T) { + t.Run("sets object form", func(t *testing.T) { + var opts llms.CallOptions + llms.WithFunctionCallBehaviorFunction("my_function")(&opts) + + want := llms.FunctionCallBehaviorFunctionObject{Name: "my_function"} + if !reflect.DeepEqual(opts.FunctionCallBehavior, want) { + t.Errorf("FunctionCallBehavior = %v, want %v", opts.FunctionCallBehavior, want) + } + }) + + t.Run("unset is nil", func(t *testing.T) { + var opts llms.CallOptions + if opts.FunctionCallBehavior != nil { + t.Errorf("default FunctionCallBehavior = %v, want nil", opts.FunctionCallBehavior) + } + }) + + t.Run("string and object forms are distinct", func(t *testing.T) { + var opts llms.CallOptions + llms.WithFunctionCallBehaviorFunction("get_weather")(&opts) + + if opts.FunctionCallBehavior == llms.FunctionCallBehaviorAuto { + t.Error("object-form FunctionCallBehavior should not equal string-form auto") + } + }) +} + func TestMultipleOptions(t *testing.T) { var opts llms.CallOptions