Skip to content

Commit

Permalink
Merge pull request #43 from OVINC-CN/feat_hunyuan
Browse files Browse the repository at this point in the history
feat: support to use tencent cloud api for hunyuan #40
  • Loading branch information
OrenZhang authored Apr 27, 2024
2 parents 697d31b + bcf2495 commit d798ce4
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 90 deletions.
83 changes: 24 additions & 59 deletions apps/chat/client.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import abc
import base64
import datetime
import hashlib
import hmac
import json
from typing import List, Union

import google.generativeai as genai
import qianfan
import requests
import tiktoken
from django.conf import settings
from django.contrib.auth import get_user_model
Expand All @@ -19,16 +14,12 @@
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
from openai.types.images_response import ImagesResponse
from qianfan import QfMessages, QfResponse
from requests import Response
from rest_framework.request import Request
from tencentcloud.common import credential as tencent_cloud_credential
from tencentcloud.hunyuan.v20230901 import hunyuan_client
from tencentcloud.hunyuan.v20230901 import models as hunyuan_models

from apps.chat.constants import (
AI_API_REQUEST_TIMEOUT,
HUNYUAN_DATA_PATTERN,
GeminiRole,
OpenAIRole,
)
from apps.chat.exceptions import UnexpectedError
from apps.chat.constants import GeminiRole, OpenAIRole
from apps.chat.models import AIModel, ChatLog, HunYuanChuck, Message

USER_MODEL = get_user_model()
Expand Down Expand Up @@ -185,22 +176,11 @@ def chat(self, *args, **kwargs) -> any:
# call hunyuan api
response = self.call_api()
# explain completion
completion_text = bytes()
for chunk in response:
completion_text += chunk
# match hunyuan data content
match = HUNYUAN_DATA_PATTERN.search(completion_text)
if match is None:
continue
# load content
resp_text = completion_text[match.regs[0][0] : match.regs[0][1]]
completion_text = completion_text[match.regs[0][1] :]
resp_text = json.loads(resp_text.decode()[6:])
chunk = HunYuanChuck.create(resp_text)
if chunk.error.code:
raise UnexpectedError(detail=chunk.error.message)
chunk = json.loads(chunk["data"])
chunk = HunYuanChuck.create(chunk)
self.record(response=chunk)
yield chunk.choices[0].delta.content
yield chunk.Choices[0].Delta.Content
if not self.log:
return
self.log.finished_at = int(timezone.now().timestamp() * 1000)
Expand All @@ -211,16 +191,16 @@ def chat(self, *args, **kwargs) -> any:
def record(self, response: HunYuanChuck) -> None:
# check log exist
if self.log:
self.log.content += response.choices[0].delta.content
self.log.prompt_tokens = response.usage.prompt_tokens
self.log.completion_tokens = response.usage.completion_tokens
self.log.content += response.Choices[0].Delta.Content
self.log.prompt_tokens = response.Usage.PromptTokens
self.log.completion_tokens = response.Usage.CompletionTokens
self.log.prompt_token_unit_price = self.model_inst.prompt_price
self.log.completion_token_unit_price = self.model_inst.completion_price
self.log.currency_unit = self.model_inst.currency_unit
return
# create log
self.log = ChatLog.objects.create(
chat_id=response.id,
chat_id=response.Id,
user=self.user,
model=self.model,
messages=self.messages,
Expand All @@ -229,35 +209,20 @@ def record(self, response: HunYuanChuck) -> None:
)
return self.record(response=response)

