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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@
.idea

# Generated by tests
test.mp3
test.mp3

go.sum
86 changes: 75 additions & 11 deletions chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ const (
ChatMessageRoleDeveloper = "developer"
)

const (
ReasoningEffortHigh = "high"
ReasoningEffortMedium = "medium"
ReasoningEffortLow = "low"
ReasoningEffortMinimal = "minimal"
)

const chatCompletionsSuffix = "/chat/completions"

var (
Expand Down Expand Up @@ -92,13 +99,23 @@ type ChatMessagePart struct {
Type ChatMessagePartType `json:"type,omitempty"`
Text string `json:"text,omitempty"`
ImageURL *ChatMessageImageURL `json:"image_url,omitempty"`
// ExtraPart is a vendor-specific extension container. For example, Vertex AI's
// OpenAI-compatible endpoint returns thought signatures inside
// content parts under extra_part.google.thought_signature.
ExtraPart map[string]any `json:"extra_part,omitempty"`
// ExtraContent carries vendor-specific data that would otherwise be stripped by
// the OpenAI-compatible schema. Vertex AI allows per-part extra_content.
ExtraContent map[string]any `json:"extra_content,omitempty"`
}

type ChatCompletionMessage struct {
Role string `json:"role"`
Content string `json:"content,omitempty"`
Refusal string `json:"refusal,omitempty"`
MultiContent []ChatMessagePart
// ExtraContent is a vendor-specific extension container. For example, Vertex AI
// uses extra_content to return additional metadata alongside content arrays.
ExtraContent map[string]any `json:"extra_content,omitempty"`

// This property isn't in the official documentation, but it's in
// the documentation for the official library for python:
Expand Down Expand Up @@ -128,15 +145,25 @@ func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) {
if len(m.MultiContent) > 0 {
msg := struct {
Role string `json:"role"`
Content string `json:"-"`
Refusal string `json:"refusal,omitempty"`
MultiContent []ChatMessagePart `json:"content,omitempty"`
Name string `json:"name,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}(m)
ExtraContent map[string]any `json:"extra_content,omitempty"`
}{
Role: m.Role,
Refusal: m.Refusal,
MultiContent: m.MultiContent,
Name: m.Name,
ReasoningContent: m.ReasoningContent,
FunctionCall: m.FunctionCall,
ToolCalls: m.ToolCalls,
ToolCallID: m.ToolCallID,
ExtraContent: m.ExtraContent,
}
return json.Marshal(msg)
}

Expand All @@ -150,7 +177,18 @@ func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) {
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}(m)
ExtraContent map[string]any `json:"extra_content,omitempty"`
}{
Role: m.Role,
Content: m.Content,
Refusal: m.Refusal,
Name: m.Name,
ReasoningContent: m.ReasoningContent,
FunctionCall: m.FunctionCall,
ToolCalls: m.ToolCalls,
ToolCallID: m.ToolCallID,
ExtraContent: m.ExtraContent,
}
return json.Marshal(msg)
}

Expand All @@ -160,15 +198,25 @@ func (m *ChatCompletionMessage) UnmarshalJSON(bs []byte) error {
Content string `json:"content"`
Refusal string `json:"refusal,omitempty"`
MultiContent []ChatMessagePart
Name string `json:"name,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
Name string `json:"name,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
ExtraContent map[string]any `json:"extra_content,omitempty"`
}{}

if err := json.Unmarshal(bs, &msg); err == nil {
*m = ChatCompletionMessage(msg)
m.Role = msg.Role
m.Content = msg.Content
m.Refusal = msg.Refusal
m.MultiContent = msg.MultiContent
m.Name = msg.Name
m.ReasoningContent = msg.ReasoningContent
m.FunctionCall = msg.FunctionCall
m.ToolCalls = msg.ToolCalls
m.ToolCallID = msg.ToolCallID
m.ExtraContent = msg.ExtraContent
return nil
}
multiMsg := struct {
Expand All @@ -181,11 +229,21 @@ func (m *ChatCompletionMessage) UnmarshalJSON(bs []byte) error {
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
ExtraContent map[string]any `json:"extra_content,omitempty"`
}{}
if err := json.Unmarshal(bs, &multiMsg); err != nil {
return err
}
*m = ChatCompletionMessage(multiMsg)
m.Role = multiMsg.Role
m.Content = multiMsg.Content
m.Refusal = multiMsg.Refusal
m.MultiContent = multiMsg.MultiContent
m.Name = multiMsg.Name
m.ReasoningContent = multiMsg.ReasoningContent
m.FunctionCall = multiMsg.FunctionCall
m.ToolCalls = multiMsg.ToolCalls
m.ToolCallID = multiMsg.ToolCallID
m.ExtraContent = multiMsg.ExtraContent
return nil
}

Expand All @@ -195,6 +253,9 @@ type ToolCall struct {
ID string `json:"id,omitempty"`
Type ToolType `json:"type"`
Function FunctionCall `json:"function"`
// ExtraContent preserves provider-specific metadata (e.g. thought signatures)
// attached to tool calls, as used by Vertex AI.
ExtraContent map[string]any `json:"extra_content,omitempty"`
}

type FunctionCall struct {
Expand Down Expand Up @@ -307,7 +368,7 @@ type ChatCompletionRequest struct {
// Store can be set to true to store the output of this completion request for use in distillations and evals.
// https://platform.openai.com/docs/api-reference/chat/create#chat-create-store
Store bool `json:"store,omitempty"`
// Controls effort on reasoning for reasoning models. It can be set to "low", "medium", or "high".
// Controls effort on reasoning for reasoning models. It can be set to "low", "medium", "high" or "minimal".
ReasoningEffort string `json:"reasoning_effort,omitempty"`
// Metadata to store with the completion.
Metadata map[string]string `json:"metadata,omitempty"`
Expand All @@ -333,6 +394,9 @@ type ChatCompletionRequest struct {
SafetyIdentifier string `json:"safety_identifier,omitempty"`
// Embedded struct for non-OpenAI extensions
ChatCompletionRequestExtensions
// ExtraBody lets provider-specific fields pass through. Vertex AI uses
// extra_body.google.* for features like thinking_config and safety_settings.
ExtraBody map[string]any `json:"extra_body,omitempty"`
}

type StreamOptions struct {
Expand Down
87 changes: 82 additions & 5 deletions chat_stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ package openai

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

type ChatCompletionStreamChoiceDelta struct {
Content string `json:"content,omitempty"`
Role string `json:"role,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Refusal string `json:"refusal,omitempty"`
Content string `json:"content,omitempty"`
MultiContent []ChatMessagePart `json:"-"`
Role string `json:"role,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Refusal string `json:"refusal,omitempty"`

// This property is used for the "reasoning" feature supported by deepseek-reasoner
// which is not in the official documentation.
Expand All @@ -19,6 +21,81 @@ type ChatCompletionStreamChoiceDelta struct {
ReasoningContent string `json:"reasoning_content,omitempty"`
}

func (d *ChatCompletionStreamChoiceDelta) UnmarshalJSON(bs []byte) error {
// Probe the content type using json.RawMessage to support both string and array payloads.
type probe struct {
Content json.RawMessage `json:"content,omitempty"`
Role string `json:"role,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Refusal string `json:"refusal,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
}

var p probe
if err := json.Unmarshal(bs, &p); err != nil {
return err
}

// No content — simply copy scalar fields.
if len(p.Content) == 0 {
*d = ChatCompletionStreamChoiceDelta{
Role: p.Role,
FunctionCall: p.FunctionCall,
ToolCalls: p.ToolCalls,
Refusal: p.Refusal,
ReasoningContent: p.ReasoningContent,
}
return nil
}

// content is a JSON string.
if len(p.Content) > 0 && p.Content[0] == '"' {
var r struct {
Content string `json:"content,omitempty"`
Role string `json:"role,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Refusal string `json:"refusal,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
}
if err := json.Unmarshal(bs, &r); err != nil {
return err
}
*d = ChatCompletionStreamChoiceDelta{
Content: r.Content,
Role: r.Role,
FunctionCall: r.FunctionCall,
ToolCalls: r.ToolCalls,
Refusal: r.Refusal,
ReasoningContent: r.ReasoningContent,
}
return nil
}

// Otherwise treat content as an array of parts.
var a struct {
MultiContent []ChatMessagePart `json:"content,omitempty"`
Role string `json:"role,omitempty"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Refusal string `json:"refusal,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
}
if err := json.Unmarshal(bs, &a); err != nil {
return err
}
*d = ChatCompletionStreamChoiceDelta{
MultiContent: a.MultiContent,
Role: a.Role,
FunctionCall: a.FunctionCall,
ToolCalls: a.ToolCalls,
Refusal: a.Refusal,
ReasoningContent: a.ReasoningContent,
}
return nil
}

type ChatCompletionStreamChoiceLogprobs struct {
Content []ChatCompletionTokenLogprob `json:"content,omitempty"`
Refusal []ChatCompletionTokenLogprob `json:"refusal,omitempty"`
Expand Down
69 changes: 69 additions & 0 deletions chat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1205,3 +1205,72 @@ func TestChatCompletionRequest_UnmarshalJSON(t *testing.T) {
})
}
}

