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 Resource string
} }
type InstanceProps struct {
Model string
Plan bool
}
func (c *ChatInstance) GetEndpoint() string { func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint return c.Endpoint
} }

View File

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

View File

@ -4,71 +4,144 @@ import (
adaptercommon "chat/adapter/common" adaptercommon "chat/adapter/common"
"chat/globals" "chat/globals"
"chat/utils" "chat/utils"
"errors"
"fmt" "fmt"
"strings" "regexp"
) )
func (c *ChatInstance) GetChatEndpoint(model string) string { func (c *ChatInstance) GetChatEndpoint() string {
return fmt.Sprintf("%s/api/paas/v3/model-api/%s/sse-invoke", c.GetEndpoint(), c.GetModel(model)) 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 { switch model {
case globals.ZhiPuChatGLMTurbo: case globals.ZhiPuChatGLMTurbo:
return ChatGLMTurbo return GLMTurbo
case globals.ZhiPuChatGLMPro: case globals.ZhiPuChatGLMPro:
return ChatGLMPro return GLMPro
case globals.ZhiPuChatGLMStd: case globals.ZhiPuChatGLMStd:
return ChatGLMStd return GLMStd
case globals.ZhiPuChatGLMLite: case globals.ZhiPuChatGLMLite:
return ChatGLMLite return GLMLite
default: default:
return ChatGLMStd return GLMStd
} }
} }
func (c *ChatInstance) FormatMessages(messages []globals.Message) []globals.Message { func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {
messages = utils.DeepCopy[[]globals.Message](messages) if props.Model == globals.GPT3TurboInstruct {
for i := range messages { // for completions
if messages[i].Role == globals.Tool { return CompletionRequest{
continue Model: c.ConvertModel(props.Model),
} Prompt: c.GetCompletionPrompt(props.Message),
MaxToken: props.MaxTokens,
if messages[i].Role == globals.System { Stream: stream,
messages[i].Role = globals.User
} }
} }
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{ return ChatRequest{
Prompt: c.FormatMessages(props.Message), Model: props.Model,
TopP: props.TopP, Messages: messages,
Temperature: props.Temperature, 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 { // CreateChatRequest is the native http request body for chatglm
return utils.EventSource( func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {
"POST", res, err := utils.Post(
c.GetChatEndpoint(props.Model), c.GetChatEndpoint(),
map[string]string{ c.GetHeader(),
"Content-Type": "application/json", c.GetChatBody(props, false),
"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})
},
props.Proxy, 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" factory "chat/adapter/common"
"chat/globals" "chat/globals"
"chat/utils" "chat/utils"
"github.com/dgrijalva/jwt-go" "fmt"
"strings" "strings"
"time" "time"
"github.com/dgrijalva/jwt-go"
) )
type ChatInstance struct { type ChatInstance struct {
@ -14,6 +16,27 @@ type ChatInstance struct {
ApiKey string 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 { func (c *ChatInstance) GetToken() string {
// get jwt token for zhipuai api // get jwt token for zhipuai api
segment := strings.Split(c.ApiKey, ".") segment := strings.Split(c.ApiKey, ".")
@ -37,17 +60,16 @@ func (c *ChatInstance) GetToken() string {
return token return token
} }
func (c *ChatInstance) GetEndpoint() string { func NewChatInstance(endpoint, apiKey string) *ChatInstance {
return c.Endpoint
}
func NewChatInstance(endpoint, apikey string) *ChatInstance {
return &ChatInstance{ return &ChatInstance{
Endpoint: endpoint, Endpoint: endpoint,
ApiKey: apikey, ApiKey: apiKey,
} }
} }
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { 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" import "chat/globals"
const ( const (
ChatGLMTurbo = "chatglm_turbo" GLM4 = "glm-4"
ChatGLMPro = "chatglm_pro" GLM4Vision = "glm-4v"
ChatGLMStd = "chatglm_std" GLMTurbo = "glm-3-turbo" // GLM3 Turbo
ChatGLMLite = "chatglm_lite" GLMPro = "chatglm_pro" // GLM3 Pro (deprecated)
GLMStd = "chatglm_std" // GLM3 Standard (deprecated)
GLMLite = "chatglm_lite" // GLM3 Lite (deprecated)
) )
type Payload struct { type ImageUrl struct {
ApiKey string `json:"api_key"` Url string `json:"url"`
Exp int64 `json:"exp"` Detail *string `json:"detail,omitempty"`
TimeStamp int64 `json:"timestamp"`
} }
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 { type ChatRequest struct {
Prompt []globals.Message `json:"prompt"` Model string `json:"model"`
Temperature *float32 `json:"temperature,omitempty"` Messages interface{} `json:"messages"`
TopP *float32 `json:"top_p,omitempty"` MaxToken *int `json:"max_tokens,omitempty"`
Ref *ChatRef `json:"ref,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 { // CompletionRequest is the request body for chatglm completion
Enable *bool `json:"enable,omitempty"` type CompletionRequest struct {
SearchQuery *string `json:"search_query,omitempty"` Model string `json:"model"`
Prompt string `json:"prompt"`
MaxToken *int `json:"max_tokens,omitempty"`
Stream bool `json:"stream"`
} }
type Occurrence struct { // ChatResponse is the native http request body for chatglm
Code int `json:"code"` type ChatResponse struct {
Msg string `json:"msg"` ID string `json:"id"`
Success bool `json:"success"` 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", endpoint: "https://api.anthropic.com",
format: "<x-api-key>", format: "<x-api-key>",
description: 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: [ models: [
"claude-instant-1.2", "claude-instant-1.2",
"claude-2", "claude-2",
@ -188,14 +188,12 @@ export const ChannelInfos: Record<string, ChannelInfo> = {
endpoint: "https://open.bigmodel.cn", endpoint: "https://open.bigmodel.cn",
format: "<api-key>", format: "<api-key>",
models: [ models: [
"zhipu-chatglm-turbo", "glm-4",
"zhipu-chatglm-pro", "glm-4v",
"zhipu-chatglm-std", "glm-3-turbo"
"zhipu-chatglm-lite",
], ],
description: description:
"> 智谱 ChatGLM 密钥格式为 **api-key**,接入点填写 *https://open.bigmodel.cn* \n" + "> 智谱 ChatGLM 密钥格式为 **api-key**,接入点填写 *https://open.bigmodel.cn* \n"
"> 智谱 ChatGLM 模型为了区分和 LocalAI 的开源 ChatGLM 模型,规定模型名称前缀为 **zhipu-**,系统内部已经做好适配,正常填入模板模型即可,无需额外任何设置 \n",
}, },
qwen: { qwen: {
endpoint: "https://dashscope.aliyuncs.com", endpoint: "https://dashscope.aliyuncs.com",
@ -299,4 +297,4 @@ export function getChannelType(type?: string): string {
export function getShortChannelType(type?: string): string { export function getShortChannelType(type?: string): string {
if (type && type in ShortChannelTypes) return ShortChannelTypes[type]; if (type && type in ShortChannelTypes) return ShortChannelTypes[type];
return ShortChannelTypes.openai; return ShortChannelTypes.openai;
} }

View File

@ -70,6 +70,9 @@ export const modelColorMapper: Record<string, string> = {
"zhipu-chatglm-pro": "lime-500", "zhipu-chatglm-pro": "lime-500",
"zhipu-chatglm-std": "lime-500", "zhipu-chatglm-std": "lime-500",
"zhipu-chatglm-lite": "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": "indigo-600",
"qwen-plus-net": "indigo-600", "qwen-plus-net": "indigo-600",

View File

@ -167,7 +167,13 @@ export const pricing: PricingDataset = [
currency: Currency.CNY, 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, input: 0.005,
output: 0.005, output: 0.005,
currency: Currency.CNY, currency: Currency.CNY,

View File

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