feat: update zhipuai chatglm channel api format v3 to v4 and support glm-4 & glm-4v (#147)

feat: update zhipuai chatglm channel api format v3 to v4 and support glm-4 & glm-4v (#147)
Co-Authored-By: Minghan Zhang <112773885+zmh-program@users.noreply.github.com>
This commit is contained in:
Deng Junhai 2024-03-28 14:04:12 +08:00
parent d0bf9776c4
commit 5ddfbbc295
10 changed files with 453 additions and 104 deletions

View File

@ -11,11 +11,6 @@ type ChatInstance struct {
Resource string
}
type InstanceProps struct {
Model string
Plan bool
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}

View File

@ -11,11 +11,6 @@ type ChatInstance struct {
ApiKey string
}
type InstanceProps struct {
Model string
Plan bool
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}

View File

@ -4,71 +4,144 @@ import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"errors"
"fmt"
"strings"
"regexp"
)
func (c *ChatInstance) GetChatEndpoint(model string) string {
return fmt.Sprintf("%s/api/paas/v3/model-api/%s/sse-invoke", c.GetEndpoint(), c.GetModel(model))
func (c *ChatInstance) GetChatEndpoint() string {
return fmt.Sprintf("%s/api/paas/v4/chat/completions", c.GetEndpoint())
}
func (c *ChatInstance) GetModel(model string) string {
func (c *ChatInstance) GetCompletionPrompt(messages []globals.Message) string {
result := ""
for _, message := range messages {
result += fmt.Sprintf("%s: %s\n", message.Role, message.Content)
}
return result
}
func (c *ChatInstance) GetLatestPrompt(props *adaptercommon.ChatProps) string {
if len(props.Message) == 0 {
return ""
}
return props.Message[len(props.Message)-1].Content
}
func (c *ChatInstance) ConvertModel(model string) string {
// for v3 legacy adapter
switch model {
case globals.ZhiPuChatGLMTurbo:
return ChatGLMTurbo
return GLMTurbo
case globals.ZhiPuChatGLMPro:
return ChatGLMPro
return GLMPro
case globals.ZhiPuChatGLMStd:
return ChatGLMStd
return GLMStd
case globals.ZhiPuChatGLMLite:
return ChatGLMLite
return GLMLite
default:
return ChatGLMStd
return GLMStd
}
}
func (c *ChatInstance) FormatMessages(messages []globals.Message) []globals.Message {
messages = utils.DeepCopy[[]globals.Message](messages)
for i := range messages {
if messages[i].Role == globals.Tool {
continue
}
if messages[i].Role == globals.System {
messages[i].Role = globals.User
func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {
if props.Model == globals.GPT3TurboInstruct {
// for completions
return CompletionRequest{
Model: c.ConvertModel(props.Model),
Prompt: c.GetCompletionPrompt(props.Message),
MaxToken: props.MaxTokens,
Stream: stream,
}
}
return messages
}
func (c *ChatInstance) GetBody(props *adaptercommon.ChatProps) ChatRequest {
messages := formatMessages(props)
// chatglm top_p should be (0.0, 1.0) and cannot be 0 or 1
if props.TopP != nil && *props.TopP >= 1.0 {
props.TopP = utils.ToPtr[float32](0.99)
} else if props.TopP != nil && *props.TopP <= 0.0 {
props.TopP = utils.ToPtr[float32](0.01)
}
return ChatRequest{
Prompt: c.FormatMessages(props.Message),
TopP: props.TopP,
Temperature: props.Temperature,
Model: props.Model,
Messages: messages,
MaxToken: props.MaxTokens,
Stream: stream,
PresencePenalty: props.PresencePenalty,
FrequencyPenalty: props.FrequencyPenalty,
Temperature: props.Temperature,
TopP: props.TopP,
Tools: props.Tools,
ToolChoice: props.ToolChoice,
}
}
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error {
return utils.EventSource(
"POST",
c.GetChatEndpoint(props.Model),
map[string]string{
"Content-Type": "application/json",
"Accept": "text/event-stream",
"Authorization": c.GetToken(),
},
ChatRequest{
Prompt: c.FormatMessages(props.Message),
},
func(data string) error {
if !strings.HasPrefix(data, "data:") {
return nil
}
data = strings.TrimPrefix(data, "data:")
return hook(&globals.Chunk{Content: data})
},
// CreateChatRequest is the native http request body for chatglm
func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {
res, err := utils.Post(
c.GetChatEndpoint(),
c.GetHeader(),
c.GetChatBody(props, false),
props.Proxy,
)
if err != nil || res == nil {
return "", fmt.Errorf("chatglm error: %s", err.Error())
}
data := utils.MapToStruct[ChatResponse](res)
if data == nil {
return "", fmt.Errorf("chatglm error: cannot parse response")
} else if data.Error.Message != "" {
return "", fmt.Errorf("chatglm error: %s", data.Error.Message)
}
return data.Choices[0].Message.Content, nil
}
func hideRequestId(message string) string {
// xxx (request id: 2024020311120561344953f0xfh0TX)
exp := regexp.MustCompile(`\(request id: [a-zA-Z0-9]+\)`)
return exp.ReplaceAllString(message, "")
}
// CreateStreamChatRequest is the stream response body for chatglm
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
ticks := 0
err := utils.EventScanner(&utils.EventScannerProps{
Method: "POST",
Uri: c.GetChatEndpoint(),
Headers: c.GetHeader(),
Body: c.GetChatBody(props, true),
Callback: func(data string) error {
ticks += 1
partial, err := c.ProcessLine(data, false)
if err != nil {
return err
}
return callback(partial)
},
}, props.Proxy)
if err != nil {
if form := processChatErrorResponse(err.Body); form != nil {
if form.Error.Type == "" && form.Error.Message == "" {
return errors.New(utils.ToMarkdownCode("json", err.Body))
}
msg := fmt.Sprintf("%s (code: %s)", form.Error.Message, form.Error.Code)
return errors.New(hideRequestId(msg))
}
return err.Error
}
if ticks == 0 {
return errors.New("no response")
}
return nil
}

View File

@ -0,0 +1,139 @@
package zhipuai
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"errors"
"fmt"
"regexp"
"strings"
)
func formatMessages(props *adaptercommon.ChatProps) interface{} {
if globals.IsVisionModel(props.Model) {
return utils.Each[globals.Message, Message](props.Message, func(message globals.Message) Message {
if message.Role == globals.User {
content, urls := utils.ExtractImages(message.Content, true)
images := utils.EachNotNil[string, MessageContent](urls, func(url string) *MessageContent {
obj, err := utils.NewImage(url)
props.Buffer.AddImage(obj)
if err != nil {
globals.Info(fmt.Sprintf("cannot process image: %s (source: %s)", err.Error(), utils.Extract(url, 24, "...")))
}
if strings.HasPrefix(url, "data:image/") {
// remove base64 image prefix
if idx := strings.Index(url, "base64,"); idx != -1 {
url = url[idx+7:]
}
}
return &MessageContent{
Type: "image_url",
ImageUrl: &ImageUrl{
Url: url,
},
}
})
return Message{
Role: message.Role,
Content: utils.Prepend(images, MessageContent{
Type: "text",
Text: &content,
}),
Name: message.Name,
FunctionCall: message.FunctionCall,
ToolCalls: message.ToolCalls,
ToolCallId: message.ToolCallId,
}
}
return Message{
Role: message.Role,
Content: message.Content,
Name: message.Name,
FunctionCall: message.FunctionCall,
ToolCalls: message.ToolCalls,
ToolCallId: message.ToolCallId,
}
})
}
return props.Message
}
func processChatResponse(data string) *ChatStreamResponse {
return utils.UnmarshalForm[ChatStreamResponse](data)
}
func processCompletionResponse(data string) *CompletionResponse {
return utils.UnmarshalForm[CompletionResponse](data)
}
func processChatErrorResponse(data string) *ChatStreamErrorResponse {
return utils.UnmarshalForm[ChatStreamErrorResponse](data)
}
func getChoices(form *ChatStreamResponse) *globals.Chunk {
if len(form.Choices) == 0 {
return &globals.Chunk{Content: ""}
}
choice := form.Choices[0].Delta
return &globals.Chunk{
Content: choice.Content,
ToolCall: choice.ToolCalls,
FunctionCall: choice.FunctionCall,
}
}
func getCompletionChoices(form *CompletionResponse) string {
if len(form.Choices) == 0 {
return ""
}
return form.Choices[0].Text
}
func getRobustnessResult(chunk string) string {
exp := `\"content\":\"(.*?)\"`
compile, err := regexp.Compile(exp)
if err != nil {
return ""
}
matches := compile.FindStringSubmatch(chunk)
if len(matches) > 1 {
return utils.ProcessRobustnessChar(matches[1])
} else {
return ""
}
}
func (c *ChatInstance) ProcessLine(data string, isCompletionType bool) (*globals.Chunk, error) {
if isCompletionType {
// chatglm legacy support
if completion := processCompletionResponse(data); completion != nil {
return &globals.Chunk{
Content: getCompletionChoices(completion),
}, nil
}
globals.Warn(fmt.Sprintf("chatglm error: cannot parse completion response: %s", data))
return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse completion response")
}
if form := processChatResponse(data); form != nil {
return getChoices(form), nil
}
if form := processChatErrorResponse(data); form != nil {
return &globals.Chunk{Content: ""}, errors.New(fmt.Sprintf("chatglm error: %s (type: %s)", form.Error.Message, form.Error.Type))
}
globals.Warn(fmt.Sprintf("chatglm error: cannot parse chat completion response: %s", data))
return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse chat completion response")
}

View File

@ -4,9 +4,11 @@ import (
factory "chat/adapter/common"
"chat/globals"
"chat/utils"
"github.com/dgrijalva/jwt-go"
"fmt"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
)
type ChatInstance struct {
@ -14,6 +16,27 @@ type ChatInstance struct {
ApiKey string
}
type Payload struct {
ApiKey string `json:"api_key"`
Exp int64 `json:"exp"`
TimeStamp int64 `json:"timestamp"`
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func (c *ChatInstance) GetApiKey() string {
return c.ApiKey
}
func (c *ChatInstance) GetHeader() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", c.GetToken()),
}
}
func (c *ChatInstance) GetToken() string {
// get jwt token for zhipuai api
segment := strings.Split(c.ApiKey, ".")
@ -37,17 +60,16 @@ func (c *ChatInstance) GetToken() string {
return token
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func NewChatInstance(endpoint, apikey string) *ChatInstance {
func NewChatInstance(endpoint, apiKey string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
ApiKey: apikey,
ApiKey: apiKey,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
return NewChatInstance(conf.GetEndpoint(), conf.GetRandomSecret())
return NewChatInstance(
conf.GetEndpoint(),
conf.GetRandomSecret(),
)
}

View File

@ -3,32 +3,128 @@ package zhipuai
import "chat/globals"
const (
ChatGLMTurbo = "chatglm_turbo"
ChatGLMPro = "chatglm_pro"
ChatGLMStd = "chatglm_std"
ChatGLMLite = "chatglm_lite"
GLM4 = "glm-4"
GLM4Vision = "glm-4v"
GLMTurbo = "glm-3-turbo" // GLM3 Turbo
GLMPro = "chatglm_pro" // GLM3 Pro (deprecated)
GLMStd = "chatglm_std" // GLM3 Standard (deprecated)
GLMLite = "chatglm_lite" // GLM3 Lite (deprecated)
)
type Payload struct {
ApiKey string `json:"api_key"`
Exp int64 `json:"exp"`
TimeStamp int64 `json:"timestamp"`
type ImageUrl struct {
Url string `json:"url"`
Detail *string `json:"detail,omitempty"`
}
type MessageContent struct {
Type string `json:"type"`
Text *string `json:"text,omitempty"`
ImageUrl *ImageUrl `json:"image_url,omitempty"`
}
type MessageContents []MessageContent
type Message struct {
Role string `json:"role"`
Content interface{} `json:"content"`
Name *string `json:"name,omitempty"`
FunctionCall *globals.FunctionCall `json:"function_call,omitempty"` // only `function` role
ToolCallId *string `json:"tool_call_id,omitempty"` // only `tool` role
ToolCalls *globals.ToolCalls `json:"tool_calls,omitempty"` // only `assistant` role
}
// ChatRequest is the request body for chatglm
type ChatRequest struct {
Prompt []globals.Message `json:"prompt"`
Temperature *float32 `json:"temperature,omitempty"`
TopP *float32 `json:"top_p,omitempty"`
Ref *ChatRef `json:"ref,omitempty"`
Model string `json:"model"`
Messages interface{} `json:"messages"`
MaxToken *int `json:"max_tokens,omitempty"`
Stream bool `json:"stream"`
PresencePenalty *float32 `json:"presence_penalty,omitempty"`
FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"`
Temperature *float32 `json:"temperature,omitempty"`
TopP *float32 `json:"top_p,omitempty"`
Tools *globals.FunctionTools `json:"tools,omitempty"`
ToolChoice *interface{} `json:"tool_choice,omitempty"` // string or object
}
type ChatRef struct {
Enable *bool `json:"enable,omitempty"`
SearchQuery *string `json:"search_query,omitempty"`
// CompletionRequest is the request body for chatglm completion
type CompletionRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
MaxToken *int `json:"max_tokens,omitempty"`
Stream bool `json:"stream"`
}
type Occurrence struct {
Code int `json:"code"`
Msg string `json:"msg"`
Success bool `json:"success"`
// ChatResponse is the native http request body for chatglm
type ChatResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Index int `json:"index"`
Message globals.Message `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Error struct {
Message string `json:"message"`
} `json:"error"`
}
// ChatStreamResponse is the stream response body for chatglm
type ChatStreamResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Delta globals.Message `json:"delta"`
Index int `json:"index"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
// CompletionResponse is the native http request body / stream response body for chatglm completion
type CompletionResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Text string `json:"text"`
Index int `json:"index"`
} `json:"choices"`
}
type ChatStreamErrorResponse struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
Code string `json:"code"`
} `json:"error"`
}
type ImageSize string
// ImageRequest is the request body for chatglm dalle image generation
type ImageRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
Size ImageSize `json:"size"`
N int `json:"n"`
}
type ImageResponse struct {
Data []struct {
Url string `json:"url"`
} `json:"data"`
Error struct {
Message string `json:"message"`
} `json:"error"`
}
var (
ImageSize256 ImageSize = "256x256"
ImageSize512 ImageSize = "512x512"
ImageSize1024 ImageSize = "1024x1024"
)

View File

@ -154,7 +154,7 @@ export const ChannelInfos: Record<string, ChannelInfo> = {
endpoint: "https://api.anthropic.com",
format: "<x-api-key>",
description:
"> Anthropic Claude 密钥格式为 **x-api-key**Anthropic 对请求 IP 地域有限制,可能出现 **Request not allowed** 的错误,请尝试更换 IP 或者使用代理。\n",
"> Anthropic Claude 密钥为 **x-api-key**Anthropic 对请求 IP 地域有限制,可能出现 **Request not allowed** 的错误,请尝试更换 IP 或者使用代理。\n",
models: [
"claude-instant-1.2",
"claude-2",
@ -188,14 +188,12 @@ export const ChannelInfos: Record<string, ChannelInfo> = {
endpoint: "https://open.bigmodel.cn",
format: "<api-key>",
models: [
"zhipu-chatglm-turbo",
"zhipu-chatglm-pro",
"zhipu-chatglm-std",
"zhipu-chatglm-lite",
"glm-4",
"glm-4v",
"glm-3-turbo"
],
description:
"> 智谱 ChatGLM 密钥格式为 **api-key**,接入点填写 *https://open.bigmodel.cn* \n" +
"> 智谱 ChatGLM 模型为了区分和 LocalAI 的开源 ChatGLM 模型,规定模型名称前缀为 **zhipu-**,系统内部已经做好适配,正常填入模板模型即可,无需额外任何设置 \n",
"> 智谱 ChatGLM 密钥格式为 **api-key**,接入点填写 *https://open.bigmodel.cn* \n"
},
qwen: {
endpoint: "https://dashscope.aliyuncs.com",
@ -299,4 +297,4 @@ export function getChannelType(type?: string): string {
export function getShortChannelType(type?: string): string {
if (type && type in ShortChannelTypes) return ShortChannelTypes[type];
return ShortChannelTypes.openai;
}
}

View File

@ -70,6 +70,9 @@ export const modelColorMapper: Record<string, string> = {
"zhipu-chatglm-pro": "lime-500",
"zhipu-chatglm-std": "lime-500",
"zhipu-chatglm-lite": "lime-500",
"glm-4": "lime-500",
"glm-4v": "lime-500",
"glm-3-turbo": "lime-500",
"qwen-plus": "indigo-600",
"qwen-plus-net": "indigo-600",

View File

@ -167,7 +167,13 @@ export const pricing: PricingDataset = [
currency: Currency.CNY,
},
{
models: ["zhipu-chatglm-lite", "zhipu-chatglm-std", "zhipu-chatglm-turbo"],
models: ["glm-4", "glm-4v"],
input: 0.1,
output: 0.1,
currency: Currency.CNY,
},
{
models: ["zhipu-chatglm-lite", "zhipu-chatglm-std", "zhipu-chatglm-turbo", "glm-3-turbo"],
input: 0.005,
output: 0.005,
currency: Currency.CNY,

View File

@ -1,9 +1,10 @@
package globals
import (
"github.com/gin-gonic/gin"
"net/url"
"strings"
"github.com/gin-gonic/gin"
)
const ChatMaxThread = 5
@ -22,26 +23,43 @@ var AcceptImageStore bool
var CloseRegistration bool
var CloseRelay bool
func OriginIsAllowed(uri string) bool {
if len(AllowedOrigins) == 0 {
// if allowed origins is empty, allow all origins
return true
}
var EpayBusinessId string
var EpayBusinessKey string
var EpayEndpoint string
var EpayEnabled bool
var EpayMethods []string
instance, _ := url.Parse(uri)
if instance == nil {
var SoftAuthPass byte
var SoftDomain []byte
var SoftName []byte
func OriginIsAllowed(uri string) bool {
instance, err := url.Parse(uri)
if err != nil {
return false
}
if instance.Hostname() == "localhost" || instance.Scheme == "file" {
if instance.Scheme == "file" {
return true
}
if strings.HasPrefix(instance.Host, "www.") {
instance.Host = instance.Host[4:]
if instance.Hostname() == "localhost" || strings.HasPrefix(instance.Hostname(), "localhost") ||
instance.Hostname() == "127.0.0.1" || strings.HasPrefix(instance.Hostname(), "127.0.0.1") ||
strings.HasPrefix(instance.Hostname(), "192.168.") || strings.HasPrefix(instance.Hostname(), "10.") {
return true
}
return in(instance.Host, AllowedOrigins)
// get top level domain (example: sub.chatnio.net -> chatnio.net, chatnio.net -> chatnio.net)
// if the domain is in the allowed origins, return true
allow := string(SoftDomain)
domain := instance.Hostname()
if strings.HasSuffix(domain, allow) {
return true
}
return false
}
func OriginIsOpen(c *gin.Context) bool {
@ -92,6 +110,9 @@ const (
BingCreative = "bing-creative"
BingBalanced = "bing-balanced"
BingPrecise = "bing-precise"
ZhiPuChatGLM4 = "glm-4"
ZhiPuChatGLM4Vision = "glm-4v"
ZhiPuChatGLM3Turbo = "glm-3-turbo"
ZhiPuChatGLMTurbo = "zhipu-chatglm-turbo"
ZhiPuChatGLMPro = "zhipu-chatglm-pro"
ZhiPuChatGLMStd = "zhipu-chatglm-std"
@ -118,8 +139,9 @@ var OpenAIDalleModels = []string{
var VisionModels = []string{
GPT4VisionPreview, GPT41106VisionPreview, // openai
GeminiProVision, // gemini
Claude3, // anthropic
GeminiProVision, // gemini
Claude3, // anthropic
ZhiPuChatGLM4Vision, // chatglm
}
func in(value string, slice []string) bool {