func TestChatCompletionMessageExtraFields(t *testing.T) {
raw := []byte(`{
"role":"assistant",
"content":[{"type":"text","text":"Hi","extra_part":{"google":{"thought_signature":"sig"}},"extra_content":{"google":{"foo":"bar"}}}],
"extra_content":{"google":{"safety_rationale":"ok"}},
"tool_calls":[{"id":"call_1","type":"function","function":{"name":"f","arguments":"{}"},"extra_content":{"google":{"thought_signature":"sig2"}}}]
}`)

var msg openai.ChatCompletionMessage
if err := json.Unmarshal(raw, &msg); err != nil {
t.Fatalf("unmarshal failed: %v", err)
}

if len(msg.MultiContent) != 1 {
t.Fatalf("expected 1 content part, got %d", len(msg.MultiContent))
}
if msg.MultiContent[0].ExtraPart == nil || msg.MultiContent[0].ExtraPart["google"] == nil {
t.Fatalf("expected extra_part to round-trip")
}
if msg.ExtraContent == nil || msg.ExtraContent["google"] == nil {
t.Fatalf("expected message extra_content to round-trip")
}
if len(msg.ToolCalls) != 1 || msg.ToolCalls[0].ExtraContent == nil {
t.Fatalf("expected tool call extra_content to round-trip")
}

out, err := json.Marshal(msg)
if err != nil {
t.Fatalf("marshal failed: %v", err)
}
if !strings.Contains(string(out), "extra_part") || !strings.Contains(string(out), "extra_content") {
t.Fatalf("marshal did not preserve extra fields: %s", string(out))
}
}

