From a1f48451e7a129af1d5d83be99549b0e13883c2d Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Thu, 28 Dec 2023 21:28:19 +0800 Subject: [PATCH] feat: add azure openai format --- README.md | 2 +- adapter/adapter.go | 19 ++++ adapter/azure/chat.go | 147 +++++++++++++++++++++++++ adapter/azure/image.go | 62 +++++++++++ adapter/azure/processor.go | 216 +++++++++++++++++++++++++++++++++++++ adapter/azure/struct.go | 52 +++++++++ adapter/azure/types.go | 124 +++++++++++++++++++++ adapter/palm2/formatter.go | 6 +- app/src/admin/channel.ts | 44 +++++--- channel/channel.go | 4 +- globals/constant.go | 29 ++--- 11 files changed, 673 insertions(+), 32 deletions(-) create mode 100644 adapter/azure/chat.go create mode 100644 adapter/azure/image.go create mode 100644 adapter/azure/processor.go create mode 100644 adapter/azure/struct.go create mode 100644 adapter/azure/types.go diff --git a/README.md b/README.md index 03db9e7..a517a21 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ - [x] Anthropic Claude (claude-2, claude-instant) - [x] Slack Claude (deprecated) - [x] Sparkdesk (v1.5, v2, v3) -- [x] Google PaLM2 +- [x] Google Gemini (PaLM2) - [x] New Bing (creative, balanced, precise) - [x] ChatGLM (turbo, pro, std, lite) - [x] DashScope Tongyi (plus, turbo) diff --git a/adapter/adapter.go b/adapter/adapter.go index 2967c3d..a7ad755 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -1,6 +1,7 @@ package adapter import ( + "chat/adapter/azure" "chat/adapter/baichuan" "chat/adapter/bing" "chat/adapter/chatgpt" @@ -67,6 +68,24 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global Buffer: props.Buffer, }, hook) + case globals.AzureOpenAIChannelType: + return azure.NewChatInstanceFromConfig(conf).CreateStreamChatRequest(&azure.ChatProps{ + Model: model, + Message: props.Message, + Token: utils.Multi( + props.Token == 0, + utils.Multi(props.Infinity || props.Plan, nil, utils.ToPtr(2500)), + &props.Token, + ), + PresencePenalty: props.PresencePenalty, + FrequencyPenalty: props.FrequencyPenalty, + Temperature: props.Temperature, + TopP: props.TopP, + Tools: props.Tools, + ToolChoice: props.ToolChoice, + Buffer: props.Buffer, + }, hook) + case globals.ClaudeChannelType: return claude.NewChatInstanceFromConfig(conf).CreateStreamChatRequest(&claude.ChatProps{ Model: model, diff --git a/adapter/azure/chat.go b/adapter/azure/chat.go new file mode 100644 index 0000000..e67d474 --- /dev/null +++ b/adapter/azure/chat.go @@ -0,0 +1,147 @@ +package azure + +import ( + "chat/globals" + "chat/utils" + "fmt" + "strings" +) + +type ChatProps struct { + Model string + Message []globals.Message + Token *int + PresencePenalty *float32 + FrequencyPenalty *float32 + Temperature *float32 + TopP *float32 + Tools *globals.FunctionTools + ToolChoice *interface{} + Buffer utils.Buffer +} + +func (c *ChatInstance) GetChatEndpoint(props *ChatProps) string { + model := strings.ReplaceAll(props.Model, ".", "") + if props.Model == globals.GPT3TurboInstruct { + return fmt.Sprintf("%s/openai/deployments/%s/completions?api-version=%s", c.GetResource(), model, c.GetEndpoint()) + } + return fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=%s", c.GetResource(), model, c.GetEndpoint()) +} + +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 *ChatProps) string { + if len(props.Message) == 0 { + return "" + } + + return props.Message[len(props.Message)-1].Content +} + +func (c *ChatInstance) GetChatBody(props *ChatProps, stream bool) interface{} { + if props.Model == globals.GPT3TurboInstruct { + // for completions + return CompletionRequest{ + Prompt: c.GetCompletionPrompt(props.Message), + MaxToken: props.Token, + Stream: stream, + } + } + + return ChatRequest{ + Messages: formatMessages(props), + MaxToken: props.Token, + Stream: stream, + PresencePenalty: props.PresencePenalty, + FrequencyPenalty: props.FrequencyPenalty, + Temperature: props.Temperature, + TopP: props.TopP, + Tools: props.Tools, + ToolChoice: props.ToolChoice, + } +} + +// CreateChatRequest is the native http request body for chatgpt +func (c *ChatInstance) CreateChatRequest(props *ChatProps) (string, error) { + if globals.IsDalleModel(props.Model) { + return c.CreateImage(props) + } + + res, err := utils.Post( + c.GetChatEndpoint(props), + c.GetHeader(), + c.GetChatBody(props, false), + ) + + if err != nil || res == nil { + return "", fmt.Errorf("chatgpt error: %s", err.Error()) + } + + data := utils.MapToStruct[ChatResponse](res) + if data == nil { + return "", fmt.Errorf("chatgpt error: cannot parse response") + } else if data.Error.Message != "" { + return "", fmt.Errorf("chatgpt error: %s", data.Error.Message) + } + return data.Choices[0].Message.Content, nil +} + +// CreateStreamChatRequest is the stream response body for chatgpt +func (c *ChatInstance) CreateStreamChatRequest(props *ChatProps, callback globals.Hook) error { + if globals.IsDalleModel(props.Model) { + if url, err := c.CreateImage(props); err != nil { + return err + } else { + return callback(url) + } + } + + buf := "" + cursor := 0 + chunk := "" + instruct := props.Model == globals.GPT3TurboInstruct + + err := utils.EventSource( + "POST", + c.GetChatEndpoint(props), + c.GetHeader(), + c.GetChatBody(props, true), + func(data string) error { + data, err := c.ProcessLine(props.Buffer, instruct, buf, data) + chunk += data + + if err != nil { + if strings.HasPrefix(err.Error(), "chatgpt error") { + return err + } + + // error when break line + buf = buf + data + return nil + } + + buf = "" + if data != "" { + cursor += 1 + if err := callback(data); err != nil { + return err + } + } + return nil + }, + ) + + if err != nil { + return err + } else if len(chunk) == 0 { + return fmt.Errorf("empty response") + } + + return nil +} diff --git a/adapter/azure/image.go b/adapter/azure/image.go new file mode 100644 index 0000000..5a7e62a --- /dev/null +++ b/adapter/azure/image.go @@ -0,0 +1,62 @@ +package azure + +import ( + "chat/globals" + "chat/utils" + "fmt" + "strings" +) + +type ImageProps struct { + Model string + Prompt string + Size ImageSize +} + +func (c *ChatInstance) GetImageEndpoint(model string) string { + model = strings.ReplaceAll(model, ".", "") + return fmt.Sprintf("%s/openai/deployments/%s/images/generations?api-version=%s", c.GetResource(), model, c.GetEndpoint()) +} + +// CreateImageRequest will create a dalle image from prompt, return url of image and error +func (c *ChatInstance) CreateImageRequest(props ImageProps) (string, error) { + res, err := utils.Post( + c.GetImageEndpoint(props.Model), + c.GetHeader(), ImageRequest{ + Prompt: props.Prompt, + Size: utils.Multi[ImageSize]( + props.Model == globals.Dalle3, + ImageSize1024, + ImageSize512, + ), + N: 1, + }) + if err != nil || res == nil { + return "", fmt.Errorf("chatgpt error: %s", err.Error()) + } + + data := utils.MapToStruct[ImageResponse](res) + if data == nil { + return "", fmt.Errorf("chatgpt error: cannot parse response") + } else if data.Error.Message != "" { + return "", fmt.Errorf("chatgpt error: %s", data.Error.Message) + } + + return data.Data[0].Url, nil +} + +// CreateImage will create a dalle image from prompt, return markdown of image +func (c *ChatInstance) CreateImage(props *ChatProps) (string, error) { + url, err := c.CreateImageRequest(ImageProps{ + Model: props.Model, + Prompt: c.GetLatestPrompt(props), + }) + if err != nil { + if strings.Contains(err.Error(), "safety") { + return err.Error(), nil + } + return "", err + } + + return utils.GetImageMarkdown(url), nil +} diff --git a/adapter/azure/processor.go b/adapter/azure/processor.go new file mode 100644 index 0000000..c315fd3 --- /dev/null +++ b/adapter/azure/processor.go @@ -0,0 +1,216 @@ +package azure + +import ( + "chat/globals" + "chat/utils" + "errors" + "fmt" + "regexp" + "strings" +) + +func processFormat(data string) string { + rep := strings.NewReplacer( + "data: {", + "\"data\": {", + ) + item := rep.Replace(data) + if !strings.HasPrefix(item, "{") { + item = "{" + item + } + if !strings.HasSuffix(item, "}}") { + item = item + "}" + } + + return item +} + +func formatMessages(props *ChatProps) interface{} { + if props.Model == globals.GPT4Vision { + base := props.Message[len(props.Message)-1].Content + urls := utils.ExtractImageUrls(base) + + if len(urls) > 0 { + base = fmt.Sprintf("%s %s", strings.Join(urls, " "), base) + } + props.Message[len(props.Message)-1].Content = base + return props.Message + } else if props.Model == globals.GPT41106VisionPreview { + return utils.Each[globals.Message, Message](props.Message, func(message globals.Message) Message { + if message.Role == globals.User { + urls := utils.ExtractImageUrls(message.Content) + images := utils.EachNotNil[string, MessageContent](urls, func(url string) *MessageContent { + obj, err := utils.NewImage(url) + if err != nil { + return nil + } + + props.Buffer.AddImage(obj) + + return &MessageContent{ + Type: "image_url", + ImageUrl: &ImageUrl{ + Url: url, + }, + } + }) + + return Message{ + Role: message.Role, + Content: utils.Prepend(images, MessageContent{ + Type: "text", + Text: &message.Content, + }), + ToolCalls: message.ToolCalls, + ToolCallId: message.ToolCallId, + } + } + + return Message{ + Role: message.Role, + Content: MessageContents{ + MessageContent{ + Type: "text", + Text: &message.Content, + }, + }, + ToolCalls: message.ToolCalls, + ToolCallId: message.ToolCallId, + } + }) + } + + return props.Message +} + +func processChatResponse(data string) *ChatStreamResponse { + if strings.HasPrefix(data, "{") { + var form *ChatStreamResponse + if form = utils.UnmarshalForm[ChatStreamResponse](data); form != nil { + return form + } + + if form = utils.UnmarshalForm[ChatStreamResponse](data[:len(data)-1]); form != nil { + return form + } + } + + return nil +} + +func processCompletionResponse(data string) *CompletionResponse { + if strings.HasPrefix(data, "{") { + var form *CompletionResponse + if form = utils.UnmarshalForm[CompletionResponse](data); form != nil { + return form + } + + if form = utils.UnmarshalForm[CompletionResponse](data[:len(data)-1]); form != nil { + return form + } + } + + return nil +} + +func processChatErrorResponse(data string) *ChatStreamErrorResponse { + if strings.HasPrefix(data, "{") { + var form *ChatStreamErrorResponse + if form = utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil { + return form + } + if form = utils.UnmarshalForm[ChatStreamErrorResponse](data + "}"); form != nil { + return form + } + } + + return nil +} + +func isDone(data string) bool { + return utils.Contains[string](data, []string{ + "{data: [DONE]}", "{data: [DONE]}}", + "{[DONE]}", "{data:}", "{data:}}", + }) +} + +func getChoices(form *ChatStreamResponse) string { + if len(form.Data.Choices) == 0 { + return "" + } + + return form.Data.Choices[0].Delta.Content +} + +func getCompletionChoices(form *CompletionResponse) string { + if len(form.Data.Choices) == 0 { + return "" + } + + return form.Data.Choices[0].Text +} + +func getToolCalls(form *ChatStreamResponse) *globals.ToolCalls { + if len(form.Data.Choices) == 0 { + return nil + } + + return form.Data.Choices[0].Delta.ToolCalls +} + +func getRobustnessResult(chunk string) string { + exp := `\"content\":\"(.*?)\"` + compile, err := regexp.Compile(exp) + if err != nil { + return "" + } + + matches := compile.FindStringSubmatch(chunk) + if len(matches) > 1 { + partial := matches[1] + // if the unicode character is in the string, like `hi\\u2019s`, we need to convert it to `hi's` + if utils.ContainUnicode(partial) { + partial = utils.DecodeUnicode(partial) + } + + return partial + } else { + return "" + } +} + +func (c *ChatInstance) ProcessLine(obj utils.Buffer, instruct bool, buf, data string) (string, error) { + item := processFormat(buf + data) + if isDone(item) { + return "", nil + } + + if form := processChatResponse(item); form == nil { + if instruct { + // legacy support + if completion := processCompletionResponse(item); completion != nil { + return getCompletionChoices(completion), nil + } + } + + // recursive call + if len(buf) > 0 { + return c.ProcessLine(obj, instruct, "", buf+item) + } + + if err := processChatErrorResponse(item); err == nil || err.Data.Error.Message == "" { + if res := getRobustnessResult(item); res != "" { + return res, nil + } + + globals.Warn(fmt.Sprintf("chatgpt error: cannot parse response: %s", item)) + return data, errors.New("parser error: cannot parse response") + } else { + return "", fmt.Errorf("chatgpt error: %s (type: %s)", err.Data.Error.Message, err.Data.Error.Type) + } + + } else { + obj.SetToolCalls(getToolCalls(form)) + return getChoices(form), nil + } +} diff --git a/adapter/azure/struct.go b/adapter/azure/struct.go new file mode 100644 index 0000000..3f5b6b5 --- /dev/null +++ b/adapter/azure/struct.go @@ -0,0 +1,52 @@ +package azure + +import ( + "chat/globals" +) + +type ChatInstance struct { + Endpoint string + ApiKey string + Resource string +} + +type InstanceProps struct { + Model string + Plan bool +} + +func (c *ChatInstance) GetEndpoint() string { + return c.Endpoint +} + +func (c *ChatInstance) GetApiKey() string { + return c.ApiKey +} + +func (c *ChatInstance) GetResource() string { + return c.Resource +} + +func (c *ChatInstance) GetHeader() map[string]string { + return map[string]string{ + "Content-Type": "application/json", + "api-key": c.GetApiKey(), + } +} + +func NewChatInstance(endpoint, apiKey string, resource string) *ChatInstance { + return &ChatInstance{ + Endpoint: endpoint, + ApiKey: apiKey, + Resource: resource, + } +} + +func NewChatInstanceFromConfig(conf globals.ChannelConfig) *ChatInstance { + param := conf.SplitRandomSecret(2) + return NewChatInstance( + conf.GetEndpoint(), + param[0], + param[1], + ) +} diff --git a/adapter/azure/types.go b/adapter/azure/types.go new file mode 100644 index 0000000..29c982e --- /dev/null +++ b/adapter/azure/types.go @@ -0,0 +1,124 @@ +package azure + +import "chat/globals" + +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 MessageContents `json:"content"` + 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 chatgpt +type ChatRequest struct { + 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 +} + +// CompletionRequest is the request body for chatgpt completion +type CompletionRequest struct { + Model string `json:"model"` + Prompt string `json:"prompt"` + MaxToken *int `json:"max_tokens,omitempty"` + Stream bool `json:"stream"` +} + +// ChatResponse is the native http request body for chatgpt +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 chatgpt +type ChatStreamResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Data struct { + Choices []struct { + Delta globals.Message `json:"delta"` + Index int `json:"index"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` + } `json:"data"` +} + +// CompletionResponse is the native http request body / stream response body for chatgpt completion +type CompletionResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Data struct { + Choices []struct { + Text string `json:"text"` + Index int `json:"index"` + } `json:"choices"` + } `json:"data"` +} + +type ChatStreamErrorResponse struct { + Data struct { + Error struct { + Message string `json:"message"` + Type string `json:"type"` + } `json:"error"` + } `json:"data"` +} + +type ImageSize string + +// ImageRequest is the request body for chatgpt 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/adapter/palm2/formatter.go b/adapter/palm2/formatter.go index cd15db3..b96d20c 100644 --- a/adapter/palm2/formatter.go +++ b/adapter/palm2/formatter.go @@ -87,7 +87,11 @@ func (c *ChatInstance) GetGeminiContents(model string, message []globals.Message if len(result) == 0 && getGeminiRole(item.Role) == GeminiModelType { // gemini model: first message must be user - continue + + result = append(result, GeminiContent{ + Role: GeminiUserType, + Parts: getGeminiContent(make([]GeminiChatPart, 0), "", model), + }) } if len(result) > 0 && role == result[len(result)-1].Role { diff --git a/app/src/admin/channel.ts b/app/src/admin/channel.ts index 0284579..044d161 100644 --- a/app/src/admin/channel.ts +++ b/app/src/admin/channel.ts @@ -14,7 +14,6 @@ export type Channel = { }; export type ChannelInfo = { - id: number; description?: string; endpoint: string; format: string; @@ -23,6 +22,7 @@ export type ChannelInfo = { export const ChannelTypes: Record = { openai: "OpenAI", + azure: "Azure OpenAI", claude: "Claude", slack: "Slack", sparkdesk: "讯飞星火", @@ -40,7 +40,6 @@ export const ChannelTypes: Record = { export const ChannelInfos: Record = { openai: { - id: 0, endpoint: "https://api.openai.com", format: "", models: [ @@ -64,26 +63,50 @@ export const ChannelInfos: Record = { "dall-e-3", ], }, + azure: { + endpoint: "2023-12-01-preview", + format: "|", + description: + "> Azure 密钥 API Key 1 和 API Key 2 任填一个即可,密钥格式为 **|**, api-endpoint 为 Azure 的 **API 端点**。\n" + + "> 接入点填 **API Version**,如 2023-12-01-preview。\n" + + "Azure 模型名称忽略点号等问题内部已经进行适配,无需额外任何设置。", + models: [ + "gpt-3.5-turbo", + "gpt-3.5-turbo-instruct", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-16k-0613", + "gpt-3.5-turbo-16k-0301", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "dall-e-2", + "dall-e-3", + ], + }, claude: { - id: 1, endpoint: "https://api.anthropic.com", format: "", models: ["claude-instant-1", "claude-2", "claude-2.1"], }, slack: { - id: 2, endpoint: "your-channel", format: "|", models: ["claude-slack"], }, sparkdesk: { - id: 3, endpoint: "wss://spark-api.xf-yun.com", format: "||", models: ["spark-desk-v1.5", "spark-desk-v2", "spark-desk-v3"], }, chatglm: { - id: 4, endpoint: "https://open.bigmodel.cn", format: "", models: [ @@ -94,32 +117,27 @@ export const ChannelInfos: Record = { ], }, qwen: { - id: 5, endpoint: "https://dashscope.aliyuncs.com", format: "", models: ["qwen-turbo", "qwen-plus", "qwen-turbo-net", "qwen-plus-net"], }, hunyuan: { - id: 6, endpoint: "https://hunyuan.cloud.tencent.com", format: "||", models: ["hunyuan"], // endpoint }, zhinao: { - id: 7, endpoint: "https://api.360.cn", format: "", models: ["360-gpt-v9"], }, baichuan: { - id: 8, endpoint: "https://api.baichuan-ai.com", format: "", models: ["baichuan-53b"], }, skylark: { - id: 9, endpoint: "https://maas-api.ml-platform-cn-beijing.volces.com", format: "|", models: [ @@ -130,7 +148,6 @@ export const ChannelInfos: Record = { ], }, bing: { - id: 10, endpoint: "wss://your.bing.service", format: "", models: ["bing-creative", "bing-balanced", "bing-precise"], @@ -138,13 +155,11 @@ export const ChannelInfos: Record = { "> Bing 服务需要自行搭建,详情请参考 [chatnio-bing-service](https://github.com/Deeptrain-Community/chatnio-bing-service) (如为 bing2api 可直接使用 OpenAI 格式映射)", }, palm: { - id: 11, endpoint: "https://generativelanguage.googleapis.com", format: "", models: ["chat-bison-001", "gemini-pro", "gemini-pro-vision"], }, midjourney: { - id: 12, endpoint: "https://your.midjourney.proxy", format: "|", models: ["midjourney", "midjourney-fast", "midjourney-turbo"], @@ -154,7 +169,6 @@ export const ChannelInfos: Record = { "> 注意:**请在系统设置中设置后端的公网 IP / 域名,否则无法接收回调**", }, oneapi: { - id: 13, endpoint: "https://openai.justsong.cn/api", format: "", models: [], diff --git a/channel/channel.go b/channel/channel.go index de06e14..c25abaf 100644 --- a/channel/channel.go +++ b/channel/channel.go @@ -179,13 +179,15 @@ func (c *Channel) ProcessError(err error) error { return nil } content := err.Error() + + fmt.Println(content) if strings.Contains(content, c.GetEndpoint()) { // hide the endpoint replacer := fmt.Sprintf("channel://%d", c.GetId()) content = strings.Replace(content, c.GetEndpoint(), replacer, -1) } - if domain := c.GetDomain(); strings.Contains(content, domain) { + if domain := c.GetDomain(); len(strings.TrimSpace(domain)) > 0 && strings.Contains(content, domain) { content = strings.Replace(content, domain, "channel", -1) } diff --git a/globals/constant.go b/globals/constant.go index 55fd28d..e7920c7 100644 --- a/globals/constant.go +++ b/globals/constant.go @@ -8,20 +8,21 @@ const ( ) const ( - OpenAIChannelType = "openai" - ClaudeChannelType = "claude" - SlackChannelType = "slack" - SparkdeskChannelType = "sparkdesk" - ChatGLMChannelType = "chatglm" - HunyuanChannelType = "hunyuan" - QwenChannelType = "qwen" - ZhinaoChannelType = "zhinao" - BaichuanChannelType = "baichuan" - SkylarkChannelType = "skylark" - BingChannelType = "bing" - PalmChannelType = "palm" - MidjourneyChannelType = "midjourney" - OneAPIChannelType = "oneapi" + OpenAIChannelType = "openai" + AzureOpenAIChannelType = "azure" + ClaudeChannelType = "claude" + SlackChannelType = "slack" + SparkdeskChannelType = "sparkdesk" + ChatGLMChannelType = "chatglm" + HunyuanChannelType = "hunyuan" + QwenChannelType = "qwen" + ZhinaoChannelType = "zhinao" + BaichuanChannelType = "baichuan" + SkylarkChannelType = "skylark" + BingChannelType = "bing" + PalmChannelType = "palm" + MidjourneyChannelType = "midjourney" + OneAPIChannelType = "oneapi" ) const (