Files
Enterprise-Knowledge-Base/internal/models/chat/chat_provider_spec.go
T
2026-04-13 17:47:24 +08:00

267 lines
8.3 KiB
Go

package chat
import (
"context"
"net/http"
"strings"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/models/provider"
modelutils "github.com/Tencent/WeKnora/internal/models/utils"
"github.com/google/uuid"
"github.com/sashabaranov/go-openai"
)
// ProviderSpec describes provider-specific behavior for chat completions.
// Each spec is registered with a ProviderName and optionally a model matcher.
type ProviderSpec struct {
Provider provider.ProviderName
// ModelMatcher: if non-nil, this spec only applies when the model name matches.
// Used for sub-provider routing (e.g. Qwen3 within Aliyun).
ModelMatcher func(modelName string) bool
// RequestCustomizer: provider-specific request modification.
RequestCustomizer func(req *openai.ChatCompletionRequest, opts *ChatOptions, isStream bool) (any, bool)
// EndpointCustomizer: provider-specific endpoint URL override.
EndpointCustomizer func(baseURL string, modelID string, isStream bool) string
// HeaderCustomizer: provider-specific raw HTTP header customization.
HeaderCustomizer func(chat *RemoteAPIChat, req *http.Request, body []byte) error
}
// chatProviderSpecs is the ordered list of provider specs.
// Order matters: more specific specs (with ModelMatcher) should come before generic ones.
var chatProviderSpecs = []ProviderSpec{
// WeKnoraCloud
{
Provider: provider.ProviderWeKnoraCloud,
RequestCustomizer: weKnoraCloudRequestCustomizer,
EndpointCustomizer: func(baseURL string, _ string, _ bool) string {
return strings.TrimRight(baseURL, "/") + "/api/v1/chat/completions"
},
HeaderCustomizer: weKnoraCloudHeaderCustomizer,
},
// Aliyun Qwen Thinking Models (must be before generic Aliyun)
{
Provider: provider.ProviderAliyun,
ModelMatcher: func(name string) bool { return provider.IsQwenThinkingModel(name) },
RequestCustomizer: qwenThinkingRequestCustomizer,
},
// LKEAP
{
Provider: provider.ProviderLKEAP,
RequestCustomizer: lkeapRequestCustomizer,
},
// DeepSeek
{
Provider: provider.ProviderDeepSeek,
RequestCustomizer: deepseekRequestCustomizer,
},
// Generic (vLLM)
{
Provider: provider.ProviderGeneric,
RequestCustomizer: genericRequestCustomizer,
},
// Volcengine (火山引擎 Ark)
{
Provider: provider.ProviderVolcengine,
RequestCustomizer: volcengineRequestCustomizer,
},
// NVIDIA
{
Provider: provider.ProviderNvidia,
RequestCustomizer: genericRequestCustomizer,
},
}
// findProviderSpec finds the matching spec for the given provider and model name.
func findProviderSpec(providerName provider.ProviderName, modelName string) *ProviderSpec {
for i := range chatProviderSpecs {
spec := &chatProviderSpecs[i]
if spec.Provider != providerName {
continue
}
if spec.ModelMatcher != nil && !spec.ModelMatcher(modelName) {
continue
}
return spec
}
return nil
}
// --- Type definitions (moved from wrapper files) ---
// QwenChatCompletionRequest Qwen 模型的自定义请求结构体
type QwenChatCompletionRequest struct {
openai.ChatCompletionRequest
EnableThinking *bool `json:"enable_thinking,omitempty"`
}
// ThinkingConfig 思维链配置(LKEAP / Volcengine 等通用格式)
type ThinkingConfig struct {
Type string `json:"type"` // "enabled" 或 "disabled"
}
// ThinkingChatCompletionRequest 带 thinking 字段的自定义请求结构体
// 适用于 LKEAP、Volcengine 等使用 { "thinking": { "type": "enabled" } } 格式的 provider
type ThinkingChatCompletionRequest struct {
openai.ChatCompletionRequest
Thinking *ThinkingConfig `json:"thinking,omitempty"`
}
// --- Customizer functions ---
type weKnoraCloudChatRequest struct {
Model string `json:"model"`
Messages []weKnoraCloudMessage `json:"messages"`
Stream bool `json:"stream"`
MaxTokens int `json:"max_tokens,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"top_p,omitempty"`
}
type weKnoraCloudMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
func weKnoraCloudRequestCustomizer(req *openai.ChatCompletionRequest, opts *ChatOptions, isStream bool) (any, bool) {
weKnoraCloudReq := weKnoraCloudChatRequest{
Model: req.Model,
Messages: convertToWeKnoraCloudMessagesFromOpenAI(req.Messages),
Stream: isStream,
}
if opts != nil {
weKnoraCloudReq.Temperature = opts.Temperature
weKnoraCloudReq.TopP = opts.TopP
weKnoraCloudReq.MaxTokens = opts.MaxTokens
}
return weKnoraCloudReq, true
}
func weKnoraCloudHeaderCustomizer(chat *RemoteAPIChat, req *http.Request, body []byte) error {
requestID := uuid.NewString()
headers := modelutils.Sign(chat.appID, chat.appSecret, requestID, string(body))
for k, v := range headers {
req.Header.Set(k, v)
}
return nil
}
func convertToWeKnoraCloudMessagesFromOpenAI(messages []openai.ChatCompletionMessage) []weKnoraCloudMessage {
result := make([]weKnoraCloudMessage, 0, len(messages))
for _, m := range messages {
content := m.Content
if content == "" && len(m.MultiContent) > 0 {
var textParts []string
for _, part := range m.MultiContent {
if part.Type == openai.ChatMessagePartTypeText && part.Text != "" {
textParts = append(textParts, part.Text)
}
}
content = strings.Join(textParts, "\n")
}
result = append(result, weKnoraCloudMessage{Role: m.Role, Content: content})
}
return result
}
// qwenThinkingRequestCustomizer 自定义 Qwen 系列(阿里云)模型的思考请求
func qwenThinkingRequestCustomizer(
req *openai.ChatCompletionRequest, opts *ChatOptions, isStream bool,
) (any, bool) {
if !isStream {
// Qwen3 模型在非流式请求时需要显式禁用 thinking
qwenReq := QwenChatCompletionRequest{
ChatCompletionRequest: *req,
}
enableThinking := false
qwenReq.EnableThinking = &enableThinking
return qwenReq, true
}
// 流式请求:根据 opts.Thinking 启用思考
qwenReq := QwenChatCompletionRequest{
ChatCompletionRequest: *req,
}
thinking := false
if opts != nil && opts.Thinking != nil {
thinking = *opts.Thinking
}
qwenReq.EnableThinking = &thinking
// 必须返回 true 以使用 raw HTTP,否则 SDK 会过滤掉 enable_thinking 字段
return qwenReq, true
}
// lkeapRequestCustomizer 自定义 LKEAP 请求
// 仅对 DeepSeek V3.x 系列模型设置 thinking 参数;R1 系列默认开启思维链
// 参考:https://cloud.tencent.com/document/product/1772/115963
func lkeapRequestCustomizer(
req *openai.ChatCompletionRequest, opts *ChatOptions, _ bool,
) (any, bool) {
modelName := req.Model
if !strings.Contains(strings.ToLower(modelName), "deepseek-v3") || opts == nil || opts.Thinking == nil {
return nil, false
}
lkeapReq := ThinkingChatCompletionRequest{
ChatCompletionRequest: *req,
}
thinkingType := "disabled"
if *opts.Thinking {
thinkingType = "enabled"
}
lkeapReq.Thinking = &ThinkingConfig{Type: thinkingType}
return lkeapReq, true
}
// deepseekRequestCustomizer 自定义 DeepSeek 请求
// DeepSeek 模型不支持 tool_choice 参数,需要清除
func deepseekRequestCustomizer(
req *openai.ChatCompletionRequest, opts *ChatOptions, _ bool,
) (any, bool) {
if opts != nil && opts.ToolChoice != "" {
logger.Infof(context.Background(), "deepseek model, skip tool_choice")
req.ToolChoice = nil
}
return nil, false
}
// genericRequestCustomizer 自定义 Generic 请求
// Generic provider(如 vLLM)使用 ChatTemplateKwargs 传递 thinking 参数
func genericRequestCustomizer(
req *openai.ChatCompletionRequest, opts *ChatOptions, _ bool,
) (any, bool) {
thinking := false
if opts != nil && opts.Thinking != nil {
thinking = *opts.Thinking
}
req.ChatTemplateKwargs = map[string]interface{}{
"enable_thinking": thinking,
}
return req, true
}
// volcengineRequestCustomizer 自定义火山引擎请求
// 火山引擎使用 thinking 参数控制深度思考,格式同 LKEAP: { "type": "enabled"/"disabled" }
func volcengineRequestCustomizer(
req *openai.ChatCompletionRequest, opts *ChatOptions, _ bool,
) (any, bool) {
if opts == nil || opts.Thinking == nil {
return nil, false
}
vcReq := ThinkingChatCompletionRequest{
ChatCompletionRequest: *req,
}
thinkingType := "disabled"
if *opts.Thinking {
thinkingType = "enabled"
}
vcReq.Thinking = &ThinkingConfig{Type: thinkingType}
return vcReq, true
}