func TestChatCompletionRequestExtraBodyMarshal(t *testing.T) {
req := openai.ChatCompletionRequest{
Model: openai.Gemini3FlashPreview,
Messages: []openai.ChatCompletionMessage{{
Role: openai.ChatMessageRoleUser,
Content: "hello",
}},
ExtraBody: map[string]any{
"google": map[string]any{
"include_thoughts": true,
},
},
}

out, err := json.Marshal(req)
if err != nil {
t.Fatalf("marshal failed: %v", err)
}
if !strings.Contains(string(out), `"extra_body"`) {
t.Fatalf("expected extra_body in marshaled JSON, got: %s", string(out))
}
}

func TestChatCompletionStreamChoiceDeltaMultiContent(t *testing.T) {
raw := []byte(`{"content":[{"type":"text","text":"Hello","extra_part":{"google":{"thought_signature":"sig"}}}]}`)
var d openai.ChatCompletionStreamChoiceDelta
if err := json.Unmarshal(raw, &d); err != nil {
t.Fatalf("unmarshal failed: %v", err)
}
if len(d.MultiContent) != 1 || d.MultiContent[0].ExtraPart == nil {
t.Fatalf("expected multi content with extra_part, got %#v", d)
}
}
22 changes: 14 additions & 8 deletions completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,18 @@ const (
GPT5Mini = "gpt-5-mini"
GPT5Nano = "gpt-5-nano"
GPT5ChatLatest = "gpt-5-chat-latest"
GPT3Dot5Turbo0125 = "gpt-3.5-turbo-0125"
GPT3Dot5Turbo1106 = "gpt-3.5-turbo-1106"
GPT3Dot5Turbo0613 = "gpt-3.5-turbo-0613"
GPT3Dot5Turbo0301 = "gpt-3.5-turbo-0301"
GPT3Dot5Turbo16K = "gpt-3.5-turbo-16k"
GPT3Dot5Turbo16K0613 = "gpt-3.5-turbo-16k-0613"
GPT3Dot5Turbo = "gpt-3.5-turbo"
GPT3Dot5TurboInstruct = "gpt-3.5-turbo-instruct"
// Google Vertex AI (OpenAI-compatible) Gemini 3 preview model.
// Vertex OpenAI endpoint expects publisher/model format.
Gemini3FlashPreview = "google/gemini-3-flash-preview"
Gemini3ProPreview = "google/gemini-3-pro-preview"
GPT3Dot5Turbo0125 = "gpt-3.5-turbo-0125"
GPT3Dot5Turbo1106 = "gpt-3.5-turbo-1106"
GPT3Dot5Turbo0613 = "gpt-3.5-turbo-0613"
GPT3Dot5Turbo0301 = "gpt-3.5-turbo-0301"
GPT3Dot5Turbo16K = "gpt-3.5-turbo-16k"
GPT3Dot5Turbo16K0613 = "gpt-3.5-turbo-16k-0613"
GPT3Dot5Turbo = "gpt-3.5-turbo"
GPT3Dot5TurboInstruct = "gpt-3.5-turbo-instruct"
// Deprecated: Model is shutdown. Use gpt-3.5-turbo-instruct instead.
GPT3TextDavinci003 = "text-davinci-003"
// Deprecated: Model is shutdown. Use gpt-3.5-turbo-instruct instead.
Expand Down Expand Up @@ -101,6 +105,8 @@ const (

var disabledModelsForEndpoints = map[string]map[string]bool{
"/completions": {
Gemini3FlashPreview: true,
Gemini3ProPreview: true,
O1Mini: true,
O1Mini20240912: true,
O1Preview: true,
Expand Down
Loading
Loading