From 5297ba3c708b998aa278a51e9aecb5b6ef8b4370 Mon Sep 17 00:00:00 2001 From: xsl <1872744675@qq.com> Date: Tue, 21 Jan 2025 13:11:02 +0800 Subject: [PATCH 01/11] support deepseek field "reasoning_content" --- chat_stream.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/chat_stream.go b/chat_stream.go index 58b2651c0..de05f97a4 100644 --- a/chat_stream.go +++ b/chat_stream.go @@ -6,11 +6,12 @@ import ( ) 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"` + ReasoningContent string `json:"reasoning_content,omitempty"` + Role string `json:"role,omitempty"` + FunctionCall *FunctionCall `json:"function_call,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + Refusal string `json:"refusal,omitempty"` } type ChatCompletionStreamChoiceLogprobs struct { From 1d922c6fee20403cea1facede0e3a5b686156d8e Mon Sep 17 00:00:00 2001 From: xsl <1872744675@qq.com> Date: Tue, 21 Jan 2025 15:33:11 +0800 Subject: [PATCH 02/11] support deepseek field "reasoning_content" --- chat.go | 77 ++++++++++++++++++++++++++++------------------------ chat_test.go | 5 ++-- 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/chat.go b/chat.go index fcaf79cf7..803842993 100644 --- a/chat.go +++ b/chat.go @@ -92,10 +92,11 @@ type ChatMessagePart struct { } type ChatCompletionMessage struct { - Role string `json:"role"` - Content string `json:"content"` - Refusal string `json:"refusal,omitempty"` - MultiContent []ChatMessagePart + Role string `json:"role"` + ReasoningContent string `json:"reasoning_content,omitempty"` + Content string `json:"content"` + Refusal string `json:"refusal,omitempty"` + MultiContent []ChatMessagePart // This property isn't in the official documentation, but it's in // the documentation for the official library for python: @@ -118,41 +119,44 @@ 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"` - FunctionCall *FunctionCall `json:"function_call,omitempty"` - ToolCalls []ToolCall `json:"tool_calls,omitempty"` - ToolCallID string `json:"tool_call_id,omitempty"` + Role string `json:"role"` + ReasoningContent string `json:"reasoning_content,omitempty"` + Content string `json:"-"` + Refusal string `json:"refusal,omitempty"` + MultiContent []ChatMessagePart `json:"content,omitempty"` + Name string `json:"name,omitempty"` + FunctionCall *FunctionCall `json:"function_call,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` }(m) return json.Marshal(msg) } msg := struct { - Role string `json:"role"` - Content string `json:"content"` - Refusal string `json:"refusal,omitempty"` - MultiContent []ChatMessagePart `json:"-"` - Name string `json:"name,omitempty"` - FunctionCall *FunctionCall `json:"function_call,omitempty"` - ToolCalls []ToolCall `json:"tool_calls,omitempty"` - ToolCallID string `json:"tool_call_id,omitempty"` + Role string `json:"role"` + ReasoningContent string `json:"reasoning_content,omitempty"` + Content string `json:"content"` + Refusal string `json:"refusal,omitempty"` + MultiContent []ChatMessagePart `json:"-"` + Name string `json:"name,omitempty"` + FunctionCall *FunctionCall `json:"function_call,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` }(m) return json.Marshal(msg) } func (m *ChatCompletionMessage) UnmarshalJSON(bs []byte) error { msg := struct { - Role string `json:"role"` - Content string `json:"content"` - Refusal string `json:"refusal,omitempty"` - MultiContent []ChatMessagePart - Name string `json:"name,omitempty"` - FunctionCall *FunctionCall `json:"function_call,omitempty"` - ToolCalls []ToolCall `json:"tool_calls,omitempty"` - ToolCallID string `json:"tool_call_id,omitempty"` + Role string `json:"role"` + ReasoningContent string `json:"reasoning_content,omitempty"` + Content string `json:"content"` + Refusal string `json:"refusal,omitempty"` + MultiContent []ChatMessagePart + Name string `json:"name,omitempty"` + FunctionCall *FunctionCall `json:"function_call,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` }{} if err := json.Unmarshal(bs, &msg); err == nil { @@ -160,14 +164,15 @@ func (m *ChatCompletionMessage) UnmarshalJSON(bs []byte) error { return nil } multiMsg := struct { - Role string `json:"role"` - Content string - Refusal string `json:"refusal,omitempty"` - MultiContent []ChatMessagePart `json:"content"` - Name string `json:"name,omitempty"` - FunctionCall *FunctionCall `json:"function_call,omitempty"` - ToolCalls []ToolCall `json:"tool_calls,omitempty"` - ToolCallID string `json:"tool_call_id,omitempty"` + Role string `json:"role"` + ReasoningContent string `json:"reasoning_content,omitempty"` + Content string + Refusal string `json:"refusal,omitempty"` + MultiContent []ChatMessagePart `json:"content"` + Name string `json:"name,omitempty"` + FunctionCall *FunctionCall `json:"function_call,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` }{} if err := json.Unmarshal(bs, &multiMsg); err != nil { return err diff --git a/chat_test.go b/chat_test.go index 134026cdb..ba171f9ef 100644 --- a/chat_test.go +++ b/chat_test.go @@ -300,8 +300,9 @@ func TestO1ModelChatCompletions(t *testing.T) { MaxCompletionTokens: 1000, Messages: []openai.ChatCompletionMessage{ { - Role: openai.ChatMessageRoleUser, - Content: "Hello!", + Role: openai.ChatMessageRoleUser, + Content: "Hello!", + ReasoningContent: "hi!", }, }, }) From 5fef03f97fbe5bf0fc1733241e059a826bd68968 Mon Sep 17 00:00:00 2001 From: xsl <1872744675@qq.com> Date: Thu, 23 Jan 2025 19:58:03 +0800 Subject: [PATCH 03/11] Comment ends in a period (godot) --- openai_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openai_test.go b/openai_test.go index 729d8880c..a55f3a858 100644 --- a/openai_test.go +++ b/openai_test.go @@ -29,9 +29,9 @@ func setupAzureTestServer() (client *openai.Client, server *test.ServerTest, tea // numTokens Returns the number of GPT-3 encoded tokens in the given text. // This function approximates based on the rule of thumb stated by OpenAI: -// https://beta.openai.com/tokenizer +// https://beta.openai.com/tokenizer. // -// TODO: implement an actual tokenizer for GPT-3 and Codex (once available) +// TODO: implement an actual tokenizer for GPT-3 and Codex (once available). func numTokens(s string) int { return int(float32(len(s)) / 4) } From e0259156e96321c9c5fd42b86b715df72751ed3e Mon Sep 17 00:00:00 2001 From: xsl <1872744675@qq.com> Date: Wed, 5 Feb 2025 20:40:16 +0800 Subject: [PATCH 04/11] add comment on field reasoning_content --- chat.go | 22 +++++++++++++--------- chat_stream.go | 16 ++++++++++------ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/chat.go b/chat.go index 803842993..1b4810f17 100644 --- a/chat.go +++ b/chat.go @@ -92,11 +92,10 @@ type ChatMessagePart struct { } type ChatCompletionMessage struct { - Role string `json:"role"` - ReasoningContent string `json:"reasoning_content,omitempty"` - Content string `json:"content"` - Refusal string `json:"refusal,omitempty"` - MultiContent []ChatMessagePart + Role string `json:"role"` + Content string `json:"content"` + Refusal string `json:"refusal,omitempty"` + MultiContent []ChatMessagePart // This property isn't in the official documentation, but it's in // the documentation for the official library for python: @@ -104,6 +103,11 @@ type ChatCompletionMessage struct { // - https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb Name string `json:"name,omitempty"` + // This property is used for the "reasoning" feature supported by deepseek-reasoner which is not in the official documentation. + // the doc from deepseek: + // - https://api-docs.deepseek.com/api/create-chat-completion#responses + ReasoningContent string `json:"reasoning_content,omitempty"` + FunctionCall *FunctionCall `json:"function_call,omitempty"` // For Role=assistant prompts this may be set to the tool calls generated by the model, such as function calls. @@ -120,11 +124,11 @@ func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) { if len(m.MultiContent) > 0 { msg := struct { Role string `json:"role"` - ReasoningContent string `json:"reasoning_content,omitempty"` 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"` @@ -134,11 +138,11 @@ func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) { msg := struct { Role string `json:"role"` - ReasoningContent string `json:"reasoning_content,omitempty"` Content string `json:"content"` 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"` @@ -149,11 +153,11 @@ func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) { func (m *ChatCompletionMessage) UnmarshalJSON(bs []byte) error { msg := struct { Role string `json:"role"` - ReasoningContent string `json:"reasoning_content,omitempty"` 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"` @@ -165,11 +169,11 @@ func (m *ChatCompletionMessage) UnmarshalJSON(bs []byte) error { } multiMsg := struct { Role string `json:"role"` - ReasoningContent string `json:"reasoning_content,omitempty"` 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"` diff --git a/chat_stream.go b/chat_stream.go index de05f97a4..3df865bb2 100644 --- a/chat_stream.go +++ b/chat_stream.go @@ -6,12 +6,16 @@ import ( ) type ChatCompletionStreamChoiceDelta struct { - Content string `json:"content,omitempty"` - ReasoningContent string `json:"reasoning_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"` + 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. + // the doc from deepseek: + // - https://api-docs.deepseek.com/api/create-chat-completion#responses + ReasoningContent string `json:"reasoning_content,omitempty"` } type ChatCompletionStreamChoiceLogprobs struct { From 1105cfc66db27609cff4871d76e88f5a4380b09c Mon Sep 17 00:00:00 2001 From: xsl <1872744675@qq.com> Date: Wed, 5 Feb 2025 21:00:02 +0800 Subject: [PATCH 05/11] fix go lint error --- chat.go | 3 ++- chat_stream.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/chat.go b/chat.go index 99efc8375..f7345cfef 100644 --- a/chat.go +++ b/chat.go @@ -103,7 +103,8 @@ type ChatCompletionMessage struct { // - https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb Name string `json:"name,omitempty"` - // This property is used for the "reasoning" feature supported by deepseek-reasoner which is not in the official documentation. + // This property is used for the "reasoning" feature supported by deepseek-reasoner + // which is not in the official documentation. // the doc from deepseek: // - https://api-docs.deepseek.com/api/create-chat-completion#responses ReasoningContent string `json:"reasoning_content,omitempty"` diff --git a/chat_stream.go b/chat_stream.go index 3df865bb2..5486631ce 100644 --- a/chat_stream.go +++ b/chat_stream.go @@ -12,7 +12,8 @@ type ChatCompletionStreamChoiceDelta struct { 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. + // This property is used for the "reasoning" feature supported by deepseek-reasoner + // which is not in the official documentation. // the doc from deepseek: // - https://api-docs.deepseek.com/api/create-chat-completion#responses ReasoningContent string `json:"reasoning_content,omitempty"` From 09ab25f18384f3d43f470c48f6b1e9c4b70fb7a6 Mon Sep 17 00:00:00 2001 From: xsl <1872744675@qq.com> Date: Wed, 5 Feb 2025 21:23:38 +0800 Subject: [PATCH 06/11] chore: trigger CI From 52738616d5a40e80c3688c6cc5e3ec1bf786f79c Mon Sep 17 00:00:00 2001 From: xsl <1872744675@qq.com> Date: Fri, 7 Feb 2025 17:12:02 +0800 Subject: [PATCH 07/11] make field "content" in MarshalJSON function omitempty --- chat.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat.go b/chat.go index b25766992..ac712378b 100644 --- a/chat.go +++ b/chat.go @@ -139,7 +139,7 @@ func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) { msg := struct { Role string `json:"role"` - Content string `json:"content"` + Content string `json:"content,omitempty"` Refusal string `json:"refusal,omitempty"` MultiContent []ChatMessagePart `json:"-"` Name string `json:"name,omitempty"` From fd8334a5c6c46eb776363fded1f1dd31e44f7042 Mon Sep 17 00:00:00 2001 From: xsl <1872744675@qq.com> Date: Wed, 12 Mar 2025 13:42:42 +0800 Subject: [PATCH 08/11] remove reasoning_content in TestO1ModelChatCompletions func --- chat_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/chat_test.go b/chat_test.go index bf7442789..e90142da6 100644 --- a/chat_test.go +++ b/chat_test.go @@ -386,9 +386,8 @@ func TestO1ModelChatCompletions(t *testing.T) { MaxCompletionTokens: 1000, Messages: []openai.ChatCompletionMessage{ { - Role: openai.ChatMessageRoleUser, - Content: "Hello!", - ReasoningContent: "hi!", + Role: openai.ChatMessageRoleUser, + Content: "Hello!", }, }, }) From a450fb86ff3cb0c345613b6187cb9bb267da0204 Mon Sep 17 00:00:00 2001 From: xsl <1872744675@qq.com> Date: Thu, 13 Mar 2025 14:01:06 +0800 Subject: [PATCH 09/11] feat: Add test and handler for deepseek-reasoner chat model completions, including support for reasoning content in responses. --- chat_test.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/chat_test.go b/chat_test.go index e90142da6..b9a2ecc51 100644 --- a/chat_test.go +++ b/chat_test.go @@ -411,6 +411,24 @@ func TestO3ModelChatCompletions(t *testing.T) { checks.NoError(t, err, "CreateChatCompletion error") } +func TestDeepseekR1ModelChatCompletions(t *testing.T) { + client, server, teardown := setupOpenAITestServer() + defer teardown() + server.RegisterHandler("/v1/chat/completions", handleDeepseekR1ChatCompletionEndpoint) + _, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{ + Model: "deepseek-reasoner", + MaxCompletionTokens: 1000, + MaxTokens: 100, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleUser, + Content: "Hello!", + }, + }, + }) + checks.NoError(t, err, "CreateChatCompletion error") +} + // TestCompletions Tests the completions endpoint of the API using the mocked server. func TestChatCompletionsWithHeaders(t *testing.T) { client, server, teardown := setupOpenAITestServer() @@ -822,6 +840,66 @@ func handleChatCompletionEndpoint(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, string(resBytes)) } +func handleDeepseekR1ChatCompletionEndpoint(w http.ResponseWriter, r *http.Request) { + var err error + var resBytes []byte + + // completions only accepts POST requests + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } + var completionReq openai.ChatCompletionRequest + if completionReq, err = getChatCompletionBody(r); err != nil { + http.Error(w, "could not read request", http.StatusInternalServerError) + return + } + res := openai.ChatCompletionResponse{ + ID: strconv.Itoa(int(time.Now().Unix())), + Object: "test-object", + Created: time.Now().Unix(), + // would be nice to validate Model during testing, but + // this may not be possible with how much upkeep + // would be required / wouldn't make much sense + Model: completionReq.Model, + } + // create completions + n := completionReq.N + if n == 0 { + n = 1 + } + for i := 0; i < n; i++ { + reasoningContent := "User says hello! And I need to reply" + tokens := numTokens(reasoningContent) + completionStr := strings.Repeat("a", completionReq.MaxTokens-tokens) + res.Choices = append(res.Choices, openai.ChatCompletionChoice{ + Message: openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleAssistant, + ReasoningContent: reasoningContent, + Content: completionStr, + }, + Index: i, + }) + } + inputTokens := numTokens(completionReq.Messages[0].Content) * n + completionTokens := completionReq.MaxTokens * n + res.Usage = openai.Usage{ + PromptTokens: inputTokens, + CompletionTokens: completionTokens, + TotalTokens: inputTokens + completionTokens, + } + resBytes, _ = json.Marshal(res) + w.Header().Set(xCustomHeader, xCustomHeaderValue) + for k, v := range rateLimitHeaders { + switch val := v.(type) { + case int: + w.Header().Set(k, strconv.Itoa(val)) + default: + w.Header().Set(k, fmt.Sprintf("%s", v)) + } + } + fmt.Fprintln(w, string(resBytes)) +} + // getChatCompletionBody Returns the body of the request to create a completion. func getChatCompletionBody(r *http.Request) (openai.ChatCompletionRequest, error) { completion := openai.ChatCompletionRequest{} From 6ef63e21d277c852a37b56cae69116bc7df5bb98 Mon Sep 17 00:00:00 2001 From: xsl <1872744675@qq.com> Date: Thu, 13 Mar 2025 14:02:15 +0800 Subject: [PATCH 10/11] feat: Add test and handler for deepseek-reasoner chat model completions, including support for reasoning content in responses. --- chat_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chat_test.go b/chat_test.go index b9a2ecc51..b9d8ac74a 100644 --- a/chat_test.go +++ b/chat_test.go @@ -867,6 +867,9 @@ func handleDeepseekR1ChatCompletionEndpoint(w http.ResponseWriter, r *http.Reque if n == 0 { n = 1 } + if completionReq.MaxTokens == 0 { + completionReq.MaxTokens = 100 + } for i := 0; i < n; i++ { reasoningContent := "User says hello! And I need to reply" tokens := numTokens(reasoningContent) From 319a8ea883f98ae35abe891e705dea74c618ad7f Mon Sep 17 00:00:00 2001 From: xsl <1872744675@qq.com> Date: Thu, 13 Mar 2025 14:08:41 +0800 Subject: [PATCH 11/11] feat: Add test and handler for deepseek-reasoner chat model completions, including support for reasoning content in responses. --- chat_test.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/chat_test.go b/chat_test.go index b9d8ac74a..514706c96 100644 --- a/chat_test.go +++ b/chat_test.go @@ -417,8 +417,7 @@ func TestDeepseekR1ModelChatCompletions(t *testing.T) { server.RegisterHandler("/v1/chat/completions", handleDeepseekR1ChatCompletionEndpoint) _, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{ Model: "deepseek-reasoner", - MaxCompletionTokens: 1000, - MaxTokens: 100, + MaxCompletionTokens: 100, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, @@ -867,13 +866,12 @@ func handleDeepseekR1ChatCompletionEndpoint(w http.ResponseWriter, r *http.Reque if n == 0 { n = 1 } - if completionReq.MaxTokens == 0 { - completionReq.MaxTokens = 100 + if completionReq.MaxCompletionTokens == 0 { + completionReq.MaxCompletionTokens = 1000 } for i := 0; i < n; i++ { reasoningContent := "User says hello! And I need to reply" - tokens := numTokens(reasoningContent) - completionStr := strings.Repeat("a", completionReq.MaxTokens-tokens) + completionStr := strings.Repeat("a", completionReq.MaxCompletionTokens-numTokens(reasoningContent)) res.Choices = append(res.Choices, openai.ChatCompletionChoice{ Message: openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleAssistant,