diff --git a/relay/adaptor/tencent/adaptor.go b/relay/adaptor/tencent/adaptor.go index a97476d6cd..0de92d4af7 100644 --- a/relay/adaptor/tencent/adaptor.go +++ b/relay/adaptor/tencent/adaptor.go @@ -2,35 +2,43 @@ package tencent import ( "errors" - "fmt" "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/relay/adaptor" "github.com/songquanpeng/one-api/relay/adaptor/openai" "github.com/songquanpeng/one-api/relay/meta" "github.com/songquanpeng/one-api/relay/model" "io" "net/http" + "strconv" "strings" ) // https://cloud.tencent.com/document/api/1729/101837 type Adaptor struct { - Sign string + Sign string + Action string + Version string + Timestamp int64 } func (a *Adaptor) Init(meta *meta.Meta) { - + a.Action = "ChatCompletions" + a.Version = "2023-09-01" + a.Timestamp = helper.GetTimestamp() } func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - return fmt.Sprintf("%s/hyllm/v1/chat/completions", meta.BaseURL), nil + return meta.BaseURL + "/", nil } func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { adaptor.SetupCommonRequestHeader(c, req, meta) req.Header.Set("Authorization", a.Sign) - req.Header.Set("X-TC-Action", meta.ActualModelName) + req.Header.Set("X-TC-Action", a.Action) + req.Header.Set("X-TC-Version", a.Version) + req.Header.Set("X-TC-Timestamp", strconv.FormatInt(a.Timestamp, 10)) return nil } @@ -40,15 +48,13 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G } apiKey := c.Request.Header.Get("Authorization") apiKey = strings.TrimPrefix(apiKey, "Bearer ") - appId, secretId, secretKey, err := ParseConfig(apiKey) + _, secretId, secretKey, err := ParseConfig(apiKey) if err != nil { return nil, err } tencentRequest := ConvertRequest(*request) - tencentRequest.AppId = appId - tencentRequest.SecretId = secretId // we have to calculate the sign here - a.Sign = GetSign(*tencentRequest, secretKey) + a.Sign = GetSign(*tencentRequest, a, secretId, secretKey) return tencentRequest, nil } diff --git a/relay/adaptor/tencent/constants.go b/relay/adaptor/tencent/constants.go index fe176c2ce8..be415a94c8 100644 --- a/relay/adaptor/tencent/constants.go +++ b/relay/adaptor/tencent/constants.go @@ -1,7 +1,8 @@ package tencent var ModelList = []string{ - "ChatPro", - "ChatStd", - "hunyuan", + "hunyuan-lite", + "hunyuan-standard", + "hunyuan-standard-256K", + "hunyuan-pro", } diff --git a/relay/adaptor/tencent/main.go b/relay/adaptor/tencent/main.go index 2ca5724e81..0a57dcf747 100644 --- a/relay/adaptor/tencent/main.go +++ b/relay/adaptor/tencent/main.go @@ -3,8 +3,8 @@ package tencent import ( "bufio" "crypto/hmac" - "crypto/sha1" - "encoding/base64" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -19,34 +19,26 @@ import ( "github.com/songquanpeng/one-api/relay/model" "io" "net/http" - "sort" "strconv" "strings" + "time" ) -// https://cloud.tencent.com/document/product/1729/97732 - func ConvertRequest(request model.GeneralOpenAIRequest) *ChatRequest { - messages := make([]Message, 0, len(request.Messages)) + messages := make([]*Message, 0, len(request.Messages)) for i := 0; i < len(request.Messages); i++ { message := request.Messages[i] - messages = append(messages, Message{ + messages = append(messages, &Message{ Content: message.StringContent(), Role: message.Role, }) } - stream := 0 - if request.Stream { - stream = 1 - } return &ChatRequest{ - Timestamp: helper.GetTimestamp(), - Expired: helper.GetTimestamp() + 24*60*60, - QueryID: random.GetUUID(), - Temperature: request.Temperature, - TopP: request.TopP, - Stream: stream, + Model: &request.Model, + Stream: &request.Stream, Messages: messages, + TopP: &request.TopP, + Temperature: &request.Temperature, } } @@ -54,7 +46,11 @@ func responseTencent2OpenAI(response *ChatResponse) *openai.TextResponse { fullTextResponse := openai.TextResponse{ Object: "chat.completion", Created: helper.GetTimestamp(), - Usage: response.Usage, + Usage: model.Usage{ + PromptTokens: response.Usage.PromptTokens, + CompletionTokens: response.Usage.CompletionTokens, + TotalTokens: response.Usage.TotalTokens, + }, } if len(response.Choices) > 0 { choice := openai.TextResponseChoice{ @@ -154,6 +150,7 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { var TencentResponse ChatResponse + var responseP ChatResponseP responseBody, err := io.ReadAll(resp.Body) if err != nil { return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil @@ -162,10 +159,11 @@ func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, * if err != nil { return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil } - err = json.Unmarshal(responseBody, &TencentResponse) + err = json.Unmarshal(responseBody, &responseP) if err != nil { return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil } + TencentResponse = responseP.Response if TencentResponse.Error.Code != 0 { return &model.ErrorWithStatusCode{ Error: model.Error{ @@ -202,29 +200,62 @@ func ParseConfig(config string) (appId int64, secretId string, secretKey string, return } -func GetSign(req ChatRequest, secretKey string) string { - params := make([]string, 0) - params = append(params, "app_id="+strconv.FormatInt(req.AppId, 10)) - params = append(params, "secret_id="+req.SecretId) - params = append(params, "timestamp="+strconv.FormatInt(req.Timestamp, 10)) - params = append(params, "query_id="+req.QueryID) - params = append(params, "temperature="+strconv.FormatFloat(req.Temperature, 'f', -1, 64)) - params = append(params, "top_p="+strconv.FormatFloat(req.TopP, 'f', -1, 64)) - params = append(params, "stream="+strconv.Itoa(req.Stream)) - params = append(params, "expired="+strconv.FormatInt(req.Expired, 10)) +func sha256hex(s string) string { + b := sha256.Sum256([]byte(s)) + return hex.EncodeToString(b[:]) +} - var messageStr string - for _, msg := range req.Messages { - messageStr += fmt.Sprintf(`{"role":"%s","content":"%s"},`, msg.Role, msg.Content) - } - messageStr = strings.TrimSuffix(messageStr, ",") - params = append(params, "messages=["+messageStr+"]") +func hmacSha256(s, key string) string { + hashed := hmac.New(sha256.New, []byte(key)) + hashed.Write([]byte(s)) + return string(hashed.Sum(nil)) +} + +func GetSign(req ChatRequest, adaptor *Adaptor, secId, secKey string) string { + // build canonical request string + host := "hunyuan.tencentcloudapi.com" + httpRequestMethod := "POST" + canonicalURI := "/" + canonicalQueryString := "" + canonicalHeaders := fmt.Sprintf("content-type:%s\nhost:%s\nx-tc-action:%s\n", + "application/json", host, strings.ToLower(adaptor.Action)) + signedHeaders := "content-type;host;x-tc-action" + payload, _ := json.Marshal(req) + hashedRequestPayload := sha256hex(string(payload)) + canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", + httpRequestMethod, + canonicalURI, + canonicalQueryString, + canonicalHeaders, + signedHeaders, + hashedRequestPayload) + // build string to sign + algorithm := "TC3-HMAC-SHA256" + requestTimestamp := strconv.FormatInt(adaptor.Timestamp, 10) + timestamp, _ := strconv.ParseInt(requestTimestamp, 10, 64) + t := time.Unix(timestamp, 0).UTC() + // must be the format 2006-01-02, ref to package time for more info + date := t.Format("2006-01-02") + credentialScope := fmt.Sprintf("%s/%s/tc3_request", date, "hunyuan") + hashedCanonicalRequest := sha256hex(canonicalRequest) + string2sign := fmt.Sprintf("%s\n%s\n%s\n%s", + algorithm, + requestTimestamp, + credentialScope, + hashedCanonicalRequest) + + // sign string + secretDate := hmacSha256(date, "TC3"+secKey) + secretService := hmacSha256("hunyuan", secretDate) + secretKey := hmacSha256("tc3_request", secretService) + signature := hex.EncodeToString([]byte(hmacSha256(string2sign, secretKey))) - sort.Strings(params) - url := "hunyuan.cloud.tencent.com/hyllm/v1/chat/completions?" + strings.Join(params, "&") - mac := hmac.New(sha1.New, []byte(secretKey)) - signURL := url - mac.Write([]byte(signURL)) - sign := mac.Sum([]byte(nil)) - return base64.StdEncoding.EncodeToString(sign) + // build authorization + authorization := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", + algorithm, + secId, + credentialScope, + signedHeaders, + signature) + return authorization } diff --git a/relay/adaptor/tencent/model.go b/relay/adaptor/tencent/model.go index 71286be9b3..fb97724e93 100644 --- a/relay/adaptor/tencent/model.go +++ b/relay/adaptor/tencent/model.go @@ -1,63 +1,75 @@ package tencent -import ( - "github.com/songquanpeng/one-api/relay/model" -) - type Message struct { - Role string `json:"role"` - Content string `json:"content"` + Role string `json:"Role"` + Content string `json:"Content"` } type ChatRequest struct { - AppId int64 `json:"app_id"` // 腾讯云账号的 APPID - SecretId string `json:"secret_id"` // 官网 SecretId - // Timestamp当前 UNIX 时间戳,单位为秒,可记录发起 API 请求的时间。 - // 例如1529223702,如果与当前时间相差过大,会引起签名过期错误 - Timestamp int64 `json:"timestamp"` - // Expired 签名的有效期,是一个符合 UNIX Epoch 时间戳规范的数值, - // 单位为秒;Expired 必须大于 Timestamp 且 Expired-Timestamp 小于90天 - Expired int64 `json:"expired"` - QueryID string `json:"query_id"` //请求 Id,用于问题排查 - // Temperature 较高的数值会使输出更加随机,而较低的数值会使其更加集中和确定 - // 默认 1.0,取值区间为[0.0,2.0],非必要不建议使用,不合理的取值会影响效果 - // 建议该参数和 top_p 只设置1个,不要同时更改 top_p - Temperature float64 `json:"temperature"` - // TopP 影响输出文本的多样性,取值越大,生成文本的多样性越强 - // 默认1.0,取值区间为[0.0, 1.0],非必要不建议使用, 不合理的取值会影响效果 - // 建议该参数和 temperature 只设置1个,不要同时更改 - TopP float64 `json:"top_p"` - // Stream 0:同步,1:流式 (默认,协议:SSE) - // 同步请求超时:60s,如果内容较长建议使用流式 - Stream int `json:"stream"` - // Messages 会话内容, 长度最多为40, 按对话时间从旧到新在数组中排列 - // 输入 content 总数最大支持 3000 token。 - Messages []Message `json:"messages"` + // 模型名称,可选值包括 hunyuan-lite、hunyuan-standard、hunyuan-standard-256K、hunyuan-pro。 + // 各模型介绍请阅读 [产品概述](https://cloud.tencent.com/document/product/1729/104753) 中的说明。 + // + // 注意: + // 不同的模型计费不同,请根据 [购买指南](https://cloud.tencent.com/document/product/1729/97731) 按需调用。 + Model *string `json:"Model"` + // 聊天上下文信息。 + // 说明: + // 1. 长度最多为 40,按对话时间从旧到新在数组中排列。 + // 2. Message.Role 可选值:system、user、assistant。 + // 其中,system 角色可选,如存在则必须位于列表的最开始。user 和 assistant 需交替出现(一问一答),以 user 提问开始和结束,且 Content 不能为空。Role 的顺序示例:[system(可选) user assistant user assistant user ...]。 + // 3. Messages 中 Content 总长度不能超过模型输入长度上限(可参考 [产品概述](https://cloud.tencent.com/document/product/1729/104753) 文档),超过则会截断最前面的内容,只保留尾部内容。 + Messages []*Message `json:"Messages"` + // 流式调用开关。 + // 说明: + // 1. 未传值时默认为非流式调用(false)。 + // 2. 流式调用时以 SSE 协议增量返回结果(返回值取 Choices[n].Delta 中的值,需要拼接增量数据才能获得完整结果)。 + // 3. 非流式调用时: + // 调用方式与普通 HTTP 请求无异。 + // 接口响应耗时较长,**如需更低时延建议设置为 true**。 + // 只返回一次最终结果(返回值取 Choices[n].Message 中的值)。 + // + // 注意: + // 通过 SDK 调用时,流式和非流式调用需用**不同的方式**获取返回值,具体参考 SDK 中的注释或示例(在各语言 SDK 代码仓库的 examples/hunyuan/v20230901/ 目录中)。 + Stream *bool `json:"Stream"` + // 说明: + // 1. 影响输出文本的多样性,取值越大,生成文本的多样性越强。 + // 2. 取值区间为 [0.0, 1.0],未传值时使用各模型推荐值。 + // 3. 非必要不建议使用,不合理的取值会影响效果。 + TopP *float64 `json:"TopP"` + // 说明: + // 1. 较高的数值会使输出更加随机,而较低的数值会使其更加集中和确定。 + // 2. 取值区间为 [0.0, 2.0],未传值时使用各模型推荐值。 + // 3. 非必要不建议使用,不合理的取值会影响效果。 + Temperature *float64 `json:"Temperature"` } type Error struct { - Code int `json:"code"` - Message string `json:"message"` + Code int `json:"Code"` + Message string `json:"Message"` } type Usage struct { - InputTokens int `json:"input_tokens"` - OutputTokens int `json:"output_tokens"` - TotalTokens int `json:"total_tokens"` + PromptTokens int `json:"PromptTokens"` + CompletionTokens int `json:"CompletionTokens"` + TotalTokens int `json:"TotalTokens"` } type ResponseChoices struct { - FinishReason string `json:"finish_reason,omitempty"` // 流式结束标志位,为 stop 则表示尾包 - Messages Message `json:"messages,omitempty"` // 内容,同步模式返回内容,流模式为 null 输出 content 内容总数最多支持 1024token。 - Delta Message `json:"delta,omitempty"` // 内容,流模式返回内容,同步模式为 null 输出 content 内容总数最多支持 1024token。 + FinishReason string `json:"FinishReason,omitempty"` // 流式结束标志位,为 stop 则表示尾包 + Messages Message `json:"Message,omitempty"` // 内容,同步模式返回内容,流模式为 null 输出 content 内容总数最多支持 1024token。 + Delta Message `json:"Delta,omitempty"` // 内容,流模式返回内容,同步模式为 null 输出 content 内容总数最多支持 1024token。 } type ChatResponse struct { - Choices []ResponseChoices `json:"choices,omitempty"` // 结果 - Created string `json:"created,omitempty"` // unix 时间戳的字符串 - Id string `json:"id,omitempty"` // 会话 id - Usage model.Usage `json:"usage,omitempty"` // token 数量 - Error Error `json:"error,omitempty"` // 错误信息 注意:此字段可能返回 null,表示取不到有效值 - Note string `json:"note,omitempty"` // 注释 - ReqID string `json:"req_id,omitempty"` // 唯一请求 Id,每次请求都会返回。用于反馈接口入参 + Choices []ResponseChoices `json:"Choices,omitempty"` // 结果 + Created int64 `json:"Created,omitempty"` // unix 时间戳的字符串 + Id string `json:"Id,omitempty"` // 会话 id + Usage Usage `json:"Usage,omitempty"` // token 数量 + Error Error `json:"Error,omitempty"` // 错误信息 注意:此字段可能返回 null,表示取不到有效值 + Note string `json:"Note,omitempty"` // 注释 + ReqID string `json:"Req_id,omitempty"` // 唯一请求 Id,每次请求都会返回。用于反馈接口入参 +} + +type ChatResponseP struct { + Response ChatResponse `json:"Response,omitempty"` } diff --git a/relay/channeltype/url.go b/relay/channeltype/url.go index 489a21deb2..513d183bcc 100644 --- a/relay/channeltype/url.go +++ b/relay/channeltype/url.go @@ -24,7 +24,7 @@ var ChannelBaseURLs = []string{ "https://openrouter.ai/api", // 20 "https://api.aiproxy.io", // 21 "https://fastgpt.run/api/openapi", // 22 - "https://hunyuan.cloud.tencent.com", // 23 + "https://hunyuan.tencentcloudapi.com", // 23 "https://generativelanguage.googleapis.com", // 24 "https://api.moonshot.cn", // 25 "https://api.baichuan-ai.com", // 26