From 1421a0833e2fa706f9eaf7a887fe15bb09aea7c8 Mon Sep 17 00:00:00 2001 From: Megumin Date: Wed, 2 Apr 2025 21:13:38 +0800 Subject: [PATCH 01/12] feat: temperature pointer --- api_integration_test.go | 6 +++--- assistant_test.go | 4 ++-- audio.go | 2 +- audio_api_test.go | 6 +++--- batch_test.go | 4 ++-- chat.go | 2 +- chat_stream_test.go | 4 ++-- chat_test.go | 22 +++++++++++++--------- client.go | 2 +- client_test.go | 4 ++-- completion_test.go | 4 ++-- config_test.go | 2 +- edits_test.go | 4 ++-- embeddings_test.go | 4 ++-- engines_test.go | 4 ++-- error_test.go | 2 +- example_test.go | 2 +- examples/chatbot/main.go | 2 +- examples/completion-with-tool/main.go | 4 ++-- examples/completion/main.go | 2 +- examples/images/main.go | 2 +- examples/voice-to-text/main.go | 2 +- files_api_test.go | 4 ++-- files_test.go | 4 ++-- fine_tunes_test.go | 4 ++-- fine_tuning_job_test.go | 4 ++-- go.mod | 2 +- image_api_test.go | 4 ++-- image_test.go | 4 ++-- internal/error_accumulator_test.go | 4 ++-- internal/form_builder_test.go | 2 +- internal/test/helpers.go | 2 +- jsonschema/json_test.go | 2 +- jsonschema/validate_test.go | 2 +- messages_test.go | 6 +++--- models_test.go | 4 ++-- moderation_test.go | 4 ++-- openai_test.go | 4 ++-- reasoning_validator.go | 2 +- run_test.go | 4 ++-- speech_test.go | 6 +++--- stream_reader.go | 2 +- stream_reader_test.go | 6 +++--- stream_test.go | 4 ++-- thread_test.go | 4 ++-- vector_store_test.go | 4 ++-- 46 files changed, 91 insertions(+), 87 deletions(-) diff --git a/api_integration_test.go b/api_integration_test.go index 7828d9451..9f55c56e5 100644 --- a/api_integration_test.go +++ b/api_integration_test.go @@ -10,9 +10,9 @@ import ( "os" "testing" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" - "github.com/sashabaranov/go-openai/jsonschema" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai/jsonschema" ) func TestAPI(t *testing.T) { diff --git a/assistant_test.go b/assistant_test.go index 40de0e50f..7ae0b5a2e 100644 --- a/assistant_test.go +++ b/assistant_test.go @@ -3,8 +3,8 @@ package openai_test import ( "context" - openai "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + openai "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" "encoding/json" "fmt" diff --git a/audio.go b/audio.go index f321f93d6..636b897eb 100644 --- a/audio.go +++ b/audio.go @@ -8,7 +8,7 @@ import ( "net/http" "os" - utils "github.com/sashabaranov/go-openai/internal" + utils "github.com/meguminnnnnnnnn/go-openai/internal" ) // Whisper Defines the models provided by OpenAI to use when processing audio with OpenAI. diff --git a/audio_api_test.go b/audio_api_test.go index 6c6a35643..af3e12493 100644 --- a/audio_api_test.go +++ b/audio_api_test.go @@ -12,9 +12,9 @@ import ( "strings" "testing" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) // TestAudio Tests the transcription and translation endpoints of the API using the mocked server. diff --git a/batch_test.go b/batch_test.go index f4714f4eb..9504944b4 100644 --- a/batch_test.go +++ b/batch_test.go @@ -7,8 +7,8 @@ import ( "reflect" "testing" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) func TestUploadBatchFile(t *testing.T) { diff --git a/chat.go b/chat.go index c8a3e81b3..3621a48c8 100644 --- a/chat.go +++ b/chat.go @@ -233,7 +233,7 @@ type ChatCompletionRequest struct { // MaxCompletionTokens An upper bound for the number of tokens that can be generated for a completion, // including visible output tokens and reasoning tokens https://platform.openai.com/docs/guides/reasoning MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` - Temperature float32 `json:"temperature,omitempty"` + Temperature *float32 `json:"temperature,omitempty"` TopP float32 `json:"top_p,omitempty"` N int `json:"n,omitempty"` Stream bool `json:"stream,omitempty"` diff --git a/chat_stream_test.go b/chat_stream_test.go index eabb0f3a2..0e19b44d7 100644 --- a/chat_stream_test.go +++ b/chat_stream_test.go @@ -10,8 +10,8 @@ import ( "strconv" "testing" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) func TestChatCompletionsStreamWrongModel(t *testing.T) { diff --git a/chat_test.go b/chat_test.go index 514706c96..673390087 100644 --- a/chat_test.go +++ b/chat_test.go @@ -12,9 +12,9 @@ import ( "testing" "time" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" - "github.com/sashabaranov/go-openai/jsonschema" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai/jsonschema" ) const ( @@ -91,6 +91,10 @@ func TestO1ModelsChatCompletionsDeprecatedFields(t *testing.T) { } } +func ptrOf[T any](v T) *T { + return &v +} + func TestO1ModelsChatCompletionsBetaLimitations(t *testing.T) { tests := []struct { name string @@ -119,7 +123,7 @@ func TestO1ModelsChatCompletionsBetaLimitations(t *testing.T) { Role: openai.ChatMessageRoleAssistant, }, }, - Temperature: float32(2), + Temperature: ptrOf(float32(2)), }, expectedError: openai.ErrReasoningModelLimitationsOther, }, @@ -136,7 +140,7 @@ func TestO1ModelsChatCompletionsBetaLimitations(t *testing.T) { Role: openai.ChatMessageRoleAssistant, }, }, - Temperature: float32(1), + Temperature: ptrOf(float32(1)), TopP: float32(0.1), }, expectedError: openai.ErrReasoningModelLimitationsOther, @@ -154,7 +158,7 @@ func TestO1ModelsChatCompletionsBetaLimitations(t *testing.T) { Role: openai.ChatMessageRoleAssistant, }, }, - Temperature: float32(1), + Temperature: ptrOf(float32(1)), TopP: float32(1), N: 2, }, @@ -239,7 +243,7 @@ func TestO3ModelsChatCompletionsBetaLimitations(t *testing.T) { Role: openai.ChatMessageRoleAssistant, }, }, - Temperature: float32(2), + Temperature: ptrOf(float32(2)), }, expectedError: openai.ErrReasoningModelLimitationsOther, }, @@ -256,7 +260,7 @@ func TestO3ModelsChatCompletionsBetaLimitations(t *testing.T) { Role: openai.ChatMessageRoleAssistant, }, }, - Temperature: float32(1), + Temperature: ptrOf(float32(1)), TopP: float32(0.1), }, expectedError: openai.ErrReasoningModelLimitationsOther, @@ -274,7 +278,7 @@ func TestO3ModelsChatCompletionsBetaLimitations(t *testing.T) { Role: openai.ChatMessageRoleAssistant, }, }, - Temperature: float32(1), + Temperature: ptrOf(float32(1)), TopP: float32(1), N: 2, }, diff --git a/client.go b/client.go index cef375348..373d53f21 100644 --- a/client.go +++ b/client.go @@ -10,7 +10,7 @@ import ( "net/url" "strings" - utils "github.com/sashabaranov/go-openai/internal" + utils "github.com/meguminnnnnnnnn/go-openai/internal" ) // Client is OpenAI GPT-3 API client. diff --git a/client_test.go b/client_test.go index 321971445..e333759df 100644 --- a/client_test.go +++ b/client_test.go @@ -10,8 +10,8 @@ import ( "reflect" "testing" - "github.com/sashabaranov/go-openai/internal/test" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai/internal/test" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) var errTestRequestBuilderFailed = errors.New("test request builder failed") diff --git a/completion_test.go b/completion_test.go index 27e2d150e..63c83dce3 100644 --- a/completion_test.go +++ b/completion_test.go @@ -12,8 +12,8 @@ import ( "testing" "time" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) func TestCompletionsWrongModel(t *testing.T) { diff --git a/config_test.go b/config_test.go index 960230804..a86e2f232 100644 --- a/config_test.go +++ b/config_test.go @@ -3,7 +3,7 @@ package openai_test import ( "testing" - "github.com/sashabaranov/go-openai" + "github.com/meguminnnnnnnnn/go-openai" ) func TestGetAzureDeploymentByModel(t *testing.T) { diff --git a/edits_test.go b/edits_test.go index d2a6db40d..1898d77ce 100644 --- a/edits_test.go +++ b/edits_test.go @@ -9,8 +9,8 @@ import ( "testing" "time" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) // TestEdits Tests the edits endpoint of the API using the mocked server. diff --git a/embeddings_test.go b/embeddings_test.go index 438978169..192e3ddcb 100644 --- a/embeddings_test.go +++ b/embeddings_test.go @@ -11,8 +11,8 @@ import ( "reflect" "testing" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) func TestEmbedding(t *testing.T) { diff --git a/engines_test.go b/engines_test.go index d26aa5541..90b7973be 100644 --- a/engines_test.go +++ b/engines_test.go @@ -7,8 +7,8 @@ import ( "net/http" "testing" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) // TestGetEngine Tests the retrieve engine endpoint of the API using the mocked server. diff --git a/error_test.go b/error_test.go index 48cbe4f29..1d8fe5e2d 100644 --- a/error_test.go +++ b/error_test.go @@ -6,7 +6,7 @@ import ( "reflect" "testing" - "github.com/sashabaranov/go-openai" + "github.com/meguminnnnnnnnn/go-openai" ) func TestAPIErrorUnmarshalJSON(t *testing.T) { diff --git a/example_test.go b/example_test.go index 5910ffb84..1a55952b7 100644 --- a/example_test.go +++ b/example_test.go @@ -11,7 +11,7 @@ import ( "net/url" "os" - "github.com/sashabaranov/go-openai" + "github.com/meguminnnnnnnnn/go-openai" ) func Example() { diff --git a/examples/chatbot/main.go b/examples/chatbot/main.go index ad41e957d..e4895dac4 100644 --- a/examples/chatbot/main.go +++ b/examples/chatbot/main.go @@ -6,7 +6,7 @@ import ( "fmt" "os" - "github.com/sashabaranov/go-openai" + "github.com/meguminnnnnnnnn/go-openai" ) func main() { diff --git a/examples/completion-with-tool/main.go b/examples/completion-with-tool/main.go index 26126e41b..181066dba 100644 --- a/examples/completion-with-tool/main.go +++ b/examples/completion-with-tool/main.go @@ -5,8 +5,8 @@ import ( "fmt" "os" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/jsonschema" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/jsonschema" ) func main() { diff --git a/examples/completion/main.go b/examples/completion/main.go index 8c5cbd5ca..b1b980f78 100644 --- a/examples/completion/main.go +++ b/examples/completion/main.go @@ -5,7 +5,7 @@ import ( "fmt" "os" - "github.com/sashabaranov/go-openai" + "github.com/meguminnnnnnnnn/go-openai" ) func main() { diff --git a/examples/images/main.go b/examples/images/main.go index 5ee649d22..eca84afd9 100644 --- a/examples/images/main.go +++ b/examples/images/main.go @@ -5,7 +5,7 @@ import ( "fmt" "os" - "github.com/sashabaranov/go-openai" + "github.com/meguminnnnnnnnn/go-openai" ) func main() { diff --git a/examples/voice-to-text/main.go b/examples/voice-to-text/main.go index 713e748e1..d1ddc4fd1 100644 --- a/examples/voice-to-text/main.go +++ b/examples/voice-to-text/main.go @@ -6,7 +6,7 @@ import ( "fmt" "os" - "github.com/sashabaranov/go-openai" + "github.com/meguminnnnnnnnn/go-openai" ) func main() { diff --git a/files_api_test.go b/files_api_test.go index aa4fda458..22245f0b4 100644 --- a/files_api_test.go +++ b/files_api_test.go @@ -12,8 +12,8 @@ import ( "testing" "time" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) func TestFileBytesUpload(t *testing.T) { diff --git a/files_test.go b/files_test.go index 3c1b99fb4..1960e2394 100644 --- a/files_test.go +++ b/files_test.go @@ -7,8 +7,8 @@ import ( "os" "testing" - utils "github.com/sashabaranov/go-openai/internal" - "github.com/sashabaranov/go-openai/internal/test/checks" + utils "github.com/meguminnnnnnnnn/go-openai/internal" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) func TestFileBytesUploadWithFailingFormBuilder(t *testing.T) { diff --git a/fine_tunes_test.go b/fine_tunes_test.go index 2ab6817f7..39bd8eea9 100644 --- a/fine_tunes_test.go +++ b/fine_tunes_test.go @@ -7,8 +7,8 @@ import ( "net/http" "testing" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) const testFineTuneID = "fine-tune-id" diff --git a/fine_tuning_job_test.go b/fine_tuning_job_test.go index 5f63ef24c..892dff7c9 100644 --- a/fine_tuning_job_test.go +++ b/fine_tuning_job_test.go @@ -7,8 +7,8 @@ import ( "net/http" "testing" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) const testFineTuninigJobID = "fine-tuning-job-id" diff --git a/go.mod b/go.mod index 42cc7b391..3b781ed20 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/sashabaranov/go-openai +module github.com/meguminnnnnnnnn/go-openai go 1.18 diff --git a/image_api_test.go b/image_api_test.go index f6057b77d..7c35b857a 100644 --- a/image_api_test.go +++ b/image_api_test.go @@ -11,8 +11,8 @@ import ( "testing" "time" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) func TestImages(t *testing.T) { diff --git a/image_test.go b/image_test.go index 644005515..f14121695 100644 --- a/image_test.go +++ b/image_test.go @@ -1,8 +1,8 @@ package openai //nolint:testpackage // testing private field import ( - utils "github.com/sashabaranov/go-openai/internal" - "github.com/sashabaranov/go-openai/internal/test/checks" + utils "github.com/meguminnnnnnnnn/go-openai/internal" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" "context" "fmt" diff --git a/internal/error_accumulator_test.go b/internal/error_accumulator_test.go index d48f28177..f76ade0b9 100644 --- a/internal/error_accumulator_test.go +++ b/internal/error_accumulator_test.go @@ -5,8 +5,8 @@ import ( "errors" "testing" - utils "github.com/sashabaranov/go-openai/internal" - "github.com/sashabaranov/go-openai/internal/test" + utils "github.com/meguminnnnnnnnn/go-openai/internal" + "github.com/meguminnnnnnnnn/go-openai/internal/test" ) func TestErrorAccumulatorBytes(t *testing.T) { diff --git a/internal/form_builder_test.go b/internal/form_builder_test.go index 76922c1ba..51d4c90ce 100644 --- a/internal/form_builder_test.go +++ b/internal/form_builder_test.go @@ -1,7 +1,7 @@ package openai //nolint:testpackage // testing private field import ( - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" "bytes" "errors" diff --git a/internal/test/helpers.go b/internal/test/helpers.go index dc5fa6646..5c638ef01 100644 --- a/internal/test/helpers.go +++ b/internal/test/helpers.go @@ -1,7 +1,7 @@ package test import ( - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" "net/http" "os" diff --git a/jsonschema/json_test.go b/jsonschema/json_test.go index 84f25fa85..af97dcb46 100644 --- a/jsonschema/json_test.go +++ b/jsonschema/json_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/sashabaranov/go-openai/jsonschema" + "github.com/meguminnnnnnnnn/go-openai/jsonschema" ) func TestDefinition_MarshalJSON(t *testing.T) { diff --git a/jsonschema/validate_test.go b/jsonschema/validate_test.go index 6fa30ab0c..026c5e21f 100644 --- a/jsonschema/validate_test.go +++ b/jsonschema/validate_test.go @@ -3,7 +3,7 @@ package jsonschema_test import ( "testing" - "github.com/sashabaranov/go-openai/jsonschema" + "github.com/meguminnnnnnnnn/go-openai/jsonschema" ) func Test_Validate(t *testing.T) { diff --git a/messages_test.go b/messages_test.go index b25755f98..a726adf04 100644 --- a/messages_test.go +++ b/messages_test.go @@ -7,9 +7,9 @@ import ( "net/http" "testing" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) var emptyStr = "" diff --git a/models_test.go b/models_test.go index 7fd010c34..ab70a6857 100644 --- a/models_test.go +++ b/models_test.go @@ -9,8 +9,8 @@ import ( "testing" "time" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) const testFineTuneModelID = "fine-tune-model-id" diff --git a/moderation_test.go b/moderation_test.go index a97f25bc6..95cb879b7 100644 --- a/moderation_test.go +++ b/moderation_test.go @@ -11,8 +11,8 @@ import ( "testing" "time" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) // TestModeration Tests the moderations endpoint of the API using the mocked server. diff --git a/openai_test.go b/openai_test.go index a55f3a858..cabaf10a4 100644 --- a/openai_test.go +++ b/openai_test.go @@ -1,8 +1,8 @@ package openai_test import ( - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test" ) func setupOpenAITestServer() (client *openai.Client, server *test.ServerTest, teardown func()) { diff --git a/reasoning_validator.go b/reasoning_validator.go index 2910b1395..b8bc51d2c 100644 --- a/reasoning_validator.go +++ b/reasoning_validator.go @@ -61,7 +61,7 @@ func (v *ReasoningValidator) validateReasoningModelParams(request ChatCompletion if request.LogProbs { return ErrReasoningModelLimitationsLogprobs } - if request.Temperature > 0 && request.Temperature != 1 { + if request.Temperature != nil { return ErrReasoningModelLimitationsOther } if request.TopP > 0 && request.TopP != 1 { diff --git a/run_test.go b/run_test.go index cdf99db05..02505e981 100644 --- a/run_test.go +++ b/run_test.go @@ -3,8 +3,8 @@ package openai_test import ( "context" - openai "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + openai "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" "encoding/json" "fmt" diff --git a/speech_test.go b/speech_test.go index 67a3feabc..3f1cedf47 100644 --- a/speech_test.go +++ b/speech_test.go @@ -11,9 +11,9 @@ import ( "path/filepath" "testing" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) func TestSpeechIntegration(t *testing.T) { diff --git a/stream_reader.go b/stream_reader.go index 6faefe0a7..17cf31866 100644 --- a/stream_reader.go +++ b/stream_reader.go @@ -8,7 +8,7 @@ import ( "net/http" "regexp" - utils "github.com/sashabaranov/go-openai/internal" + utils "github.com/meguminnnnnnnnn/go-openai/internal" ) var ( diff --git a/stream_reader_test.go b/stream_reader_test.go index 449a14b43..4098fba08 100644 --- a/stream_reader_test.go +++ b/stream_reader_test.go @@ -6,9 +6,9 @@ import ( "errors" "testing" - utils "github.com/sashabaranov/go-openai/internal" - "github.com/sashabaranov/go-openai/internal/test" - "github.com/sashabaranov/go-openai/internal/test/checks" + utils "github.com/meguminnnnnnnnn/go-openai/internal" + "github.com/meguminnnnnnnnn/go-openai/internal/test" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) var errTestUnmarshalerFailed = errors.New("test unmarshaler failed") diff --git a/stream_test.go b/stream_test.go index 9dd95bb5f..3156360a0 100644 --- a/stream_test.go +++ b/stream_test.go @@ -10,8 +10,8 @@ import ( "testing" "time" - "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) func TestCompletionsStreamWrongModel(t *testing.T) { diff --git a/thread_test.go b/thread_test.go index 1ac0f3c0e..c8fbe98ce 100644 --- a/thread_test.go +++ b/thread_test.go @@ -7,8 +7,8 @@ import ( "net/http" "testing" - openai "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + openai "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) // TestThread Tests the thread endpoint of the API using the mocked server. diff --git a/vector_store_test.go b/vector_store_test.go index 58b9a857e..2ddaef976 100644 --- a/vector_store_test.go +++ b/vector_store_test.go @@ -3,8 +3,8 @@ package openai_test import ( "context" - openai "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/internal/test/checks" + openai "github.com/meguminnnnnnnnn/go-openai" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" "encoding/json" "fmt" From b107e16d61a0e6673ca1313098f6c2533ebfbaa8 Mon Sep 17 00:00:00 2001 From: Megumin Date: Tue, 8 Apr 2025 15:15:28 +0800 Subject: [PATCH 02/12] feat: add multi content type --- chat.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/chat.go b/chat.go index 3621a48c8..a89b23d9b 100644 --- a/chat.go +++ b/chat.go @@ -79,17 +79,30 @@ type ChatMessageImageURL struct { Detail ImageURLDetail `json:"detail,omitempty"` } +type ChatMessageInputAudio struct { + Data string `json:"data,omitempty"` + Format string `json:"format,omitempty"` +} + +type ChatMessageVideoURL struct { + URL string `json:"url,omitempty"` +} + type ChatMessagePartType string const ( - ChatMessagePartTypeText ChatMessagePartType = "text" - ChatMessagePartTypeImageURL ChatMessagePartType = "image_url" + ChatMessagePartTypeText ChatMessagePartType = "text" + ChatMessagePartTypeImageURL ChatMessagePartType = "image_url" + ChatMessagePartTypeInputAudio ChatMessagePartType = "input_audio" + ChatMessagePartTypeVideoURL ChatMessagePartType = "video_url" ) type ChatMessagePart struct { - Type ChatMessagePartType `json:"type,omitempty"` - Text string `json:"text,omitempty"` - ImageURL *ChatMessageImageURL `json:"image_url,omitempty"` + Type ChatMessagePartType `json:"type,omitempty"` + Text string `json:"text,omitempty"` + ImageURL *ChatMessageImageURL `json:"image_url,omitempty"` + InputAudio *ChatMessageInputAudio `json:"input_audio,omitempty"` + VideoURL *ChatMessageVideoURL `json:"video_url,omitempty"` } type ChatCompletionMessage struct { From 0ec1babd3dd578882bd1ea5c4d6db902e4e1107c Mon Sep 17 00:00:00 2001 From: Back Yu Date: Mon, 19 May 2025 15:41:23 +0800 Subject: [PATCH 03/12] =?UTF-8?q?chat:=20=E6=96=B0=E5=A2=9EExtraFields?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 62 +++++++++++++++++++++++++++++--- chat.go | 24 +++++++++---- chat_test.go | 49 +++++++++++++++++++++++++ go.mod | 4 +++ go.sum | 5 +++ internal/marshaller.go | 30 +++++++++++++++- internal/request_builder.go | 1 + internal/request_builder_test.go | 49 +++++++++++++++++++++++++ 8 files changed, 211 insertions(+), 13 deletions(-) create mode 100644 go.sum diff --git a/README.md b/README.md index 77b85e519..a8eabd06b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/sashabaranov/go-openai)](https://goreportcard.com/report/github.com/sashabaranov/go-openai) [![codecov](https://codecov.io/gh/sashabaranov/go-openai/branch/master/graph/badge.svg?token=bCbIfHLIsW)](https://codecov.io/gh/sashabaranov/go-openai) -This library provides unofficial Go clients for [OpenAI API](https://platform.openai.com/). We support: +This library provides unofficial Go clients for [OpenAI API](https://platform.openai.com/). We support: * ChatGPT 4o, o1 * GPT-3, GPT-4 @@ -720,7 +720,7 @@ if errors.As(err, &e) { case 401: // invalid auth or key (do not retry) case 429: - // rate limiting or engine overload (wait and retry) + // rate limiting or engine overload (wait and retry) case 500: // openai server error (retry) default: @@ -867,6 +867,58 @@ func main() { } ``` + +
+Using ExtraFields + +```go +package main + +import ( + "context" + "fmt" + openai "github.com/sashabaranov/go-openai" +) + +func main() { + client := openai.NewClient("your token") + ctx := context.Background() + + // Create chat request + req := openai.ChatCompletionRequest{ + Model: openai.GPT3Dot5Turbo, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleUser, + Content: "Hello!", + }, + }, + } + + // Add custom fields + extraFields := map[string]any{ + "custom_field": "test_value", + "numeric_field": 42, + "bool_field": true, + } + req.SetExtraFields(extraFields) + + // Get custom fields + gotFields := req.GetExtraFields() + fmt.Printf("Extra fields: %v\n", gotFields) + + // Send request + resp, err := client.CreateChatCompletion(ctx, req) + if err != nil { + fmt.Printf("ChatCompletion error: %v\n", err) + return + } + + fmt.Println(resp.Choices[0].Message.Content) +} +``` +
+ See the `examples/` folder for more. ## Frequently Asked Questions @@ -887,18 +939,18 @@ Due to the factors mentioned above, different answers may be returned even for t By adopting these strategies, you can expect more consistent results. -**Related Issues:** +**Related Issues:** [omitempty option of request struct will generate incorrect request when parameter is 0.](https://github.com/sashabaranov/go-openai/issues/9) ### Does Go OpenAI provide a method to count tokens? No, Go OpenAI does not offer a feature to count tokens, and there are no plans to provide such a feature in the future. However, if there's a way to implement a token counting feature with zero dependencies, it might be possible to merge that feature into Go OpenAI. Otherwise, it would be more appropriate to implement it in a dedicated library or repository. -For counting tokens, you might find the following links helpful: +For counting tokens, you might find the following links helpful: - [Counting Tokens For Chat API Calls](https://github.com/pkoukk/tiktoken-go#counting-tokens-for-chat-api-calls) - [How to count tokens with tiktoken](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb) -**Related Issues:** +**Related Issues:** [Is it possible to join the implementation of GPT3 Tokenizer](https://github.com/sashabaranov/go-openai/issues/62) ## Contributing diff --git a/chat.go b/chat.go index a89b23d9b..a5c66644e 100644 --- a/chat.go +++ b/chat.go @@ -286,13 +286,23 @@ type ChatCompletionRequest struct { ReasoningEffort string `json:"reasoning_effort,omitempty"` // Metadata to store with the completion. Metadata map[string]string `json:"metadata,omitempty"` - // Configuration for a predicted output. - Prediction *Prediction `json:"prediction,omitempty"` - // ChatTemplateKwargs provides a way to add non-standard parameters to the request body. - // Additional kwargs to pass to the template renderer. Will be accessible by the chat template. - // Such as think mode for qwen3. "chat_template_kwargs": {"enable_thinking": false} - // https://qwen.readthedocs.io/en/latest/deployment/vllm.html#thinking-non-thinking-modes - ChatTemplateKwargs map[string]any `json:"chat_template_kwargs,omitempty"` + + // Extra fields to be sent in the request. + // Useful for experimental features not yet officially supported. + extraFields map[string]any +} + +// SetExtraFields adds extra fields to the JSON object. +// +// SetExtraFields will override any existing fields with the same key. +// For security reasons, ensure this is only used with trusted input data. +func (r *ChatCompletionRequest) SetExtraFields(extraFields map[string]any) { + r.extraFields = extraFields +} + +// GetExtraFields returns the extra fields set in the request. +func (r *ChatCompletionRequest) GetExtraFields() map[string]any { + return r.extraFields } type StreamOptions struct { diff --git a/chat_test.go b/chat_test.go index 673390087..c587bf80d 100644 --- a/chat_test.go +++ b/chat_test.go @@ -950,3 +950,52 @@ func TestFinishReason(t *testing.T) { } } } + +func TestChatCompletionRequestExtraFields(t *testing.T) { + req := openai.ChatCompletionRequest{ + Model: "gpt-4", + } + + // 测试设置额外字段 + extraFields := map[string]any{ + "custom_field": "test_value", + "numeric_field": 42, + "bool_field": true, + } + req.SetExtraFields(extraFields) + + // 测试获取额外字段 + gotFields := req.GetExtraFields() + + // 验证字段数量 + if len(gotFields) != len(extraFields) { + t.Errorf("Expected %d extra fields, got %d", len(extraFields), len(gotFields)) + } + + // 验证字段值 + for key, expectedValue := range extraFields { + gotValue, exists := gotFields[key] + if !exists { + t.Errorf("Expected field %s not found", key) + continue + } + if gotValue != expectedValue { + t.Errorf("Field %s: expected %v, got %v", key, expectedValue, gotValue) + } + } + + // 测试覆盖已存在的字段 + newFields := map[string]any{ + "custom_field": "new_value", + } + req.SetExtraFields(newFields) + gotFields = req.GetExtraFields() + + if len(gotFields) != len(newFields) { + t.Errorf("Expected %d extra fields after override, got %d", len(newFields), len(gotFields)) + } + + if gotFields["custom_field"] != "new_value" { + t.Errorf("Expected overridden value 'new_value', got %v", gotFields["custom_field"]) + } +} diff --git a/go.mod b/go.mod index 3b781ed20..e7952240e 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,7 @@ module github.com/meguminnnnnnnnn/go-openai go 1.18 + +require github.com/evanphx/json-patch v0.5.2 + +require github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..f41860f17 --- /dev/null +++ b/go.sum @@ -0,0 +1,5 @@ +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/internal/marshaller.go b/internal/marshaller.go index 223a4dc1c..4dc7f8e88 100644 --- a/internal/marshaller.go +++ b/internal/marshaller.go @@ -2,6 +2,9 @@ package openai import ( "encoding/json" + "fmt" + + jsonpatch "github.com/evanphx/json-patch" ) type Marshaller interface { @@ -11,5 +14,30 @@ type Marshaller interface { type JSONMarshaller struct{} func (jm *JSONMarshaller) Marshal(value any) ([]byte, error) { - return json.Marshal(value) + originalBytes, err := json.Marshal(value) + if err != nil { + return nil, err + } + // Check if the value implements the GetExtraFields interface + getExtraFieldsBody, ok := value.(interface { + GetExtraFields() map[string]any + }) + if !ok { + // If not, return the original bytes + return originalBytes, nil + } + extraFields := getExtraFieldsBody.GetExtraFields() + if len(extraFields) == 0 { + // If there are no extra fields, return the original bytes + return originalBytes, nil + } + patchBytes, err := json.Marshal(extraFields) + if err != nil { + return nil, fmt.Errorf("Marshal extraFields(%+v) err: %w", extraFields, err) + } + finalBytes, err := jsonpatch.MergePatch(originalBytes, patchBytes) + if err != nil { + return nil, fmt.Errorf("MergePatch originalBytes(%s) patchBytes(%s) err: %w", originalBytes, patchBytes, err) + } + return finalBytes, nil } diff --git a/internal/request_builder.go b/internal/request_builder.go index 5699f6b18..de3a9814d 100644 --- a/internal/request_builder.go +++ b/internal/request_builder.go @@ -38,6 +38,7 @@ func (b *HTTPRequestBuilder) Build( if err != nil { return } + bodyReader = bytes.NewBuffer(reqBytes) } } diff --git a/internal/request_builder_test.go b/internal/request_builder_test.go index e26022a6b..b235cfb6d 100644 --- a/internal/request_builder_test.go +++ b/internal/request_builder_test.go @@ -3,6 +3,7 @@ package openai //nolint:testpackage // testing private field import ( "bytes" "context" + "encoding/json" "errors" "net/http" "reflect" @@ -59,3 +60,51 @@ func TestRequestBuilderReturnsRequestWhenRequestOfArgsIsNil(t *testing.T) { t.Errorf("Build() got = %v, want %v", got, want) } } + +type testExtraFieldsRequest struct { + Model string `json:"model"` + extraFields map[string]any +} + +func (r *testExtraFieldsRequest) GetExtraFields() map[string]any { + return r.extraFields +} + +func TestRequestBuilderReturnsRequestWhenRequestHasExtraFields(t *testing.T) { + b := NewRequestBuilder() + var ( + ctx = context.Background() + method = http.MethodPost + url = "/foo" + request = &testExtraFieldsRequest{ + Model: "test-model", + } + ) + request.extraFields = map[string]any{"extra_field": "extra_value"} + + reqBytes, err := b.marshaller.Marshal(request) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + // 验证序列化结果包含原始字段和额外字段 + var result map[string]interface{} + if err := json.Unmarshal(reqBytes, &result); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if result["model"] != "test-model" { + t.Errorf("Expected model to be 'test-model', got %v", result["model"]) + } + if result["extra_field"] != "extra_value" { + t.Errorf("Expected extra_field to be 'extra_value', got %v", result["extra_field"]) + } + + want, _ := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(reqBytes)) + got, _ := b.Build(ctx, method, url, request, nil) + if !reflect.DeepEqual(got.Body, want.Body) || + !reflect.DeepEqual(got.URL, want.URL) || + !reflect.DeepEqual(got.Method, want.Method) { + t.Errorf("Build() got = %v, want %v", got, want) + } +} From 65cf08bcac2d39c28bcb868fc185f2df92e6627b Mon Sep 17 00:00:00 2001 From: Back Yu Date: Mon, 19 May 2025 16:18:53 +0800 Subject: [PATCH 04/12] =?UTF-8?q?chat:=20=E4=BF=AE=E5=A4=8DGetExtraFields(?= =?UTF-8?q?)=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat.go b/chat.go index a5c66644e..c83369d77 100644 --- a/chat.go +++ b/chat.go @@ -301,7 +301,7 @@ func (r *ChatCompletionRequest) SetExtraFields(extraFields map[string]any) { } // GetExtraFields returns the extra fields set in the request. -func (r *ChatCompletionRequest) GetExtraFields() map[string]any { +func (r ChatCompletionRequest) GetExtraFields() map[string]any { return r.extraFields } From 3a9c552bd221c45d6a953b11f5756ad20a30d709 Mon Sep 17 00:00:00 2001 From: Megumin Date: Mon, 9 Jun 2025 17:53:47 +0800 Subject: [PATCH 05/12] fix: GetExtraFields & gomod --- audio_test.go | 6 +++--- chat.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/audio_test.go b/audio_test.go index 51b3f465d..ac2d65327 100644 --- a/audio_test.go +++ b/audio_test.go @@ -11,9 +11,9 @@ import ( "path/filepath" "testing" - utils "github.com/sashabaranov/go-openai/internal" - "github.com/sashabaranov/go-openai/internal/test" - "github.com/sashabaranov/go-openai/internal/test/checks" + utils "github.com/meguminnnnnnnnn/go-openai/internal" + "github.com/meguminnnnnnnnn/go-openai/internal/test" + "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" ) func TestAudioWithFailingFormBuilder(t *testing.T) { diff --git a/chat.go b/chat.go index c83369d77..a5c66644e 100644 --- a/chat.go +++ b/chat.go @@ -301,7 +301,7 @@ func (r *ChatCompletionRequest) SetExtraFields(extraFields map[string]any) { } // GetExtraFields returns the extra fields set in the request. -func (r ChatCompletionRequest) GetExtraFields() map[string]any { +func (r *ChatCompletionRequest) GetExtraFields() map[string]any { return r.extraFields } From 0d508a1dcddeb9c2d908eb19af1aa9efa5ba2be1 Mon Sep 17 00:00:00 2001 From: Megumin Date: Fri, 20 Jun 2025 17:28:28 +0800 Subject: [PATCH 06/12] fix: pointer to extra fields --- chat.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat.go b/chat.go index a5c66644e..c83369d77 100644 --- a/chat.go +++ b/chat.go @@ -301,7 +301,7 @@ func (r *ChatCompletionRequest) SetExtraFields(extraFields map[string]any) { } // GetExtraFields returns the extra fields set in the request. -func (r *ChatCompletionRequest) GetExtraFields() map[string]any { +func (r ChatCompletionRequest) GetExtraFields() map[string]any { return r.extraFields } From 4a7937d5e3d43c44834f6addb34ebd49672b6fb2 Mon Sep 17 00:00:00 2001 From: mrh Date: Tue, 22 Jul 2025 18:14:29 +0800 Subject: [PATCH 07/12] feat: add WithRequestBodySetter Option --- chat.go | 38 +++++++++++++++++++++++++++++++++++++- chat_test.go | 22 ++++++++++++++++++++++ option.go | 15 +++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 option.go diff --git a/chat.go b/chat.go index f567364df..63f744889 100644 --- a/chat.go +++ b/chat.go @@ -1,11 +1,15 @@ package openai import ( + "bytes" "context" "encoding/json" "errors" + "io" "net/http" + openai "github.com/meguminnnnnnnnn/go-openai/internal" + "github.com/meguminnnnnnnnn/go-openai/jsonschema" ) @@ -482,6 +486,7 @@ type ChatCompletionResponse struct { func (c *Client) CreateChatCompletion( ctx context.Context, request ChatCompletionRequest, + opts ...ChatCompletionRequestOption, ) (response ChatCompletionResponse, err error) { if request.Stream { err = ErrChatCompletionStreamNotSupported @@ -499,11 +504,26 @@ func (c *Client) CreateChatCompletion( return } + ccOpts := &chatCompletionRequestOptions{} + for _, opt := range opts { + opt(ccOpts) + } + + body := any(request) + if ccOpts.RequestBodySetter != nil { + var newBody io.Reader + newBody, err = c.getNewRequestBody(request, ccOpts.RequestBodySetter) + if err != nil { + return response, err + } + body = newBody + } + req, err := c.newRequest( ctx, http.MethodPost, c.fullURL(urlSuffix, withModel(request.Model)), - withBody(request), + withBody(body), ) if err != nil { return @@ -512,3 +532,19 @@ func (c *Client) CreateChatCompletion( err = c.sendRequest(req, &response) return } + +func (c *Client) getNewRequestBody(request ChatCompletionRequest, setter RequestBodySetter) (io.Reader, error) { + marshaller := openai.JSONMarshaller{} + + body, err := marshaller.Marshal(request) + if err != nil { + return nil, err + } + + newBody, err := setter(body) + if err != nil { + return nil, err + } + + return bytes.NewBuffer(newBody), nil +} diff --git a/chat_test.go b/chat_test.go index c71e2cfb4..6951ecb9a 100644 --- a/chat_test.go +++ b/chat_test.go @@ -52,6 +52,28 @@ func TestChatCompletionsWrongModel(t *testing.T) { checks.ErrorIs(t, err, openai.ErrChatCompletionInvalidModel, msg) } +func TestChatCompletionRequestWithRequestBodySetter(t *testing.T) { + client, server, teardown := setupOpenAITestServer() + defer teardown() + server.RegisterHandler("/v1/chat/completions", handleChatCompletionEndpoint) + + opt := openai.WithRequestBodySetter(func(rawBody []byte) ([]byte, error) { + return rawBody, nil + }) + + _, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{ + Model: openai.O1Preview, + MaxCompletionTokens: 1000, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleUser, + Content: "Hello!", + }, + }, + }, opt) + checks.NoError(t, err) +} + func TestO1ModelsChatCompletionsDeprecatedFields(t *testing.T) { tests := []struct { name string diff --git a/option.go b/option.go new file mode 100644 index 000000000..361b113c0 --- /dev/null +++ b/option.go @@ -0,0 +1,15 @@ +package openai + +type chatCompletionRequestOptions struct { + RequestBodySetter RequestBodySetter +} + +type ChatCompletionRequestOption func(*chatCompletionRequestOptions) + +type RequestBodySetter func(rawBody []byte) ([]byte, error) + +func WithRequestBodySetter(setter RequestBodySetter) ChatCompletionRequestOption { + return func(opts *chatCompletionRequestOptions) { + opts.RequestBodySetter = setter + } +} From 400861269b6266cbb5685a350b02cffa8cf82802 Mon Sep 17 00:00:00 2001 From: mrh Date: Tue, 22 Jul 2025 22:21:24 +0800 Subject: [PATCH 08/12] feat: CreateChatCompletionStream add ChatCompletionRequestOption --- chat_stream.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/chat_stream.go b/chat_stream.go index 80d16cc63..288ce5af6 100644 --- a/chat_stream.go +++ b/chat_stream.go @@ -2,6 +2,7 @@ package openai import ( "context" + "io" "net/http" ) @@ -78,6 +79,7 @@ type ChatCompletionStream struct { func (c *Client) CreateChatCompletionStream( ctx context.Context, request ChatCompletionRequest, + opts ...ChatCompletionRequestOption, ) (stream *ChatCompletionStream, err error) { urlSuffix := chatCompletionsSuffix if !checkEndpointSupportsModel(urlSuffix, request.Model) { @@ -91,11 +93,26 @@ func (c *Client) CreateChatCompletionStream( return } + ccOpts := &chatCompletionRequestOptions{} + for _, opt := range opts { + opt(ccOpts) + } + + body := any(request) + if ccOpts.RequestBodySetter != nil { + var newBody io.Reader + newBody, err = c.getNewRequestBody(request, ccOpts.RequestBodySetter) + if err != nil { + return stream, err + } + body = newBody + } + req, err := c.newRequest( ctx, http.MethodPost, c.fullURL(urlSuffix, withModel(request.Model)), - withBody(request), + withBody(body), ) if err != nil { return nil, err From 3ed488d257531378d90c6a647351c1510fcc0899 Mon Sep 17 00:00:00 2001 From: mrh Date: Wed, 23 Jul 2025 17:55:08 +0800 Subject: [PATCH 09/12] feat: add WithExtraHeader option --- chat.go | 9 +++++---- chat_stream.go | 5 +++-- chat_test.go | 4 ++-- client.go | 8 ++++++++ option.go | 15 +++++++++++---- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/chat.go b/chat.go index 63f744889..dcadb5559 100644 --- a/chat.go +++ b/chat.go @@ -510,9 +510,9 @@ func (c *Client) CreateChatCompletion( } body := any(request) - if ccOpts.RequestBodySetter != nil { + if ccOpts.RequestBodyModifier != nil { var newBody io.Reader - newBody, err = c.getNewRequestBody(request, ccOpts.RequestBodySetter) + newBody, err = c.getNewRequestBody(request, ccOpts.RequestBodyModifier) if err != nil { return response, err } @@ -524,6 +524,7 @@ func (c *Client) CreateChatCompletion( http.MethodPost, c.fullURL(urlSuffix, withModel(request.Model)), withBody(body), + withExtraHeader(ccOpts.ExtraHeader), ) if err != nil { return @@ -533,7 +534,7 @@ func (c *Client) CreateChatCompletion( return } -func (c *Client) getNewRequestBody(request ChatCompletionRequest, setter RequestBodySetter) (io.Reader, error) { +func (c *Client) getNewRequestBody(request ChatCompletionRequest, modifier RequestBodyModifier) (io.Reader, error) { marshaller := openai.JSONMarshaller{} body, err := marshaller.Marshal(request) @@ -541,7 +542,7 @@ func (c *Client) getNewRequestBody(request ChatCompletionRequest, setter Request return nil, err } - newBody, err := setter(body) + newBody, err := modifier(body) if err != nil { return nil, err } diff --git a/chat_stream.go b/chat_stream.go index 288ce5af6..83f500892 100644 --- a/chat_stream.go +++ b/chat_stream.go @@ -99,9 +99,9 @@ func (c *Client) CreateChatCompletionStream( } body := any(request) - if ccOpts.RequestBodySetter != nil { + if ccOpts.RequestBodyModifier != nil { var newBody io.Reader - newBody, err = c.getNewRequestBody(request, ccOpts.RequestBodySetter) + newBody, err = c.getNewRequestBody(request, ccOpts.RequestBodyModifier) if err != nil { return stream, err } @@ -113,6 +113,7 @@ func (c *Client) CreateChatCompletionStream( http.MethodPost, c.fullURL(urlSuffix, withModel(request.Model)), withBody(body), + withExtraHeader(ccOpts.ExtraHeader), ) if err != nil { return nil, err diff --git a/chat_test.go b/chat_test.go index 6951ecb9a..8d8aa737f 100644 --- a/chat_test.go +++ b/chat_test.go @@ -52,12 +52,12 @@ func TestChatCompletionsWrongModel(t *testing.T) { checks.ErrorIs(t, err, openai.ErrChatCompletionInvalidModel, msg) } -func TestChatCompletionRequestWithRequestBodySetter(t *testing.T) { +func TestChatCompletionRequestWithRequestBodyModifier(t *testing.T) { client, server, teardown := setupOpenAITestServer() defer teardown() server.RegisterHandler("/v1/chat/completions", handleChatCompletionEndpoint) - opt := openai.WithRequestBodySetter(func(rawBody []byte) ([]byte, error) { + opt := openai.WithRequestBodyModifier(func(rawBody []byte) ([]byte, error) { return rawBody, nil }) diff --git a/client.go b/client.go index 5abcdd29e..c393ecac2 100644 --- a/client.go +++ b/client.go @@ -98,6 +98,14 @@ func withExtraBody(extraBody map[string]any) requestOption { } } +func withExtraHeader(header map[string]string) requestOption { + return func(args *requestOptions) { + for k, v := range header { + args.header.Set(k, v) + } + } +} + func withContentType(contentType string) requestOption { return func(args *requestOptions) { args.header.Set("Content-Type", contentType) diff --git a/option.go b/option.go index 361b113c0..61dc75d8a 100644 --- a/option.go +++ b/option.go @@ -1,15 +1,22 @@ package openai type chatCompletionRequestOptions struct { - RequestBodySetter RequestBodySetter + RequestBodyModifier RequestBodyModifier + ExtraHeader map[string]string } type ChatCompletionRequestOption func(*chatCompletionRequestOptions) -type RequestBodySetter func(rawBody []byte) ([]byte, error) +type RequestBodyModifier func(rawBody []byte) ([]byte, error) -func WithRequestBodySetter(setter RequestBodySetter) ChatCompletionRequestOption { +func WithRequestBodyModifier(modifier RequestBodyModifier) ChatCompletionRequestOption { return func(opts *chatCompletionRequestOptions) { - opts.RequestBodySetter = setter + opts.RequestBodyModifier = modifier + } +} + +func WithExtraHeader(header map[string]string) ChatCompletionRequestOption { + return func(opts *chatCompletionRequestOptions) { + opts.ExtraHeader = header } } From 277a4ab75990881c41c7e76f96d522e6654f7157 Mon Sep 17 00:00:00 2001 From: mrh Date: Tue, 5 Aug 2025 11:26:34 +0800 Subject: [PATCH 10/12] feat: support usage extra unmarshaler --- common.go | 43 +++++++++++++++++++++++++++++++----- common_test.go | 41 ++++++++++++++++++++++++++++++++++ go.mod | 18 +++++++++++++-- go.sum | 35 +++++++++++++++++++++++++++++ internal/unmarshaler.go | 40 +++++++++++++++++++++++++++++++++ internal/unmarshaler_test.go | 25 +++++++++++++++++++++ 6 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 common_test.go diff --git a/common.go b/common.go index d1936d656..797ef0fa2 100644 --- a/common.go +++ b/common.go @@ -1,14 +1,47 @@ package openai +import ( + "encoding/json" + "fmt" + "reflect" + + openai "github.com/meguminnnnnnnnn/go-openai/internal" +) + // common.go defines common types used throughout the OpenAI API. // Usage Represents the total token usage per request to OpenAI. type Usage struct { - PromptTokens int `json:"prompt_tokens"` - CompletionTokens int `json:"completion_tokens"` - TotalTokens int `json:"total_tokens"` - PromptTokensDetails *PromptTokensDetails `json:"prompt_tokens_details"` - CompletionTokensDetails *CompletionTokensDetails `json:"completion_tokens_details"` + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + PromptTokensDetails *PromptTokensDetails `json:"prompt_tokens_details"` + CompletionTokensDetails *CompletionTokensDetails `json:"completion_tokens_details"` + ExtraFields map[string]json.RawMessage `json:"-"` +} + +func (u *Usage) UnmarshalJSON(data []byte) error { + if u == nil { + return fmt.Errorf("usage is nil") + } + + type Alias Usage + alias := &Alias{} + err := json.Unmarshal(data, alias) + if err != nil { + return err + } + + *u = Usage(*alias) + + extra, err := openai.UnmarshalExtraFields(reflect.TypeOf(u), data) + if err != nil { + return err + } + + u.ExtraFields = extra + + return nil } // CompletionTokensDetails Breakdown of tokens used in a completion. diff --git a/common_test.go b/common_test.go new file mode 100644 index 000000000..18676227d --- /dev/null +++ b/common_test.go @@ -0,0 +1,41 @@ +package openai_test + +import ( + "encoding/json" + "testing" + + "github.com/meguminnnnnnnnn/go-openai" + "github.com/stretchr/testify/assert" +) + +func TestUsageUnmarshalJSON(t *testing.T) { + data := []byte(`{ + "prompt_tokens": 10, + "completion_tokens": 20, + "total_tokens": 30, + "prompt_tokens_details": { + "cached_tokens": 15 + }, + "completion_tokens_details": { + "audio_tokens": 10 + }, + "extra_field": "extra_value" + }`) + + usage := &openai.Usage{} + err := json.Unmarshal(data, usage) + assert.NoError(t, err) + assert.Equal(t, 10, usage.PromptTokens) + assert.Equal(t, 20, usage.CompletionTokens) + assert.Equal(t, 30, usage.TotalTokens) + assert.NotNil(t, usage.PromptTokensDetails) + assert.Equal(t, 15, usage.PromptTokensDetails.CachedTokens) + assert.NotNil(t, usage.CompletionTokensDetails) + assert.Equal(t, 10, usage.CompletionTokensDetails.AudioTokens) + assert.Len(t, usage.ExtraFields, 1) + + var extraValue string + err = json.Unmarshal(usage.ExtraFields["extra_field"], &extraValue) + assert.NoError(t, err) + assert.Equal(t, "extra_value", extraValue) +} diff --git a/go.mod b/go.mod index e7952240e..9f64f4b4c 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,20 @@ module github.com/meguminnnnnnnnn/go-openai go 1.18 -require github.com/evanphx/json-patch v0.5.2 +require ( + github.com/bytedance/sonic v1.14.0 + github.com/evanphx/json-patch v0.5.2 + github.com/stretchr/testify v1.10.0 +) -require github.com/pkg/errors v0.9.1 // indirect +require ( + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index f41860f17..3498f97bb 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,40 @@ +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/internal/unmarshaler.go b/internal/unmarshaler.go index 882876022..6bf87d2b8 100644 --- a/internal/unmarshaler.go +++ b/internal/unmarshaler.go @@ -2,6 +2,10 @@ package openai import ( "encoding/json" + "fmt" + "reflect" + + "github.com/bytedance/sonic" ) type Unmarshaler interface { @@ -13,3 +17,39 @@ type JSONUnmarshaler struct{} func (jm *JSONUnmarshaler) Unmarshal(data []byte, v any) error { return json.Unmarshal(data, v) } + +func UnmarshalExtraFields(typ reflect.Type, data []byte) (map[string]json.RawMessage, error) { + m := make(map[string]json.RawMessage) + if err := sonic.Unmarshal(data, &m); err != nil { + return nil, err + } + + for typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + + if typ.Kind() != reflect.Struct { + return nil, fmt.Errorf("type is not a struct") + } + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + + jsonTag := field.Tag.Get("json") + if jsonTag != "" { + delete(m, jsonTag) + } else { + if !field.IsExported() { + continue + } + delete(m, field.Name) + } + } + + extra := make(map[string]json.RawMessage, len(m)) + for k, v := range m { + extra[k] = v + } + + return extra, nil +} diff --git a/internal/unmarshaler_test.go b/internal/unmarshaler_test.go index d6efd7873..3c0b522f0 100644 --- a/internal/unmarshaler_test.go +++ b/internal/unmarshaler_test.go @@ -1,10 +1,13 @@ package openai_test import ( + "encoding/json" + "reflect" "testing" openai "github.com/meguminnnnnnnnn/go-openai/internal" "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" + "github.com/stretchr/testify/assert" ) func TestJSONUnmarshaler_Normal(t *testing.T) { @@ -35,3 +38,25 @@ func TestJSONUnmarshaler_EmptyInput(t *testing.T) { err := jm.Unmarshal(nil, &v) checks.HasError(t, err, "should return error for nil input") } + +func TestUnmarshalExtraFields(t *testing.T) { + type TestStruct struct { + Field1 string `json:"field1"` + Field2 int + Field3 struct { + Field4 string `json:"field4"` + } `json:"field3"` + } + + testData := []byte(`{"field1":"value1","Field2":2,"field3":{"field4":"value4"},"extraField1":"extraValue1"}`) + testStruct := &TestStruct{} + extra, err := openai.UnmarshalExtraFields(reflect.TypeOf(testStruct), testData) + assert.NoError(t, err) + assert.Len(t, extra, 1) + + var extraValue1 string + err = json.Unmarshal(extra["extraField1"], &extraValue1) + assert.NoError(t, err) + + assert.Equal(t, "extraValue1", extraValue1) +} From 3730e26002f1a77e1ae9985da258561ac7e36ccb Mon Sep 17 00:00:00 2001 From: mrh997 Date: Thu, 21 Aug 2025 14:41:17 +0800 Subject: [PATCH 11/12] feat: remove MaxTokens limitation of o-series --- chat_stream_test.go | 148 ++++++++++++++++++++--------------------- chat_test.go | 76 ++++++++++----------- reasoning_validator.go | 3 - 3 files changed, 112 insertions(+), 115 deletions(-) diff --git a/chat_stream_test.go b/chat_stream_test.go index 0e19b44d7..fe4f441f4 100644 --- a/chat_stream_test.go +++ b/chat_stream_test.go @@ -934,80 +934,80 @@ func TestCreateChatCompletionStreamWithReasoningModel(t *testing.T) { } } -func TestCreateChatCompletionStreamReasoningValidatorFails(t *testing.T) { - client, _, _ := setupOpenAITestServer() - - stream, err := client.CreateChatCompletionStream(context.Background(), openai.ChatCompletionRequest{ - MaxTokens: 100, // This will trigger the validator to fail - Model: openai.O3Mini, - Messages: []openai.ChatCompletionMessage{ - { - Role: openai.ChatMessageRoleUser, - Content: "Hello!", - }, - }, - Stream: true, - }) - - if stream != nil { - t.Error("Expected nil stream when validation fails") - stream.Close() - } - - if !errors.Is(err, openai.ErrReasoningModelMaxTokensDeprecated) { - t.Errorf("Expected ErrReasoningModelMaxTokensDeprecated, got: %v", err) - } -} - -func TestCreateChatCompletionStreamO3ReasoningValidatorFails(t *testing.T) { - client, _, _ := setupOpenAITestServer() - - stream, err := client.CreateChatCompletionStream(context.Background(), openai.ChatCompletionRequest{ - MaxTokens: 100, // This will trigger the validator to fail - Model: openai.O3, - Messages: []openai.ChatCompletionMessage{ - { - Role: openai.ChatMessageRoleUser, - Content: "Hello!", - }, - }, - Stream: true, - }) - - if stream != nil { - t.Error("Expected nil stream when validation fails") - stream.Close() - } - - if !errors.Is(err, openai.ErrReasoningModelMaxTokensDeprecated) { - t.Errorf("Expected ErrReasoningModelMaxTokensDeprecated for O3, got: %v", err) - } -} - -func TestCreateChatCompletionStreamO4MiniReasoningValidatorFails(t *testing.T) { - client, _, _ := setupOpenAITestServer() - - stream, err := client.CreateChatCompletionStream(context.Background(), openai.ChatCompletionRequest{ - MaxTokens: 100, // This will trigger the validator to fail - Model: openai.O4Mini, - Messages: []openai.ChatCompletionMessage{ - { - Role: openai.ChatMessageRoleUser, - Content: "Hello!", - }, - }, - Stream: true, - }) - - if stream != nil { - t.Error("Expected nil stream when validation fails") - stream.Close() - } - - if !errors.Is(err, openai.ErrReasoningModelMaxTokensDeprecated) { - t.Errorf("Expected ErrReasoningModelMaxTokensDeprecated for O4Mini, got: %v", err) - } -} +// func TestCreateChatCompletionStreamReasoningValidatorFails(t *testing.T) { +// client, _, _ := setupOpenAITestServer() +// +// stream, err := client.CreateChatCompletionStream(context.Background(), openai.ChatCompletionRequest{ +// MaxTokens: 100, // This will trigger the validator to fail +// Model: openai.O3Mini, +// Messages: []openai.ChatCompletionMessage{ +// { +// Role: openai.ChatMessageRoleUser, +// Content: "Hello!", +// }, +// }, +// Stream: true, +// }) +// +// if stream != nil { +// t.Error("Expected nil stream when validation fails") +// stream.Close() +// } +// +// if !errors.Is(err, openai.ErrReasoningModelMaxTokensDeprecated) { +// t.Errorf("Expected ErrReasoningModelMaxTokensDeprecated, got: %v", err) +// } +//} +// +// func TestCreateChatCompletionStreamO3ReasoningValidatorFails(t *testing.T) { +// client, _, _ := setupOpenAITestServer() +// +// stream, err := client.CreateChatCompletionStream(context.Background(), openai.ChatCompletionRequest{ +// MaxTokens: 100, // This will trigger the validator to fail +// Model: openai.O3, +// Messages: []openai.ChatCompletionMessage{ +// { +// Role: openai.ChatMessageRoleUser, +// Content: "Hello!", +// }, +// }, +// Stream: true, +// }) +// +// if stream != nil { +// t.Error("Expected nil stream when validation fails") +// stream.Close() +// } +// +// if !errors.Is(err, openai.ErrReasoningModelMaxTokensDeprecated) { +// t.Errorf("Expected ErrReasoningModelMaxTokensDeprecated for O3, got: %v", err) +// } +//} +// +// func TestCreateChatCompletionStreamO4MiniReasoningValidatorFails(t *testing.T) { +// client, _, _ := setupOpenAITestServer() +// +// stream, err := client.CreateChatCompletionStream(context.Background(), openai.ChatCompletionRequest{ +// MaxTokens: 100, // This will trigger the validator to fail +// Model: openai.O4Mini, +// Messages: []openai.ChatCompletionMessage{ +// { +// Role: openai.ChatMessageRoleUser, +// Content: "Hello!", +// }, +// }, +// Stream: true, +// }) +// +// if stream != nil { +// t.Error("Expected nil stream when validation fails") +// stream.Close() +// } +// +// if !errors.Is(err, openai.ErrReasoningModelMaxTokensDeprecated) { +// t.Errorf("Expected ErrReasoningModelMaxTokensDeprecated for O4Mini, got: %v", err) +// } +//} func compareChatStreamResponseChoices(c1, c2 openai.ChatCompletionStreamChoice) bool { if c1.Index != c2.Index { diff --git a/chat_test.go b/chat_test.go index 8d8aa737f..85dd84fdf 100644 --- a/chat_test.go +++ b/chat_test.go @@ -74,44 +74,44 @@ func TestChatCompletionRequestWithRequestBodyModifier(t *testing.T) { checks.NoError(t, err) } -func TestO1ModelsChatCompletionsDeprecatedFields(t *testing.T) { - tests := []struct { - name string - in openai.ChatCompletionRequest - expectedError error - }{ - { - name: "o1-preview_MaxTokens_deprecated", - in: openai.ChatCompletionRequest{ - MaxTokens: 5, - Model: openai.O1Preview, - }, - expectedError: openai.ErrReasoningModelMaxTokensDeprecated, - }, - { - name: "o1-mini_MaxTokens_deprecated", - in: openai.ChatCompletionRequest{ - MaxTokens: 5, - Model: openai.O1Mini, - }, - expectedError: openai.ErrReasoningModelMaxTokensDeprecated, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - config := openai.DefaultConfig("whatever") - config.BaseURL = "http://localhost/v1" - client := openai.NewClientWithConfig(config) - ctx := context.Background() - - _, err := client.CreateChatCompletion(ctx, tt.in) - checks.HasError(t, err) - msg := fmt.Sprintf("CreateChatCompletion should return wrong model error, returned: %s", err) - checks.ErrorIs(t, err, tt.expectedError, msg) - }) - } -} +// func TestO1ModelsChatCompletionsDeprecatedFields(t *testing.T) { +// tests := []struct { +// name string +// in openai.ChatCompletionRequest +// expectedError error +// }{ +// { +// name: "o1-preview_MaxTokens_deprecated", +// in: openai.ChatCompletionRequest{ +// MaxTokens: 5, +// Model: openai.O1Preview, +// }, +// expectedError: openai.ErrReasoningModelMaxTokensDeprecated, +// }, +// { +// name: "o1-mini_MaxTokens_deprecated", +// in: openai.ChatCompletionRequest{ +// MaxTokens: 5, +// Model: openai.O1Mini, +// }, +// expectedError: openai.ErrReasoningModelMaxTokensDeprecated, +// }, +// } +// +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// config := openai.DefaultConfig("whatever") +// config.BaseURL = "http://localhost/v1" +// client := openai.NewClientWithConfig(config) +// ctx := context.Background() +// +// _, err := client.CreateChatCompletion(ctx, tt.in) +// checks.HasError(t, err) +// msg := fmt.Sprintf("CreateChatCompletion should return wrong model error, returned: %s", err) +// checks.ErrorIs(t, err, tt.expectedError, msg) +// }) +// } +//} func ptrOf[T any](v T) *T { return &v diff --git a/reasoning_validator.go b/reasoning_validator.go index b8bc51d2c..a9fe2d990 100644 --- a/reasoning_validator.go +++ b/reasoning_validator.go @@ -55,9 +55,6 @@ func (v *ReasoningValidator) Validate(request ChatCompletionRequest) error { // validateReasoningModelParams checks reasoning model parameters. func (v *ReasoningValidator) validateReasoningModelParams(request ChatCompletionRequest) error { - if request.MaxTokens > 0 { - return ErrReasoningModelMaxTokensDeprecated - } if request.LogProbs { return ErrReasoningModelLimitationsLogprobs } From 64fb16548d8ec2bc9d2ef37af816a1246b1fb092 Mon Sep 17 00:00:00 2001 From: "zhuangjie.1125" Date: Wed, 10 Sep 2025 15:16:50 +0800 Subject: [PATCH 12/12] feat: chat completion message add extra fields --- chat.go | 82 ++++++++++++++++++++++++++--------------- chat_stream.go | 33 +++++++++++++++++ chat_stream_test.go | 32 ++++++++++++++++ chat_test.go | 40 ++++++++++++++++++++ internal/unmarshaler.go | 8 +++- 5 files changed, 164 insertions(+), 31 deletions(-) diff --git a/chat.go b/chat.go index dcadb5559..d5c45d99f 100644 --- a/chat.go +++ b/chat.go @@ -7,6 +7,7 @@ import ( "errors" "io" "net/http" + "reflect" openai "github.com/meguminnnnnnnnn/go-openai/internal" @@ -136,6 +137,8 @@ type ChatCompletionMessage struct { // For Role=tool prompts this should be set to the ID given in the assistant's prior request to call a tool. ToolCallID string `json:"tool_call_id,omitempty"` + + ExtraFields map[string]json.RawMessage `json:"-"` } func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) { @@ -144,29 +147,31 @@ 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"` + 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"` + ExtraFields map[string]json.RawMessage `json:"-"` }(m) return json.Marshal(msg) } msg := struct { - Role string `json:"role"` - Content string `json:"content,omitempty"` - Refusal string `json:"refusal,omitempty"` - MultiContent []ChatMessagePart `json:"-"` - 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"` + Role string `json:"role"` + Content string `json:"content,omitempty"` + Refusal string `json:"refusal,omitempty"` + MultiContent []ChatMessagePart `json:"-"` + 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"` + ExtraFields map[string]json.RawMessage `json:"-"` }(m) return json.Marshal(msg) } @@ -177,32 +182,49 @@ 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"` + ExtraFields map[string]json.RawMessage `json:"-"` }{} if err := json.Unmarshal(bs, &msg); err == nil { *m = ChatCompletionMessage(msg) + var extra map[string]json.RawMessage + extra, err = openai.UnmarshalExtraFields(reflect.TypeOf(m), bs) + if err != nil { + return err + } + + m.ExtraFields = extra return nil } + multiMsg := struct { Role string `json:"role"` Content string - Refusal string `json:"refusal,omitempty"` - MultiContent []ChatMessagePart `json:"content"` - 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"` + Refusal string `json:"refusal,omitempty"` + MultiContent []ChatMessagePart `json:"content"` + 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"` + ExtraFields map[string]json.RawMessage `json:"-"` }{} if err := json.Unmarshal(bs, &multiMsg); err != nil { return err } *m = ChatCompletionMessage(multiMsg) + + extra, err := openai.UnmarshalExtraFields(reflect.TypeOf(m), bs) + if err != nil { + return err + } + + m.ExtraFields = extra return nil } diff --git a/chat_stream.go b/chat_stream.go index 83f500892..956705750 100644 --- a/chat_stream.go +++ b/chat_stream.go @@ -2,8 +2,12 @@ package openai import ( "context" + "encoding/json" "io" "net/http" + "reflect" + + openai "github.com/meguminnnnnnnnn/go-openai/internal" ) type ChatCompletionStreamChoiceDelta struct { @@ -18,6 +22,35 @@ type ChatCompletionStreamChoiceDelta struct { // the doc from deepseek: // - https://api-docs.deepseek.com/api/create-chat-completion#responses ReasoningContent string `json:"reasoning_content,omitempty"` + + ExtraFields map[string]json.RawMessage `json:"-"` +} + +func (c *ChatCompletionStreamChoiceDelta) UnmarshalJSON(bs []byte) error { + msg := 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"` + + ExtraFields map[string]json.RawMessage `json:"-"` + }{} + err := json.Unmarshal(bs, &msg) + if err != nil { + return err + } + + *c = msg + var extra map[string]json.RawMessage + extra, err = openai.UnmarshalExtraFields(reflect.TypeOf(c), bs) + if err != nil { + return err + } + + c.ExtraFields = extra + return nil } type ChatCompletionStreamChoiceLogprobs struct { diff --git a/chat_stream_test.go b/chat_stream_test.go index fe4f441f4..b1397699b 100644 --- a/chat_stream_test.go +++ b/chat_stream_test.go @@ -12,6 +12,7 @@ import ( "github.com/meguminnnnnnnnn/go-openai" "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" + "github.com/stretchr/testify/assert" ) func TestChatCompletionsStreamWrongModel(t *testing.T) { @@ -1021,3 +1022,34 @@ func compareChatStreamResponseChoices(c1, c2 openai.ChatCompletionStreamChoice) } return true } + +func TestChatCompletionStreamChoiceDelta_UnmarshalJSON(t *testing.T) { + bs := []byte(`{ + "content": "Hello!", + "role": "user", + "multimodal_content": { + "type": "inline_data", + "inline_data": { + "mime_type": "image/png", + "data": "iVB" + } + } +} +`) + + delta := openai.ChatCompletionStreamChoiceDelta{} + err := json.Unmarshal(bs, &delta) + assert.NoError(t, err) + multimodalContent, ok := delta.ExtraFields["multimodal_content"] + assert.True(t, ok) + content := map[string]any{} + err = json.Unmarshal(multimodalContent, &content) + assert.NoError(t, err) + assert.Equal(t, map[string]any{ + "type": "inline_data", + "inline_data": map[string]interface{}{ + "mime_type": "image/png", + "data": "iVB", + }, + }, content) +} diff --git a/chat_test.go b/chat_test.go index 85dd84fdf..3f8cdfacd 100644 --- a/chat_test.go +++ b/chat_test.go @@ -15,6 +15,7 @@ import ( "github.com/meguminnnnnnnnn/go-openai" "github.com/meguminnnnnnnnn/go-openai/internal/test/checks" "github.com/meguminnnnnnnnn/go-openai/jsonschema" + "github.com/stretchr/testify/assert" ) const ( @@ -1160,3 +1161,42 @@ func TestChatCompletionRequest_UnmarshalJSON(t *testing.T) { }) } } + +func TestChatCompletionMessage_UnmarshalJSON(t *testing.T) { + bs := []byte(`{ + "role": "system", + "content": "You are a helpful math tutor.", + "name": "name", + "multimodal_contents": [ + { + "type": "text", + "text": "ok" + }, + { + "type": "text", + "text": "Generate a picture of a Shiba Inu dog for you。" + }, + { + "type": "inline_data", + "inline_data": { + "mime_type": "image/png", + "data": "iVBI" + } + } + ] +}`) + chatMessage := &openai.ChatCompletionMessage{} + err := json.Unmarshal(bs, chatMessage) + assert.Nil(t, err) + + multimodalContent := chatMessage.ExtraFields["multimodal_contents"] + mContents := make([]map[string]any, 0) + err = json.Unmarshal(multimodalContent, &mContents) + assert.Nil(t, err) + + assert.Equal(t, mContents, []map[string]any{ + {"type": "text", "text": "ok"}, + {"type": "text", "text": "Generate a picture of a Shiba Inu dog for you。"}, + {"type": "inline_data", "inline_data": map[string]any{"mime_type": "image/png", "data": "iVBI"}}, + }) +} diff --git a/internal/unmarshaler.go b/internal/unmarshaler.go index 6bf87d2b8..21330c287 100644 --- a/internal/unmarshaler.go +++ b/internal/unmarshaler.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "reflect" + "strings" "github.com/bytedance/sonic" ) @@ -37,7 +38,12 @@ func UnmarshalExtraFields(typ reflect.Type, data []byte) (map[string]json.RawMes jsonTag := field.Tag.Get("json") if jsonTag != "" { - delete(m, jsonTag) + labels := strings.Split(jsonTag, ",") + if labels[0] == "-" { + continue + } + + delete(m, labels[0]) } else { if !field.IsExported() { continue