diff --git a/common/constants.go b/common/constants.go index f5a1e27..26684e3 100644 --- a/common/constants.go +++ b/common/constants.go @@ -199,6 +199,8 @@ const ( ChannelTypeFastGPT = 22 ChannelTypeTencent = 23 ChannelTypeGemini = 24 + ChannelTypeMoonshot = 25 + ChannelTypeZhipu_v4 = 26 ) var ChannelBaseURLs = []string{ @@ -227,4 +229,6 @@ var ChannelBaseURLs = []string{ "https://fastgpt.run/api/openapi", // 22 "https://hunyuan.cloud.tencent.com", //23 "", //24 + "https://api.moonshot.cn", //25 + "https://open.bigmodel.cn", //26 } diff --git a/common/model-ratio.go b/common/model-ratio.go index 3b0f540..30b87ee 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -74,6 +74,9 @@ var ModelRatio = map[string]float64{ "chatglm_pro": 0.7143, // ¥0.01 / 1k tokens "chatglm_std": 0.3572, // ¥0.005 / 1k tokens "chatglm_lite": 0.1429, // ¥0.002 / 1k tokens + "glm-4": 7.143, // ¥0.1 / 1k tokens + "glm-4v": 7.143, // ¥0.1 / 1k tokens + "glm-3-turbo": 0.3572, "qwen-turbo": 0.8572, // ¥0.012 / 1k tokens "qwen-plus": 10, // ¥0.14 / 1k tokens "text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens diff --git a/controller/model.go b/controller/model.go index 05d725f..d98d711 100644 --- a/controller/model.go +++ b/controller/model.go @@ -4,6 +4,10 @@ import ( "fmt" "github.com/gin-gonic/gin" "one-api/dto" + "one-api/relay" + "one-api/relay/channel/ai360" + "one-api/relay/channel/moonshot" + "one-api/relay/constant" ) // https://platform.openai.com/docs/api-reference/models/list @@ -53,574 +57,46 @@ func init() { IsBlocking: false, }) // https://platform.openai.com/docs/models/model-endpoint-compatibility - openAIModels = []OpenAIModels{ - { - Id: "midjourney", + for i := 0; i < constant.APITypeDummy; i++ { + if i == constant.APITypeAIProxyLibrary { + continue + } + adaptor := relay.GetAdaptor(i) + channelName := adaptor.GetChannelName() + modelNames := adaptor.GetModelList() + for _, modelName := range modelNames { + openAIModels = append(openAIModels, OpenAIModels{ + Id: modelName, + Object: "model", + Created: 1626777600, + OwnedBy: channelName, + Permission: permission, + Root: modelName, + Parent: nil, + }) + } + } + for _, modelName := range ai360.ModelList { + openAIModels = append(openAIModels, OpenAIModels{ + Id: modelName, Object: "model", - Created: 1677649963, - OwnedBy: "Midjourney", - Permission: permission, - Root: "midjourney", - Parent: nil, - }, - { - Id: "dall-e-2", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "dall-e-2", - Parent: nil, - }, - { - Id: "dall-e-3", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "dall-e-3", - Parent: nil, - }, - { - Id: "whisper-1", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "whisper-1", - Parent: nil, - }, - { - Id: "tts-1", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "tts-1", - Parent: nil, - }, - { - Id: "tts-1-1106", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "tts-1-1106", - Parent: nil, - }, - { - Id: "tts-1-hd", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "tts-1-hd", - Parent: nil, - }, - { - Id: "tts-1-hd-1106", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "tts-1-hd-1106", - Parent: nil, - }, - { - Id: "gpt-3.5-turbo", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-3.5-turbo", - Parent: nil, - }, - { - Id: "gpt-3.5-turbo-0301", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-3.5-turbo-0301", - Parent: nil, - }, - { - Id: "gpt-3.5-turbo-0613", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-3.5-turbo-0613", - Parent: nil, - }, - { - Id: "gpt-3.5-turbo-16k", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-3.5-turbo-16k", - Parent: nil, - }, - { - Id: "gpt-3.5-turbo-16k-0613", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-3.5-turbo-16k-0613", - Parent: nil, - }, - { - Id: "gpt-3.5-turbo-1106", - Object: "model", - Created: 1699593571, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-3.5-turbo-1106", - Parent: nil, - }, - { - Id: "gpt-3.5-turbo-0125", - Object: "model", - Created: 1706232090, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-3.5-turbo-0125", - Parent: nil, - }, - { - Id: "gpt-3.5-turbo-instruct", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-3.5-turbo-instruct", - Parent: nil, - }, - { - Id: "gpt-4", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-4", - Parent: nil, - }, - { - Id: "gpt-4-0314", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-4-0314", - Parent: nil, - }, - { - Id: "gpt-4-0613", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-4-0613", - Parent: nil, - }, - { - Id: "gpt-4-32k", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-4-32k", - Parent: nil, - }, - { - Id: "gpt-4-32k-0314", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-4-32k-0314", - Parent: nil, - }, - { - Id: "gpt-4-32k-0613", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-4-32k-0613", - Parent: nil, - }, - { - Id: "gpt-4-1106-preview", - Object: "model", - Created: 1699593571, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-4-1106-preview", - Parent: nil, - }, - { - Id: "gpt-4-0125-preview", - Object: "model", - Created: 1699593571, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-4-0125-preview", - Parent: nil, - }, - { - Id: "gpt-4-turbo-preview", - Object: "model", - Created: 1699593571, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-4-turbo-preview", - Parent: nil, - }, - { - Id: "gpt-4-vision-preview", - Object: "model", - Created: 1699593571, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-4-vision-preview", - Parent: nil, - }, - { - Id: "gpt-4-1106-vision-preview", - Object: "model", - Created: 1699593571, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-4-1106-vision-preview", - Parent: nil, - }, - { - Id: "text-embedding-3-small", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "text-embedding-ada-002", - Parent: nil, - }, - { - Id: "text-embedding-3-large", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "text-embedding-ada-002", - Parent: nil, - }, - { - Id: "text-embedding-ada-002", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "text-embedding-ada-002", - Parent: nil, - }, - { - Id: "text-davinci-003", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "text-davinci-003", - Parent: nil, - }, - { - Id: "text-davinci-002", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "text-davinci-002", - Parent: nil, - }, - { - Id: "text-curie-001", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "text-curie-001", - Parent: nil, - }, - { - Id: "text-babbage-001", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "text-babbage-001", - Parent: nil, - }, - { - Id: "text-ada-001", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "text-ada-001", - Parent: nil, - }, - { - Id: "text-moderation-latest", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "text-moderation-latest", - Parent: nil, - }, - { - Id: "text-moderation-stable", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "text-moderation-stable", - Parent: nil, - }, - { - Id: "text-davinci-edit-001", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "text-davinci-edit-001", - Parent: nil, - }, - { - Id: "code-davinci-edit-001", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "code-davinci-edit-001", - Parent: nil, - }, - { - Id: "babbage-002", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "babbage-002", - Parent: nil, - }, - { - Id: "davinci-002", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "davinci-002", - Parent: nil, - }, - { - Id: "claude-instant-1", - Object: "model", - Created: 1677649963, - OwnedBy: "anthropic", - Permission: permission, - Root: "claude-instant-1", - Parent: nil, - }, - { - Id: "claude-2", - Object: "model", - Created: 1677649963, - OwnedBy: "anthropic", - Permission: permission, - Root: "claude-2", - Parent: nil, - }, - { - Id: "ERNIE-Bot", - Object: "model", - Created: 1677649963, - OwnedBy: "baidu", - Permission: permission, - Root: "ERNIE-Bot", - Parent: nil, - }, - { - Id: "ERNIE-Bot-turbo", - Object: "model", - Created: 1677649963, - OwnedBy: "baidu", - Permission: permission, - Root: "ERNIE-Bot-turbo", - Parent: nil, - }, - { - Id: "ERNIE-Bot-4", - Object: "model", - Created: 1677649963, - OwnedBy: "baidu", - Permission: permission, - Root: "ERNIE-Bot-4", - Parent: nil, - }, - { - Id: "Embedding-V1", - Object: "model", - Created: 1677649963, - OwnedBy: "baidu", - Permission: permission, - Root: "Embedding-V1", - Parent: nil, - }, - { - Id: "PaLM-2", - Object: "model", - Created: 1677649963, - OwnedBy: "google", - Permission: permission, - Root: "PaLM-2", - Parent: nil, - }, - { - Id: "gemini-pro", - Object: "model", - Created: 1677649963, - OwnedBy: "google", - Permission: permission, - Root: "gemini-pro", - Parent: nil, - }, - { - Id: "gemini-pro-vision", - Object: "model", - Created: 1677649963, - OwnedBy: "google", - Permission: permission, - Root: "gemini-pro-vision", - Parent: nil, - }, - { - Id: "chatglm_turbo", - Object: "model", - Created: 1677649963, - OwnedBy: "zhipu", - Permission: permission, - Root: "chatglm_turbo", - Parent: nil, - }, - { - Id: "chatglm_pro", - Object: "model", - Created: 1677649963, - OwnedBy: "zhipu", - Permission: permission, - Root: "chatglm_pro", - Parent: nil, - }, - { - Id: "chatglm_std", - Object: "model", - Created: 1677649963, - OwnedBy: "zhipu", - Permission: permission, - Root: "chatglm_std", - Parent: nil, - }, - { - Id: "chatglm_lite", - Object: "model", - Created: 1677649963, - OwnedBy: "zhipu", - Permission: permission, - Root: "chatglm_lite", - Parent: nil, - }, - { - Id: "qwen-turbo", - Object: "model", - Created: 1677649963, - OwnedBy: "ali", - Permission: permission, - Root: "qwen-turbo", - Parent: nil, - }, - { - Id: "qwen-plus", - Object: "model", - Created: 1677649963, - OwnedBy: "ali", - Permission: permission, - Root: "qwen-plus", - Parent: nil, - }, - { - Id: "text-embedding-v1", - Object: "model", - Created: 1677649963, - OwnedBy: "ali", - Permission: permission, - Root: "text-embedding-v1", - Parent: nil, - }, - { - Id: "SparkDesk", - Object: "model", - Created: 1677649963, - OwnedBy: "xunfei", - Permission: permission, - Root: "SparkDesk", - Parent: nil, - }, - { - Id: "360GPT_S2_V9", - Object: "model", - Created: 1677649963, + Created: 1626777600, OwnedBy: "360", Permission: permission, - Root: "360GPT_S2_V9", + Root: modelName, Parent: nil, - }, - { - Id: "embedding-bert-512-v1", + }) + } + for _, modelName := range moonshot.ModelList { + openAIModels = append(openAIModels, OpenAIModels{ + Id: modelName, Object: "model", - Created: 1677649963, - OwnedBy: "360", + Created: 1626777600, + OwnedBy: "moonshot", Permission: permission, - Root: "embedding-bert-512-v1", + Root: modelName, Parent: nil, - }, - { - Id: "embedding_s1_v1", - Object: "model", - Created: 1677649963, - OwnedBy: "360", - Permission: permission, - Root: "embedding_s1_v1", - Parent: nil, - }, - { - Id: "semantic_similarity_s1_v1", - Object: "model", - Created: 1677649963, - OwnedBy: "360", - Permission: permission, - Root: "semantic_similarity_s1_v1", - Parent: nil, - }, - { - Id: "hunyuan", - Object: "model", - Created: 1677649963, - OwnedBy: "tencent", - Permission: permission, - Root: "hunyuan", - Parent: nil, - }, + }) } openAIModelsMap = make(map[string]OpenAIModels) for _, model := range openAIModels { diff --git a/dto/text_request.go b/dto/text_request.go index fa8770a..4565d66 100644 --- a/dto/text_request.go +++ b/dto/text_request.go @@ -14,6 +14,7 @@ type GeneralOpenAIRequest struct { MaxTokens uint `json:"max_tokens,omitempty"` Temperature float64 `json:"temperature,omitempty"` TopP float64 `json:"top_p,omitempty"` + Stop any `json:"stop,omitempty"` N int `json:"n,omitempty"` Input any `json:"input,omitempty"` Instruction string `json:"instruction,omitempty"` diff --git a/dto/text_response.go b/dto/text_response.go index 752793a..93eb40d 100644 --- a/dto/text_response.go +++ b/dto/text_response.go @@ -35,9 +35,12 @@ type OpenAIEmbeddingResponse struct { type ChatCompletionsStreamResponseChoice struct { Delta struct { - Content string `json:"content"` + Content string `json:"content"` + Role string `json:"role,omitempty"` + ToolCalls any `json:"tool_calls,omitempty"` } `json:"delta"` FinishReason *string `json:"finish_reason,omitempty"` + Index int `json:"index,omitempty"` } type ChatCompletionsStreamResponse struct { diff --git a/relay/channel/ai360/constants.go b/relay/channel/ai360/constants.go new file mode 100644 index 0000000..cfc3cb2 --- /dev/null +++ b/relay/channel/ai360/constants.go @@ -0,0 +1,8 @@ +package ai360 + +var ModelList = []string{ + "360GPT_S2_V9", + "embedding-bert-512-v1", + "embedding_s1_v1", + "semantic_similarity_s1_v1", +} diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index be8b621..f64b8a4 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -9,15 +9,19 @@ import ( "one-api/common" "one-api/dto" "one-api/relay/channel" + "one-api/relay/channel/ai360" + "one-api/relay/channel/moonshot" relaycommon "one-api/relay/common" "one-api/service" "strings" ) type Adaptor struct { + ChannelType int } func (a *Adaptor) Init(info *relaycommon.RelayInfo, request dto.GeneralOpenAIRequest) { + a.ChannelType = info.ChannelType } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { @@ -76,7 +80,14 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom } func (a *Adaptor) GetModelList() []string { - return ModelList + switch a.ChannelType { + case common.ChannelType360: + return ai360.ModelList + case common.ChannelTypeMoonshot: + return moonshot.ModelList + default: + return ModelList + } } func (a *Adaptor) GetChannelName() string { diff --git a/relay/channel/zhipu_v4/adaptor.go b/relay/channel/zhipu_v4/adaptor.go new file mode 100644 index 0000000..076c024 --- /dev/null +++ b/relay/channel/zhipu_v4/adaptor.go @@ -0,0 +1,57 @@ +package zhipu_v4 + +import ( + "errors" + "fmt" + "github.com/gin-gonic/gin" + "io" + "net/http" + "one-api/dto" + "one-api/relay/channel" + relaycommon "one-api/relay/common" +) + +type Adaptor struct { +} + +func (a *Adaptor) Init(info *relaycommon.RelayInfo, request dto.GeneralOpenAIRequest) { +} + +func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + return fmt.Sprintf("%s/api/paas/v4/chat/completions", info.BaseUrl), nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error { + channel.SetupApiRequestHeader(info, c, req) + token := getZhipuToken(info.ApiKey) + req.Header.Set("Authorization", token) + return nil +} + +func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *dto.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + return requestOpenAI2Zhipu(*request), nil +} + +func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) { + return channel.DoApiRequest(a, c, info, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) { + if info.IsStream { + err, usage = zhipuStreamHandler(c, resp) + } else { + err, usage = zhipuHandler(c, resp) + } + return +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return ChannelName +} diff --git a/relay/channel/zhipu_v4/constants.go b/relay/channel/zhipu_v4/constants.go new file mode 100644 index 0000000..756629a --- /dev/null +++ b/relay/channel/zhipu_v4/constants.go @@ -0,0 +1,7 @@ +package zhipu_v4 + +var ModelList = []string{ + "glm-4", "glm-4v", "glm-3-turbo", +} + +var ChannelName = "zhipu_v4" diff --git a/relay/channel/zhipu_v4/dto.go b/relay/channel/zhipu_v4/dto.go new file mode 100644 index 0000000..b17a89f --- /dev/null +++ b/relay/channel/zhipu_v4/dto.go @@ -0,0 +1,59 @@ +package zhipu_v4 + +import ( + "one-api/dto" + "time" +) + +// type ZhipuMessage struct { +// Role string `json:"role,omitempty"` +// Content string `json:"content,omitempty"` +// ToolCalls any `json:"tool_calls,omitempty"` +// ToolCallId any `json:"tool_call_id,omitempty"` +// } +// +// type ZhipuRequest struct { +// Model string `json:"model"` +// Stream bool `json:"stream,omitempty"` +// Messages []ZhipuMessage `json:"messages"` +// Temperature float64 `json:"temperature,omitempty"` +// TopP float64 `json:"top_p,omitempty"` +// MaxTokens int `json:"max_tokens,omitempty"` +// Stop []string `json:"stop,omitempty"` +// RequestId string `json:"request_id,omitempty"` +// Tools any `json:"tools,omitempty"` +// ToolChoice any `json:"tool_choice,omitempty"` +// } +// +// type ZhipuV4TextResponseChoice struct { +// Index int `json:"index"` +// ZhipuMessage `json:"message"` +// FinishReason string `json:"finish_reason"` +// } +type ZhipuV4Response struct { + Id string `json:"id"` + Created int64 `json:"created"` + Model string `json:"model"` + TextResponseChoices []dto.OpenAITextResponseChoice `json:"choices"` + Usage dto.Usage `json:"usage"` + Error dto.OpenAIError `json:"error"` +} + +// +//type ZhipuV4StreamResponseChoice struct { +// Index int `json:"index,omitempty"` +// Delta ZhipuMessage `json:"delta"` +// FinishReason *string `json:"finish_reason,omitempty"` +//} + +type ZhipuV4StreamResponse struct { + Id string `json:"id"` + Created int64 `json:"created"` + Choices []dto.ChatCompletionsStreamResponseChoice `json:"choices"` + Usage dto.Usage `json:"usage"` +} + +type tokenData struct { + Token string + ExpiryTime time.Time +} diff --git a/relay/channel/zhipu_v4/relay-zhipu_v4.go b/relay/channel/zhipu_v4/relay-zhipu_v4.go new file mode 100644 index 0000000..dde1b5f --- /dev/null +++ b/relay/channel/zhipu_v4/relay-zhipu_v4.go @@ -0,0 +1,262 @@ +package zhipu_v4 + +import ( + "bufio" + "bytes" + "encoding/json" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt" + "io" + "net/http" + "one-api/common" + "one-api/dto" + "one-api/service" + "strings" + "sync" + "time" +) + +// https://open.bigmodel.cn/doc/api#chatglm_std +// chatglm_std, chatglm_lite +// https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/invoke +// https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/sse-invoke + +var zhipuTokens sync.Map +var expSeconds int64 = 24 * 3600 + +func getZhipuToken(apikey string) string { + data, ok := zhipuTokens.Load(apikey) + if ok { + tokenData := data.(tokenData) + if time.Now().Before(tokenData.ExpiryTime) { + return tokenData.Token + } + } + + split := strings.Split(apikey, ".") + if len(split) != 2 { + common.SysError("invalid zhipu key: " + apikey) + return "" + } + + id := split[0] + secret := split[1] + + expMillis := time.Now().Add(time.Duration(expSeconds)*time.Second).UnixNano() / 1e6 + expiryTime := time.Now().Add(time.Duration(expSeconds) * time.Second) + + timestamp := time.Now().UnixNano() / 1e6 + + payload := jwt.MapClaims{ + "api_key": id, + "exp": expMillis, + "timestamp": timestamp, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) + + token.Header["alg"] = "HS256" + token.Header["sign_type"] = "SIGN" + + tokenString, err := token.SignedString([]byte(secret)) + if err != nil { + return "" + } + + zhipuTokens.Store(apikey, tokenData{ + Token: tokenString, + ExpiryTime: expiryTime, + }) + + return tokenString +} + +func requestOpenAI2Zhipu(request dto.GeneralOpenAIRequest) *dto.GeneralOpenAIRequest { + messages := make([]dto.Message, 0, len(request.Messages)) + for _, message := range request.Messages { + messages = append(messages, dto.Message{ + Role: message.Role, + Content: message.Content, + ToolCalls: message.ToolCalls, + ToolCallId: message.ToolCallId, + }) + } + str, ok := request.Stop.(string) + var Stop []string + if ok { + Stop = []string{str} + } else { + Stop, _ = request.Stop.([]string) + } + return &dto.GeneralOpenAIRequest{ + Model: request.Model, + Stream: request.Stream, + Messages: messages, + Temperature: request.Temperature, + TopP: request.TopP, + MaxTokens: request.MaxTokens, + Stop: Stop, + Tools: request.Tools, + ToolChoice: request.ToolChoice, + } +} + +//func responseZhipu2OpenAI(response *dto.OpenAITextResponse) *dto.OpenAITextResponse { +// fullTextResponse := dto.OpenAITextResponse{ +// Id: response.Id, +// Object: "chat.completion", +// Created: common.GetTimestamp(), +// Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.TextResponseChoices)), +// Usage: response.Usage, +// } +// for i, choice := range response.TextResponseChoices { +// content, _ := json.Marshal(strings.Trim(choice.Content, "\"")) +// openaiChoice := dto.OpenAITextResponseChoice{ +// Index: i, +// Message: dto.Message{ +// Role: choice.Role, +// Content: content, +// }, +// FinishReason: "", +// } +// if i == len(response.TextResponseChoices)-1 { +// openaiChoice.FinishReason = "stop" +// } +// fullTextResponse.Choices = append(fullTextResponse.Choices, openaiChoice) +// } +// return &fullTextResponse +//} + +func streamResponseZhipu2OpenAI(zhipuResponse *ZhipuV4StreamResponse) *dto.ChatCompletionsStreamResponse { + var choice dto.ChatCompletionsStreamResponseChoice + choice.Delta.Content = zhipuResponse.Choices[0].Delta.Content + choice.Delta.Role = zhipuResponse.Choices[0].Delta.Role + choice.Delta.ToolCalls = zhipuResponse.Choices[0].Delta.ToolCalls + choice.Index = zhipuResponse.Choices[0].Index + choice.FinishReason = zhipuResponse.Choices[0].FinishReason + response := dto.ChatCompletionsStreamResponse{ + Id: zhipuResponse.Id, + Object: "chat.completion.chunk", + Created: zhipuResponse.Created, + Model: "glm-4", + Choices: []dto.ChatCompletionsStreamResponseChoice{choice}, + } + return &response +} + +func lastStreamResponseZhipuV42OpenAI(zhipuResponse *ZhipuV4StreamResponse) (*dto.ChatCompletionsStreamResponse, *dto.Usage) { + response := streamResponseZhipu2OpenAI(zhipuResponse) + return response, &zhipuResponse.Usage +} + +func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { + var usage *dto.Usage + scanner := bufio.NewScanner(resp.Body) + scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := strings.Index(string(data), "\n"); i >= 0 { + return i + 1, data[0:i], nil + } + if atEOF { + return len(data), data, nil + } + return 0, nil, nil + }) + dataChan := make(chan string) + stopChan := make(chan bool) + go func() { + for scanner.Scan() { + data := scanner.Text() + if len(data) < 6 { // ignore blank line or wrong format + continue + } + if data[:6] != "data: " && data[:6] != "[DONE]" { + continue + } + dataChan <- data + } + stopChan <- true + }() + service.SetEventStreamHeaders(c) + c.Stream(func(w io.Writer) bool { + select { + case data := <-dataChan: + if strings.HasPrefix(data, "data: [DONE]") { + data = data[:12] + } + // some implementations may add \r at the end of data + data = strings.TrimSuffix(data, "\r") + + var streamResponse ZhipuV4StreamResponse + err := json.Unmarshal([]byte(data), &streamResponse) + if err != nil { + common.SysError("error unmarshalling stream response: " + err.Error()) + } + var response *dto.ChatCompletionsStreamResponse + if strings.Contains(data, "prompt_tokens") { + response, usage = lastStreamResponseZhipuV42OpenAI(&streamResponse) + } else { + response = streamResponseZhipu2OpenAI(&streamResponse) + } + jsonResponse, err := json.Marshal(response) + if err != nil { + common.SysError("error marshalling stream response: " + err.Error()) + return true + } + c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)}) + return true + case <-stopChan: + return false + } + }) + err := resp.Body.Close() + if err != nil { + return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + return nil, usage +} + +func zhipuHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { + var textResponse ZhipuV4Response + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + } + err = resp.Body.Close() + if err != nil { + return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + err = json.Unmarshal(responseBody, &textResponse) + if err != nil { + return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + } + if textResponse.Error.Type != "" { + return &dto.OpenAIErrorWithStatusCode{ + OpenAIError: textResponse.Error, + StatusCode: resp.StatusCode, + }, nil + } + // Reset response body + resp.Body = io.NopCloser(bytes.NewBuffer(responseBody)) + + // We shouldn't set the header before we parse the response body, because the parse part may fail. + // And then we will have to send an error response, but in this case, the header has already been set. + // So the HTTPClient will be confused by the response. + // For example, Postman will report error, and we cannot check the response at all. + for k, v := range resp.Header { + c.Writer.Header().Set(k, v[0]) + } + c.Writer.WriteHeader(resp.StatusCode) + _, err = io.Copy(c.Writer, resp.Body) + if err != nil { + return service.OpenAIErrorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil + } + err = resp.Body.Close() + if err != nil { + return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + + return nil, &textResponse.Usage +} diff --git a/relay/constant/api_type.go b/relay/constant/api_type.go index 485f8e0..51b62c1 100644 --- a/relay/constant/api_type.go +++ b/relay/constant/api_type.go @@ -15,6 +15,7 @@ const ( APITypeAIProxyLibrary APITypeTencent APITypeGemini + APITypeZhipu_v4 APITypeDummy // this one is only for count, do not add any channel after this ) @@ -40,6 +41,8 @@ func ChannelType2APIType(channelType int) int { apiType = APITypeTencent case common.ChannelTypeGemini: apiType = APITypeGemini + case common.ChannelTypeZhipu_v4: + apiType = APITypeZhipu_v4 } return apiType } diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index d1e613e..328b1e6 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -11,6 +11,7 @@ import ( "one-api/relay/channel/tencent" "one-api/relay/channel/xunfei" "one-api/relay/channel/zhipu" + "one-api/relay/channel/zhipu_v4" "one-api/relay/constant" ) @@ -36,6 +37,8 @@ func GetAdaptor(apiType int) channel.Adaptor { return &xunfei.Adaptor{} case constant.APITypeZhipu: return &zhipu.Adaptor{} + case constant.APITypeZhipu_v4: + return &zhipu_v4.Adaptor{} } return nil } diff --git a/service/token_counter.go b/service/token_counter.go index de476f1..2f2e126 100644 --- a/service/token_counter.go +++ b/service/token_counter.go @@ -154,26 +154,29 @@ func CountTokenMessages(messages []dto.Message, model string) (int, error) { for _, m := range arrayContent { if m.Type == "image_url" { var imageTokenNum int - if str, ok := m.ImageUrl.(string); ok { - imageTokenNum, err = getImageToken(&dto.MessageImageUrl{Url: str, Detail: "auto"}) + if model == "glm-4v" { + imageTokenNum = 1047 } else { - imageUrlMap := m.ImageUrl.(map[string]interface{}) - detail, ok := imageUrlMap["detail"] - if ok { - imageUrlMap["detail"] = detail.(string) + if str, ok := m.ImageUrl.(string); ok { + imageTokenNum, err = getImageToken(&dto.MessageImageUrl{Url: str, Detail: "auto"}) } else { - imageUrlMap["detail"] = "auto" + imageUrlMap := m.ImageUrl.(map[string]interface{}) + detail, ok := imageUrlMap["detail"] + if ok { + imageUrlMap["detail"] = detail.(string) + } else { + imageUrlMap["detail"] = "auto" + } + imageUrl := dto.MessageImageUrl{ + Url: imageUrlMap["url"].(string), + Detail: imageUrlMap["detail"].(string), + } + imageTokenNum, err = getImageToken(&imageUrl) } - imageUrl := dto.MessageImageUrl{ - Url: imageUrlMap["url"].(string), - Detail: imageUrlMap["detail"].(string), + if err != nil { + return 0, err } - imageTokenNum, err = getImageToken(&imageUrl) } - if err != nil { - return 0, err - } - tokenNum += imageTokenNum log.Printf("image token num: %d", imageTokenNum) } else { diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index b378d23..0e0ebc1 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -9,6 +9,8 @@ export const CHANNEL_OPTIONS = [ {key: 17, text: '阿里通义千问', value: 17, color: 'orange', label: '阿里通义千问'}, {key: 18, text: '讯飞星火认知', value: 18, color: 'blue', label: '讯飞星火认知'}, {key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet', label: '智谱 ChatGLM'}, + {key: 16, text: '智谱 GLM-4V', value: 26, color: 'green', label: '智谱 GLM-4V'}, + {key: 16, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot'}, {key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑'}, {key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元'}, {key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道'}, diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 903dbfe..f61c38e 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -89,6 +89,9 @@ const EditChannel = (props) => { case 24: localModels = ['gemini-pro']; break; + case 26: + localModels = ['glm-4', 'glm-4v', 'glm-3-turbo']; + break; } setInputs((inputs) => ({...inputs, models: localModels})); }