Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,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
}
Expand Down Expand Up @@ -477,11 +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.
Comment on lines +485 to +487
Copy link
Preview

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment describes general benefits of using a map but doesn't explain the specific purpose of this code block, which is to handle ExtraBody field merging. Consider updating to: 'Create a dynamic request body by merging ExtraBody fields into the main request payload.'

Suggested change
// 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.
// Create a dynamic request body by merging ExtraBody fields into the main request payload.
// This approach allows flexible inclusion of additional fields, ensuring the request contains all necessary data.

Copilot uses AI. Check for mistakes.

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)
Copy link
Preview

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The json.Unmarshal error is being ignored with blank identifier. This could cause silent failures if the JSON unmarshaling fails. The error should be checked and returned.

Suggested change
_ = json.Unmarshal(jsonData, &body)
err = json.Unmarshal(jsonData, &body)
if err != nil {
return
}

Copilot uses AI. Check for mistakes.


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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you are using this correctly: withExtraBody merges extraBody into request body. However, in the example you've mentioned it goes as a separate field:

curl "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer GEMINI_API_KEY" \
-d '{
    "model": "gemini-2.5-flash",
      "messages": [{"role": "user", "content": "Explain to me how AI works"}],
      "extra_body": {
        "google": {
           "thinking_config": {
             "include_thoughts": true
           }
        }
      }
    }'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your inspection. I have seen it working correctly during testing and use. For example:

_, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
	Model: "doubao-seed-1-6-250615",
	Messages: []openai.ChatCompletionMessage{
		{
			Role:    openai.ChatMessageRoleUser,
			Content: "Hello!",
		},
	},
	ExtraBody: map[string]any{
		"thinking":map[string]any {
			"type": "disabled",
		},
	},
})

After the request is processed as the body, it will change to:

{
    "model": "doubao-seed-1-6-250615",
    "messages": [{"role": "user", "content": "Hello!"}],
    "thinking": {
        "type": "disabled"
    }
}

For the gemini example, I'm sorry that I didn't give a clear example that caused your misunderstanding. In Gemini's official API, they have additional support for extra_body fields. Let me explain the example call in the documentation:

from openai import OpenAI

client = OpenAI(
    api_key="GEMINI_API_KEY",
    base_url="https://generativelanguage.googleapis.com/v1beta/openai/"
)

response = client.chat.completions.create(
    model="gemini-2.5-flash",
    messages=[{"role": "user", "content": "Explain to me how AI works"}],
    extra_body={
      'extra_body': {
        "google": {
          "thinking_config": {
            "thinking_budget": 800,
            "include_thoughts": True
          }
        }
      }
    }
)

They use OpenAI's official python library OpenAI. In OpenAI extra_body will be promoted to root. The final body is:

{
    "messages": [
        {
            "role": "user",
            "content": "Explain to me how AI works"
        }
    ],
    "model": "gemini-2.5-flash",
    "extra_body": {
        "google": {
            "thinking_config": {
                "thinking_budget": 800,
                "include_thoughts": true
            }
        }
    }
}

So Gemini's example is actually to ensure that extra_body is still under extra_body after being extracted to root, so there is an extra layer of extra_body.
Like the example in #1025, the requirement is to extract the contents of extra_body to the root.
I put a sample code here, which can print out the processed body, you can test it:

import httpx
from openai import OpenAI


def log_request(request):
    print("\n===== HTTP Request Details ======")
    print(f"Method: {request.method}")
    print(f"URL: {request.url}")
    print("Headers:")
    for key, value in request.headers.items():
        print(f"  {key}: {value}")
    print("Body:")
    print(request.content.decode())
    print("================================\n")


client = OpenAI(
    api_key="GEMINI_API_KEY",
    base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
    http_client=httpx.Client(event_hooks={"request": [log_request]}),
)

response = client.chat.completions.create(
    model="gemini-2.5-flash",
    messages=[{"role": "user", "content": "Explain to me how AI works"}],
    extra_body={
        'extra_body': {
          "google": {
              "thinking_config": {
                  "thinking_budget": 800,
                  "include_thoughts": True
              }
          }
        }
    }
)

print(response)

)
if err != nil {
return
Expand Down
20 changes: 19 additions & 1 deletion chat_stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package openai

import (
"context"
"encoding/json"
"net/http"
)

Expand Down Expand Up @@ -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
Expand Down
99 changes: 99 additions & 0 deletions chat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,105 @@ 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
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)
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)
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",
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)
_, 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,
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()
Expand Down
Loading