diff --git a/adapter/azure/struct.go b/adapter/azure/struct.go index 6b66f6a..8cc302b 100644 --- a/adapter/azure/struct.go +++ b/adapter/azure/struct.go @@ -11,11 +11,6 @@ type ChatInstance struct { Resource string } -type InstanceProps struct { - Model string - Plan bool -} - func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } diff --git a/adapter/openai/struct.go b/adapter/openai/struct.go index b3dcb20..67b1772 100644 --- a/adapter/openai/struct.go +++ b/adapter/openai/struct.go @@ -11,11 +11,6 @@ type ChatInstance struct { ApiKey string } -type InstanceProps struct { - Model string - Plan bool -} - func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } diff --git a/adapter/zhipuai/chat.go b/adapter/zhipuai/chat.go index 35ccc50..f8f69a5 100644 --- a/adapter/zhipuai/chat.go +++ b/adapter/zhipuai/chat.go @@ -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 } diff --git a/adapter/zhipuai/processor.go b/adapter/zhipuai/processor.go new file mode 100644 index 0000000..291695a --- /dev/null +++ b/adapter/zhipuai/processor.go @@ -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") +} diff --git a/adapter/zhipuai/struct.go b/adapter/zhipuai/struct.go index 6c3c5fd..3f78586 100644 --- a/adapter/zhipuai/struct.go +++ b/adapter/zhipuai/struct.go @@ -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(), + ) } diff --git a/adapter/zhipuai/types.go b/adapter/zhipuai/types.go index d79af3b..14c7cfa 100644 --- a/adapter/zhipuai/types.go +++ b/adapter/zhipuai/types.go @@ -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" +) diff --git a/app/src/admin/channel.ts b/app/src/admin/channel.ts index 0e3371e..7d98980 100644 --- a/app/src/admin/channel.ts +++ b/app/src/admin/channel.ts @@ -154,7 +154,7 @@ export const ChannelInfos: Record = { endpoint: "https://api.anthropic.com", format: "", 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 = { endpoint: "https://open.bigmodel.cn", format: "", 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; -} +} \ No newline at end of file diff --git a/app/src/admin/colors.ts b/app/src/admin/colors.ts index 63c52ed..b80ce00 100644 --- a/app/src/admin/colors.ts +++ b/app/src/admin/colors.ts @@ -70,6 +70,9 @@ export const modelColorMapper: Record = { "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", diff --git a/app/src/admin/datasets/charge.ts b/app/src/admin/datasets/charge.ts index 570d3f0..3e4cfde 100644 --- a/app/src/admin/datasets/charge.ts +++ b/app/src/admin/datasets/charge.ts @@ -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, diff --git a/globals/variables.go b/globals/variables.go index 3f45a18..0eee1ac 100644 --- a/globals/variables.go +++ b/globals/variables.go @@ -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 {