From 261e5008406e924fd227aff2027b34c7a1f0c00d Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Thu, 28 Dec 2023 09:32:14 +0800 Subject: [PATCH] feat: add gemini pro, gemini pro vision models --- adapter/palm2/chat.go | 82 +++++++++++++++++++----- adapter/palm2/formatter.go | 106 ++++++++++++++++++++++++++++++++ adapter/palm2/types.go | 64 +++++++++++++++++-- app/public/icons/gemini.jpeg | Bin 0 -> 5540 bytes app/src/admin/channel.ts | 4 +- app/src/admin/colors.ts | 2 + app/src/conf.ts | 21 +++++++ app/src/routes/admin/System.tsx | 4 +- globals/variables.go | 2 + utils/char.go | 4 +- utils/encrypt.go | 25 ++++++++ utils/image.go | 17 +++++ 12 files changed, 304 insertions(+), 27 deletions(-) create mode 100644 adapter/palm2/formatter.go create mode 100644 app/public/icons/gemini.jpeg diff --git a/adapter/palm2/chat.go b/adapter/palm2/chat.go index 7e1fc038..703db090 100644 --- a/adapter/palm2/chat.go +++ b/adapter/palm2/chat.go @@ -6,13 +6,23 @@ import ( "fmt" ) +var geminiMaxImages = 16 + type ChatProps struct { - Model string - Message []globals.Message + Model string + Message []globals.Message + Temperature *float64 + TopP *float64 + TopK *int + MaxOutputTokens *int } func (c *ChatInstance) GetChatEndpoint(model string) string { - return fmt.Sprintf("%s/v1beta2/models/%s:generateMessage?key=%s", c.Endpoint, model, c.ApiKey) + if model == globals.ChatBison001 { + return fmt.Sprintf("%s/v1beta2/models/%s:generateMessage?key=%s", c.Endpoint, model, c.ApiKey) + } + + return fmt.Sprintf("%s/v1beta/models/%s:generateContent?key=%s", c.Endpoint, model, c.ApiKey) } func (c *ChatInstance) ConvertMessage(message []globals.Message) []PalmMessage { @@ -41,31 +51,73 @@ func (c *ChatInstance) ConvertMessage(message []globals.Message) []PalmMessage { return result } -func (c *ChatInstance) GetChatBody(props *ChatProps) *ChatBody { - return &ChatBody{ - Prompt: Prompt{ +func (c *ChatInstance) GetPalm2ChatBody(props *ChatProps) *PalmChatBody { + return &PalmChatBody{ + Prompt: PalmPrompt{ Messages: c.ConvertMessage(props.Message), }, } } +func (c *ChatInstance) GetGeminiChatBody(props *ChatProps) *GeminiChatBody { + return &GeminiChatBody{ + Contents: c.GetGeminiContents(props.Model, props.Message), + GenerationConfig: GeminiConfig{ + Temperature: props.Temperature, + MaxOutputTokens: props.MaxOutputTokens, + TopP: props.TopP, + TopK: props.TopK, + }, + } +} + +func (c *ChatInstance) GetPalm2ChatResponse(data interface{}) (string, error) { + if form := utils.MapToStruct[PalmChatResponse](data); form != nil { + if len(form.Candidates) == 0 { + return "", fmt.Errorf("palm2 error: the content violates content policy") + } + return form.Candidates[0].Content, nil + } + return "", fmt.Errorf("palm2 error: cannot parse response") +} + +func (c *ChatInstance) GetGeminiChatResponse(data interface{}) (string, error) { + if form := utils.MapToStruct[GeminiChatResponse](data); form != nil { + if len(form.Candidates) != 0 && len(form.Candidates[0].Content.Parts) != 0 { + return form.Candidates[0].Content.Parts[0].Text, nil + } + } + + if form := utils.MapToStruct[GeminiChatErrorResponse](data); form != nil { + return "", fmt.Errorf("gemini error: %s (code: %d, status: %s)", form.Error.Message, form.Error.Code, form.Error.Status) + } + + return "", fmt.Errorf("gemini: cannot parse response") +} + func (c *ChatInstance) CreateChatRequest(props *ChatProps) (string, error) { uri := c.GetChatEndpoint(props.Model) + + if props.Model == globals.ChatBison001 { + data, err := utils.Post(uri, map[string]string{ + "Content-Type": "application/json", + }, c.GetPalm2ChatBody(props)) + + if err != nil { + return "", fmt.Errorf("palm2 error: %s", err.Error()) + } + return c.GetPalm2ChatResponse(data) + } + data, err := utils.Post(uri, map[string]string{ "Content-Type": "application/json", - }, c.GetChatBody(props)) + }, c.GetGeminiChatBody(props)) if err != nil { - return "", fmt.Errorf("palm2 error: %s", err.Error()) + return "", fmt.Errorf("gemini error: %s", err.Error()) } - if form := utils.MapToStruct[ChatResponse](data); form != nil { - if len(form.Candidates) == 0 { - return "I don't know how to respond to that. Please try another question.", nil - } - return form.Candidates[0].Content, nil - } - return "", fmt.Errorf("palm2 error: cannot parse response") + return c.GetGeminiChatResponse(data) } // CreateStreamChatRequest is the mock stream request for palm2 diff --git a/adapter/palm2/formatter.go b/adapter/palm2/formatter.go new file mode 100644 index 00000000..cd15db3b --- /dev/null +++ b/adapter/palm2/formatter.go @@ -0,0 +1,106 @@ +package palm2 + +import ( + "chat/globals" + "chat/utils" + "strings" +) + +func getGeminiRole(role string) string { + switch role { + case globals.User: + return GeminiUserType + case globals.Assistant, globals.Tool, globals.System: + return GeminiModelType + default: + return GeminiUserType + } +} + +func getMimeType(content string) string { + segment := strings.Split(content, ".") + if len(segment) == 0 || len(segment) == 1 { + return "image/png" + } + + suffix := strings.TrimSpace(strings.ToLower(segment[len(segment)-1])) + + switch suffix { + case "png": + return "image/png" + case "jpg", "jpeg": + return "image/jpeg" + case "gif": + return "image/gif" + case "webp": + return "image/webp" + case "heif": + return "image/heif" + case "heic": + return "image/heic" + default: + return "image/png" + } +} + +func getGeminiContent(parts []GeminiChatPart, content string, model string) []GeminiChatPart { + parts = append(parts, GeminiChatPart{ + Text: &content, + }) + + if model == globals.GeminiPro { + return parts + } + + urls := utils.ExtractImageUrls(content) + if len(urls) > geminiMaxImages { + urls = urls[:geminiMaxImages] + } + + for _, url := range urls { + data, err := utils.ConvertToBase64(url) + if err != nil { + continue + } + + parts = append(parts, GeminiChatPart{ + InlineData: &GeminiInlineData{ + MimeType: getMimeType(url), + Data: data, + }, + }) + } + + return parts +} + +func (c *ChatInstance) GetGeminiContents(model string, message []globals.Message) []GeminiContent { + // gemini role should be user-model + + result := make([]GeminiContent, 0) + for _, item := range message { + role := getGeminiRole(item.Role) + if len(item.Content) == 0 { + // gemini model: message must include non empty content + continue + } + + if len(result) == 0 && getGeminiRole(item.Role) == GeminiModelType { + // gemini model: first message must be user + continue + } + + if len(result) > 0 && role == result[len(result)-1].Role { + // gemini model: messages must alternate between authors + result[len(result)-1].Parts = getGeminiContent(result[len(result)-1].Parts, item.Content, model) + continue + } + + result = append(result, GeminiContent{ + Role: getGeminiRole(item.Role), + Parts: getGeminiContent(make([]GeminiChatPart, 0), item.Content, model), + }) + } + + return result +} diff --git a/adapter/palm2/types.go b/adapter/palm2/types.go index fa17199c..c3e1ee82 100644 --- a/adapter/palm2/types.go +++ b/adapter/palm2/types.go @@ -1,20 +1,72 @@ package palm2 +const ( + GeminiUserType = "user" + GeminiModelType = "model" +) + type PalmMessage struct { Author string `json:"author"` Content string `json:"content"` } -// ChatBody is the native http request body for palm2 -type ChatBody struct { - Prompt Prompt `json:"prompt"` +// PalmChatBody is the native http request body for palm2 +type PalmChatBody struct { + Prompt PalmPrompt `json:"prompt"` } -type Prompt struct { +type PalmPrompt struct { Messages []PalmMessage `json:"messages"` } -// ChatResponse is the native http response body for palm2 -type ChatResponse struct { +// PalmChatResponse is the native http response body for palm2 +type PalmChatResponse struct { Candidates []PalmMessage `json:"candidates"` } + +// GeminiChatBody is the native http request body for gemini +type GeminiChatBody struct { + Contents []GeminiContent `json:"contents"` + GenerationConfig GeminiConfig `json:"generationConfig"` +} + +type GeminiConfig struct { + Temperature *float64 `json:"temperature,omitempty"` + MaxOutputTokens *int `json:"maxOutputTokens,omitempty"` + TopP *float64 `json:"topP,omitempty"` + TopK *int `json:"topK,omitempty"` +} + +type GeminiContent struct { + Role string `json:"role"` + Parts []GeminiChatPart `json:"parts"` +} + +type GeminiChatPart struct { + Text *string `json:"text,omitempty"` + InlineData *GeminiInlineData `json:"inline_data,omitempty"` +} + +type GeminiInlineData struct { + MimeType string `json:"mime_type"` + Data string `json:"data"` +} + +type GeminiChatResponse struct { + Candidates []struct { + Content struct { + Parts []struct { + Text string `json:"text"` + } `json:"parts"` + Role string `json:"role"` + } `json:"content"` + } `json:"candidates"` +} + +type GeminiChatErrorResponse struct { + Error struct { + Code int `json:"code"` + Message string `json:"message"` + Status string `json:"status"` + } `json:"error"` +} diff --git a/app/public/icons/gemini.jpeg b/app/public/icons/gemini.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..e16bcfa6acfbd65ce408032a40b541209157ac3e GIT binary patch literal 5540 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 literal 0 HcmV?d00001 diff --git a/app/src/admin/channel.ts b/app/src/admin/channel.ts index 934707c8..02845796 100644 --- a/app/src/admin/channel.ts +++ b/app/src/admin/channel.ts @@ -33,7 +33,7 @@ export const ChannelTypes: Record = { baichuan: "百川 AI", skylark: "火山方舟", bing: "New Bing", - palm: "Google PaLM2", + palm: "Google Gemini", midjourney: "Midjourney", oneapi: "Nio API", }; @@ -141,7 +141,7 @@ export const ChannelInfos: Record = { id: 11, endpoint: "https://generativelanguage.googleapis.com", format: "", - models: ["chat-bison-001"], + models: ["chat-bison-001", "gemini-pro", "gemini-pro-vision"], }, midjourney: { id: 12, diff --git a/app/src/admin/colors.ts b/app/src/admin/colors.ts index 173cd241..4e8c8f55 100644 --- a/app/src/admin/colors.ts +++ b/app/src/admin/colors.ts @@ -43,6 +43,8 @@ export const modelColorMapper: Record = { "spark-desk-v3": "#06b3e8", "chat-bison-001": "#f82a53", + "gemini-pro": "#f82a53", + "gemini-pro-vision": "#f82a53", "bing-creative": "#2673e7", "bing-balanced": "#2673e7", diff --git a/app/src/conf.ts b/app/src/conf.ts index 996af21d..fdc62337 100644 --- a/app/src/conf.ts +++ b/app/src/conf.ts @@ -274,6 +274,22 @@ export const supportModels: Model[] = [ tag: ["free", "english-model"], }, + // gemini + { + id: "gemini-pro", + name: "Gemini Pro", + free: true, + auth: true, + tag: ["free", "official"], + }, + { + id: "gemini-pro-vision", + name: "Gemini Pro Vision", + free: true, + auth: true, + tag: ["free", "official", "multi-modal"], + }, + // drawing models { id: "midjourney", @@ -346,6 +362,9 @@ export const defaultModels = [ "zhipu-chatglm-turbo", "baichuan-53b", + "gemini-pro", + "gemini-pro-vision", + "dall-e-2", "midjourney-fast", "stable-diffusion", @@ -412,6 +431,8 @@ export const modelAvatars: Record = { "midjourney-turbo": "midjourney.jpg", "bing-creative": "newbing.jpg", "chat-bison-001": "palm2.webp", + "gemini-pro": "gemini.jpeg", + "gemini-pro-vision": "gemini.jpeg", "zhipu-chatglm-turbo": "chatglm.png", "qwen-plus-net": "tongyi.png", "qwen-plus": "tongyi.png", diff --git a/app/src/routes/admin/System.tsx b/app/src/routes/admin/System.tsx index 5da75ffb..76c18020 100644 --- a/app/src/routes/admin/System.tsx +++ b/app/src/routes/admin/System.tsx @@ -168,8 +168,8 @@ function Mail({ data, dispatch, onChange }: CompProps) {
- - diff --git a/globals/variables.go b/globals/variables.go index 7afc448b..90ac7a69 100644 --- a/globals/variables.go +++ b/globals/variables.go @@ -69,6 +69,8 @@ const ( SparkDeskV2 = "spark-desk-v2" SparkDeskV3 = "spark-desk-v3" ChatBison001 = "chat-bison-001" + GeminiPro = "gemini-pro" + GeminiProVision = "gemini-pro-vision" BingCreative = "bing-creative" BingBalanced = "bing-balanced" BingPrecise = "bing-precise" diff --git a/utils/char.go b/utils/char.go index f3e5bafb..57ab51e2 100644 --- a/utils/char.go +++ b/utils/char.go @@ -167,8 +167,8 @@ func ExtractUrls(data string) []string { func ExtractImageUrls(data string) []string { // https://platform.openai.com/docs/guides/vision/what-type-of-files-can-i-upload - re := regexp.MustCompile(`(https?://\S+\.(?:png|jpg|jpeg|gif|webp))`) - return re.FindAllString(data, -1) + re := regexp.MustCompile(`(https?://\S+\.(?:png|jpg|jpeg|gif|webp|heif|heic))`) + return re.FindAllString(strings.ToLower(data), -1) } func ContainUnicode(data string) bool { diff --git a/utils/encrypt.go b/utils/encrypt.go index 7fc8b80b..b7ee38c1 100644 --- a/utils/encrypt.go +++ b/utils/encrypt.go @@ -6,6 +6,7 @@ import ( "crypto/md5" crand "crypto/rand" "crypto/sha256" + "encoding/base64" "encoding/hex" "io" ) @@ -22,6 +23,30 @@ func Sha2EncryptForm(form interface{}) string { return hex.EncodeToString(hash[:]) } +func Base64Encode(raw string) string { + return base64.StdEncoding.EncodeToString([]byte(raw)) +} + +func Base64EncodeBytes(raw []byte) string { + return base64.StdEncoding.EncodeToString(raw) +} + +func Base64Decode(raw string) string { + if data, err := base64.StdEncoding.DecodeString(raw); err == nil { + return string(data) + } else { + return "" + } +} + +func Base64DecodeBytes(raw string) []byte { + if data, err := base64.StdEncoding.DecodeString(raw); err == nil { + return data + } else { + return []byte{} + } +} + func Md5Encrypt(raw string) string { // return 32-bit hash hash := md5.Sum([]byte(raw)) diff --git a/utils/image.go b/utils/image.go index d789b0c8..da245269 100644 --- a/utils/image.go +++ b/utils/image.go @@ -6,6 +6,7 @@ import ( "image" "image/gif" "image/jpeg" + "io" "math" "net/http" "path" @@ -51,6 +52,22 @@ func NewImage(url string) (*Image, error) { return &Image{Object: img}, nil } +func ConvertToBase64(url string) (string, error) { + res, err := http.Get(url) + if err != nil { + return "", err + } + + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + if err != nil { + return "", err + } + + return Base64EncodeBytes(data), nil +} + func (i *Image) GetWidth() int { return i.Object.Bounds().Max.X }