From 96265718e00d9b3e3972b3b3de1dfc51326461a4 Mon Sep 17 00:00:00 2001 From: "yangyilong.128" Date: Thu, 21 Aug 2025 19:53:52 +0800 Subject: [PATCH 1/6] chat.go: Add ExtraBody field for Gemini API configuration --- chat.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/chat.go b/chat.go index 9719f6b92..11b1701e3 100644 --- a/chat.go +++ b/chat.go @@ -257,6 +257,11 @@ type ChatCompletionRequestExtensions struct { // ensuring predictable and consistent outputs in scenarios where specific // choices are required. GuidedChoice []string `json:"guided_choice,omitempty"` + // ExtraBody provides configuration options for the generation process in Gemini API. + // Additional configuration parameters to control model behavior. Will be passed directly to the Gemini API. + // Such as thinking mode for Gemini. "extra_body": {"google": {"thinking_config": {"include_thoughts": true}}} + // https://ai.google.dev/gemini-api/docs/openai + ExtraBody map[string]any `json:"extra_body,omitempty"` } // ChatCompletionRequest represents a request structure for chat completion API. From e373ca08b331b8a085a2f1d2813ad2178cce0c98 Mon Sep 17 00:00:00 2001 From: "yangyilong.128" Date: Mon, 25 Aug 2025 10:24:12 +0800 Subject: [PATCH 2/6] Move the ExtraBody field to the ChatCompletionRequest struct --- chat.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chat.go b/chat.go index 11b1701e3..520009e53 100644 --- a/chat.go +++ b/chat.go @@ -257,11 +257,6 @@ type ChatCompletionRequestExtensions struct { // ensuring predictable and consistent outputs in scenarios where specific // choices are required. GuidedChoice []string `json:"guided_choice,omitempty"` - // ExtraBody provides configuration options for the generation process in Gemini API. - // Additional configuration parameters to control model behavior. Will be passed directly to the Gemini API. - // Such as thinking mode for Gemini. "extra_body": {"google": {"thinking_config": {"include_thoughts": true}}} - // https://ai.google.dev/gemini-api/docs/openai - ExtraBody map[string]any `json:"extra_body,omitempty"` } // ChatCompletionRequest represents a request structure for chat completion API. @@ -330,6 +325,11 @@ type ChatCompletionRequest struct { // We recommend hashing their username or email address, in order to avoid sending us any identifying information. // https://platform.openai.com/docs/api-reference/chat/create#chat_create-safety_identifier SafetyIdentifier string `json:"safety_identifier,omitempty"` + // ExtraBody provides configuration options for the generation process in Gemini API. + // Additional configuration parameters to control model behavior. Will be passed directly to the Gemini API. + // Such as thinking mode for Gemini. "extra_body": {"google": {"thinking_config": {"include_thoughts": true}}} + // https://ai.google.dev/gemini-api/docs/openai + ExtraBody map[string]any `json:"extra_body,omitempty"` // Embedded struct for non-OpenAI extensions ChatCompletionRequestExtensions } From c18b4e20687590385b9d0f816c14a4c09d815671 Mon Sep 17 00:00:00 2001 From: "yangyilong.128" Date: Fri, 29 Aug 2025 11:12:36 +0800 Subject: [PATCH 3/6] extra_body should be added to the root node of the JSON --- chat.go | 1 + 1 file changed, 1 insertion(+) diff --git a/chat.go b/chat.go index 520009e53..37d400b01 100644 --- a/chat.go +++ b/chat.go @@ -487,6 +487,7 @@ func (c *Client) CreateChatCompletion( http.MethodPost, c.fullURL(urlSuffix, withModel(request.Model)), withBody(request), + withExtraBody(request.ExtraBody), ) if err != nil { return From 2112a9d1b21ef0a07e074973406ecae068fbd19e Mon Sep 17 00:00:00 2001 From: "yangyilong.128" Date: Fri, 29 Aug 2025 12:03:54 +0800 Subject: [PATCH 4/6] feat(chat): fix ExtraBody embedding and add comprehensive tests --- chat.go | 20 +++++++++- chat_test.go | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/chat.go b/chat.go index 37d400b01..90a6db2b5 100644 --- a/chat.go +++ b/chat.go @@ -482,12 +482,28 @@ func (c *Client) CreateChatCompletion( return } + // The body map is used to dynamically construct the request payload for the chat completion API. + // Instead of relying on a fixed struct, the body map allows for flexible inclusion of fields + // based on their presence, avoiding unnecessary or empty fields in the request. + extraBody := request.ExtraBody + request.ExtraBody = nil + + // Serialize request to JSON + jsonData, err := json.Marshal(request) + if err != nil { + return + } + + // Deserialize JSON to map[string]any + var body map[string]any + _ = json.Unmarshal(jsonData, &body) + req, err := c.newRequest( ctx, http.MethodPost, c.fullURL(urlSuffix, withModel(request.Model)), - withBody(request), - withExtraBody(request.ExtraBody), + withBody(body), // Main request body. + withExtraBody(extraBody), // Merge ExtraBody fields. ) if err != nil { return diff --git a/chat_test.go b/chat_test.go index 236cff736..93ffa04fa 100644 --- a/chat_test.go +++ b/chat_test.go @@ -756,6 +756,111 @@ func TestChatCompletionsFunctions(t *testing.T) { }) } +func TestChatCompletionsWithExtraBody(t *testing.T) { + client, server, teardown := setupOpenAITestServer() + defer teardown() + + // Register a custom handler that checks if ExtraBody fields are properly embedded + server.RegisterHandler("/v1/chat/completions", func(w http.ResponseWriter, r *http.Request) { + // Read the request body + reqBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "could not read request", http.StatusInternalServerError) + return + } + + // Parse the request body into a map to check all fields + var requestBody map[string]any + if err := json.Unmarshal(reqBody, &requestBody); err != nil { + http.Error(w, fmt.Sprintf("could not parse request: %v, body: %s", err, string(reqBody)), http.StatusInternalServerError) + return + } + + // Check that ExtraBody fields are present in the root level + if _, exists := requestBody["custom_field"]; !exists { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "custom_field not found in request body"}) + return + } + + if _, exists := requestBody["another_field"]; !exists { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "another_field not found in request body"}) + return + } + + // Check that regular fields are still present + if _, exists := requestBody["model"]; !exists { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "model not found in request body"}) + return + } + + if _, exists := requestBody["messages"]; !exists { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "messages not found in request body"}) + return + } + + // ExtraBody should not be present in the final request + if _, exists := requestBody["extra_body"]; exists { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "extra_body should not be present in final request"}) + return + } + + // Return a success response + res := openai.ChatCompletionResponse{ + ID: "test-id", + Object: "chat.completion", + Created: time.Now().Unix(), + Model: "gpt-3.5-turbo", + Choices: []openai.ChatCompletionChoice{ + { + Index: 0, + Message: openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleAssistant, + Content: "Hello!", + }, + FinishReason: openai.FinishReasonStop, + }, + }, + Usage: openai.Usage{ + PromptTokens: 5, + CompletionTokens: 5, + TotalTokens: 10, + }, + } + + resBytes, _ := json.Marshal(res) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(resBytes) + }) + + // Test the ExtraBody functionality + _, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{ + Model: openai.GPT3Dot5Turbo, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleUser, + Content: "Hello!", + }, + }, + ExtraBody: map[string]any{ + "custom_field": "custom_value", + "another_field": 123, + }, + }) + + checks.NoError(t, err, "CreateChatCompletion with ExtraBody error") +} + func TestAzureChatCompletions(t *testing.T) { client, server, teardown := setupAzureTestServer() defer teardown() From 095ee5f3b6ce2f67acf53c5f8e0a3fcc874fdc0d Mon Sep 17 00:00:00 2001 From: "yangyilong.128" Date: Fri, 29 Aug 2025 13:39:09 +0800 Subject: [PATCH 5/6] style(chat_test): improve error handling and code formatting --- chat_test.go | 64 ++++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/chat_test.go b/chat_test.go index 93ffa04fa..b36bcb009 100644 --- a/chat_test.go +++ b/chat_test.go @@ -759,7 +759,7 @@ func TestChatCompletionsFunctions(t *testing.T) { func TestChatCompletionsWithExtraBody(t *testing.T) { client, server, teardown := setupOpenAITestServer() defer teardown() - + // Register a custom handler that checks if ExtraBody fields are properly embedded server.RegisterHandler("/v1/chat/completions", func(w http.ResponseWriter, r *http.Request) { // Read the request body @@ -768,52 +768,42 @@ func TestChatCompletionsWithExtraBody(t *testing.T) { http.Error(w, "could not read request", http.StatusInternalServerError) return } - + // Parse the request body into a map to check all fields var requestBody map[string]any - if err := json.Unmarshal(reqBody, &requestBody); err != nil { - http.Error(w, fmt.Sprintf("could not parse request: %v, body: %s", err, string(reqBody)), http.StatusInternalServerError) + err = json.Unmarshal(reqBody, &requestBody) + if err != nil { + http.Error( + w, + fmt.Sprintf("could not parse request: %v, body: %s", err, string(reqBody)), http.StatusInternalServerError, + ) return } - + // Check that ExtraBody fields are present in the root level if _, exists := requestBody["custom_field"]; !exists { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": "custom_field not found in request body"}) - return - } - - if _, exists := requestBody["another_field"]; !exists { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": "another_field not found in request body"}) - return - } - - // Check that regular fields are still present - if _, exists := requestBody["model"]; !exists { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": "model not found in request body"}) - return - } - - if _, exists := requestBody["messages"]; !exists { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": "messages not found in request body"}) + err = json.NewEncoder(w).Encode(map[string]string{"error": "custom_field not found in request body"}) + if err != nil { + http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) + return + } return } - + // ExtraBody should not be present in the final request if _, exists := requestBody["extra_body"]; exists { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": "extra_body should not be present in final request"}) + err = json.NewEncoder(w).Encode(map[string]string{"error": "extra_body should not be present in final request"}) + if err != nil { + http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) + return + } return } - + // Return a success response res := openai.ChatCompletionResponse{ ID: "test-id", @@ -836,13 +826,17 @@ func TestChatCompletionsWithExtraBody(t *testing.T) { TotalTokens: 10, }, } - + resBytes, _ := json.Marshal(res) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write(resBytes) + _, err = w.Write(resBytes) + if err != nil { + http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) + return + } }) - + // Test the ExtraBody functionality _, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{ Model: openai.GPT3Dot5Turbo, @@ -857,7 +851,7 @@ func TestChatCompletionsWithExtraBody(t *testing.T) { "another_field": 123, }, }) - + checks.NoError(t, err, "CreateChatCompletion with ExtraBody error") } From a727da489adca0605e2fe8162ebd249ac765b4e7 Mon Sep 17 00:00:00 2001 From: "yangyilong.128" Date: Tue, 2 Sep 2025 21:17:32 +0800 Subject: [PATCH 6/6] Support merging extra_body fields into JSON root for streaming chat completion requests --- chat_stream.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/chat_stream.go b/chat_stream.go index 80d16cc63..56d2b355e 100644 --- a/chat_stream.go +++ b/chat_stream.go @@ -2,6 +2,7 @@ package openai import ( "context" + "encoding/json" "net/http" ) @@ -91,11 +92,28 @@ func (c *Client) CreateChatCompletionStream( return } + // The body map is used to dynamically construct the request payload for the chat completion API. + // Instead of relying on a fixed struct, the body map allows for flexible inclusion of fields + // based on their presence, avoiding unnecessary or empty fields in the request. + extraBody := request.ExtraBody + request.ExtraBody = nil + + // Serialize request to JSON + jsonData, err := json.Marshal(request) + if err != nil { + return + } + + // Deserialize JSON to map[string]any + var body map[string]any + _ = json.Unmarshal(jsonData, &body) + req, err := c.newRequest( ctx, http.MethodPost, c.fullURL(urlSuffix, withModel(request.Model)), - withBody(request), + withBody(body), // Main request body. + withExtraBody(extraBody), // Merge ExtraBody fields. ) if err != nil { return nil, err