Repository: sashabaranov/go-openai (go)

Issue:
Allowing Customization of Stream Return Type in CreateChatCompletionStream
## Description
I would like to propose an enhancement to the CreateChatCompletionStream functionality in order to allow customization of the stream return type. Specifically, I am interested in being able to extend the ChatCompletionStreamResponse structure with additional fields. Currently, this customization is not directly supported.
## Proposed Solution
To address this limitation, I suggest the following changes:
1.Adding Extensibility: Enable users to add custom fields to the ChatCompletionStreamResponse structure.
2.Public Access: Make the methods and fields within streamReader, sendRequestStream, and c.newRequest public for easier customization and model integration.
## Additional Context
The ability to customize the stream return type and access the internal methods and fields would greatly enhance the flexibility and usability of the CreateChatCompletionStream feature. This would allow users to integrate custom models more seamlessly and tailor the stream responses to their specific needs.
## Proposed Changes
1.Allow customization of the ChatCompletionStreamResponse structure.
2.Make the methods and fields within streamReader, sendRequestStream, and c.newRequest public for easier customization.
Your feedback and consideration on this feature request would be greatly appreciated.

Pull request:
feat: add RecvRaw
To address the need for reading raw stream lines or customizing stream responses, we have introduced a RecvRaw method to the streamReader. This enhancement allows for more flexible and direct interaction with stream data.

Fixes #887, #889.
 

