提交
This commit is contained in:
188
internal/controller/copilot/chat_completions.go
Normal file
188
internal/controller/copilot/chat_completions.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package copilot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ChatCompletions chat对话接口
|
||||
func ChatCompletions(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// 添加响应头, 解决vscode校验github所属问题
|
||||
requestID := uuid.Must(uuid.NewV4()).String()
|
||||
c.Header("x-github-request-id", requestID)
|
||||
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if nil != err {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
apiModelName := gjson.GetBytes(body, "model").String()
|
||||
// 默认设置的对话模型
|
||||
envModelName := os.Getenv("CHAT_API_MODEL_NAME")
|
||||
// 默认设置的对话请求地址
|
||||
chatAPIURL := os.Getenv("CHAT_API_BASE")
|
||||
// 默认设置的对话模型key
|
||||
apiKey := os.Getenv("CHAT_API_KEY")
|
||||
|
||||
// 轻量模型直接走代码补全接口, 节约成本
|
||||
if strings.Contains(apiModelName, os.Getenv("LIGHTWEIGHT_MODEL")) {
|
||||
envModelName = os.Getenv("CODEX_API_MODEL_NAME")
|
||||
codexAPIURL := os.Getenv("CODEX_API_BASE")
|
||||
parsedURL, err := url.Parse(codexAPIURL)
|
||||
if err != nil {
|
||||
fmt.Println("URL解析错误:", err)
|
||||
return
|
||||
}
|
||||
chatAPIURL = "https://" + parsedURL.Hostname() + "/v1/chat/completions"
|
||||
apiKey = os.Getenv("CODEX_API_KEY")
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
|
||||
body, _ = sjson.SetBytes(body, "model", envModelName)
|
||||
body, _ = sjson.SetBytes(body, "stream", true) // 强制流式输出
|
||||
|
||||
if !gjson.GetBytes(body, "function_call").Exists() {
|
||||
messages := gjson.GetBytes(body, "messages").Array()
|
||||
for i, msg := range messages {
|
||||
toolCalls := msg.Get("tool_calls").Array()
|
||||
if len(toolCalls) == 0 {
|
||||
body, _ = sjson.DeleteBytes(body, fmt.Sprintf("messages.%d.tool_calls", i))
|
||||
}
|
||||
}
|
||||
lastIndex := len(messages) - 1
|
||||
chatLocale := os.Getenv("CHAT_LOCALE")
|
||||
if chatLocale != "" && !strings.Contains(messages[lastIndex].Get("content").String(), "Respond in the following locale") {
|
||||
body, _ = sjson.SetBytes(body, "messages."+strconv.Itoa(lastIndex)+".content", messages[lastIndex].Get("content").String()+"Respond in the following locale: "+chatLocale+".")
|
||||
}
|
||||
}
|
||||
|
||||
body, _ = sjson.DeleteBytes(body, "intent")
|
||||
body, _ = sjson.DeleteBytes(body, "intent_threshold")
|
||||
body, _ = sjson.DeleteBytes(body, "intent_content")
|
||||
body, _ = sjson.DeleteBytes(body, "logprobs") // #IBZYCA
|
||||
|
||||
// 是否支持使用工具, 避免模型不支持相关功能报错
|
||||
chatUseTools, _ := strconv.ParseBool(os.Getenv("CHAT_USE_TOOLS"))
|
||||
if !chatUseTools {
|
||||
body, _ = sjson.DeleteBytes(body, "tools")
|
||||
body, _ = sjson.DeleteBytes(body, "tool_call")
|
||||
body, _ = sjson.DeleteBytes(body, "functions")
|
||||
body, _ = sjson.DeleteBytes(body, "function_call")
|
||||
body, _ = sjson.DeleteBytes(body, "tool_choice")
|
||||
}
|
||||
|
||||
ChatMaxTokens, _ := strconv.Atoi(os.Getenv("CHAT_MAX_TOKENS"))
|
||||
if int(gjson.GetBytes(body, "max_tokens").Int()) > ChatMaxTokens {
|
||||
body, _ = sjson.SetBytes(body, "max_tokens", ChatMaxTokens)
|
||||
}
|
||||
|
||||
if gjson.GetBytes(body, "n").Int() > 1 {
|
||||
body, _ = sjson.SetBytes(body, "n", 1)
|
||||
}
|
||||
|
||||
messages := gjson.GetBytes(body, "messages").Array()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
|
||||
// 拦截处理vscode对话首次预处理请求, 减少等待时间
|
||||
firstRole := gjson.GetBytes(body, "messages.0.role").String()
|
||||
firstContent := gjson.GetBytes(body, "messages.0.content").String()
|
||||
if strings.Contains(firstRole, "system") && strings.Contains(firstContent, "You are a helpful AI programming assistant to a user") &&
|
||||
!strings.Contains(firstContent, "If you cannot choose just one category, or if none of the categories seem like they would provide the user with a better result, you must always respond with") &&
|
||||
!gjson.GetBytes(body, "tool_choice").Exists() {
|
||||
_, _ = c.Writer.WriteString("data: [DONE]\n\n")
|
||||
c.Writer.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
// vs2022客户端的兼容处理
|
||||
if strings.Contains(userAgent, "VSCopilotClient") {
|
||||
lastMessage := messages[len(messages)-1]
|
||||
messageRole := lastMessage.Get("role").String()
|
||||
messageContent := lastMessage.Get("content").String()
|
||||
if strings.Contains(firstRole, "system") && strings.Contains(firstContent, "You are an AI programming assistant") {
|
||||
vs2022FirstChatTemplate(c)
|
||||
return
|
||||
}
|
||||
if messageRole == "user" && messageContent == "Write a short one-sentence question that I can ask that naturally follows from the previous few questions and answers. It should not ask a question which is already answered in the conversation. It should be a question that you are capable of answering. Reply with only the text of the question and nothing else." {
|
||||
_, _ = c.Writer.WriteString("data: [DONE]\n\n")
|
||||
c.Writer.Flush()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, chatAPIURL, io.NopCloser(bytes.NewBuffer(body)))
|
||||
if nil != err {
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
httpClientTimeout, _ := time.ParseDuration(os.Getenv("HTTP_CLIENT_TIMEOUT") + "s")
|
||||
client := &http.Client{
|
||||
Timeout: httpClientTimeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if nil != err {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
c.AbortWithStatus(http.StatusRequestTimeout)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("request conversation failed:", err.Error())
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer CloseIO(resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
log.Println("request completions failed:", string(body))
|
||||
|
||||
resp.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
|
||||
c.Status(resp.StatusCode)
|
||||
_, _ = io.Copy(c.Writer, resp.Body)
|
||||
}
|
||||
|
||||
// vs2022FirstChatTemplate is a template for the first chat completion response
|
||||
func vs2022FirstChatTemplate(c *gin.Context) {
|
||||
fixedOutput := `data: {"id":"f6202f6f-9d13-4518-b34f-65e945b0a1a2","object":"chat.completion.chunk","model":"gpt-4o-mini-2024-07-18","created":1734752124,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
|
||||
|
||||
data: {"id":"b2ab39cb-9a84-4006-b470-93a5965c6d69","object":"chat.completion.chunk","model":"gpt-4o-mini-2024-07-18","created":1734752124,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
|
||||
|
||||
data: {"id":"df5f9ce7-b653-4ffb-8d92-e21856ce1ffc","object":"chat.completion.chunk","model":"gpt-4o-mini-2024-07-18","created":1734752124,"choices":[{"index":0,"delta":{"role":"assistant","content":"Explain"},"finish_reason":null}]}
|
||||
|
||||
data: {"id":"fb58d66e-bb16-43f2-8470-2de0c8662533","object":"chat.completion.chunk","model":"gpt-4o-mini-2024-07-18","created":1734752124,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
|
||||
|
||||
data: {"id":"22ea16e2-766f-4b10-84d0-68399abc9181","object":"chat.completion.chunk","model":"gpt-4o-mini-2024-07-18","created":1734752124,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop"}]}
|
||||
|
||||
data: [DONE]
|
||||
|
||||
`
|
||||
_, _ = c.Writer.WriteString(fixedOutput)
|
||||
c.Writer.Flush()
|
||||
}
|
||||
Reference in New Issue
Block a user