diff --git a/internal/cli/http_client.go b/internal/cli/http_client.go index fd84d80ce3..111604927c 100644 --- a/internal/cli/http_client.go +++ b/internal/cli/http_client.go @@ -418,8 +418,9 @@ func (c *HTTPClient) RequestStream(method, path string, useAPIBase bool, authKin } if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) resp.Body.Close() - return nil, fmt.Errorf("HTTP %d", resp.StatusCode) + return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) } return resp.Body, nil diff --git a/internal/cli/user_command.go b/internal/cli/user_command.go index bdf3250033..2ca0fcca19 100644 --- a/internal/cli/user_command.go +++ b/internal/cli/user_command.go @@ -1545,14 +1545,29 @@ func (c *RAGFlowClient) ChatToModel(cmd *Command) (ResponseIf, error) { effort := cmd.Params["effort"].(string) verbosity := cmd.Params["verbosity"].(string) - url := fmt.Sprintf("/chat/completions") + url := "/chat/completions" + + message = strings.TrimSpace(message) + var content interface{} = message + if strings.HasPrefix(message, "[") && strings.HasSuffix(message, "]") { + var parts []map[string]interface{} + if err := json.Unmarshal([]byte(message), &parts); err == nil { + content = parts + } + } + formattedMessage := []map[string]interface{}{ + { + "role": "user", + "content": content, + }, + } payload := map[string]interface{}{ "provider_name": providerName, "instance_name": instanceName, "model_name": modelName, - "message": message, - "stream": stream, // use stream API + "messages": formattedMessage, + "stream": stream, "thinking": thinking, } diff --git a/internal/entity/models/aliyun.go b/internal/entity/models/aliyun.go index 5613e76617..81473ce864 100644 --- a/internal/entity/models/aliyun.go +++ b/internal/entity/models/aliyun.go @@ -60,54 +60,62 @@ func (z *AliyunModel) Name() string { return "siliconflow" } -// Chat sends a message and returns response -func (z *AliyunModel) Chat(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { - if message == nil { - return nil, fmt.Errorf("message is nil") +func (z *AliyunModel) ChatWithMessages(modelName string, apiConfig *APIConfig, messages []Message, chatModelConfig *ChatConfig) (*ChatResponse, error) { + if len(messages) == 0 { + return nil, fmt.Errorf("messages is empty") } var region = "default" - if apiConfig.Region != nil { + if apiConfig != nil && apiConfig.Region != nil { region = *apiConfig.Region } url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat) + // Convert messages to the format expected by API + apiMessages := make([]map[string]interface{}, len(messages)) + for i, msg := range messages { + apiMessages[i] = map[string]interface{}{ + "role": msg.Role, + "content": msg.Content, + } + } + // Build request body reqBody := map[string]interface{}{ - "model": modelName, - "messages": []map[string]string{ - {"role": "user", "content": *message}, - }, + "model": modelName, + "messages": apiMessages, "stream": false, "temperature": 1, } - if chatModelConfig.Stream != nil { - reqBody["stream"] = *chatModelConfig.Stream - } + if chatModelConfig != nil { + if chatModelConfig.Stream != nil { + reqBody["stream"] = *chatModelConfig.Stream + } - if chatModelConfig.MaxTokens != nil { - reqBody["max_tokens"] = *chatModelConfig.MaxTokens - } + if chatModelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *chatModelConfig.MaxTokens + } - if chatModelConfig.Temperature != nil { - reqBody["temperature"] = *chatModelConfig.Temperature - } + if chatModelConfig.Temperature != nil { + reqBody["temperature"] = *chatModelConfig.Temperature + } - if chatModelConfig.TopP != nil { - reqBody["top_p"] = *chatModelConfig.TopP - } + if chatModelConfig.TopP != nil { + reqBody["top_p"] = *chatModelConfig.TopP + } - if chatModelConfig.Stop != nil { - reqBody["stop"] = *chatModelConfig.Stop - } + if chatModelConfig.Stop != nil { + reqBody["stop"] = *chatModelConfig.Stop + } - if chatModelConfig.Thinking != nil { - if *chatModelConfig.Thinking { - reqBody["enable_thinking"] = true - } else { - reqBody["enable_thinking"] = false + if chatModelConfig.Thinking != nil { + if *chatModelConfig.Thinking { + reqBody["enable_thinking"] = true + } else { + reqBody["enable_thinking"] = false + } } } @@ -122,7 +130,9 @@ func (z *AliyunModel) Chat(modelName, message *string, apiConfig *APIConfig, cha } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + if apiConfig != nil && apiConfig.ApiKey != nil { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + } resp, err := z.httpClient.Do(req) if err != nil { @@ -166,7 +176,7 @@ func (z *AliyunModel) Chat(modelName, message *string, apiConfig *APIConfig, cha } var reasonContent string - if chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { + if chatModelConfig != nil && chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { reasonContent, ok = messageMap["reasoning_content"].(string) if !ok { return nil, fmt.Errorf("invalid content format") @@ -177,8 +187,6 @@ func (z *AliyunModel) Chat(modelName, message *string, apiConfig *APIConfig, cha } } - //thinking, answer := GetThinkingAndAnswer(chatModelConfig.ModelType, &content) - chatResponse := &ChatResponse{ Answer: &answer, ReasonContent: &reasonContent, @@ -187,11 +195,6 @@ func (z *AliyunModel) Chat(modelName, message *string, apiConfig *APIConfig, cha return chatResponse, nil } -// ChatWithMessages sends multiple messages with roles and returns response -func (z *AliyunModel) ChatWithMessages(modelName string, apiKey *string, messages []Message, chatModelConfig *ChatConfig) (string, error) { - return "", fmt.Errorf("%s, ChatWithMessages not implemented", z.Name()) -} - // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) func (z *AliyunModel) ChatStreamlyWithSender(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { var region = "default" diff --git a/internal/entity/models/deepseek.go b/internal/entity/models/deepseek.go index 2e8b894f93..9a9077b876 100644 --- a/internal/entity/models/deepseek.go +++ b/internal/entity/models/deepseek.go @@ -60,86 +60,93 @@ func (z *DeepSeekModel) Name() string { return "deepseek" } -// Chat sends a message and returns response -func (z *DeepSeekModel) Chat(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { - if message == nil { - return nil, fmt.Errorf("message is nil") +func (z *DeepSeekModel) ChatWithMessages(modelName string, apiConfig *APIConfig, messages []Message, chatModelConfig *ChatConfig) (*ChatResponse, error) { + if len(messages) == 0 { + return nil, fmt.Errorf("messages is empty") } var region = "default" - if apiConfig.Region != nil { + if apiConfig != nil && apiConfig.Region != nil { region = *apiConfig.Region } url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat) + // Convert messages to the format expected by API + apiMessages := make([]map[string]interface{}, len(messages)) + for i, msg := range messages { + apiMessages[i] = map[string]interface{}{ + "role": msg.Role, + "content": msg.Content, + } + } + // Build request body reqBody := map[string]interface{}{ - "model": modelName, - "messages": []map[string]string{ - {"role": "user", "content": *message}, - }, + "model": modelName, + "messages": apiMessages, "stream": false, "temperature": 1, } - if chatModelConfig.Stream != nil { - reqBody["stream"] = *chatModelConfig.Stream - } + if chatModelConfig != nil { + if chatModelConfig.Stream != nil { + reqBody["stream"] = *chatModelConfig.Stream + } - if chatModelConfig.MaxTokens != nil { - reqBody["max_tokens"] = *chatModelConfig.MaxTokens - } + if chatModelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *chatModelConfig.MaxTokens + } - if chatModelConfig.Temperature != nil { - reqBody["temperature"] = *chatModelConfig.Temperature - } + if chatModelConfig.Temperature != nil { + reqBody["temperature"] = *chatModelConfig.Temperature + } - if chatModelConfig.TopP != nil { - reqBody["top_p"] = *chatModelConfig.TopP - } + if chatModelConfig.TopP != nil { + reqBody["top_p"] = *chatModelConfig.TopP + } - if chatModelConfig.Stop != nil { - reqBody["stop"] = *chatModelConfig.Stop - } + if chatModelConfig.Stop != nil { + reqBody["stop"] = *chatModelConfig.Stop + } - if chatModelConfig.Thinking != nil { - if *chatModelConfig.Thinking { - var thinkingFlag string - switch *chatModelConfig.Effort { - case "none": - thinkingFlag = "disabled" - chatModelConfig.Thinking = nil - break - case "low": - thinkingFlag = "disabled" - chatModelConfig.Thinking = nil - break - case "medium": - thinkingFlag = "disabled" - chatModelConfig.Thinking = nil - break - case "high": - thinkingFlag = "enabled" - reqBody["reasoning_effort"] = "high" - break - case "default": - thinkingFlag = "enabled" - reqBody["reasoning_effort"] = "high" - break - case "max": - thinkingFlag = "enabled" - reqBody["reasoning_effort"] = "max" - break - default: - return nil, fmt.Errorf("invalid effort level") - } - reqBody["thinking"] = map[string]interface{}{ - "type": thinkingFlag, - } - } else { - reqBody["thinking"] = map[string]interface{}{ - "type": "disabled", + if chatModelConfig.Thinking != nil { + if *chatModelConfig.Thinking { + var thinkingFlag string + effort := "high" + if chatModelConfig.Effort != nil { + effort = *chatModelConfig.Effort + } + switch effort { + case "none": + thinkingFlag = "disabled" + chatModelConfig.Thinking = nil + case "low": + thinkingFlag = "disabled" + chatModelConfig.Thinking = nil + case "medium": + thinkingFlag = "disabled" + chatModelConfig.Thinking = nil + case "high": + thinkingFlag = "enabled" + reqBody["reasoning_effort"] = "high" + case "default": + thinkingFlag = "enabled" + reqBody["reasoning_effort"] = "high" + case "max": + thinkingFlag = "enabled" + reqBody["reasoning_effort"] = "max" + default: + thinkingFlag = "enabled" + reqBody["reasoning_effort"] = effort + } + reqBody["thinking"] = map[string]interface{}{ + "type": thinkingFlag, + } + } else { + reqBody["thinking"] = map[string]interface{}{ + "type": "disabled", + } } } } @@ -155,7 +162,9 @@ func (z *DeepSeekModel) Chat(modelName, message *string, apiConfig *APIConfig, c } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + if apiConfig != nil && apiConfig.ApiKey != nil { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + } resp, err := z.httpClient.Do(req) if err != nil { @@ -199,7 +208,7 @@ func (z *DeepSeekModel) Chat(modelName, message *string, apiConfig *APIConfig, c } var reasonContent string - if chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { + if chatModelConfig != nil && chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { reasonContent, ok = messageMap["reasoning_content"].(string) if !ok { return nil, fmt.Errorf("invalid content format") @@ -218,11 +227,6 @@ func (z *DeepSeekModel) Chat(modelName, message *string, apiConfig *APIConfig, c return chatResponse, nil } -// ChatWithMessages sends multiple messages with roles and returns response -func (z *DeepSeekModel) ChatWithMessages(modelName string, apiKey *string, messages []Message, chatModelConfig *ChatConfig) (string, error) { - return "", fmt.Errorf("%s, ChatWithMessages not implemented", z.Name()) -} - // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) func (z *DeepSeekModel) ChatStreamlyWithSender(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { var region = "default" diff --git a/internal/entity/models/dummy.go b/internal/entity/models/dummy.go index d02ac04159..aa3975f0ef 100644 --- a/internal/entity/models/dummy.go +++ b/internal/entity/models/dummy.go @@ -42,14 +42,9 @@ func (z *DummyModel) Name() string { return "dummy" } -// Chat sends a message and returns response -func (z *DummyModel) Chat(modelName, message *string, apiConfig *APIConfig, modelConfig *ChatConfig) (*ChatResponse, error) { - return nil, fmt.Errorf("not implemented") -} - // ChatWithMessages sends multiple messages with roles and returns response -func (z *DummyModel) ChatWithMessages(modelName string, apiKey *string, messages []Message, modelConfig *ChatConfig) (string, error) { - return "", fmt.Errorf("not implemented") +func (z *DummyModel) ChatWithMessages(modelName string, apiConfig *APIConfig, messages []Message, chatModelConfig *ChatConfig) (*ChatResponse, error) { + return nil, fmt.Errorf("not implemented") } // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) diff --git a/internal/entity/models/gitee.go b/internal/entity/models/gitee.go index 1eca6eb919..51250a8509 100644 --- a/internal/entity/models/gitee.go +++ b/internal/entity/models/gitee.go @@ -60,64 +60,70 @@ func (z *GiteeModel) Name() string { return "gitee" } -// Chat sends a message and returns response -func (z *GiteeModel) Chat(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { - if message == nil { - return nil, fmt.Errorf("message is nil") +// ChatWithMessages sends multiple messages with roles and returns response +func (z *GiteeModel) ChatWithMessages(modelName string, apiConfig *APIConfig, messages []Message, chatModelConfig *ChatConfig) (*ChatResponse, error) { + if apiConfig == nil || apiConfig.ApiKey == nil || *apiConfig.ApiKey == "" { + return nil, fmt.Errorf("api key is nil or empty") } - var region = "default" + if len(messages) == 0 { + return nil, fmt.Errorf("messages is empty") + } + + region := "default" if apiConfig.Region != nil { region = *apiConfig.Region } - url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat) - // I need to get the model type, such as qwen3 is the prefix, the model type will be qwen. glm is the prefix, the model type will be glm. such as the model name: qwen3-0.6b, the model type will be qwen3 - // the model name is glm-4.7, the model type will be glm - modelType := strings.Split(*modelName, "-")[0] - if modelType == "qwen" || modelType == "glm" { - url = fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.AsyncChat) + // Convert messages to the format expected by API + apiMessages := make([]map[string]interface{}, len(messages)) + for i, msg := range messages { + apiMessages[i] = map[string]interface{}{ + "role": msg.Role, + "content": msg.Content, + } } + logger.Info(fmt.Sprintf("GiteeAPI messages: %+v", apiMessages)) // Build request body reqBody := map[string]interface{}{ - "model": modelName, - "messages": []map[string]string{ - {"role": "user", "content": *message}, - }, + "model": modelName, + "messages": apiMessages, "stream": false, "temperature": 1, } - if chatModelConfig.Stream != nil { - reqBody["stream"] = *chatModelConfig.Stream - } + if chatModelConfig != nil { + if chatModelConfig.Stream != nil { + reqBody["stream"] = *chatModelConfig.Stream + } - if chatModelConfig.MaxTokens != nil { - reqBody["max_tokens"] = *chatModelConfig.MaxTokens - } + if chatModelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *chatModelConfig.MaxTokens + } - if chatModelConfig.Temperature != nil { - reqBody["temperature"] = *chatModelConfig.Temperature - } + if chatModelConfig.Temperature != nil { + reqBody["temperature"] = *chatModelConfig.Temperature + } - if chatModelConfig.TopP != nil { - reqBody["top_p"] = *chatModelConfig.TopP - } + if chatModelConfig.TopP != nil { + reqBody["top_p"] = *chatModelConfig.TopP + } - if chatModelConfig.Stop != nil { - reqBody["stop"] = *chatModelConfig.Stop - } + if chatModelConfig.Stop != nil { + reqBody["stop"] = *chatModelConfig.Stop + } - if chatModelConfig.Thinking != nil { - if *chatModelConfig.Thinking { - reqBody["thinking"] = map[string]interface{}{ - "type": "enabled", - } - } else { - reqBody["thinking"] = map[string]interface{}{ - "type": "disabled", + if chatModelConfig.Thinking != nil { + if *chatModelConfig.Thinking { + reqBody["thinking"] = map[string]interface{}{ + "type": "enabled", + } + } else { + reqBody["thinking"] = map[string]interface{}{ + "type": "disabled", + } } } } @@ -127,6 +133,8 @@ func (z *GiteeModel) Chat(modelName, message *string, apiConfig *APIConfig, chat return nil, fmt.Errorf("failed to marshal request: %w", err) } + logger.Info(fmt.Sprintf("GiteeAPI request body: %s", string(jsonData))) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) @@ -152,7 +160,7 @@ func (z *GiteeModel) Chat(modelName, message *string, apiConfig *APIConfig, chat // Parse response var result map[string]interface{} - if err = json.Unmarshal(body, &result); err != nil { + if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } @@ -176,21 +184,33 @@ func (z *GiteeModel) Chat(modelName, message *string, apiConfig *APIConfig, chat return nil, fmt.Errorf("invalid content format") } - thinking, answer := GetThinkingAndAnswer(chatModelConfig.ModelClass, &content) + // Handle thinking/reasoning if enabled + var reasonContent string + if chatModelConfig != nil && chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { + // Try to get reasoning_content directly first + if rc, ok := messageMap["reasoning_content"].(string); ok && rc != "" { + reasonContent = rc + if reasonContent[0] == '\n' { + reasonContent = reasonContent[1:] + } + } else { + // Fall back to parsing tags from content + reasoning, answer := GetThinkingAndAnswer(chatModelConfig.ModelClass, &content) + if reasoning != nil { + reasonContent = *reasoning + content = *answer + } + } + } chatResponse := &ChatResponse{ - Answer: answer, - ReasonContent: thinking, + Answer: &content, + ReasonContent: &reasonContent, } return chatResponse, nil } -// ChatWithMessages sends multiple messages with roles and returns response -func (z *GiteeModel) ChatWithMessages(modelName string, apiKey *string, messages []Message, chatModelConfig *ChatConfig) (string, error) { - return "", fmt.Errorf("%s, ChatWithMessages not implemented", z.Name()) -} - // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) func (z *GiteeModel) ChatStreamlyWithSender(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { var region = "default" diff --git a/internal/entity/models/google.go b/internal/entity/models/google.go index 4adb6490d4..158651a6f9 100644 --- a/internal/entity/models/google.go +++ b/internal/entity/models/google.go @@ -46,8 +46,15 @@ func (z *GoogleModel) Name() string { return "google" } -// Chat sends a message and returns response -func (z *GoogleModel) Chat(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (z *GoogleModel) ChatWithMessages(modelName string, apiConfig *APIConfig, messages []Message, chatModelConfig *ChatConfig) (*ChatResponse, error) { + if apiConfig == nil || apiConfig.ApiKey == nil || *apiConfig.ApiKey == "" { + return nil, fmt.Errorf("api key is nil or empty") + } + + if len(messages) == 0 { + return nil, fmt.Errorf("messages is empty") + } + ctx := context.Background() client, err := genai.NewClient(ctx, &genai.ClientConfig{ APIKey: *apiConfig.ApiKey, @@ -57,39 +64,59 @@ func (z *GoogleModel) Chat(modelName, message *string, apiConfig *APIConfig, cha return nil, err } - contents := []*genai.Content{ - genai.NewContentFromText(*message, genai.RoleUser), + // Convert messages to Google SDK format + var contents []*genai.Content + for _, msg := range messages { + var role genai.Role + switch msg.Role { + case "user": + role = genai.RoleUser + case "model", "assistant": + role = genai.RoleModel + default: + role = genai.RoleUser + } + + // Handle content based on type + switch c := msg.Content.(type) { + case string: + contents = append(contents, genai.NewContentFromText(c, role)) + case []interface{}: + // Multimodal content - group parts within a single content + var parts []*genai.Part + for _, item := range c { + if itemMap, ok := item.(map[string]interface{}); ok { + contentType, _ := itemMap["type"].(string) + switch contentType { + case "text": + if text, ok := itemMap["text"].(string); ok { + parts = append(parts, genai.NewPartFromText(text)) + } + case "image_url": + if imgMap, ok := itemMap["image_url"].(map[string]interface{}); ok { + if url, ok := imgMap["url"].(string); ok { + parts = append(parts, genai.NewPartFromURI(url, "image/jpeg")) + } + } + } + } + } + if len(parts) > 0 { + contents = append(contents, genai.NewContentFromParts(parts, role)) + } + } } - generateContentConfig := &genai.GenerateContentConfig{} - generateContentConfig.ThinkingConfig = &genai.ThinkingConfig{} - if chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { - generateContentConfig.ThinkingConfig.IncludeThoughts = true - } else { - generateContentConfig.ThinkingConfig.IncludeThoughts = false - } - - response, err := client.Models.GenerateContent(ctx, *modelName, contents, generateContentConfig) + // Generate content (non-streaming) + response, err := client.Models.GenerateContent(ctx, modelName, contents, nil) if err != nil { return nil, err } - content := response.Text() - var responseContent string - if chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { - responseContent = response.Candidates[0].Content.Parts[0].Text - } + // Extract text from response + answer := response.Text() - chatResponse := &ChatResponse{ - Answer: &content, - ReasonContent: &responseContent, - } - return chatResponse, nil -} - -// ChatWithMessages sends multiple messages with roles and returns response -func (z *GoogleModel) ChatWithMessages(modelName string, apiKey *string, messages []Message, modelConfig *ChatConfig) (string, error) { - return "", fmt.Errorf("not implemented") + return &ChatResponse{Answer: &answer}, nil } // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) diff --git a/internal/entity/models/minimax.go b/internal/entity/models/minimax.go index 90c8492d77..3a1b257600 100644 --- a/internal/entity/models/minimax.go +++ b/internal/entity/models/minimax.go @@ -179,8 +179,135 @@ func (z *MinimaxModel) Chat(modelName, message *string, apiConfig *APIConfig, mo } // ChatWithMessages sends multiple messages with roles and returns response -func (z *MinimaxModel) ChatWithMessages(modelName string, apiKey *string, messages []Message, chatModelConfig *ChatConfig) (string, error) { - return "", fmt.Errorf("%s, ChatWithMessages not implemented", z.Name()) +func (z *MinimaxModel) ChatWithMessages(modelName string, apiConfig *APIConfig, messages []Message, chatModelConfig *ChatConfig) (*ChatResponse, error) { + if apiConfig == nil || apiConfig.ApiKey == nil || *apiConfig.ApiKey == "" { + return nil, fmt.Errorf("api key is nil or empty") + } + if len(messages) == 0 { + return nil, fmt.Errorf("messages is empty") + } + + var region = "default" + if apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat) + + // Convert messages to API format + apiMessages := make([]map[string]interface{}, len(messages)) + for i, msg := range messages { + apiMessages[i] = map[string]interface{}{ + "role": msg.Role, + "content": msg.Content, + } + } + + // Build request body + reqBody := map[string]interface{}{ + "model": modelName, + "messages": apiMessages, + "stream": false, + "temperature": 1, + } + + if chatModelConfig != nil { + if chatModelConfig.Temperature != nil { + reqBody["temperature"] = *chatModelConfig.Temperature + } + + if chatModelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *chatModelConfig.MaxTokens + } + + if chatModelConfig.Stream != nil { + reqBody["stream"] = *chatModelConfig.Stream + } + + if chatModelConfig.TopP != nil { + reqBody["top_p"] = *chatModelConfig.TopP + } + + if chatModelConfig.DoSample != nil { + reqBody["do_sample"] = *chatModelConfig.DoSample + } + + if chatModelConfig.Thinking != nil { + reqBody["thinking"] = *chatModelConfig.Thinking + } + } + + jsonData, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + + resp, err := z.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to send request: %d %s", resp.StatusCode, string(body)) + } + + // Parse response + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + choices, ok := result["choices"].([]interface{}) + if !ok { + return nil, fmt.Errorf("no choices in response") + } + + firstChoice, ok := choices[0].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("no choices in response") + } + + messageMap, ok := firstChoice["message"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("no message in response") + } + + content, ok := messageMap["content"].(string) + if !ok { + return nil, fmt.Errorf("no message in response") + } + + var reasonContent string + if chatModelConfig != nil && chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { + reasonContent, ok = messageMap["reasoning_content"].(string) + if !ok { + return nil, fmt.Errorf("invalid content format") + } + if reasonContent != "" && reasonContent[0] == '\n' { + reasonContent = reasonContent[1:] + } + } + + chatResponse := &ChatResponse{ + Answer: &content, + ReasonContent: &reasonContent, + } + + return chatResponse, nil } // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) diff --git a/internal/entity/models/moonshot.go b/internal/entity/models/moonshot.go index a55787f48a..c889e07127 100644 --- a/internal/entity/models/moonshot.go +++ b/internal/entity/models/moonshot.go @@ -60,52 +60,65 @@ func (z *MoonshotModel) Name() string { return "moonshot" } -// Chat sends a message and returns response -func (k *MoonshotModel) Chat(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { - if message == nil { - return nil, fmt.Errorf("message is nil") +func (k *MoonshotModel) ChatWithMessages(modelName string, apiConfig *APIConfig, messages []Message, chatModelConfig *ChatConfig) (*ChatResponse, error) { + if len(messages) == 0 { + return nil, fmt.Errorf("messages is empty") } var region = "default" - if apiConfig.Region != nil { + if apiConfig != nil && apiConfig.Region != nil { region = *apiConfig.Region } url := fmt.Sprintf("%s/%s", k.BaseURL[region], k.URLSuffix.Chat) + // Convert messages to the format expected by API + apiMessages := make([]map[string]interface{}, len(messages)) + for i, msg := range messages { + apiMessages[i] = map[string]interface{}{ + "role": msg.Role, + "content": msg.Content, + } + } + // Build request body reqBody := map[string]interface{}{ - "model": modelName, - "messages": []map[string]string{ - {"role": "user", "content": *message}, - }, - "stream": false, + "model": modelName, + "messages": apiMessages, + "stream": false, + "temperature": 1, } - if chatModelConfig.Stream != nil { - reqBody["stream"] = *chatModelConfig.Stream - } + if chatModelConfig != nil { + if chatModelConfig.Stream != nil { + reqBody["stream"] = *chatModelConfig.Stream + } - if chatModelConfig.MaxTokens != nil { - reqBody["max_tokens"] = *chatModelConfig.MaxTokens - } + if chatModelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *chatModelConfig.MaxTokens + } - if chatModelConfig.TopP != nil { - reqBody["top_p"] = *chatModelConfig.TopP - } + if chatModelConfig.Temperature != nil { + reqBody["temperature"] = *chatModelConfig.Temperature + } - if chatModelConfig.Stop != nil { - reqBody["stop"] = *chatModelConfig.Stop - } + if chatModelConfig.TopP != nil { + reqBody["top_p"] = *chatModelConfig.TopP + } - if chatModelConfig.Thinking != nil { - if *chatModelConfig.Thinking { - reqBody["thinking"] = map[string]interface{}{ - "type": "enabled", - } - } else { - reqBody["thinking"] = map[string]interface{}{ - "type": "disabled", + if chatModelConfig.Stop != nil { + reqBody["stop"] = *chatModelConfig.Stop + } + + if chatModelConfig.Thinking != nil { + if *chatModelConfig.Thinking { + reqBody["thinking"] = map[string]interface{}{ + "type": "enabled", + } + } else { + reqBody["thinking"] = map[string]interface{}{ + "type": "disabled", + } } } } @@ -121,7 +134,9 @@ func (k *MoonshotModel) Chat(modelName, message *string, apiConfig *APIConfig, c } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + if apiConfig != nil && apiConfig.ApiKey != nil { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + } resp, err := k.httpClient.Do(req) if err != nil { @@ -165,7 +180,7 @@ func (k *MoonshotModel) Chat(modelName, message *string, apiConfig *APIConfig, c } var reasonContent string - if chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { + if chatModelConfig != nil && chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { reasonContent, ok = messageMap["reasoning_content"].(string) if !ok { return nil, fmt.Errorf("invalid content format") @@ -184,11 +199,6 @@ func (k *MoonshotModel) Chat(modelName, message *string, apiConfig *APIConfig, c return chatResponse, nil } -// ChatWithMessages sends multiple messages with roles and returns response -func (k *MoonshotModel) ChatWithMessages(modelName string, apiKey *string, messages []Message, chatModelConfig *ChatConfig) (string, error) { - return "", fmt.Errorf("%s, ChatWithMessages not implemented", k.Name()) -} - // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) func (k *MoonshotModel) ChatStreamlyWithSender(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { var region = "default" diff --git a/internal/entity/models/siliconflow.go b/internal/entity/models/siliconflow.go index 11b59e1d21..960cf883c3 100644 --- a/internal/entity/models/siliconflow.go +++ b/internal/entity/models/siliconflow.go @@ -79,65 +79,58 @@ type SiliconflowRerankResponse struct { } `json:"results"` } -// Chat sends a message and returns response -func (z *SiliconflowModel) Chat(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { - if message == nil { - return nil, fmt.Errorf("message is nil") +// ChatWithMessages sends multiple messages with roles and returns response +func (z *SiliconflowModel) ChatWithMessages(modelName string, apiConfig *APIConfig, messages []Message, chatModelConfig *ChatConfig) (*ChatResponse, error) { + if apiConfig == nil || apiConfig.ApiKey == nil || *apiConfig.ApiKey == "" { + return nil, fmt.Errorf("api key is nil or empty") } - var region = "default" + if len(messages) == 0 { + return nil, fmt.Errorf("messages is empty") + } + + region := "default" if apiConfig.Region != nil { region = *apiConfig.Region } - url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat) - // I need to get the model type, such as qwen3 is the prefix, the model type will be qwen. glm is the prefix, the model type will be glm. such as the model name: qwen3-0.6b, the model type will be qwen3 - // the model name is glm-4.7, the model type will be glm - modelType := strings.Split(*modelName, "-")[0] - if modelType == "qwen" || modelType == "glm" { - url = fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.AsyncChat) + // Convert messages to the format expected by API + apiMessages := make([]map[string]interface{}, len(messages)) + for i, msg := range messages { + apiMessages[i] = map[string]interface{}{ + "role": msg.Role, + "content": msg.Content, + } } // Build request body reqBody := map[string]interface{}{ - "model": modelName, - "messages": []map[string]string{ - {"role": "user", "content": *message}, - }, + "model": modelName, + "messages": apiMessages, "stream": false, "temperature": 1, } - if chatModelConfig.Stream != nil { - reqBody["stream"] = *chatModelConfig.Stream - } + if chatModelConfig != nil { + if chatModelConfig.Stream != nil { + reqBody["stream"] = *chatModelConfig.Stream + } - if chatModelConfig.MaxTokens != nil { - reqBody["max_tokens"] = *chatModelConfig.MaxTokens - } + if chatModelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *chatModelConfig.MaxTokens + } - if chatModelConfig.Temperature != nil { - reqBody["temperature"] = *chatModelConfig.Temperature - } + if chatModelConfig.Temperature != nil { + reqBody["temperature"] = *chatModelConfig.Temperature + } - if chatModelConfig.TopP != nil { - reqBody["top_p"] = *chatModelConfig.TopP - } + if chatModelConfig.TopP != nil { + reqBody["top_p"] = *chatModelConfig.TopP + } - if chatModelConfig.Stop != nil { - reqBody["stop"] = *chatModelConfig.Stop - } - - if chatModelConfig.Thinking != nil { - if *chatModelConfig.Thinking { - reqBody["thinking"] = map[string]interface{}{ - "type": "enabled", - } - } else { - reqBody["thinking"] = map[string]interface{}{ - "type": "disabled", - } + if chatModelConfig.Stop != nil { + reqBody["stop"] = *chatModelConfig.Stop } } @@ -171,7 +164,7 @@ func (z *SiliconflowModel) Chat(modelName, message *string, apiConfig *APIConfig // Parse response var result map[string]interface{} - if err = json.Unmarshal(body, &result); err != nil { + if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } @@ -195,21 +188,32 @@ func (z *SiliconflowModel) Chat(modelName, message *string, apiConfig *APIConfig return nil, fmt.Errorf("invalid content format") } - thinking, answer := GetThinkingAndAnswer(chatModelConfig.ModelClass, &content) + var reasonContent string + if chatModelConfig != nil && chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { + reasonContent, ok = messageMap["reasoning_content"].(string) + if !ok { + // If reasoning_content not in response, try parsing from content tags + reasoning, answer := GetThinkingAndAnswer(chatModelConfig.ModelClass, &content) + if reasoning != nil { + reasonContent = *reasoning + content = *answer + } + } else { + // if first char of reasonContent is \n remove the '\n' + if reasonContent != "" && reasonContent[0] == '\n' { + reasonContent = reasonContent[1:] + } + } + } chatResponse := &ChatResponse{ - Answer: answer, - ReasonContent: thinking, + Answer: &content, + ReasonContent: &reasonContent, } return chatResponse, nil } -// ChatWithMessages sends multiple messages with roles and returns response -func (z *SiliconflowModel) ChatWithMessages(modelName string, apiKey *string, messages []Message, chatModelConfig *ChatConfig) (string, error) { - return "", fmt.Errorf("%s, ChatWithMessages not implemented", z.Name()) -} - // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) func (z *SiliconflowModel) ChatStreamlyWithSender(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { var region = "default" @@ -289,10 +293,6 @@ func (z *SiliconflowModel) ChatStreamlyWithSender(modelName, message *string, ap return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) } - reserveText := "" - thinkingPhase := false - answerPhase := false - // SSE parsing: read line by line scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { @@ -333,34 +333,17 @@ func (z *SiliconflowModel) ChatStreamlyWithSender(modelName, message *string, ap continue } + reasoningContent, ok := delta["reasoning_content"].(string) + if ok && reasoningContent != "" { + if err := sender(nil, &reasoningContent); err != nil { + return err + } + } + content, ok := delta["content"].(string) if ok && content != "" { - if content == "" { - thinkingPhase = true - continue - - } else if content == "" { - thinkingPhase = false - answerPhase = true - continue - } - - if thinkingPhase { - if err = sender(nil, &content); err != nil { - return err - } - reserveText = "" - } else if answerPhase { - if err = sender(&content, nil); err != nil { - return err - } - reserveText = "" - } else { - content = strings.Trim(content, "\n") - content = strings.Trim(content, " ") - if content != "" { - reserveText += content - } + if err := sender(&content, nil); err != nil { + return err } } @@ -370,12 +353,6 @@ func (z *SiliconflowModel) ChatStreamlyWithSender(modelName, message *string, ap } } - if reserveText != "" { - if err = sender(&reserveText, nil); err != nil { - return err - } - } - // Send [DONE] marker for OpenAI compatibility endOfStream := "[DONE]" if err = sender(&endOfStream, nil); err != nil { diff --git a/internal/entity/models/types.go b/internal/entity/models/types.go index d27468cccb..045495d5f0 100644 --- a/internal/entity/models/types.go +++ b/internal/entity/models/types.go @@ -1,9 +1,14 @@ package models -// Message represents a chat message with role +// Message represents a chat message with role and content +// +// Content is interface{} to support different formats: +// - string: plain text message (e.g., "Hello") +// - []interface{}: multimodal content array where each element is map[string]interface{} +// (e.g., [{"type": "text", "text": "..."}, {"type": "image_url", "image_url": {"url": "..."}}]) type Message struct { - Role string - Content string + Role string `json:"role"` + Content interface{} `json:"content"` } // EmbeddingModel interface for embedding models @@ -12,10 +17,8 @@ type ModelDriver interface { Name() string - // Chat sends a message and returns response - Chat(modelName, message *string, apiConfig *APIConfig, modelConfig *ChatConfig) (*ChatResponse, error) - // ChatWithMessages sends multiple messages with roles (system, user, etc.) and returns response - ChatWithMessages(modelName string, apiKey *string, messages []Message, modelConfig *ChatConfig) (string, error) + // ChatWithMessages sends multiple messages with role and content + ChatWithMessages(modelName string, apiConfig *APIConfig, messages []Message, chatModelConfig *ChatConfig) (*ChatResponse, error) // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) ChatStreamlyWithSender(modelName, message *string, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error // Encode encodes a list of texts into embeddings diff --git a/internal/entity/models/vllm.go b/internal/entity/models/vllm.go index 6cfdef91b4..1d5055a950 100644 --- a/internal/entity/models/vllm.go +++ b/internal/entity/models/vllm.go @@ -197,8 +197,137 @@ func (z *VllmModel) Chat(modelName, message *string, apiConfig *APIConfig, chatM } // ChatWithMessages sends multiple messages with roles and returns response -func (z *VllmModel) ChatWithMessages(modelName string, apiKey *string, messages []Message, modelConfig *ChatConfig) (string, error) { - return "", fmt.Errorf("not implemented") +func (z *VllmModel) ChatWithMessages(modelName string, apiConfig *APIConfig, messages []Message, chatModelConfig *ChatConfig) (*ChatResponse, error) { + if len(messages) == 0 { + return nil, fmt.Errorf("messages is empty") + } + + var region = "default" + if apiConfig.Region != nil { + region = *apiConfig.Region + } + + url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat) + + // For qwen/glm models, use async chat endpoint + modelType := strings.Split(modelName, "-")[0] + if modelType == "qwen" || modelType == "glm" { + url = fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.AsyncChat) + } + + // Convert messages to API format + apiMessages := make([]map[string]interface{}, len(messages)) + for i, msg := range messages { + apiMessages[i] = map[string]interface{}{ + "role": msg.Role, + "content": msg.Content, + } + } + + // Build request body + reqBody := map[string]interface{}{ + "model": modelName, + "messages": apiMessages, + "stream": false, + "temperature": 1, + } + + if chatModelConfig != nil { + if chatModelConfig.Stream != nil { + reqBody["stream"] = *chatModelConfig.Stream + } + + if chatModelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *chatModelConfig.MaxTokens + } + + if chatModelConfig.Temperature != nil { + reqBody["temperature"] = *chatModelConfig.Temperature + } + + if chatModelConfig.TopP != nil { + reqBody["top_p"] = *chatModelConfig.TopP + } + + if chatModelConfig.Stop != nil { + reqBody["stop"] = *chatModelConfig.Stop + } + + if chatModelConfig.Thinking != nil { + if *chatModelConfig.Thinking { + reqBody["thinking"] = map[string]interface{}{ + "type": "enabled", + } + } else { + reqBody["thinking"] = map[string]interface{}{ + "type": "disabled", + } + } + } + } + + jsonData, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + + resp, err := z.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) + } + + // Parse response + var result map[string]interface{} + if err = json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + choices, ok := result["choices"].([]interface{}) + if !ok || len(choices) == 0 { + return nil, fmt.Errorf("no choices in response") + } + + firstChoice, ok := choices[0].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid choice format") + } + + messageMap, ok := firstChoice["message"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid message format") + } + + content, ok := messageMap["content"].(string) + if !ok { + return nil, fmt.Errorf("invalid content format") + } + + thinking, answer := GetThinkingAndAnswer(chatModelConfig.ModelClass, &content) + + chatResponse := &ChatResponse{ + Answer: answer, + ReasonContent: thinking, + } + + return chatResponse, nil } // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) diff --git a/internal/entity/models/volcengine.go b/internal/entity/models/volcengine.go index 49a120962c..3099a03b5d 100644 --- a/internal/entity/models/volcengine.go +++ b/internal/entity/models/volcengine.go @@ -211,8 +211,163 @@ func (z *VolcEngine) Chat(modelName, message *string, apiConfig *APIConfig, mode } // ChatWithMessages sends multiple messages with roles and returns response -func (z *VolcEngine) ChatWithMessages(modelName string, apiKey *string, messages []Message, chatModelConfig *ChatConfig) (string, error) { - return "", fmt.Errorf("%s, ChatWithMessages not implemented", z.Name()) +func (z *VolcEngine) ChatWithMessages(modelName string, apiConfig *APIConfig, messages []Message, chatModelConfig *ChatConfig) (*ChatResponse, error) { + if len(messages) == 0 { + return nil, fmt.Errorf("messages is empty") + } + + var region = "default" + if apiConfig != nil && apiConfig.Region != nil { + region = *apiConfig.Region + } + + url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat) + + // Convert messages to API format + apiMessages := make([]map[string]interface{}, len(messages)) + for i, msg := range messages { + apiMessages[i] = map[string]interface{}{ + "role": msg.Role, + "content": msg.Content, + } + } + + // Build request body + reqBody := map[string]interface{}{ + "model": modelName, + "messages": apiMessages, + "stream": false, + "temperature": 1, + } + + if chatModelConfig != nil { + if chatModelConfig.Stream != nil { + reqBody["stream"] = *chatModelConfig.Stream + } + + if chatModelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *chatModelConfig.MaxTokens + } + + if chatModelConfig.Temperature != nil { + reqBody["temperature"] = *chatModelConfig.Temperature + } + + if chatModelConfig.TopP != nil { + reqBody["top_p"] = *chatModelConfig.TopP + } + + if chatModelConfig.Thinking != nil { + if *chatModelConfig.Thinking { + var thinkingFlag string + effort := "medium" + if chatModelConfig.Effort != nil { + effort = *chatModelConfig.Effort + } + switch effort { + case "none", "minimal": + thinkingFlag = "disabled" + reqBody["reasoning_effort"] = "minimal" + case "low": + thinkingFlag = "enabled" + reqBody["reasoning_effort"] = "low" + case "medium": + thinkingFlag = "enabled" + reqBody["reasoning_effort"] = "medium" + case "auto", "default": + thinkingFlag = "enabled" + reqBody["reasoning_effort"] = "medium" + case "high": + thinkingFlag = "enabled" + reqBody["reasoning_effort"] = "high" + default: + thinkingFlag = "enabled" + reqBody["reasoning_effort"] = effort + } + reqBody["thinking"] = map[string]interface{}{ + "type": thinkingFlag, + } + } else { + reqBody["thinking"] = map[string]interface{}{ + "type": "disabled", + } + } + } + } + + jsonData, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + if apiConfig != nil && apiConfig.ApiKey != nil { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + } + + resp, err := z.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) + } + + // Parse response + var result map[string]interface{} + if err = json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + choices, ok := result["choices"].([]interface{}) + if !ok || len(choices) == 0 { + return nil, fmt.Errorf("no choices in response") + } + + firstChoice, ok := choices[0].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid choice format") + } + + messageMap, ok := firstChoice["message"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid message format") + } + + content, ok := messageMap["content"].(string) + if !ok { + return nil, fmt.Errorf("invalid content format") + } + + var reasonContent string + if chatModelConfig != nil && chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { + reasonContent, ok = messageMap["reasoning_content"].(string) + if !ok { + return nil, fmt.Errorf("invalid reasonContent format") + } + if reasonContent != "" && reasonContent[0] == '\n' { + reasonContent = reasonContent[1:] + } + } + + chatResponse := &ChatResponse{ + Answer: &content, + ReasonContent: &reasonContent, + } + + return chatResponse, nil } // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) diff --git a/internal/entity/models/zhipu-ai.go b/internal/entity/models/zhipu-ai.go index ee9ea289ab..bbb9d68953 100644 --- a/internal/entity/models/zhipu-ai.go +++ b/internal/entity/models/zhipu-ai.go @@ -60,57 +60,69 @@ func (z *ZhipuAIModel) Name() string { return "zhipu" } -// Chat sends a message and returns response -func (z *ZhipuAIModel) Chat(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { - if message == nil { - return nil, fmt.Errorf("message is nil") +// ChatWithMessages sends multiple messages with roles and returns response +func (z *ZhipuAIModel) ChatWithMessages(modelName string, apiConfig *APIConfig, messages []Message, chatModelConfig *ChatConfig) (*ChatResponse, error) { + if apiConfig == nil || apiConfig.ApiKey == nil || *apiConfig.ApiKey == "" { + return nil, fmt.Errorf("api key is nil or empty") } - var region = "default" + if len(messages) == 0 { + return nil, fmt.Errorf("messages is empty") + } + + region := "default" if apiConfig.Region != nil { region = *apiConfig.Region } - url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat) + // Convert messages to the format expected by API + apiMessages := make([]map[string]interface{}, len(messages)) + for i, msg := range messages { + apiMessages[i] = map[string]interface{}{ + "role": msg.Role, + "content": msg.Content, + } + } + // Build request body reqBody := map[string]interface{}{ - "model": modelName, - "messages": []map[string]string{ - {"role": "user", "content": *message}, - }, + "model": modelName, + "messages": apiMessages, "stream": false, "temperature": 1, } - if chatModelConfig.Stream != nil { - reqBody["stream"] = *chatModelConfig.Stream - } + if chatModelConfig != nil { + if chatModelConfig.Stream != nil { + reqBody["stream"] = *chatModelConfig.Stream + } - if chatModelConfig.MaxTokens != nil { - reqBody["max_tokens"] = *chatModelConfig.MaxTokens - } + if chatModelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *chatModelConfig.MaxTokens + } - if chatModelConfig.Temperature != nil { - reqBody["temperature"] = *chatModelConfig.Temperature - } + if chatModelConfig.Temperature != nil { + reqBody["temperature"] = *chatModelConfig.Temperature + } - if chatModelConfig.TopP != nil { - reqBody["top_p"] = *chatModelConfig.TopP - } + if chatModelConfig.TopP != nil { + reqBody["top_p"] = *chatModelConfig.TopP + } - if chatModelConfig.Stop != nil { - reqBody["stop"] = *chatModelConfig.Stop - } + if chatModelConfig.Stop != nil { + reqBody["stop"] = *chatModelConfig.Stop + } - if chatModelConfig.Thinking != nil { - if *chatModelConfig.Thinking { - reqBody["thinking"] = map[string]interface{}{ - "type": "enabled", - } - } else { - reqBody["thinking"] = map[string]interface{}{ - "type": "disabled", + if chatModelConfig.Thinking != nil { + if *chatModelConfig.Thinking { + reqBody["thinking"] = map[string]interface{}{ + "type": "enabled", + } + } else { + reqBody["thinking"] = map[string]interface{}{ + "type": "disabled", + } } } } @@ -145,7 +157,7 @@ func (z *ZhipuAIModel) Chat(modelName, message *string, apiConfig *APIConfig, ch // Parse response var result map[string]interface{} - if err = json.Unmarshal(body, &result); err != nil { + if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } @@ -170,7 +182,7 @@ func (z *ZhipuAIModel) Chat(modelName, message *string, apiConfig *APIConfig, ch } var reasonContent string - if chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { + if chatModelConfig != nil && chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { reasonContent, ok = messageMap["reasoning_content"].(string) if !ok { return nil, fmt.Errorf("invalid content format") @@ -189,106 +201,6 @@ func (z *ZhipuAIModel) Chat(modelName, message *string, apiConfig *APIConfig, ch return chatResponse, nil } -// ChatWithMessages sends multiple messages with roles and returns response -func (z *ZhipuAIModel) ChatWithMessages(modelName string, apiKey *string, messages []Message, chatModelConfig *ChatConfig) (string, error) { - if apiKey == nil || *apiKey == "" { - return "", fmt.Errorf("api key is nil or empty") - } - - if len(messages) == 0 { - return "", fmt.Errorf("messages is empty") - } - - url := fmt.Sprintf("%s/%s", z.BaseURL["default"], z.URLSuffix.Chat) - - // Convert messages to the format expected by API - apiMessages := make([]map[string]string, len(messages)) - for i, msg := range messages { - apiMessages[i] = map[string]string{ - "role": msg.Role, - "content": msg.Content, - } - } - - // Build request body - reqBody := map[string]interface{}{ - "model": modelName, - "messages": apiMessages, - "stream": false, - "temperature": 1, - } - - if chatModelConfig != nil { - if chatModelConfig.MaxTokens != nil { - reqBody["max_tokens"] = *chatModelConfig.MaxTokens - } - - if chatModelConfig.Temperature != nil { - reqBody["temperature"] = *chatModelConfig.Temperature - } - - if chatModelConfig.TopP != nil { - reqBody["top_p"] = *chatModelConfig.TopP - } - } - - jsonData, err := json.Marshal(reqBody) - if err != nil { - return "", fmt.Errorf("failed to marshal request: %w", err) - } - - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) - if err != nil { - return "", fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey)) - - resp, err := z.httpClient.Do(req) - if err != nil { - return "", fmt.Errorf("failed to send request: %w", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read response: %w", err) - } - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) - } - - // Parse response - var result map[string]interface{} - if err := json.Unmarshal(body, &result); err != nil { - return "", fmt.Errorf("failed to parse response: %w", err) - } - - choices, ok := result["choices"].([]interface{}) - if !ok || len(choices) == 0 { - return "", fmt.Errorf("no choices in response") - } - - firstChoice, ok := choices[0].(map[string]interface{}) - if !ok { - return "", fmt.Errorf("invalid choice format") - } - - messageMap, ok := firstChoice["message"].(map[string]interface{}) - if !ok { - return "", fmt.Errorf("invalid message format") - } - - content, ok := messageMap["content"].(string) - if !ok { - return "", fmt.Errorf("invalid content format") - } - - return content, nil -} - // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) func (z *ZhipuAIModel) ChatStreamlyWithSender(modelName, message *string, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { var region = "default" diff --git a/internal/handler/providers.go b/internal/handler/providers.go index 4db54759df..c6d6f1d513 100644 --- a/internal/handler/providers.go +++ b/internal/handler/providers.go @@ -756,14 +756,14 @@ func (h *ProviderHandler) DropInstanceModels(c *gin.Context) { } type ChatToModelRequest struct { - ProviderName *string `json:"provider_name"` - InstanceName *string `json:"instance_name"` - ModelName *string `json:"model_name"` - Message string `json:"message" binding:"required"` - Stream bool `json:"stream"` - Thinking bool `json:"thinking"` - Effort *string `json:"effort"` - Verbosity *string `json:"verbosity"` + ProviderName *string `json:"provider_name"` + InstanceName *string `json:"instance_name"` + ModelName *string `json:"model_name"` + Messages []map[string]interface{} `json:"messages"` + Stream bool `json:"stream"` + Thinking bool `json:"thinking"` + Effort *string `json:"effort"` + Verbosity *string `json:"verbosity"` } func (h *ProviderHandler) ChatToModel(c *gin.Context) { @@ -828,6 +828,24 @@ func (h *ProviderHandler) ChatToModel(c *gin.Context) { // Check if it's a stream request if req.Stream { + // Streaming with multimodal messages not yet supported + hasMultimodal := false + for _, msg := range req.Messages { + if content, ok := msg["content"]; ok { + if _, isArray := content.([]interface{}); isArray { + hasMultimodal = true + break + } + } + } + if hasMultimodal { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "Streaming with multimodal messages not yet supported", + }) + return + } + // Set SSE headers c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") @@ -859,7 +877,7 @@ func (h *ProviderHandler) ChatToModel(c *gin.Context) { } // Stream response using sender function (best performance, no channel) - errorCode, err := h.modelProviderService.ChatToModelStreamWithSender(*req.ProviderName, *req.InstanceName, *req.ModelName, userID, req.Message, &apiConfig, &chatConfig, sender) + errorCode, err := h.modelProviderService.ChatToModelStreamWithSender(*req.ProviderName, *req.InstanceName, *req.ModelName, userID, req.Messages[0]["content"].(string), &apiConfig, &chatConfig, sender) if errorCode != common.CodeSuccess { c.SSEvent("error", err.Error()) @@ -868,7 +886,19 @@ func (h *ProviderHandler) ChatToModel(c *gin.Context) { } // Non-stream response - response, errorCode, err := h.modelProviderService.ChatToModel(*req.ProviderName, *req.InstanceName, *req.ModelName, userID, req.Message, &apiConfig, &chatConfig) + var response *models.ChatResponse + var errorCode common.ErrorCode + var err error + + // Convert []map[string]interface{} to []models.Message + messages := make([]models.Message, len(req.Messages)) + for i, msg := range req.Messages { + role, _ := msg["role"].(string) + content := msg["content"] + messages[i] = models.Message{Role: role, Content: content} + } + response, errorCode, err = h.modelProviderService.ChatToModelWithMessages(*req.ProviderName, *req.InstanceName, *req.ModelName, userID, messages, &apiConfig, &chatConfig) + if err != nil { c.JSON(http.StatusOK, gin.H{ "code": errorCode, diff --git a/internal/service/chat_session.go b/internal/service/chat_session.go index 30fdb53d1b..1df7f3716f 100644 --- a/internal/service/chat_session.go +++ b/internal/service/chat_session.go @@ -549,9 +549,12 @@ func (s *ChatSessionService) asyncChatSolo(dialog *entity.Chat, session *entity. } for _, msg := range processedMessages { role, _ := msg["role"].(string) - content, _ := msg["content"].(string) - if role != "" && content != "" && role != "system" { - msgs = append(msgs, modelModule.Message{Role: role, Content: content}) + if role == "" || role == "system" { + continue + } + + if msg["content"] != nil { + msgs = append(msgs, modelModule.Message{Role: role, Content: msg["content"]}) } } @@ -559,7 +562,7 @@ func (s *ChatSessionService) asyncChatSolo(dialog *entity.Chat, session *entity. chatConfig := s.buildChatConfig(dialog, config) // Perform chat - response, err := chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, chatModel.APIConfig.ApiKey, msgs, chatConfig) + response, err := chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, chatModel.APIConfig, msgs, chatConfig) if err != nil { logger.Error("asyncChatSolo chat failed", err) return nil, err @@ -568,11 +571,11 @@ func (s *ChatSessionService) asyncChatSolo(dialog *entity.Chat, session *entity. logger.Info("asyncChatSolo completed", zap.String("tenant_id", dialog.TenantID), zap.String("llm_id", dialog.LLMID), - zap.Int("response_length", len(response))) + zap.Int("response_length", len(*response.Answer))) // Structure the answer ans := map[string]interface{}{ - "answer": response, + "answer": *response.Answer, "reference": reference[len(reference)-1], "final": true, } diff --git a/internal/service/model_service.go b/internal/service/model_service.go index 5916a1bdbe..f2a4f2f750 100644 --- a/internal/service/model_service.go +++ b/internal/service/model_service.go @@ -674,7 +674,14 @@ func (m *ModelProviderService) UpdateModelStatus(providerName, instanceName, mod return common.CodeSuccess, nil } -func (m *ModelProviderService) ChatToModel(providerName, instanceName, modelName, userID, message string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ChatConfig) (*modelModule.ChatResponse, common.ErrorCode, error) { +// ChatToModelWithMessages sends messages to the model with messages array +func (m *ModelProviderService) ChatToModelWithMessages(providerName, instanceName, modelName, userID string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ChatConfig) (*modelModule.ChatResponse, common.ErrorCode, error) { + if apiConfig == nil { + apiConfig = &modelModule.APIConfig{} + } + if modelConfig == nil { + modelConfig = &modelModule.ChatConfig{} + } // Get tenant ID from user tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner") @@ -725,10 +732,13 @@ func (m *ModelProviderService) ChatToModel(providerName, instanceName, modelName apiConfig.ApiKey = &instance.APIKey var response *modelModule.ChatResponse - response, err = providerInfo.ModelDriver.Chat(&modelName, &message, apiConfig, modelConfig) + response, err = providerInfo.ModelDriver.ChatWithMessages(modelName, apiConfig, messages, modelConfig) if err != nil { return nil, common.CodeServerError, err } + if response == nil { + return nil, common.CodeServerError, errors.New("empty chat response") + } return response, common.CodeSuccess, nil } @@ -750,9 +760,6 @@ func (m *ModelProviderService) ChatToModel(providerName, instanceName, modelName apiConfig.Region = ®ion apiConfig.ApiKey = &instance.APIKey - modelTypes := extra["model_types"] - println(modelTypes) - modelConfig.ModelClass = &providerInfo.Class newURL := map[string]string{ @@ -761,10 +768,14 @@ func (m *ModelProviderService) ChatToModel(providerName, instanceName, modelName newProviderInfo := providerInfo.ModelDriver.NewInstance(newURL) var response *modelModule.ChatResponse - response, err = newProviderInfo.Chat(&modelName, &message, apiConfig, modelConfig) + response, err = newProviderInfo.ChatWithMessages(modelName, apiConfig, messages, modelConfig) if err != nil { return nil, common.CodeServerError, err } + if response == nil { + return nil, common.CodeServerError, errors.New("empty chat response") + } + return response, common.CodeSuccess, nil } @@ -783,13 +794,16 @@ func (m *ModelProviderService) ChatWithMessagesToModelByApiKey(providerName, mod return nil, common.CodeNotFound, errors.New(fmt.Sprintf("provider %s model %s not found", providerName, modelName)) } - var response string - response, err = providerInfo.ModelDriver.ChatWithMessages(modelName, &apiKey, messages, nil) + var response *modelModule.ChatResponse + response, err = providerInfo.ModelDriver.ChatWithMessages(modelName, &modelModule.APIConfig{ApiKey: &apiKey}, messages, nil) if err != nil { return nil, common.CodeServerError, err } + if response == nil { + return nil, common.CodeServerError, errors.New("empty chat response") + } - return &response, common.CodeSuccess, nil + return response.Answer, common.CodeSuccess, nil } // ChatToModelStreamWithSender streams chat response directly via sender function (best performance, no channel)