diff --git a/adapter/adapter.go b/adapter/adapter.go index 831c693..d73abc4 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -5,8 +5,9 @@ import ( "chat/adapter/baichuan" "chat/adapter/bing" "chat/adapter/claude" - "chat/adapter/common" + adaptercommon "chat/adapter/common" "chat/adapter/dashscope" + "chat/adapter/deepseek" "chat/adapter/hunyuan" "chat/adapter/midjourney" "chat/adapter/openai" @@ -35,6 +36,7 @@ var channelFactories = map[string]adaptercommon.FactoryCreator{ globals.SkylarkChannelType: skylark.NewChatInstanceFromConfig, globals.ZhinaoChannelType: zhinao.NewChatInstanceFromConfig, globals.MidjourneyChannelType: midjourney.NewChatInstanceFromConfig, + globals.DeepseekChannelType: deepseek.NewChatInstanceFromConfig, globals.MoonshotChannelType: openai.NewChatInstanceFromConfig, // openai format globals.GroqChannelType: openai.NewChatInstanceFromConfig, // openai format diff --git a/adapter/deepseek/chat.go b/adapter/deepseek/chat.go new file mode 100644 index 0000000..2c5753f --- /dev/null +++ b/adapter/deepseek/chat.go @@ -0,0 +1,174 @@ +package deepseek + +import ( + adaptercommon "chat/adapter/common" + "chat/globals" + "chat/utils" + "errors" + "fmt" + "strings" +) + +type ChatInstance struct { + Endpoint string + ApiKey string + isFirstReasoning bool +} + +func (c *ChatInstance) GetEndpoint() string { + return c.Endpoint +} + +func (c *ChatInstance) GetApiKey() string { + return c.ApiKey +} + +func (c *ChatInstance) GetHeader() map[string]string { + return map[string]string{ + "Content-Type": "application/json", + "Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()), + } +} + +func NewChatInstance(endpoint, apiKey string) *ChatInstance { + return &ChatInstance{ + Endpoint: endpoint, + ApiKey: apiKey, + isFirstReasoning: true, + } +} + +func NewChatInstanceFromConfig(conf globals.ChannelConfig) adaptercommon.Factory { + return NewChatInstance( + conf.GetEndpoint(), + conf.GetRandomSecret(), + ) +} + +func (c *ChatInstance) GetChatEndpoint() string { + return fmt.Sprintf("%s/chat/completions", c.GetEndpoint()) +} + +func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} { + return ChatRequest{ + Model: props.Model, + Messages: props.Message, + MaxTokens: props.MaxTokens, + Stream: stream, + Temperature: props.Temperature, + TopP: props.TopP, + PresencePenalty: props.PresencePenalty, + FrequencyPenalty: props.FrequencyPenalty, + } +} + +func processChatResponse(data string) *ChatResponse { + if form := utils.UnmarshalForm[ChatResponse](data); form != nil { + return form + } + return nil +} + +func processChatStreamResponse(data string) *ChatStreamResponse { + if form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil { + return form + } + return nil +} + +func processChatErrorResponse(data string) *ChatStreamErrorResponse { + if form := utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil { + return form + } + return nil +} + +func (c *ChatInstance) ProcessLine(data string) (string, error) { + if form := processChatStreamResponse(data); form != nil { + if len(form.Choices) == 0 { + return "", nil + } + + delta := form.Choices[0].Delta + if delta.ReasoningContent != nil { + content := *delta.ReasoningContent + // replace double newlines with single newlines for markdown + if strings.Contains(content, "\n\n") { + content = strings.ReplaceAll(content, "\n\n", "\n") + } + if c.isFirstReasoning { + c.isFirstReasoning = false + return fmt.Sprintf(">%s", content), nil + } + return content, nil + } + return delta.Content, nil + } + + if form := processChatErrorResponse(data); form != nil { + if form.Error.Message != "" { + return "", errors.New(fmt.Sprintf("deepseek error: %s", form.Error.Message)) + } + } + + return "", nil +} + +func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) { + res, err := utils.Post( + c.GetChatEndpoint(), + c.GetHeader(), + c.GetChatBody(props, false), + props.Proxy, + ) + + if err != nil || res == nil { + return "", fmt.Errorf("deepseek error: %s", err.Error()) + } + + data := utils.MapToStruct[ChatResponse](res) + if data == nil { + return "", fmt.Errorf("deepseek error: cannot parse response") + } + + if len(data.Choices) == 0 { + return "", fmt.Errorf("deepseek error: no choices") + } + + message := data.Choices[0].Message + content := message.Content + if message.ReasoningContent != nil { + content = fmt.Sprintf(">%s\n\n%s", *message.ReasoningContent, content) + } + + return content, nil +} + +func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { + c.isFirstReasoning = true + err := utils.EventScanner(&utils.EventScannerProps{ + Method: "POST", + Uri: c.GetChatEndpoint(), + Headers: c.GetHeader(), + Body: c.GetChatBody(props, true), + Callback: func(data string) error { + partial, err := c.ProcessLine(data) + if err != nil { + return err + } + return callback(&globals.Chunk{Content: partial}) + }, + }, props.Proxy) + + if err != nil { + if form := processChatErrorResponse(err.Body); form != nil { + if form.Error.Type == "" && form.Error.Message == "" { + return errors.New(utils.ToMarkdownCode("json", err.Body)) + } + return errors.New(fmt.Sprintf("deepseek error: %s (type: %s)", form.Error.Message, form.Error.Type)) + } + return err.Error + } + + return nil +} diff --git a/adapter/deepseek/reflect.go b/adapter/deepseek/reflect.go new file mode 100644 index 0000000..cc38a2b --- /dev/null +++ b/adapter/deepseek/reflect.go @@ -0,0 +1,9 @@ +package deepseek + +import "reflect" + +var _ = reflect.TypeOf(ChatInstance{}) +var _ = reflect.TypeOf(ChatRequest{}) +var _ = reflect.TypeOf(ChatResponse{}) +var _ = reflect.TypeOf(ChatStreamResponse{}) +var _ = reflect.TypeOf(ChatStreamErrorResponse{}) diff --git a/adapter/deepseek/struct.go b/adapter/deepseek/struct.go new file mode 100644 index 0000000..e3ba61b --- /dev/null +++ b/adapter/deepseek/struct.go @@ -0,0 +1,56 @@ +package deepseek + +import ( + "chat/globals" +) + +// DeepSeek API is similar to OpenAI API with additional reasoning content + +type ChatRequest struct { + Model string `json:"model"` + Messages []globals.Message `json:"messages"` + MaxTokens *int `json:"max_tokens,omitempty"` + Stream bool `json:"stream"` + Temperature *float32 `json:"temperature,omitempty"` + TopP *float32 `json:"top_p,omitempty"` + PresencePenalty *float32 `json:"presence_penalty,omitempty"` + FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"` +} + +// ChatResponse is the native http request body for deepseek +type ChatResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []struct { + Index int `json:"index"` + Message globals.Message `json:"message"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` + Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + } `json:"usage"` +} + +// ChatStreamResponse is the stream response body for deepseek +type ChatStreamResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []struct { + Delta globals.Message `json:"delta"` + Index int `json:"index"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` +} + +type ChatStreamErrorResponse struct { + Error struct { + Message string `json:"message"` + Type string `json:"type"` + } `json:"error"` +} diff --git a/app/src/admin/channel.ts b/app/src/admin/channel.ts index 4522da4..cf62ab1 100644 --- a/app/src/admin/channel.ts +++ b/app/src/admin/channel.ts @@ -66,6 +66,7 @@ export const ChannelTypes: Record = { groq: "Groq Cloud", bing: "New Bing", slack: "Slack Claude", + deepseek: "深度求索 DeepSeek", }; export const ShortChannelTypes: Record = { @@ -85,6 +86,7 @@ export const ShortChannelTypes: Record = { groq: "Groq", bing: "Bing", slack: "Slack", + deepseek: "深度求索", }; export const ChannelInfos: Record = { diff --git a/globals/constant.go b/globals/constant.go index 1f9dd5b..39ca3db 100644 --- a/globals/constant.go +++ b/globals/constant.go @@ -25,6 +25,7 @@ const ( MidjourneyChannelType = "midjourney" MoonshotChannelType = "moonshot" GroqChannelType = "groq" + DeepseekChannelType = "deepseek" ) const ( diff --git a/globals/types.go b/globals/types.go index 9da33eb..0a38b00 100644 --- a/globals/types.go +++ b/globals/types.go @@ -3,12 +3,13 @@ package globals type Hook func(data *Chunk) error type Message struct { - Role string `json:"role"` - Content string `json:"content"` - Name *string `json:"name,omitempty"` - FunctionCall *FunctionCall `json:"function_call,omitempty"` // only `function` role - ToolCallId *string `json:"tool_call_id,omitempty"` // only `tool` role - ToolCalls *ToolCalls `json:"tool_calls,omitempty"` // only `assistant` role + Role string `json:"role"` + Content string `json:"content"` + Name *string `json:"name,omitempty"` + FunctionCall *FunctionCall `json:"function_call,omitempty"` // only `function` role + ToolCallId *string `json:"tool_call_id,omitempty"` // only `tool` role + ToolCalls *ToolCalls `json:"tool_calls,omitempty"` // only `assistant` role + ReasoningContent *string `json:"reasoning_content,omitempty"` // only for deepseek reasoner models } type Chunk struct { diff --git a/globals/variables.go b/globals/variables.go index 5a0b114..32c34f4 100644 --- a/globals/variables.go +++ b/globals/variables.go @@ -133,6 +133,8 @@ const ( SkylarkPlus = "skylark-plus-public" SkylarkPro = "skylark-pro-public" SkylarkChat = "skylark-chat" + DeepseekV3 = "deepseek-chat" + DeepseekR1 = "deepseek-reasoner" ) var OpenAIDalleModels = []string{