Repository: sashabaranov/go-openai
Pull request #828 (2024-08-16 15:48:30)

Allow structured outputs via function calling
**Describe the change**

Allows `strict: true` to be passed to OpenAI within a function's definition, triggering OpenAI's new [structured outputs](https://openai.com/index/introducing-structured-outputs-in-the-api/) setup to ensure the function's schema is strictly adhered to.

**Provide OpenAI documentation link**

https://openai.com/index/introducing-structured-outputs-in-the-api/

**Describe your solution**

Add `Strict` to the properties of `FunctionDefinition`. Only serialise it in the JSON if its value is `true` (to avoid wasting tokens).

**Tests**
Added a test.

**Additional context**
None

greysteil: @sashabaranov the linting error on my first commit wasn't related to this PR. I've fixed it in the second commit since it was a trivial one.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/828?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.88%. Comparing base [(`774fc9d`)](https://app.codecov.io/gh/sashabaranov/go-openai/commit/774fc9dd12ed60c10a9f9f03319ddb9cd5f8780c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov) to head [(`9dba431`)](https://app.codecov.io/gh/sashabaranov/go-openai/commit/9dba43174865b651d43ce9a2926cfee2d325847c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov).
> Report is 39 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #828      +/-   ##
==========================================
+ Coverage   98.46%   98.88%   +0.42%     
==========================================
  Files          24       26       +2     
  Lines        1364     1347      -17     
==========================================
- Hits         1343     1332      -11     
+ Misses         15        9       -6     
  Partials        6        6              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/sashabaranov/go-openai/pull/828?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov).

sashabaranov: @greysteil thank you for the PR! Could you please fix the merge conflict and add an integration test — it's pretty hard to follow all the OpenAI API updates these days
greysteil: @sashabaranov done!

Patch:
diff --git a/chat.go b/chat.go
index 826fd3b..97c89a4 100644
--- a/chat.go
+++ b/chat.go
@@ -264,6 +264,7 @@ type ToolFunction struct {
 type FunctionDefinition struct {
 	Name        string `json:"name"`
 	Description string `json:"description,omitempty"`
+	Strict      bool   `json:"strict,omitempty"`
 	// Parameters is an object describing the function.
 	// You can pass json.RawMessage to describe the schema,
 	// or you can pass in a struct which serializes to the proper JSON schema.


Test patch:
diff --git a/api_integration_test.go b/api_integration_test.go
index 3084268..57f7c40 100644
--- a/api_integration_test.go
+++ b/api_integration_test.go
@@ -239,3 +239,79 @@ func TestChatCompletionResponseFormat_JSONSchema(t *testing.T) {
 		}
 	}
 }
+
+func TestChatCompletionStructuredOutputsFunctionCalling(t *testing.T) {
+	apiToken := os.Getenv("OPENAI_TOKEN")
+	if apiToken == "" {
+		t.Skip("Skipping testing against production OpenAI API. Set OPENAI_TOKEN environment variable to enable it.")
+	}
+
+	var err error
+	c := openai.NewClient(apiToken)
+	ctx := context.Background()
+
+	resp, err := c.CreateChatCompletion(
+		ctx,
+		openai.ChatCompletionRequest{
+			Model: openai.GPT4oMini,
+			Messages: []openai.ChatCompletionMessage{
+				{
+					Role: openai.ChatMessageRoleSystem,
+					Content: "Please enter a string, and we will convert it into the following naming conventions:" +
+						"1. PascalCase: Each word starts with an uppercase letter, with no spaces or separators." +
+						"2. CamelCase: The first word starts with a lowercase letter, " +
+						"and subsequent words start with an uppercase letter, with no spaces or separators." +
+						"3. KebabCase: All letters are lowercase, with words separated by hyphens `-`." +
+						"4. SnakeCase: All letters are lowercase, with words separated by underscores `_`.",
+				},
+				{
+					Role:    openai.ChatMessageRoleUser,
+					Content: "Hello World",
+				},
+			},
+			Tools: []openai.Tool{
+				{
+					Type: openai.ToolTypeFunction,
+					Function: &openai.FunctionDefinition{
+						Name:   "display_cases",
+						Strict: true,
+						Parameters: &jsonschema.Definition{
+							Type: jsonschema.Object,
+							Properties: map[string]jsonschema.Definition{
+								"PascalCase": {
+									Type: jsonschema.String,
+								},
+								"CamelCase": {
+									Type: jsonschema.String,
+								},
+								"KebabCase": {
+									Type: jsonschema.String,
+								},
+								"SnakeCase": {
+									Type: jsonschema.String,
+								},
+							},
+							Required:             []string{"PascalCase", "CamelCase", "KebabCase", "SnakeCase"},
+							AdditionalProperties: false,
+						},
+					},
+				},
+			},
+			ToolChoice: openai.ToolChoice{
+				Type: openai.ToolTypeFunction,
+				Function: openai.ToolFunction{
+					Name: "display_cases",
+				},
+			},
+		},
+	)
+	checks.NoError(t, err, "CreateChatCompletion (use structured outputs response) returned error")
+	var result = make(map[string]string)
+	err = json.Unmarshal([]byte(resp.Choices[0].Message.ToolCalls[0].Function.Arguments), &result)
+	checks.NoError(t, err, "CreateChatCompletion (use structured outputs response) unmarshal error")
+	for _, key := range []string{"PascalCase", "CamelCase", "KebabCase", "SnakeCase"} {
+		if _, ok := result[key]; !ok {
+			t.Errorf("key:%s does not exist.", key)
+		}
+	}
+}
diff --git a/chat_test.go b/chat_test.go
index 520bf5c..37dc09d 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -277,6 +277,32 @@ func TestChatCompletionsFunctions(t *testing.T) {
 		})
 		checks.NoError(t, err, "CreateChatCompletion with functions error")
 	})
+	t.Run("StructuredOutputs", func(t *testing.T) {
+		type testMessage struct {
+			Count int      `json:"count"`
+			Words []string `json:"words"`
+		}
+		msg := testMessage{
+			Count: 2,
+			Words: []string{"hello", "world"},
+		}
+		_, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
+			MaxTokens: 5,
+			Model:     openai.GPT3Dot5Turbo0613,
+			Messages: []openai.ChatCompletionMessage{
+				{
+					Role:    openai.ChatMessageRoleUser,
+					Content: "Hello!",
+				},
+			},
+			Functions: []openai.FunctionDefinition{{
+				Name:       "test",
+				Strict:     true,
+				Parameters: &msg,
+			}},
+		})
+		checks.NoError(t, err, "CreateChatCompletion with functions error")
+	})
 }
 
 func TestAzureChatCompletions(t *testing.T) {