def call_api(self) -> Response:
data = {
"app_id": settings.QCLOUD_APP_ID,
"secret_id": settings.QCLOUD_SECRET_ID,
"timestamp": int(timezone.now().timestamp()),
"expired": int((timezone.now() + datetime.timedelta(minutes=5)).timestamp()),
"messages": self.messages,
"temperature": self.temperature,
"top_p": self.top_p,
"stream": 1,
}
message_string = ",".join(
[f"{{\"role\":\"{message['role']}\",\"content\":\"{message['content']}\"}}" for message in self.messages]
)
message_string = f"[{message_string}]"
params = {**data, "messages": message_string}
params = dict(sorted(params.items(), key=lambda x: x[0]))
url = (
settings.QCLOUD_HUNYUAN_API_URL.split("://", 1)[1]
+ "?"
+ "&".join([f"{key}={val}" for key, val in params.items()])
def call_api(self) -> hunyuan_models.ChatCompletionsResponse:
client = hunyuan_client.HunyuanClient(
tencent_cloud_credential.Credential(settings.QCLOUD_SECRET_ID, settings.QCLOUD_SECRET_KEY), ""
)
signature = hmac.new(settings.QCLOUD_SECRET_KEY.encode(), url.encode(), hashlib.sha1).digest()
encoded_signature = base64.b64encode(signature).decode()
headers = {"Authorization": encoded_signature}
resp = requests.post(
settings.QCLOUD_HUNYUAN_API_URL, json=data, headers=headers, stream=True, timeout=AI_API_REQUEST_TIMEOUT
)
return resp
req = hunyuan_models.ChatCompletionsRequest()
params = {
"Model": self.model,
"Messages": [{"Role": message["role"], "Content": message["content"]} for message in self.messages],
"TopP": self.top_p,
"Temperature": self.temperature,
"Stream": True,
}
req.from_json_string(json.dumps(params))
return client.ChatCompletions(req)


class GeminiClient(BaseClient):
Expand Down
3 changes: 0 additions & 3 deletions apps/chat/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import re
import sys

from django.utils.translation import gettext_lazy
Expand All @@ -16,8 +15,6 @@
PRICE_DIGIT_NUMS = 20
PRICE_DECIMAL_NUMS = 10

HUNYUAN_DATA_PATTERN = re.compile(rb"data:\s\{.*\}\n\n")

if "celery" in sys.argv:
TOKEN_ENCODING = ""
else:
Expand Down
42 changes: 18 additions & 24 deletions apps/chat/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# pylint: disable=C0103

from dataclasses import dataclass
from typing import List

Expand Down Expand Up @@ -140,46 +142,38 @@ def authed_models(cls, user: USER_MODEL, model: str = None) -> QuerySet:

@dataclass
class HunYuanDelta:
content: str = ""
Role: str = ""
Content: str = ""


@dataclass
class HunYuanChoice:
finish_reason: str = ""
delta: HunYuanDelta = None
FinishReason: str = ""
Delta: HunYuanDelta = None


@dataclass
class HunYuanUsage:
prompt_tokens: int = 0
completion_tokens: int = 0
total_tokens: int = 0


@dataclass
class HunYuanError:
code: int = 0
message: str = ""
PromptTokens: int = 0
CompletionTokens: int = 0
TotalTokens: int = 0


@dataclass
class HunYuanChuck:
req_id: str = ""
note: str = ""
choices: List[HunYuanChoice] = None
created: str = ""
id: str = "" # pylint: disable=C0103
usage: HunYuanUsage = None
error: HunYuanError = None
Note: str = ""
Choices: List[HunYuanChoice] = None
Created: str = ""
Id: str = ""
Usage: HunYuanUsage = None

@classmethod
def create(cls, data: dict) -> "HunYuanChuck":
chuck = cls(**data)
chuck.usage = HunYuanUsage(**data.get("usage", {}))
chuck.error = HunYuanError(**data.get("error", {}))
chuck.choices = [
HunYuanChoice(finish_reason=choice.get("finish_reason", ""), delta=HunYuanDelta(**choice.get("delta", {})))
for choice in data.get("choices", [])
chuck.Usage = HunYuanUsage(**data.get("Usage", {}))
chuck.Choices = [
HunYuanChoice(FinishReason=choice.get("FinishReason", ""), Delta=HunYuanDelta(**choice.get("Delta", {})))
for choice in data.get("Choices", [])
]
return chuck

Expand Down
4 changes: 0 additions & 4 deletions entry/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,8 @@
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")

# QCLOUD
QCLOUD_APP_ID = int(os.getenv("QCLOUD_APP_ID", "0"))
QCLOUD_SECRET_ID = os.getenv("QCLOUD_SECRET_ID")
QCLOUD_SECRET_KEY = os.getenv("QCLOUD_SECRET_KEY")
QCLOUD_HUNYUAN_API_URL = os.getenv(
"QCLOUD_HUNYUAN_API_DOMAIN", "https://hunyuan.cloud.tencent.com/hyllm/v1/chat/completions"
)

# Baidu Qianfan
QIANFAN_ACCESS_KEY = os.getenv("QIANFAN_ACCESS_KEY", "")
Expand Down
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ google-ai-generativelanguage==0.4.0

# Baidu
qianfan==0.3.0

# Hunyuan
tencentcloud-sdk-python==3.0.1137

0 comments on commit d798ce4

Please sign in to comment.