Patch:
diff --git a/completion.go b/completion.go
index 77ea8c3..f115660 100644
--- a/completion.go
+++ b/completion.go
@@ -15,7 +15,6 @@ var (
 
 var (
 	ErrO1BetaLimitationsMessageTypes = errors.New("this model has beta-limitations, user and assistant messages only, system messages are not supported")                                  //nolint:lll
-	ErrO1BetaLimitationsStreaming    = errors.New("this model has beta-limitations, streaming not supported")                                                                              //nolint:lll
 	ErrO1BetaLimitationsTools        = errors.New("this model has beta-limitations, tools, function calling, and response format parameters are not supported")                            //nolint:lll
 	ErrO1BetaLimitationsLogprobs     = errors.New("this model has beta-limitations, logprobs not supported")                                                                               //nolint:lll
 	ErrO1BetaLimitationsOther        = errors.New("this model has beta-limitations, temperature, top_p and n are fixed at 1, while presence_penalty and frequency_penalty are fixed at 0") //nolint:lll
@@ -38,6 +37,7 @@ const (
 	GPT4o                 = "gpt-4o"
 	GPT4o20240513         = "gpt-4o-2024-05-13"
 	GPT4o20240806         = "gpt-4o-2024-08-06"
+	GPT4o20241120         = "gpt-4o-2024-11-20"
 	GPT4oLatest           = "chatgpt-4o-latest"
 	GPT4oMini             = "gpt-4o-mini"
 	GPT4oMini20240718     = "gpt-4o-mini-2024-07-18"
@@ -120,6 +120,7 @@ var disabledModelsForEndpoints = map[string]map[string]bool{
 		GPT4o:                true,
 		GPT4o20240513:        true,
 		GPT4o20240806:        true,
+		GPT4o20241120:        true,
 		GPT4oLatest:          true,
 		GPT4oMini:            true,
 		GPT4oMini20240718:    true,
@@ -199,12 +200,6 @@ func validateRequestForO1Models(request ChatCompletionRequest) error {
 		return ErrO1MaxTokensDeprecated
 	}
 
-	// Beta Limitations
-	// refs:https://platform.openai.com/docs/guides/reasoning/beta-limitations
-	// Streaming: not supported
-	if request.Stream {
-		return ErrO1BetaLimitationsStreaming
-	}
 	// Logprobs: not supported.
 	if request.LogProbs {
 		return ErrO1BetaLimitationsLogprobs
diff --git a/embeddings.go b/embeddings.go
index 74eb8aa..4a0e682 100644
--- a/embeddings.go
+++ b/embeddings.go
@@ -155,7 +155,7 @@ const (
 type EmbeddingRequest struct {
 	Input          any                     `json:"input"`
 	Model          EmbeddingModel          `json:"model"`
-	User           string                  `json:"user"`
+	User           string                  `json:"user,omitempty"`
 	EncodingFormat EmbeddingEncodingFormat `json:"encoding_format,omitempty"`
 	// Dimensions The number of dimensions the resulting output embeddings should have.
 	// Only supported in text-embedding-3 and later models.
diff --git a/stream_reader.go b/stream_reader.go
index 4210a19..ecfa268 100644
--- a/stream_reader.go
+++ b/stream_reader.go
@@ -32,17 +32,28 @@ type streamReader[T streamable] struct {
 }
 
 func (stream *streamReader[T]) Recv() (response T, err error) {
-	if stream.isFinished {
-		err = io.EOF
+	rawLine, err := stream.RecvRaw()
+	if err != nil {
 		return
 	}
 
-	response, err = stream.processLines()
-	return
+	err = stream.unmarshaler.Unmarshal(rawLine, &response)
+	if err != nil {
+		return
+	}
+	return response, nil
+}
+
+func (stream *streamReader[T]) RecvRaw() ([]byte, error) {
+	if stream.isFinished {
+		return nil, io.EOF
+	}
+
+	return stream.processLines()
 }
 
 //nolint:gocognit
-func (stream *streamReader[T]) processLines() (T, error) {
+func (stream *streamReader[T]) processLines() ([]byte, error) {
 	var (
 		emptyMessagesCount uint
 		hasErrorPrefix     bool
@@ -53,9 +64,9 @@ func (stream *streamReader[T]) processLines() (T, error) {
 		if readErr != nil || hasErrorPrefix {
 			respErr := stream.unmarshalError()
 			if respErr != nil {
-				return *new(T), fmt.Errorf("error, %w", respErr.Error)
+				return nil, fmt.Errorf("error, %w", respErr.Error)
 			}
-			return *new(T), readErr
+			return nil, readErr
 		}
 
 		noSpaceLine := bytes.TrimSpace(rawLine)
@@ -68,11 +79,11 @@ func (stream *streamReader[T]) processLines() (T, error) {
 			}
 			writeErr := stream.errAccumulator.Write(noSpaceLine)
 			if writeErr != nil {
-				return *new(T), writeErr
+				return nil, writeErr
 			}
 			emptyMessagesCount++
 			if emptyMessagesCount > stream.emptyMessagesLimit {
-				return *new(T), ErrTooManyEmptyStreamMessages
+				return nil, ErrTooManyEmptyStreamMessages
 			}
 
 			continue
@@ -81,16 +92,10 @@ func (stream *streamReader[T]) processLines() (T, error) {
 		noPrefixLine := bytes.TrimPrefix(noSpaceLine, headerData)
 		if string(noPrefixLine) == "[DONE]" {
 			stream.isFinished = true
-			return *new(T), io.EOF
-		}
-
-		var response T
-		unmarshalErr := stream.unmarshaler.Unmarshal(noPrefixLine, &response)
-		if unmarshalErr != nil {
-			return *new(T), unmarshalErr
+			return nil, io.EOF
 		}
 
-		return response, nil
+		return noPrefixLine, nil
 	}
 }
 


Test patch:
diff --git a/batch_test.go b/batch_test.go
index 4b2261e..f4714f4 100644
--- a/batch_test.go
+++ b/batch_test.go
@@ -211,7 +211,7 @@ func TestUploadBatchFileRequest_AddEmbedding(t *testing.T) {
 					Input: []string{"Hello", "World"},
 				},
 			},
-		}, []byte("{\"custom_id\":\"req-1\",\"body\":{\"input\":[\"Hello\",\"World\"],\"model\":\"gpt-3.5-turbo\",\"user\":\"\"},\"method\":\"POST\",\"url\":\"/v1/embeddings\"}\n{\"custom_id\":\"req-2\",\"body\":{\"input\":[\"Hello\",\"World\"],\"model\":\"text-embedding-ada-002\",\"user\":\"\"},\"method\":\"POST\",\"url\":\"/v1/embeddings\"}")}, //nolint:lll
+		}, []byte("{\"custom_id\":\"req-1\",\"body\":{\"input\":[\"Hello\",\"World\"],\"model\":\"gpt-3.5-turbo\"},\"method\":\"POST\",\"url\":\"/v1/embeddings\"}\n{\"custom_id\":\"req-2\",\"body\":{\"input\":[\"Hello\",\"World\"],\"model\":\"text-embedding-ada-002\"},\"method\":\"POST\",\"url\":\"/v1/embeddings\"}")}, //nolint:lll
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/chat_stream_test.go b/chat_stream_test.go
index 1468414..28a9acf 100644
--- a/chat_stream_test.go
+++ b/chat_stream_test.go
@@ -36,27 +36,6 @@ func TestChatCompletionsStreamWrongModel(t *testing.T) {
 	}
 }
 
-func TestChatCompletionsStreamWithO1BetaLimitations(t *testing.T) {
-	config := openai.DefaultConfig("whatever")
-	config.BaseURL = "http://localhost/v1/chat/completions"
-	client := openai.NewClientWithConfig(config)
-	ctx := context.Background()
-
-	req := openai.ChatCompletionRequest{
-		Model: openai.O1Preview,
-		Messages: []openai.ChatCompletionMessage{
-			{
-				Role:    openai.ChatMessageRoleUser,
-				Content: "Hello!",
-			},
-		},
-	}
-	_, err := client.CreateChatCompletionStream(ctx, req)
-	if !errors.Is(err, openai.ErrO1BetaLimitationsStreaming) {
-		t.Fatalf("CreateChatCompletion should return ErrO1BetaLimitationsStreaming, but returned: %v", err)
-	}
-}
-
 func TestCreateChatCompletionStream(t *testing.T) {
 	client, server, teardown := setupOpenAITestServer()
 	defer teardown()
diff --git a/stream_reader_test.go b/stream_reader_test.go
index cd6e46e..449a14b 100644
--- a/stream_reader_test.go
+++ b/stream_reader_test.go
@@ -63,3 +63,16 @@ func TestStreamReaderReturnsErrTestErrorAccumulatorWriteFailed(t *testing.T) {
 	_, err := stream.Recv()
 	checks.ErrorIs(t, err, test.ErrTestErrorAccumulatorWriteFailed, "Did not return error when write failed", err.Error())
 }
+
+func TestStreamReaderRecvRaw(t *testing.T) {
+	stream := &streamReader[ChatCompletionStreamResponse]{
+		reader: bufio.NewReader(bytes.NewReader([]byte("data: {\"key\": \"value\"}\n"))),
+	}
+	rawLine, err := stream.RecvRaw()
+	if err != nil {
+		t.Fatalf("Did not return raw line: %v", err)
+	}
+	if !bytes.Equal(rawLine, []byte("{\"key\": \"value\"}")) {
+		t.Fatalf("Did not return raw line: %v", string(rawLine))
+	}
+}