mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-03 01:39:45 +00:00
Compare commits
44 Commits
v0.9.20-pa
...
v0.9.24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef0647285c | ||
|
|
33b1fad5f8 | ||
|
|
b899122dfe | ||
|
|
50c04a62f9 | ||
|
|
554b68484c | ||
|
|
6a1c046714 | ||
|
|
0b37bdddc6 | ||
|
|
563a426c00 | ||
|
|
f6a5d9ef7e | ||
|
|
a7d2450704 | ||
|
|
75fced3d9c | ||
|
|
5a1bbd1059 | ||
|
|
c133678cb1 | ||
|
|
1fc3c4b09d | ||
|
|
77c4c3e804 | ||
|
|
bc1f747418 | ||
|
|
62edac7c7f | ||
|
|
ff839df279 | ||
|
|
8b8511b19e | ||
|
|
7598753f4e | ||
|
|
68777bf05f | ||
|
|
b6217b22b0 | ||
|
|
196fa135fd | ||
|
|
ff3225ab44 | ||
|
|
ab36de3725 | ||
|
|
2b4617dc1b | ||
|
|
e169818404 | ||
|
|
c1a696e6f0 | ||
|
|
e07347ac53 | ||
|
|
fd38abd562 | ||
|
|
293c0277a8 | ||
|
|
344a799fcf | ||
|
|
35192e5675 | ||
|
|
9e80e4e7e5 | ||
|
|
e7bef097dd | ||
|
|
41b2341b0b | ||
|
|
e1a52f1d5a | ||
|
|
d8dc8029c0 | ||
|
|
87bc4ba419 | ||
|
|
850a553958 | ||
|
|
974df5e7b9 | ||
|
|
06cd774c10 | ||
|
|
4419be9c09 | ||
|
|
fb3b27a626 |
@@ -67,6 +67,9 @@
|
||||
# 设置 Dify 渠道是否输出工作流和节点信息到客户端
|
||||
# DIFY_DEBUG=true
|
||||
|
||||
# LinuxDo相关配置
|
||||
LINUX_DO_TOKEN_ENDPOINT=https://connect.linux.do/oauth2/token
|
||||
LINUX_DO_USER_ENDPOINT=https://connect.linux.do/api/user
|
||||
|
||||
# 节点类型
|
||||
# 如果是主节点则为master
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -16,6 +16,8 @@ new-api
|
||||
tiktoken_cache
|
||||
.eslintcache
|
||||
.gocache
|
||||
.cache
|
||||
web/bun.lock
|
||||
|
||||
electron/node_modules
|
||||
electron/dist
|
||||
|
||||
@@ -2,7 +2,9 @@ package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -128,13 +130,13 @@ func ParseMultipartFormReusable(c *gin.Context) (*multipart.Form, error) {
|
||||
}
|
||||
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
boundary := ""
|
||||
if idx := strings.Index(contentType, "boundary="); idx != -1 {
|
||||
boundary = contentType[idx+9:]
|
||||
boundary, err := parseBoundary(contentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader := multipart.NewReader(bytes.NewReader(requestBody), boundary)
|
||||
form, err := reader.ReadForm(32 << 20) // 32 MB max memory
|
||||
form, err := reader.ReadForm(multipartMemoryLimit())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -177,17 +179,16 @@ func parseFormData(data []byte, v any) error {
|
||||
|
||||
func parseMultipartFormData(c *gin.Context, data []byte, v any) error {
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
boundary := ""
|
||||
if idx := strings.Index(contentType, "boundary="); idx != -1 {
|
||||
boundary = contentType[idx+9:]
|
||||
}
|
||||
|
||||
if boundary == "" {
|
||||
return Unmarshal(data, v) // Fallback to JSON
|
||||
boundary, err := parseBoundary(contentType)
|
||||
if err != nil {
|
||||
if errors.Is(err, errBoundaryNotFound) {
|
||||
return Unmarshal(data, v) // Fallback to JSON
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
reader := multipart.NewReader(bytes.NewReader(data), boundary)
|
||||
form, err := reader.ReadForm(32 << 20) // 32 MB max memory
|
||||
form, err := reader.ReadForm(multipartMemoryLimit())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -203,3 +204,31 @@ func parseMultipartFormData(c *gin.Context, data []byte, v any) error {
|
||||
|
||||
return processFormMap(formMap, v)
|
||||
}
|
||||
|
||||
var errBoundaryNotFound = errors.New("multipart boundary not found")
|
||||
|
||||
// parseBoundary extracts the multipart boundary from the Content-Type header using mime.ParseMediaType
|
||||
func parseBoundary(contentType string) (string, error) {
|
||||
if contentType == "" {
|
||||
return "", errBoundaryNotFound
|
||||
}
|
||||
// Boundary-UUID / boundary-------xxxxxx
|
||||
_, params, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
boundary, ok := params["boundary"]
|
||||
if !ok || boundary == "" {
|
||||
return "", errBoundaryNotFound
|
||||
}
|
||||
return boundary, nil
|
||||
}
|
||||
|
||||
// multipartMemoryLimit returns the configured multipart memory limit in bytes
|
||||
func multipartMemoryLimit() int64 {
|
||||
limitMB := constant.MaxFileDownloadMB
|
||||
if limitMB <= 0 {
|
||||
limitMB = 32
|
||||
}
|
||||
return int64(limitMB) << 20
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/relay/channel/volcengine"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -91,7 +92,7 @@ func GetAllChannels(c *gin.Context) {
|
||||
if tag == nil || *tag == "" {
|
||||
continue
|
||||
}
|
||||
tagChannels, err := model.GetChannelsByTag(*tag, idSort)
|
||||
tagChannels, err := model.GetChannelsByTag(*tag, idSort, false)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -192,13 +193,29 @@ func FetchUpstreamModels(c *gin.Context) {
|
||||
url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL)
|
||||
case constant.ChannelTypeZhipu_v4:
|
||||
url = fmt.Sprintf("%s/api/paas/v4/models", baseURL)
|
||||
case constant.ChannelTypeVolcEngine:
|
||||
if baseURL == volcengine.DoubaoCodingPlan {
|
||||
url = fmt.Sprintf("%s/v1/models", volcengine.DoubaoCodingPlanOpenAIBaseURL)
|
||||
} else {
|
||||
url = fmt.Sprintf("%s/v1/models", baseURL)
|
||||
}
|
||||
default:
|
||||
url = fmt.Sprintf("%s/v1/models", baseURL)
|
||||
}
|
||||
|
||||
// 获取用于请求的可用密钥(多密钥渠道优先使用启用状态的密钥)
|
||||
key, _, apiErr := channel.GetNextEnabledKey()
|
||||
if apiErr != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": fmt.Sprintf("获取渠道密钥失败: %s", apiErr.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
key = strings.TrimSpace(key)
|
||||
|
||||
// 获取响应体 - 根据渠道类型决定是否添加 AuthHeader
|
||||
var body []byte
|
||||
key := strings.Split(channel.Key, "\n")[0]
|
||||
switch channel.Type {
|
||||
case constant.ChannelTypeAnthropic:
|
||||
body, err = GetResponseBody("GET", url, channel, GetClaudeAuthHeader(key))
|
||||
@@ -271,7 +288,7 @@ func SearchChannels(c *gin.Context) {
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if tag != nil && *tag != "" {
|
||||
tagChannel, err := model.GetChannelsByTag(*tag, idSort)
|
||||
tagChannel, err := model.GetChannelsByTag(*tag, idSort, false)
|
||||
if err == nil {
|
||||
channelData = append(channelData, tagChannel...)
|
||||
}
|
||||
@@ -1021,7 +1038,7 @@ func GetTagModels(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
channels, err := model.GetChannelsByTag(tag, false) // Assuming false for idSort is fine here
|
||||
channels, err := model.GetChannelsByTag(tag, false, false) // idSort=false, selectAll=false
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
|
||||
@@ -44,7 +44,7 @@ func getGitHubUserInfoByCode(code string) (*GitHubUser, error) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
client := http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Timeout: 20 * time.Second,
|
||||
}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -84,7 +84,7 @@ func getLinuxdoUserInfoByCode(code string, c *gin.Context) (*LinuxdoUser, error)
|
||||
}
|
||||
|
||||
// Get access token using Basic auth
|
||||
tokenEndpoint := "https://connect.linux.do/oauth2/token"
|
||||
tokenEndpoint := common.GetEnvOrDefaultString("LINUX_DO_TOKEN_ENDPOINT", "https://connect.linux.do/oauth2/token")
|
||||
credentials := common.LinuxDOClientId + ":" + common.LinuxDOClientSecret
|
||||
basicAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(credentials))
|
||||
|
||||
@@ -129,7 +129,7 @@ func getLinuxdoUserInfoByCode(code string, c *gin.Context) (*LinuxdoUser, error)
|
||||
}
|
||||
|
||||
// Get user info
|
||||
userEndpoint := "https://connect.linux.do/api/user"
|
||||
userEndpoint := common.GetEnvOrDefaultString("LINUX_DO_USER_ENDPOINT", "https://connect.linux.do/api/user")
|
||||
req, err = http.NewRequest("GET", userEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
"github.com/QuantumNous/new-api/relay/channel/moonshot"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
@@ -109,6 +111,17 @@ func init() {
|
||||
func ListModels(c *gin.Context, modelType int) {
|
||||
userOpenAiModels := make([]dto.OpenAIModels, 0)
|
||||
|
||||
acceptUnsetRatioModel := operation_setting.SelfUseModeEnabled
|
||||
if !acceptUnsetRatioModel {
|
||||
userId := c.GetInt("id")
|
||||
if userId > 0 {
|
||||
userSettings, _ := model.GetUserSetting(userId, false)
|
||||
if userSettings.AcceptUnsetRatioModel {
|
||||
acceptUnsetRatioModel = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modelLimitEnable := common.GetContextKeyBool(c, constant.ContextKeyTokenModelLimitEnabled)
|
||||
if modelLimitEnable {
|
||||
s, ok := common.GetContextKey(c, constant.ContextKeyTokenModelLimit)
|
||||
@@ -119,6 +132,12 @@ func ListModels(c *gin.Context, modelType int) {
|
||||
tokenModelLimit = map[string]bool{}
|
||||
}
|
||||
for allowModel, _ := range tokenModelLimit {
|
||||
if !acceptUnsetRatioModel {
|
||||
_, _, exist := ratio_setting.GetModelRatioOrPrice(allowModel)
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if oaiModel, ok := openAIModelsMap[allowModel]; ok {
|
||||
oaiModel.SupportedEndpointTypes = model.GetModelSupportEndpointTypes(allowModel)
|
||||
userOpenAiModels = append(userOpenAiModels, oaiModel)
|
||||
@@ -161,6 +180,12 @@ func ListModels(c *gin.Context, modelType int) {
|
||||
models = model.GetGroupEnabledModels(group)
|
||||
}
|
||||
for _, modelName := range models {
|
||||
if !acceptUnsetRatioModel {
|
||||
_, _, exist := ratio_setting.GetModelRatioOrPrice(modelName)
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if oaiModel, ok := openAIModelsMap[modelName]; ok {
|
||||
oaiModel.SupportedEndpointTypes = model.GetModelSupportEndpointTypes(modelName)
|
||||
userOpenAiModels = append(userOpenAiModels, oaiModel)
|
||||
@@ -175,6 +200,7 @@ func ListModels(c *gin.Context, modelType int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch modelType {
|
||||
case constant.ChannelTypeAnthropic:
|
||||
useranthropicModels := make([]dto.AnthropicModel, len(userOpenAiModels))
|
||||
|
||||
@@ -52,6 +52,7 @@ func updateVideoTaskAll(ctx context.Context, platform constant.TaskPlatform, cha
|
||||
info.ChannelMeta = &relaycommon.ChannelMeta{
|
||||
ChannelBaseUrl: cacheGetChannel.GetBaseURL(),
|
||||
}
|
||||
info.ApiKey = cacheGetChannel.Key
|
||||
adaptor.Init(info)
|
||||
for _, taskId := range taskIds {
|
||||
if err := updateVideoSingleTask(ctx, adaptor, cacheGetChannel, taskId, taskM); err != nil {
|
||||
|
||||
@@ -141,6 +141,8 @@ func (r *GeminiChatRequest) SetTools(tools []GeminiChatTool) {
|
||||
type GeminiThinkingConfig struct {
|
||||
IncludeThoughts bool `json:"includeThoughts,omitempty"`
|
||||
ThinkingBudget *int `json:"thinkingBudget,omitempty"`
|
||||
// TODO Conflict with thinkingbudget.
|
||||
// ThinkingLevel json.RawMessage `json:"thinkingLevel,omitempty"`
|
||||
}
|
||||
|
||||
func (c *GeminiThinkingConfig) SetThinkingBudget(budget int) {
|
||||
@@ -182,8 +184,12 @@ type FunctionCall struct {
|
||||
}
|
||||
|
||||
type GeminiFunctionResponse struct {
|
||||
Name string `json:"name"`
|
||||
Response map[string]interface{} `json:"response"`
|
||||
Name string `json:"name"`
|
||||
Response map[string]interface{} `json:"response"`
|
||||
WillContinue json.RawMessage `json:"willContinue,omitempty"`
|
||||
Scheduling json.RawMessage `json:"scheduling,omitempty"`
|
||||
Parts json.RawMessage `json:"parts,omitempty"`
|
||||
ID json.RawMessage `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiPartExecutableCode struct {
|
||||
@@ -202,11 +208,15 @@ type GeminiFileData struct {
|
||||
}
|
||||
|
||||
type GeminiPart struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
Thought bool `json:"thought,omitempty"`
|
||||
InlineData *GeminiInlineData `json:"inlineData,omitempty"`
|
||||
FunctionCall *FunctionCall `json:"functionCall,omitempty"`
|
||||
FunctionResponse *GeminiFunctionResponse `json:"functionResponse,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Thought bool `json:"thought,omitempty"`
|
||||
InlineData *GeminiInlineData `json:"inlineData,omitempty"`
|
||||
FunctionCall *FunctionCall `json:"functionCall,omitempty"`
|
||||
ThoughtSignature json.RawMessage `json:"thoughtSignature,omitempty"`
|
||||
FunctionResponse *GeminiFunctionResponse `json:"functionResponse,omitempty"`
|
||||
// Optional. Media resolution for the input media.
|
||||
MediaResolution json.RawMessage `json:"mediaResolution,omitempty"`
|
||||
VideoMetadata json.RawMessage `json:"videoMetadata,omitempty"`
|
||||
FileData *GeminiFileData `json:"fileData,omitempty"`
|
||||
ExecutableCode *GeminiPartExecutableCode `json:"executableCode,omitempty"`
|
||||
CodeExecutionResult *GeminiPartCodeExecutionResult `json:"codeExecutionResult,omitempty"`
|
||||
|
||||
@@ -66,10 +66,11 @@ type GeneralOpenAIRequest struct {
|
||||
// 注意:默认过滤此字段以保护用户隐私,但过滤后可能导致 Codex 无法正常使用
|
||||
Store json.RawMessage `json:"store,omitempty"`
|
||||
// Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the user field
|
||||
PromptCacheKey string `json:"prompt_cache_key,omitempty"`
|
||||
LogitBias json.RawMessage `json:"logit_bias,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
Prediction json.RawMessage `json:"prediction,omitempty"`
|
||||
PromptCacheKey string `json:"prompt_cache_key,omitempty"`
|
||||
PromptCacheRetention json.RawMessage `json:"prompt_cache_retention,omitempty"`
|
||||
LogitBias json.RawMessage `json:"logit_bias,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
Prediction json.RawMessage `json:"prediction,omitempty"`
|
||||
// gemini
|
||||
ExtraBody json.RawMessage `json:"extra_body,omitempty"`
|
||||
//xai
|
||||
@@ -798,19 +799,20 @@ type OpenAIResponsesRequest struct {
|
||||
PreviousResponseID string `json:"previous_response_id,omitempty"`
|
||||
Reasoning *Reasoning `json:"reasoning,omitempty"`
|
||||
// 服务层级字段,用于指定 API 服务等级。允许透传可能导致实际计费高于预期,默认应过滤
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
Store json.RawMessage `json:"store,omitempty"`
|
||||
PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
Text json.RawMessage `json:"text,omitempty"`
|
||||
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
||||
Tools json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
Truncation string `json:"truncation,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
MaxToolCalls uint `json:"max_tool_calls,omitempty"`
|
||||
Prompt json.RawMessage `json:"prompt,omitempty"`
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
Store json.RawMessage `json:"store,omitempty"`
|
||||
PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"`
|
||||
PromptCacheRetention json.RawMessage `json:"prompt_cache_retention,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
Text json.RawMessage `json:"text,omitempty"`
|
||||
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
||||
Tools json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
Truncation string `json:"truncation,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
MaxToolCalls uint `json:"max_tool_calls,omitempty"`
|
||||
Prompt json.RawMessage `json:"prompt,omitempty"`
|
||||
}
|
||||
|
||||
func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
|
||||
6
electron/package-lock.json
generated
6
electron/package-lock.json
generated
@@ -2784,9 +2784,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
10
go.mod
10
go.mod
@@ -43,10 +43,10 @@ require (
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
github.com/tiktoken-go/tokenizer v0.6.2
|
||||
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/image v0.23.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/sync v0.18.0
|
||||
gorm.io/driver/mysql v1.4.3
|
||||
gorm.io/driver/postgres v1.5.2
|
||||
gorm.io/gorm v1.25.2
|
||||
@@ -111,8 +111,8 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
golang.org/x/arch v0.21.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
|
||||
20
go.sum
20
go.sum
@@ -281,18 +281,18 @@ go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
|
||||
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
|
||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -304,15 +304,15 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
|
||||
@@ -272,13 +272,17 @@ func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Chan
|
||||
return channels, err
|
||||
}
|
||||
|
||||
func GetChannelsByTag(tag string, idSort bool) ([]*Channel, error) {
|
||||
func GetChannelsByTag(tag string, idSort bool, selectAll bool) ([]*Channel, error) {
|
||||
var channels []*Channel
|
||||
order := "priority desc"
|
||||
if idSort {
|
||||
order = "id desc"
|
||||
}
|
||||
err := DB.Where("tag = ?", tag).Order(order).Find(&channels).Error
|
||||
query := DB.Where("tag = ?", tag).Order(order)
|
||||
if !selectAll {
|
||||
query = query.Omit("key")
|
||||
}
|
||||
err := query.Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
|
||||
@@ -728,7 +732,7 @@ func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *
|
||||
return err
|
||||
}
|
||||
if shouldReCreateAbilities {
|
||||
channels, err := GetChannelsByTag(updatedTag, false)
|
||||
channels, err := GetChannelsByTag(updatedTag, false, false)
|
||||
if err == nil {
|
||||
for _, channel := range channels {
|
||||
err = channel.UpdateAbilities(nil)
|
||||
|
||||
@@ -429,3 +429,14 @@ func TaskCountAllUserTask(userId int, queryParams SyncTaskQueryParams) int64 {
|
||||
_ = query.Count(&total).Error
|
||||
return total
|
||||
}
|
||||
func (t *Task) ToOpenAIVideo() *dto.OpenAIVideo {
|
||||
openAIVideo := dto.NewOpenAIVideo()
|
||||
openAIVideo.ID = t.TaskID
|
||||
openAIVideo.Status = t.Status.ToVideoStatus()
|
||||
openAIVideo.Model = t.Properties.OriginModelName
|
||||
openAIVideo.SetProgressStr(t.Progress)
|
||||
openAIVideo.CreatedAt = t.CreatedAt
|
||||
openAIVideo.CompletedAt = t.UpdatedAt
|
||||
openAIVideo.SetMetadata("url", t.FailReason)
|
||||
return openAIVideo
|
||||
}
|
||||
|
||||
@@ -47,7 +47,11 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
case constant.RelayModeImagesGenerations:
|
||||
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/text2image/image-synthesis", info.ChannelBaseUrl)
|
||||
case constant.RelayModeImagesEdits:
|
||||
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/multimodal-generation/generation", info.ChannelBaseUrl)
|
||||
if isWanModel(info.OriginModelName) {
|
||||
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/image2image/image-synthesis", info.ChannelBaseUrl)
|
||||
} else {
|
||||
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/multimodal-generation/generation", info.ChannelBaseUrl)
|
||||
}
|
||||
case constant.RelayModeCompletions:
|
||||
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/completions", info.ChannelBaseUrl)
|
||||
default:
|
||||
@@ -71,6 +75,9 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel
|
||||
req.Set("X-DashScope-Async", "enable")
|
||||
}
|
||||
if info.RelayMode == constant.RelayModeImagesEdits {
|
||||
if isWanModel(info.OriginModelName) {
|
||||
req.Set("X-DashScope-Async", "enable")
|
||||
}
|
||||
req.Set("Content-Type", "application/json")
|
||||
}
|
||||
return nil
|
||||
@@ -107,6 +114,9 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
|
||||
}
|
||||
return aliRequest, nil
|
||||
} else if info.RelayMode == constant.RelayModeImagesEdits {
|
||||
if isWanModel(info.OriginModelName) {
|
||||
return oaiFormEdit2WanxImageEdit(c, info, request)
|
||||
}
|
||||
// ali image edit https://bailian.console.aliyun.com/?tab=api#/api/?type=model&url=2976416
|
||||
// 如果用户使用表单,则需要解析表单数据
|
||||
if strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
@@ -161,7 +171,11 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
|
||||
case constant.RelayModeImagesGenerations:
|
||||
err, usage = aliImageHandler(c, resp, info)
|
||||
case constant.RelayModeImagesEdits:
|
||||
err, usage = aliImageEditHandler(c, resp, info)
|
||||
if isWanModel(info.OriginModelName) {
|
||||
err, usage = aliImageHandler(c, resp, info)
|
||||
} else {
|
||||
err, usage = aliImageEditHandler(c, resp, info)
|
||||
}
|
||||
case constant.RelayModeRerank:
|
||||
err, usage = RerankHandler(c, resp, info)
|
||||
default:
|
||||
|
||||
@@ -112,6 +112,19 @@ type AliImageInput struct {
|
||||
Messages []AliMessage `json:"messages,omitempty"`
|
||||
}
|
||||
|
||||
type WanImageInput struct {
|
||||
Prompt string `json:"prompt"` // 必需:文本提示词,描述生成图像中期望包含的元素和视觉特点
|
||||
Images []string `json:"images"` // 必需:图像URL数组,长度不超过2,支持HTTP/HTTPS URL或Base64编码
|
||||
NegativePrompt string `json:"negative_prompt,omitempty"` // 可选:反向提示词,描述不希望在画面中看到的内容
|
||||
}
|
||||
|
||||
type WanImageParameters struct {
|
||||
N int `json:"n,omitempty"` // 生成图片数量,取值范围1-4,默认4
|
||||
Watermark *bool `json:"watermark,omitempty"` // 是否添加水印标识,默认false
|
||||
Seed int `json:"seed,omitempty"` // 随机数种子,取值范围[0, 2147483647]
|
||||
Strength float64 `json:"strength,omitempty"` // 修改幅度 0.0-1.0,默认0.5(部分模型支持)
|
||||
}
|
||||
|
||||
type AliRerankParameters struct {
|
||||
TopN *int `json:"top_n,omitempty"`
|
||||
ReturnDocuments *bool `json:"return_documents,omitempty"`
|
||||
|
||||
@@ -58,11 +58,7 @@ func oaiImage2Ali(request dto.ImageRequest) (*AliImageRequest, error) {
|
||||
return &imageRequest, nil
|
||||
}
|
||||
|
||||
func oaiFormEdit2AliImageEdit(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (*AliImageRequest, error) {
|
||||
var imageRequest AliImageRequest
|
||||
imageRequest.Model = request.Model
|
||||
imageRequest.ResponseFormat = request.ResponseFormat
|
||||
|
||||
func getImageBase64sFromForm(c *gin.Context, fieldName string) ([]string, error) {
|
||||
mf := c.Request.MultipartForm
|
||||
if mf == nil {
|
||||
if _, err := c.MultipartForm(); err != nil {
|
||||
@@ -127,7 +123,18 @@ func oaiFormEdit2AliImageEdit(c *gin.Context, info *relaycommon.RelayInfo, reque
|
||||
imageBase64s = append(imageBase64s, dataURL)
|
||||
image.Close()
|
||||
}
|
||||
return imageBase64s, nil
|
||||
}
|
||||
|
||||
func oaiFormEdit2AliImageEdit(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (*AliImageRequest, error) {
|
||||
var imageRequest AliImageRequest
|
||||
imageRequest.Model = request.Model
|
||||
imageRequest.ResponseFormat = request.ResponseFormat
|
||||
|
||||
imageBase64s, err := getImageBase64sFromForm(c, "image")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get image base64s from form failed: %w", err)
|
||||
}
|
||||
//dto.MediaContent{}
|
||||
mediaContents := make([]AliMediaContent, len(imageBase64s))
|
||||
for i, b64 := range imageBase64s {
|
||||
|
||||
39
relay/channel/ali/image_wan.go
Normal file
39
relay/channel/ali/image_wan.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package ali
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func oaiFormEdit2WanxImageEdit(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (*AliImageRequest, error) {
|
||||
var err error
|
||||
var imageRequest AliImageRequest
|
||||
imageRequest.Model = request.Model
|
||||
imageRequest.ResponseFormat = request.ResponseFormat
|
||||
wanInput := WanImageInput{
|
||||
Prompt: request.Prompt,
|
||||
}
|
||||
|
||||
if err := common.UnmarshalBodyReusable(c, &wanInput); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wanInput.Images, err = getImageBase64sFromForm(c, "image"); err != nil {
|
||||
return nil, fmt.Errorf("get image base64s from form failed: %w", err)
|
||||
}
|
||||
wanParams := WanImageParameters{
|
||||
N: int(request.N),
|
||||
}
|
||||
imageRequest.Input = wanInput
|
||||
imageRequest.Parameters = wanParams
|
||||
return &imageRequest, nil
|
||||
}
|
||||
|
||||
func isWanModel(modelName string) bool {
|
||||
return strings.Contains(modelName, "wan")
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
)
|
||||
|
||||
type AwsClaudeRequest struct {
|
||||
// AnthropicVersion should be "bedrock-2023-05-31"
|
||||
AnthropicVersion string `json:"anthropic_version"`
|
||||
AnthropicBeta json.RawMessage `json:"anthropic_beta,omitempty"`
|
||||
System any `json:"system,omitempty"`
|
||||
Messages []dto.ClaudeMessage `json:"messages"`
|
||||
MaxTokens uint `json:"max_tokens,omitempty"`
|
||||
@@ -22,29 +28,28 @@ type AwsClaudeRequest struct {
|
||||
Thinking *dto.Thinking `json:"thinking,omitempty"`
|
||||
}
|
||||
|
||||
func copyRequest(req *dto.ClaudeRequest) *AwsClaudeRequest {
|
||||
return &AwsClaudeRequest{
|
||||
AnthropicVersion: "bedrock-2023-05-31",
|
||||
System: req.System,
|
||||
Messages: req.Messages,
|
||||
MaxTokens: req.MaxTokens,
|
||||
Temperature: req.Temperature,
|
||||
TopP: req.TopP,
|
||||
TopK: req.TopK,
|
||||
StopSequences: req.StopSequences,
|
||||
Tools: req.Tools,
|
||||
ToolChoice: req.ToolChoice,
|
||||
Thinking: req.Thinking,
|
||||
}
|
||||
}
|
||||
|
||||
func formatRequest(requestBody io.Reader) (*AwsClaudeRequest, error) {
|
||||
func formatRequest(requestBody io.Reader, requestHeader http.Header) (*AwsClaudeRequest, error) {
|
||||
var awsClaudeRequest AwsClaudeRequest
|
||||
err := common.DecodeJson(requestBody, &awsClaudeRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
awsClaudeRequest.AnthropicVersion = "bedrock-2023-05-31"
|
||||
|
||||
// check header anthropic-beta
|
||||
anthropicBetaValues := requestHeader.Get("anthropic-beta")
|
||||
if len(anthropicBetaValues) > 0 {
|
||||
var tempArray []string
|
||||
tempArray = strings.Split(anthropicBetaValues, ",")
|
||||
if len(tempArray) > 0 {
|
||||
betaJson, err := json.Marshal(tempArray)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
awsClaudeRequest.AnthropicBeta = betaJson
|
||||
}
|
||||
}
|
||||
logger.LogJson(context.Background(), "json", awsClaudeRequest)
|
||||
return &awsClaudeRequest, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ func doAwsClientRequest(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor,
|
||||
}
|
||||
a.AwsClient = awsCli
|
||||
|
||||
println(info.UpstreamModelName)
|
||||
// 获取对应的AWS模型ID
|
||||
awsModelId := getAwsModelID(info.UpstreamModelName)
|
||||
|
||||
@@ -83,6 +82,10 @@ func doAwsClientRequest(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor,
|
||||
awsModelId = awsModelCrossRegion(awsModelId, awsRegionPrefix)
|
||||
}
|
||||
|
||||
// init empty request.header
|
||||
requestHeader := http.Header{}
|
||||
a.SetupRequestHeader(c, &requestHeader, info)
|
||||
|
||||
if isNovaModel(awsModelId) {
|
||||
var novaReq *NovaRequest
|
||||
err = common.DecodeJson(requestBody, &novaReq)
|
||||
@@ -104,7 +107,7 @@ func doAwsClientRequest(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor,
|
||||
awsReq.Body = reqBody
|
||||
return nil, nil
|
||||
} else {
|
||||
awsClaudeReq, err := formatRequest(requestBody)
|
||||
awsClaudeReq, err := formatRequest(requestBody, requestHeader)
|
||||
if err != nil {
|
||||
return nil, types.NewError(errors.Wrap(err, "format aws request fail"), types.ErrorCodeBadRequestBody)
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
|
||||
return nil, errors.New("request is nil")
|
||||
}
|
||||
|
||||
geminiRequest, err := CovertGemini2OpenAI(c, *request, info)
|
||||
geminiRequest, err := CovertOpenAI2Gemini(c, *request, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ var ModelList = []string{
|
||||
"gemini-1.5-pro-latest", "gemini-1.5-flash-latest",
|
||||
// preview version
|
||||
"gemini-2.0-flash-lite-preview",
|
||||
"gemini-3-pro-preview",
|
||||
// gemini exp
|
||||
"gemini-exp-1206",
|
||||
// flash exp
|
||||
|
||||
@@ -44,6 +44,8 @@ var geminiSupportedMimeTypes = map[string]bool{
|
||||
"video/flv": true,
|
||||
}
|
||||
|
||||
const thoughtSignatureBypassValue = "context_engineering_is_the_way_to_go"
|
||||
|
||||
// Gemini 允许的思考预算范围
|
||||
const (
|
||||
pro25MinBudget = 128
|
||||
@@ -181,7 +183,7 @@ func ThinkingAdaptor(geminiRequest *dto.GeminiChatRequest, info *relaycommon.Rel
|
||||
}
|
||||
|
||||
// Setting safety to the lowest possible values since Gemini is already powerless enough
|
||||
func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, info *relaycommon.RelayInfo) (*dto.GeminiChatRequest, error) {
|
||||
func CovertOpenAI2Gemini(c *gin.Context, textRequest dto.GeneralOpenAIRequest, info *relaycommon.RelayInfo) (*dto.GeminiChatRequest, error) {
|
||||
|
||||
geminiRequest := dto.GeminiChatRequest{
|
||||
Contents: make([]dto.GeminiChatContent, 0, len(textRequest.Messages)),
|
||||
@@ -193,6 +195,10 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
|
||||
},
|
||||
}
|
||||
|
||||
attachThoughtSignature := (info.ChannelType == constant.ChannelTypeGemini ||
|
||||
info.ChannelType == constant.ChannelTypeVertexAi) &&
|
||||
model_setting.GetGeminiSettings().FunctionCallThoughtSignatureEnabled
|
||||
|
||||
if model_setting.IsGeminiModelSupportImagine(info.UpstreamModelName) {
|
||||
geminiRequest.GenerationConfig.ResponseModalities = []string{
|
||||
"TEXT",
|
||||
@@ -371,6 +377,8 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
|
||||
content := dto.GeminiChatContent{
|
||||
Role: message.Role,
|
||||
}
|
||||
shouldAttachThoughtSignature := attachThoughtSignature && (message.Role == "assistant" || message.Role == "model")
|
||||
signatureAttached := false
|
||||
// isToolCall := false
|
||||
if message.ToolCalls != nil {
|
||||
// message.Role = "model"
|
||||
@@ -388,6 +396,10 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
|
||||
Arguments: args,
|
||||
},
|
||||
}
|
||||
if shouldAttachThoughtSignature && !signatureAttached && hasFunctionCallContent(toolCall.FunctionCall) && len(toolCall.ThoughtSignature) == 0 {
|
||||
toolCall.ThoughtSignature = json.RawMessage(strconv.Quote(thoughtSignatureBypassValue))
|
||||
signatureAttached = true
|
||||
}
|
||||
parts = append(parts, toolCall)
|
||||
tool_call_ids[call.ID] = call.Function.Name
|
||||
}
|
||||
@@ -496,6 +508,28 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
|
||||
return &geminiRequest, nil
|
||||
}
|
||||
|
||||
func hasFunctionCallContent(call *dto.FunctionCall) bool {
|
||||
if call == nil {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(call.FunctionName) != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
switch v := call.Arguments.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case string:
|
||||
return strings.TrimSpace(v) != ""
|
||||
case map[string]interface{}:
|
||||
return len(v) > 0
|
||||
case []interface{}:
|
||||
return len(v) > 0
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get a list of supported MIME types for error messages
|
||||
func getSupportedMimeTypesList() []string {
|
||||
keys := make([]string, 0, len(geminiSupportedMimeTypes))
|
||||
|
||||
@@ -42,7 +42,7 @@ type Adaptor struct {
|
||||
// support OAI models: o1-mini/o3-mini/o4-mini/o1/o3 etc...
|
||||
// minimal effort only available in gpt-5
|
||||
func parseReasoningEffortFromModelSuffix(model string) (string, string) {
|
||||
effortSuffixes := []string{"-high", "-minimal", "-low", "-medium"}
|
||||
effortSuffixes := []string{"-high", "-minimal", "-low", "-medium", "-none"}
|
||||
for _, suffix := range effortSuffixes {
|
||||
if strings.HasSuffix(model, suffix) {
|
||||
effort := strings.TrimPrefix(suffix, "-")
|
||||
|
||||
297
relay/channel/task/hailuo/adaptor.go
Normal file
297
relay/channel/task/hailuo/adaptor.go
Normal file
@@ -0,0 +1,297 @@
|
||||
package hailuo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/relay/channel"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
)
|
||||
|
||||
// https://platform.minimaxi.com/docs/api-reference/video-generation-intro
|
||||
type TaskAdaptor struct {
|
||||
ChannelType int
|
||||
apiKey string
|
||||
baseURL string
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
|
||||
a.ChannelType = info.ChannelType
|
||||
a.baseURL = info.ChannelBaseUrl
|
||||
a.apiKey = info.ApiKey
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) {
|
||||
return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate)
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
return fmt.Sprintf("%s%s", a.baseURL, TextToVideoEndpoint), nil
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+a.apiKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) {
|
||||
v, exists := c.Get("task_request")
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("request not found in context")
|
||||
}
|
||||
req, ok := v.(relaycommon.TaskSubmitReq)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid request type in context")
|
||||
}
|
||||
|
||||
body, err := a.convertToRequestPayload(&req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "convert request payload failed")
|
||||
}
|
||||
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytes.NewReader(data), nil
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) {
|
||||
return channel.DoTaskApiRequest(a, c, info, requestBody)
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) {
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
|
||||
var hResp VideoResponse
|
||||
if err := json.Unmarshal(responseBody, &hResp); err != nil {
|
||||
taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if hResp.BaseResp.StatusCode != StatusSuccess {
|
||||
taskErr = service.TaskErrorWrapper(
|
||||
fmt.Errorf("hailuo api error: %s", hResp.BaseResp.StatusMsg),
|
||||
strconv.Itoa(hResp.BaseResp.StatusCode),
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
ov := dto.NewOpenAIVideo()
|
||||
ov.ID = hResp.TaskID
|
||||
ov.TaskID = hResp.TaskID
|
||||
ov.CreatedAt = time.Now().Unix()
|
||||
ov.Model = info.OriginModelName
|
||||
|
||||
c.JSON(http.StatusOK, ov)
|
||||
return hResp.TaskID, responseBody, nil
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) FetchTask(baseUrl, key string, body map[string]any) (*http.Response, error) {
|
||||
taskID, ok := body["task_id"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid task_id")
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s%s?task_id=%s", baseUrl, QueryTaskEndpoint, taskID)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, uri, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+key)
|
||||
|
||||
return service.GetHttpClient().Do(req)
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) GetModelList() []string {
|
||||
return ModelList
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) GetChannelName() string {
|
||||
return ChannelName
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*VideoRequest, error) {
|
||||
modelConfig := GetModelConfig(req.Model)
|
||||
duration := DefaultDuration
|
||||
if req.Duration > 0 {
|
||||
duration = req.Duration
|
||||
}
|
||||
resolution := modelConfig.DefaultResolution
|
||||
if req.Size != "" {
|
||||
resolution = a.parseResolutionFromSize(req.Size, modelConfig)
|
||||
}
|
||||
|
||||
videoRequest := &VideoRequest{
|
||||
Model: req.Model,
|
||||
Prompt: req.Prompt,
|
||||
Duration: &duration,
|
||||
Resolution: resolution,
|
||||
}
|
||||
if err := req.UnmarshalMetadata(&videoRequest); err != nil {
|
||||
return nil, errors.Wrap(err, "unmarshal metadata to video request failed")
|
||||
}
|
||||
|
||||
return videoRequest, nil
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) parseResolutionFromSize(size string, modelConfig ModelConfig) string {
|
||||
switch {
|
||||
case strings.Contains(size, "1080"):
|
||||
return Resolution1080P
|
||||
case strings.Contains(size, "768"):
|
||||
return Resolution768P
|
||||
case strings.Contains(size, "720"):
|
||||
return Resolution720P
|
||||
case strings.Contains(size, "512"):
|
||||
return Resolution512P
|
||||
default:
|
||||
return modelConfig.DefaultResolution
|
||||
}
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) {
|
||||
resTask := QueryTaskResponse{}
|
||||
if err := json.Unmarshal(respBody, &resTask); err != nil {
|
||||
return nil, errors.Wrap(err, "unmarshal task result failed")
|
||||
}
|
||||
|
||||
taskResult := relaycommon.TaskInfo{}
|
||||
|
||||
if resTask.BaseResp.StatusCode == StatusSuccess {
|
||||
taskResult.Code = 0
|
||||
} else {
|
||||
taskResult.Code = resTask.BaseResp.StatusCode
|
||||
taskResult.Reason = resTask.BaseResp.StatusMsg
|
||||
taskResult.Status = model.TaskStatusFailure
|
||||
taskResult.Progress = "100%"
|
||||
}
|
||||
|
||||
switch resTask.Status {
|
||||
case TaskStatusPreparing, TaskStatusQueueing, TaskStatusProcessing:
|
||||
taskResult.Status = model.TaskStatusInProgress
|
||||
taskResult.Progress = "30%"
|
||||
if resTask.Status == TaskStatusProcessing {
|
||||
taskResult.Progress = "50%"
|
||||
}
|
||||
case TaskStatusSuccess:
|
||||
taskResult.Status = model.TaskStatusSuccess
|
||||
taskResult.Progress = "100%"
|
||||
taskResult.Url = a.buildVideoURL(resTask.TaskID, resTask.FileID)
|
||||
case TaskStatusFailed:
|
||||
taskResult.Status = model.TaskStatusFailure
|
||||
taskResult.Progress = "100%"
|
||||
if taskResult.Reason == "" {
|
||||
taskResult.Reason = "task failed"
|
||||
}
|
||||
default:
|
||||
taskResult.Status = model.TaskStatusInProgress
|
||||
taskResult.Progress = "30%"
|
||||
}
|
||||
|
||||
return &taskResult, nil
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error) {
|
||||
var hailuoResp QueryTaskResponse
|
||||
if err := json.Unmarshal(originTask.Data, &hailuoResp); err != nil {
|
||||
return nil, errors.Wrap(err, "unmarshal hailuo task data failed")
|
||||
}
|
||||
|
||||
openAIVideo := originTask.ToOpenAIVideo()
|
||||
if hailuoResp.BaseResp.StatusCode != StatusSuccess {
|
||||
openAIVideo.Error = &dto.OpenAIVideoError{
|
||||
Message: hailuoResp.BaseResp.StatusMsg,
|
||||
Code: strconv.Itoa(hailuoResp.BaseResp.StatusCode),
|
||||
}
|
||||
}
|
||||
|
||||
jsonData, err := common.Marshal(openAIVideo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "marshal openai video failed")
|
||||
}
|
||||
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) buildVideoURL(_, fileID string) string {
|
||||
if a.apiKey == "" || a.baseURL == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/files/retrieve?file_id=%s", a.baseURL, fileID)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+a.apiKey)
|
||||
|
||||
resp, err := service.GetHttpClient().Do(req)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var retrieveResp RetrieveFileResponse
|
||||
if err := json.Unmarshal(responseBody, &retrieveResp); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if retrieveResp.BaseResp.StatusCode != StatusSuccess {
|
||||
return ""
|
||||
}
|
||||
|
||||
return retrieveResp.File.DownloadURL
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsInt(slice []int, item int) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
52
relay/channel/task/hailuo/constants.go
Normal file
52
relay/channel/task/hailuo/constants.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package hailuo
|
||||
|
||||
const (
|
||||
ChannelName = "hailuo-video"
|
||||
)
|
||||
|
||||
var ModelList = []string{
|
||||
"MiniMax-Hailuo-2.3",
|
||||
"MiniMax-Hailuo-2.3-Fast",
|
||||
"MiniMax-Hailuo-02",
|
||||
"T2V-01-Director",
|
||||
"T2V-01",
|
||||
"I2V-01-Director",
|
||||
"I2V-01-live",
|
||||
"I2V-01",
|
||||
"S2V-01",
|
||||
}
|
||||
|
||||
const (
|
||||
TextToVideoEndpoint = "/v1/video_generation"
|
||||
QueryTaskEndpoint = "/v1/query/video_generation"
|
||||
)
|
||||
|
||||
const (
|
||||
StatusSuccess = 0
|
||||
StatusRateLimit = 1002
|
||||
StatusAuthFailed = 1004
|
||||
StatusNoBalance = 1008
|
||||
StatusSensitive = 1026
|
||||
StatusParamError = 2013
|
||||
StatusInvalidKey = 2049
|
||||
)
|
||||
|
||||
const (
|
||||
TaskStatusPreparing = "Preparing"
|
||||
TaskStatusQueueing = "Queueing"
|
||||
TaskStatusProcessing = "Processing"
|
||||
TaskStatusSuccess = "Success"
|
||||
TaskStatusFailed = "Fail"
|
||||
)
|
||||
|
||||
const (
|
||||
Resolution512P = "512P"
|
||||
Resolution720P = "720P"
|
||||
Resolution768P = "768P"
|
||||
Resolution1080P = "1080P"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultDuration = 6
|
||||
DefaultResolution = Resolution720P
|
||||
)
|
||||
170
relay/channel/task/hailuo/models.go
Normal file
170
relay/channel/task/hailuo/models.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package hailuo
|
||||
|
||||
type SubjectReference struct {
|
||||
Type string `json:"type"` // Subject type, currently only supports "character"
|
||||
Image []string `json:"image"` // Array of subject reference images (currently only supports single image)
|
||||
}
|
||||
|
||||
type VideoRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
PromptOptimizer *bool `json:"prompt_optimizer,omitempty"`
|
||||
FastPretreatment *bool `json:"fast_pretreatment,omitempty"`
|
||||
Duration *int `json:"duration,omitempty"`
|
||||
Resolution string `json:"resolution,omitempty"`
|
||||
CallbackURL string `json:"callback_url,omitempty"`
|
||||
AigcWatermark *bool `json:"aigc_watermark,omitempty"`
|
||||
FirstFrameImage string `json:"first_frame_image,omitempty"` // For image-to-video and start-end-to-video
|
||||
LastFrameImage string `json:"last_frame_image,omitempty"` // For start-end-to-video
|
||||
SubjectReference []SubjectReference `json:"subject_reference,omitempty"` // For subject-reference-to-video
|
||||
}
|
||||
|
||||
type VideoResponse struct {
|
||||
TaskID string `json:"task_id"`
|
||||
BaseResp BaseResp `json:"base_resp"`
|
||||
}
|
||||
|
||||
type BaseResp struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
StatusMsg string `json:"status_msg"`
|
||||
}
|
||||
|
||||
type QueryTaskRequest struct {
|
||||
TaskID string `json:"task_id"`
|
||||
}
|
||||
|
||||
type QueryTaskResponse struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Status string `json:"status"`
|
||||
FileID string `json:"file_id,omitempty"`
|
||||
VideoWidth int `json:"video_width,omitempty"`
|
||||
VideoHeight int `json:"video_height,omitempty"`
|
||||
BaseResp BaseResp `json:"base_resp"`
|
||||
}
|
||||
|
||||
type ErrorInfo struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
StatusMsg string `json:"status_msg"`
|
||||
}
|
||||
|
||||
type TaskStatusInfo struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Status string `json:"status"`
|
||||
FileID string `json:"file_id,omitempty"`
|
||||
VideoURL string `json:"video_url,omitempty"`
|
||||
ErrorCode int `json:"error_code,omitempty"`
|
||||
ErrorMsg string `json:"error_msg,omitempty"`
|
||||
}
|
||||
|
||||
type ModelConfig struct {
|
||||
Name string
|
||||
DefaultResolution string
|
||||
SupportedDurations []int
|
||||
SupportedResolutions []string
|
||||
HasPromptOptimizer bool
|
||||
HasFastPretreatment bool
|
||||
}
|
||||
|
||||
type RetrieveFileResponse struct {
|
||||
File FileObject `json:"file"`
|
||||
BaseResp BaseResp `json:"base_resp"`
|
||||
}
|
||||
|
||||
type FileObject struct {
|
||||
FileID int64 `json:"file_id"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Filename string `json:"filename"`
|
||||
Purpose string `json:"purpose"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
}
|
||||
|
||||
func GetModelConfig(model string) ModelConfig {
|
||||
configs := map[string]ModelConfig{
|
||||
"MiniMax-Hailuo-2.3": {
|
||||
Name: "MiniMax-Hailuo-2.3",
|
||||
DefaultResolution: Resolution768P,
|
||||
SupportedDurations: []int{6, 10},
|
||||
SupportedResolutions: []string{Resolution768P, Resolution1080P},
|
||||
HasPromptOptimizer: true,
|
||||
HasFastPretreatment: true,
|
||||
},
|
||||
"MiniMax-Hailuo-2.3-Fast": {
|
||||
Name: "MiniMax-Hailuo-2.3-Fast",
|
||||
DefaultResolution: Resolution768P,
|
||||
SupportedDurations: []int{6, 10},
|
||||
SupportedResolutions: []string{Resolution768P, Resolution1080P},
|
||||
HasPromptOptimizer: true,
|
||||
HasFastPretreatment: true,
|
||||
},
|
||||
"MiniMax-Hailuo-02": {
|
||||
Name: "MiniMax-Hailuo-02",
|
||||
DefaultResolution: Resolution768P,
|
||||
SupportedDurations: []int{6, 10},
|
||||
SupportedResolutions: []string{Resolution512P, Resolution768P, Resolution1080P},
|
||||
HasPromptOptimizer: true,
|
||||
HasFastPretreatment: true,
|
||||
},
|
||||
"T2V-01-Director": {
|
||||
Name: "T2V-01-Director",
|
||||
DefaultResolution: Resolution768P,
|
||||
SupportedDurations: []int{6},
|
||||
SupportedResolutions: []string{Resolution768P, Resolution1080P},
|
||||
HasPromptOptimizer: true,
|
||||
HasFastPretreatment: false,
|
||||
},
|
||||
"T2V-01": {
|
||||
Name: "T2V-01",
|
||||
DefaultResolution: Resolution720P,
|
||||
SupportedDurations: []int{6},
|
||||
SupportedResolutions: []string{Resolution720P},
|
||||
HasPromptOptimizer: true,
|
||||
HasFastPretreatment: false,
|
||||
},
|
||||
"I2V-01-Director": {
|
||||
Name: "I2V-01-Director",
|
||||
DefaultResolution: Resolution720P,
|
||||
SupportedDurations: []int{6},
|
||||
SupportedResolutions: []string{Resolution720P, Resolution1080P},
|
||||
HasPromptOptimizer: true,
|
||||
HasFastPretreatment: false,
|
||||
},
|
||||
"I2V-01-live": {
|
||||
Name: "I2V-01-live",
|
||||
DefaultResolution: Resolution720P,
|
||||
SupportedDurations: []int{6},
|
||||
SupportedResolutions: []string{Resolution720P, Resolution1080P},
|
||||
HasPromptOptimizer: true,
|
||||
HasFastPretreatment: false,
|
||||
},
|
||||
"I2V-01": {
|
||||
Name: "I2V-01",
|
||||
DefaultResolution: Resolution720P,
|
||||
SupportedDurations: []int{6},
|
||||
SupportedResolutions: []string{Resolution720P, Resolution1080P},
|
||||
HasPromptOptimizer: true,
|
||||
HasFastPretreatment: false,
|
||||
},
|
||||
"S2V-01": {
|
||||
Name: "S2V-01",
|
||||
DefaultResolution: Resolution720P,
|
||||
SupportedDurations: []int{6},
|
||||
SupportedResolutions: []string{Resolution720P},
|
||||
HasPromptOptimizer: true,
|
||||
HasFastPretreatment: false,
|
||||
},
|
||||
}
|
||||
|
||||
if config, exists := configs[model]; exists {
|
||||
return config
|
||||
}
|
||||
|
||||
return ModelConfig{
|
||||
Name: model,
|
||||
DefaultResolution: DefaultResolution,
|
||||
SupportedDurations: []int{6},
|
||||
SupportedResolutions: []string{DefaultResolution},
|
||||
HasPromptOptimizer: true,
|
||||
HasFastPretreatment: false,
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,9 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
|
||||
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
if strings.HasPrefix(info.UpstreamModelName, "claude") {
|
||||
a.RequestMode = RequestModeClaude
|
||||
} else if strings.Contains(info.UpstreamModelName, "llama") {
|
||||
} else if strings.Contains(info.UpstreamModelName, "llama") ||
|
||||
// open source models
|
||||
strings.Contains(info.UpstreamModelName, "-maas") {
|
||||
a.RequestMode = RequestModeLlama
|
||||
} else {
|
||||
a.RequestMode = RequestModeGemini
|
||||
@@ -220,6 +222,9 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel
|
||||
if a.AccountCredentials.ProjectID != "" {
|
||||
req.Set("x-goog-user-project", a.AccountCredentials.ProjectID)
|
||||
}
|
||||
if strings.Contains(info.UpstreamModelName, "claude") {
|
||||
claude.CommonClaudeHeadersOperation(c, req, info)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -291,7 +296,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
|
||||
info.UpstreamModelName = claudeReq.Model
|
||||
return vertexClaudeReq, nil
|
||||
} else if a.RequestMode == RequestModeGemini {
|
||||
geminiRequest, err := gemini.CovertGemini2OpenAI(c, *request, info)
|
||||
geminiRequest, err := gemini.CovertOpenAI2Gemini(c, *request, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -23,8 +23,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
contextKeyTTSRequest = "volcengine_tts_request"
|
||||
contextKeyResponseFormat = "response_format"
|
||||
contextKeyTTSRequest = "volcengine_tts_request"
|
||||
contextKeyResponseFormat = "response_format"
|
||||
DoubaoCodingPlan = "doubao-coding-plan"
|
||||
DoubaoCodingPlanClaudeBaseURL = "https://ark.cn-beijing.volces.com/api/coding"
|
||||
DoubaoCodingPlanOpenAIBaseURL = "https://ark.cn-beijing.volces.com/api/coding/v3"
|
||||
)
|
||||
|
||||
type Adaptor struct {
|
||||
@@ -238,6 +241,9 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
|
||||
switch info.RelayFormat {
|
||||
case types.RelayFormatClaude:
|
||||
if baseUrl == DoubaoCodingPlan {
|
||||
return fmt.Sprintf("%s/v1/messages", DoubaoCodingPlanClaudeBaseURL), nil
|
||||
}
|
||||
if strings.HasPrefix(info.UpstreamModelName, "bot") {
|
||||
return fmt.Sprintf("%s/api/v3/bots/chat/completions", baseUrl), nil
|
||||
}
|
||||
@@ -245,6 +251,9 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
default:
|
||||
switch info.RelayMode {
|
||||
case constant.RelayModeChatCompletions:
|
||||
if baseUrl == DoubaoCodingPlan {
|
||||
return fmt.Sprintf("%s/chat/completions", DoubaoCodingPlanOpenAIBaseURL), nil
|
||||
}
|
||||
if strings.HasPrefix(info.UpstreamModelName, "bot") {
|
||||
return fmt.Sprintf("%s/api/v3/bots/chat/completions", baseUrl), nil
|
||||
}
|
||||
|
||||
@@ -498,11 +498,11 @@ type TaskSubmitReq struct {
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (t TaskSubmitReq) GetPrompt() string {
|
||||
func (t *TaskSubmitReq) GetPrompt() string {
|
||||
return t.Prompt
|
||||
}
|
||||
|
||||
func (t TaskSubmitReq) HasImage() bool {
|
||||
func (t *TaskSubmitReq) HasImage() bool {
|
||||
return len(t.Images) > 0
|
||||
}
|
||||
|
||||
@@ -537,6 +537,20 @@ func (t *TaskSubmitReq) UnmarshalJSON(data []byte) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
func (t *TaskSubmitReq) UnmarshalMetadata(v any) error {
|
||||
metadata := t.Metadata
|
||||
if metadata != nil {
|
||||
metadataBytes, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal metadata failed: %w", err)
|
||||
}
|
||||
err = json.Unmarshal(metadataBytes, v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal metadata to target failed: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TaskInfo struct {
|
||||
Code int `json:"code"`
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
taskali "github.com/QuantumNous/new-api/relay/channel/task/ali"
|
||||
taskdoubao "github.com/QuantumNous/new-api/relay/channel/task/doubao"
|
||||
taskGemini "github.com/QuantumNous/new-api/relay/channel/task/gemini"
|
||||
"github.com/QuantumNous/new-api/relay/channel/task/hailuo"
|
||||
taskjimeng "github.com/QuantumNous/new-api/relay/channel/task/jimeng"
|
||||
"github.com/QuantumNous/new-api/relay/channel/task/kling"
|
||||
tasksora "github.com/QuantumNous/new-api/relay/channel/task/sora"
|
||||
@@ -153,6 +154,8 @@ func GetTaskAdaptor(platform constant.TaskPlatform) channel.TaskAdaptor {
|
||||
return &tasksora.TaskAdaptor{}
|
||||
case constant.ChannelTypeGemini:
|
||||
return &taskGemini.TaskAdaptor{}
|
||||
case constant.ChannelTypeMiniMax:
|
||||
return &hailuo.TaskAdaptor{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -50,9 +50,18 @@ func GetClaudeSettings() *ClaudeSettings {
|
||||
func (c *ClaudeSettings) WriteHeaders(originModel string, httpHeader *http.Header) {
|
||||
if headers, ok := c.HeadersSettings[originModel]; ok {
|
||||
for headerKey, headerValues := range headers {
|
||||
httpHeader.Del(headerKey)
|
||||
// get existing values for this header key
|
||||
existingValues := httpHeader.Values(headerKey)
|
||||
existingValuesMap := make(map[string]bool)
|
||||
for _, v := range existingValues {
|
||||
existingValuesMap[v] = true
|
||||
}
|
||||
|
||||
// add only values that don't already exist
|
||||
for _, headerValue := range headerValues {
|
||||
httpHeader.Add(headerKey, headerValue)
|
||||
if !existingValuesMap[headerValue] {
|
||||
httpHeader.Add(headerKey, headerValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ type GeminiSettings struct {
|
||||
SupportedImagineModels []string `json:"supported_imagine_models"`
|
||||
ThinkingAdapterEnabled bool `json:"thinking_adapter_enabled"`
|
||||
ThinkingAdapterBudgetTokensPercentage float64 `json:"thinking_adapter_budget_tokens_percentage"`
|
||||
FunctionCallThoughtSignatureEnabled bool `json:"function_call_thought_signature_enabled"`
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
@@ -29,6 +30,7 @@ var defaultGeminiSettings = GeminiSettings{
|
||||
},
|
||||
ThinkingAdapterEnabled: false,
|
||||
ThinkingAdapterBudgetTokensPercentage: 0.6,
|
||||
FunctionCallThoughtSignatureEnabled: true,
|
||||
}
|
||||
|
||||
// 全局实例
|
||||
|
||||
@@ -823,3 +823,16 @@ func FormatMatchingModelName(name string) string {
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// result: 倍率or价格, usePrice, exist
|
||||
func GetModelRatioOrPrice(model string) (float64, bool, bool) { // price or ratio
|
||||
price, usePrice := GetModelPrice(model, false)
|
||||
if usePrice {
|
||||
return price, true, true
|
||||
}
|
||||
modelRatio, success, _ := GetModelRatio(model)
|
||||
if success {
|
||||
return modelRatio, false, true
|
||||
}
|
||||
return 37.5, false, false
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "react-template",
|
||||
|
||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { UserContext } from '../../context/User';
|
||||
import {
|
||||
@@ -87,6 +87,9 @@ const LoginForm = () => {
|
||||
const [agreedToTerms, setAgreedToTerms] = useState(false);
|
||||
const [hasUserAgreement, setHasUserAgreement] = useState(false);
|
||||
const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false);
|
||||
const [githubButtonText, setGithubButtonText] = useState('使用 GitHub 继续');
|
||||
const [githubButtonDisabled, setGithubButtonDisabled] = useState(false);
|
||||
const githubTimeoutRef = useRef(null);
|
||||
|
||||
const logo = getLogo();
|
||||
const systemName = getSystemName();
|
||||
@@ -116,6 +119,12 @@ const LoginForm = () => {
|
||||
isPasskeySupported()
|
||||
.then(setPasskeySupported)
|
||||
.catch(() => setPasskeySupported(false));
|
||||
|
||||
return () => {
|
||||
if (githubTimeoutRef.current) {
|
||||
clearTimeout(githubTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -267,7 +276,20 @@ const LoginForm = () => {
|
||||
showInfo(t('请先阅读并同意用户协议和隐私政策'));
|
||||
return;
|
||||
}
|
||||
if (githubButtonDisabled) {
|
||||
return;
|
||||
}
|
||||
setGithubLoading(true);
|
||||
setGithubButtonDisabled(true);
|
||||
setGithubButtonText(t('正在跳转 GitHub...'));
|
||||
if (githubTimeoutRef.current) {
|
||||
clearTimeout(githubTimeoutRef.current);
|
||||
}
|
||||
githubTimeoutRef.current = setTimeout(() => {
|
||||
setGithubLoading(false);
|
||||
setGithubButtonText(t('请求超时,请刷新页面后重新发起 GitHub 登录'));
|
||||
setGithubButtonDisabled(true);
|
||||
}, 20000);
|
||||
try {
|
||||
onGitHubOAuthClicked(status.github_client_id);
|
||||
} finally {
|
||||
@@ -444,8 +466,9 @@ const LoginForm = () => {
|
||||
icon={<IconGithubLogo size='large' />}
|
||||
onClick={handleGitHubClick}
|
||||
loading={githubLoading}
|
||||
disabled={githubButtonDisabled}
|
||||
>
|
||||
<span className='ml-3'>{t('使用 GitHub 继续')}</span>
|
||||
<span className='ml-3'>{githubButtonText}</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
API,
|
||||
@@ -85,6 +85,9 @@ const RegisterForm = () => {
|
||||
const [agreedToTerms, setAgreedToTerms] = useState(false);
|
||||
const [hasUserAgreement, setHasUserAgreement] = useState(false);
|
||||
const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false);
|
||||
const [githubButtonText, setGithubButtonText] = useState('使用 GitHub 继续');
|
||||
const [githubButtonDisabled, setGithubButtonDisabled] = useState(false);
|
||||
const githubTimeoutRef = useRef(null);
|
||||
|
||||
const logo = getLogo();
|
||||
const systemName = getSystemName();
|
||||
@@ -128,6 +131,14 @@ const RegisterForm = () => {
|
||||
return () => clearInterval(countdownInterval); // Clean up on unmount
|
||||
}, [disableButton, countdown]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (githubTimeoutRef.current) {
|
||||
clearTimeout(githubTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onWeChatLoginClicked = () => {
|
||||
setWechatLoading(true);
|
||||
setShowWeChatLoginModal(true);
|
||||
@@ -232,7 +243,20 @@ const RegisterForm = () => {
|
||||
};
|
||||
|
||||
const handleGitHubClick = () => {
|
||||
if (githubButtonDisabled) {
|
||||
return;
|
||||
}
|
||||
setGithubLoading(true);
|
||||
setGithubButtonDisabled(true);
|
||||
setGithubButtonText(t('正在跳转 GitHub...'));
|
||||
if (githubTimeoutRef.current) {
|
||||
clearTimeout(githubTimeoutRef.current);
|
||||
}
|
||||
githubTimeoutRef.current = setTimeout(() => {
|
||||
setGithubLoading(false);
|
||||
setGithubButtonText(t('请求超时,请刷新页面后重新发起 GitHub 登录'));
|
||||
setGithubButtonDisabled(true);
|
||||
}, 20000);
|
||||
try {
|
||||
onGitHubOAuthClicked(status.github_client_id);
|
||||
} finally {
|
||||
@@ -347,8 +371,9 @@ const RegisterForm = () => {
|
||||
icon={<IconGithubLogo size='large' />}
|
||||
onClick={handleGitHubClick}
|
||||
loading={githubLoading}
|
||||
disabled={githubButtonDisabled}
|
||||
>
|
||||
<span className='ml-3'>{t('使用 GitHub 继续')}</span>
|
||||
<span className='ml-3'>{githubButtonText}</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
@@ -189,6 +189,7 @@ const EditChannelModal = (props) => {
|
||||
const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式
|
||||
const [keyMode, setKeyMode] = useState('append'); // 密钥模式:replace(覆盖)或 append(追加)
|
||||
const [isEnterpriseAccount, setIsEnterpriseAccount] = useState(false); // 是否为企业账户
|
||||
const [doubaoApiEditUnlocked, setDoubaoApiEditUnlocked] = useState(false); // 豆包渠道自定义 API 地址隐藏入口
|
||||
|
||||
// 密钥显示状态
|
||||
const [keyDisplayState, setKeyDisplayState] = useState({
|
||||
@@ -218,6 +219,7 @@ const EditChannelModal = (props) => {
|
||||
'channelExtraSettings',
|
||||
];
|
||||
const formContainerRef = useRef(null);
|
||||
const doubaoApiClickCountRef = useRef(0);
|
||||
|
||||
// 2FA状态更新辅助函数
|
||||
const updateTwoFAState = (updates) => {
|
||||
@@ -306,6 +308,20 @@ const EditChannelModal = (props) => {
|
||||
scrollToSection(availableSections[newIndex]);
|
||||
};
|
||||
|
||||
const handleApiConfigSecretClick = () => {
|
||||
if (inputs.type !== 45) return;
|
||||
const next = doubaoApiClickCountRef.current + 1;
|
||||
doubaoApiClickCountRef.current = next;
|
||||
if (next >= 10) {
|
||||
setDoubaoApiEditUnlocked((unlocked) => {
|
||||
if (!unlocked) {
|
||||
showInfo(t('已解锁豆包自定义 API 地址编辑'));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 渠道额外设置状态
|
||||
const [channelSettings, setChannelSettings] = useState({
|
||||
force_format: false,
|
||||
@@ -724,6 +740,13 @@ const EditChannelModal = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (inputs.type !== 45) {
|
||||
doubaoApiClickCountRef.current = 0;
|
||||
setDoubaoApiEditUnlocked(false);
|
||||
}
|
||||
}, [inputs.type]);
|
||||
|
||||
useEffect(() => {
|
||||
const modelMap = new Map();
|
||||
|
||||
@@ -823,6 +846,9 @@ const EditChannelModal = (props) => {
|
||||
setKeyMode('append');
|
||||
// 重置企业账户状态
|
||||
setIsEnterpriseAccount(false);
|
||||
// 重置豆包隐藏入口状态
|
||||
setDoubaoApiEditUnlocked(false);
|
||||
doubaoApiClickCountRef.current = 0;
|
||||
// 清空表单中的key_mode字段
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('key_mode', undefined);
|
||||
@@ -1959,7 +1985,10 @@ const EditChannelModal = (props) => {
|
||||
<div ref={(el) => (formSectionRefs.current.apiConfig = el)}>
|
||||
<Card className='!rounded-2xl shadow-sm border-0 mb-6'>
|
||||
{/* Header: API Config */}
|
||||
<div className='flex items-center mb-2'>
|
||||
<div
|
||||
className='flex items-center mb-2'
|
||||
onClick={handleApiConfigSecretClick}
|
||||
>
|
||||
<Avatar
|
||||
size='small'
|
||||
color='green'
|
||||
@@ -2094,7 +2123,7 @@ const EditChannelModal = (props) => {
|
||||
inputs.type !== 8 &&
|
||||
inputs.type !== 22 &&
|
||||
inputs.type !== 36 &&
|
||||
inputs.type !== 45 && (
|
||||
(inputs.type !== 45 || doubaoApiEditUnlocked) && (
|
||||
<div>
|
||||
<Form.Input
|
||||
field='base_url'
|
||||
@@ -2147,7 +2176,7 @@ const EditChannelModal = (props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inputs.type === 45 && (
|
||||
{inputs.type === 45 && !doubaoApiEditUnlocked && (
|
||||
<div>
|
||||
<Form.Select
|
||||
field='base_url'
|
||||
@@ -2167,6 +2196,10 @@ const EditChannelModal = (props) => {
|
||||
label:
|
||||
'https://ark.ap-southeast.bytepluses.com',
|
||||
},
|
||||
{
|
||||
value: 'doubao-coding-plan',
|
||||
label: 'Doubao Coding Plan',
|
||||
},
|
||||
]}
|
||||
defaultValue='https://ark.cn-beijing.volces.com'
|
||||
/>
|
||||
|
||||
@@ -145,8 +145,9 @@ export const getModelCategories = (() => {
|
||||
model.model_name.toLowerCase().includes('gpt') ||
|
||||
model.model_name.toLowerCase().includes('dall-e') ||
|
||||
model.model_name.toLowerCase().includes('whisper') ||
|
||||
model.model_name.toLowerCase().includes('tts') ||
|
||||
model.model_name.toLowerCase().includes('text-') ||
|
||||
model.model_name.toLowerCase().includes('tts-1') ||
|
||||
model.model_name.toLowerCase().includes('text-embedding-3') ||
|
||||
model.model_name.toLowerCase().includes('text-moderation') ||
|
||||
model.model_name.toLowerCase().includes('babbage') ||
|
||||
model.model_name.toLowerCase().includes('davinci') ||
|
||||
model.model_name.toLowerCase().includes('curie') ||
|
||||
@@ -163,19 +164,31 @@ export const getModelCategories = (() => {
|
||||
gemini: {
|
||||
label: 'Gemini',
|
||||
icon: <Gemini.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('gemini'),
|
||||
filter: (model) =>
|
||||
model.model_name.toLowerCase().includes('gemini') ||
|
||||
model.model_name.toLowerCase().includes('gemma') ||
|
||||
model.model_name.toLowerCase().includes('learnlm') ||
|
||||
model.model_name.toLowerCase().startsWith('embedding-') ||
|
||||
model.model_name.toLowerCase().includes('text-embedding-004') ||
|
||||
model.model_name.toLowerCase().includes('imagen-4') ||
|
||||
model.model_name.toLowerCase().includes('veo-') ||
|
||||
model.model_name.toLowerCase().includes('aqa') ,
|
||||
},
|
||||
moonshot: {
|
||||
label: 'Moonshot',
|
||||
icon: <Moonshot />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('moonshot'),
|
||||
filter: (model) =>
|
||||
model.model_name.toLowerCase().includes('moonshot') ||
|
||||
model.model_name.toLowerCase().includes('kimi'),
|
||||
},
|
||||
zhipu: {
|
||||
label: t('智谱'),
|
||||
icon: <Zhipu.Color />,
|
||||
filter: (model) =>
|
||||
model.model_name.toLowerCase().includes('chatglm') ||
|
||||
model.model_name.toLowerCase().includes('glm-'),
|
||||
model.model_name.toLowerCase().includes('glm-') ||
|
||||
model.model_name.toLowerCase().includes('cogview') ||
|
||||
model.model_name.toLowerCase().includes('cogvideo'),
|
||||
},
|
||||
qwen: {
|
||||
label: t('通义千问'),
|
||||
@@ -190,7 +203,9 @@ export const getModelCategories = (() => {
|
||||
minimax: {
|
||||
label: 'MiniMax',
|
||||
icon: <Minimax.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('abab'),
|
||||
filter: (model) =>
|
||||
model.model_name.toLowerCase().includes('abab') ||
|
||||
model.model_name.toLowerCase().includes('minimax'),
|
||||
},
|
||||
baidu: {
|
||||
label: t('文心一言'),
|
||||
@@ -215,7 +230,10 @@ export const getModelCategories = (() => {
|
||||
cohere: {
|
||||
label: 'Cohere',
|
||||
icon: <Cohere.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('command'),
|
||||
filter: (model) =>
|
||||
model.model_name.toLowerCase().includes('command') ||
|
||||
model.model_name.toLowerCase().includes('c4ai-') ||
|
||||
model.model_name.toLowerCase().includes('embed-'),
|
||||
},
|
||||
cloudflare: {
|
||||
label: 'Cloudflare',
|
||||
@@ -227,11 +245,6 @@ export const getModelCategories = (() => {
|
||||
icon: <Ai360.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('360'),
|
||||
},
|
||||
yi: {
|
||||
label: t('零一万物'),
|
||||
icon: <Yi.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('yi'),
|
||||
},
|
||||
jina: {
|
||||
label: 'Jina',
|
||||
icon: <Jina />,
|
||||
@@ -240,7 +253,12 @@ export const getModelCategories = (() => {
|
||||
mistral: {
|
||||
label: 'Mistral AI',
|
||||
icon: <Mistral.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('mistral'),
|
||||
filter: (model) =>
|
||||
model.model_name.toLowerCase().includes('mistral') ||
|
||||
model.model_name.toLowerCase().includes('codestral') ||
|
||||
model.model_name.toLowerCase().includes('pixtral') ||
|
||||
model.model_name.toLowerCase().includes('voxtral') ||
|
||||
model.model_name.toLowerCase().includes('magistral'),
|
||||
},
|
||||
xai: {
|
||||
label: 'xAI',
|
||||
@@ -257,6 +275,11 @@ export const getModelCategories = (() => {
|
||||
icon: <Doubao.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('doubao'),
|
||||
},
|
||||
yi: {
|
||||
label: t('零一万物'),
|
||||
icon: <Yi.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('yi'),
|
||||
},
|
||||
};
|
||||
|
||||
lastLocale = currentLocale;
|
||||
@@ -1772,10 +1795,13 @@ export function renderClaudeModelPrice(
|
||||
|
||||
// Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied)
|
||||
const nonCachedTokens = inputTokens;
|
||||
const legacyCacheCreationTokens = hasSplitCacheCreation
|
||||
? 0
|
||||
: cacheCreationTokens;
|
||||
const effectiveInputTokens =
|
||||
nonCachedTokens +
|
||||
cacheTokens * cacheRatio +
|
||||
cacheCreationTokens * cacheCreationRatio +
|
||||
legacyCacheCreationTokens * cacheCreationRatio +
|
||||
cacheCreationTokens5m * cacheCreationRatio5m +
|
||||
cacheCreationTokens1h * cacheCreationRatio1h;
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ export const useApiRequest = (
|
||||
if (data.choices?.[0]) {
|
||||
const choice = data.choices[0];
|
||||
let content = choice.message?.content || '';
|
||||
let reasoningContent = choice.message?.reasoning_content || '';
|
||||
let reasoningContent = choice.message?.reasoning_content || choice.message?.reasoning || '';
|
||||
|
||||
const processed = processThinkTags(content, reasoningContent);
|
||||
|
||||
@@ -333,6 +333,9 @@ export const useApiRequest = (
|
||||
if (delta.reasoning_content) {
|
||||
streamMessageUpdate(delta.reasoning_content, 'reasoning');
|
||||
}
|
||||
if (delta.reasoning) {
|
||||
streamMessageUpdate(delta.reasoning, 'reasoning');
|
||||
}
|
||||
if (delta.content) {
|
||||
streamMessageUpdate(delta.content, 'content');
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
"Gemini思考适配设置": "Gemini thinking adaptation settings",
|
||||
"Gemini版本设置": "Gemini version settings",
|
||||
"Gemini设置": "Gemini settings",
|
||||
"启用FunctionCall思维签名填充": "Enable FunctionCall thoughtSignature fill",
|
||||
"仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature": "Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format",
|
||||
"GitHub": "GitHub",
|
||||
"GitHub Client ID": "GitHub Client ID",
|
||||
"GitHub Client Secret": "GitHub Client Secret",
|
||||
@@ -2109,6 +2111,8 @@
|
||||
"请填写完整的产品信息": "Please fill in complete product information",
|
||||
"产品ID已存在": "Product ID already exists",
|
||||
"统一的": "The Unified",
|
||||
"大模型接口网关": "LLM API Gateway"
|
||||
"大模型接口网关": "LLM API Gateway",
|
||||
"正在跳转 GitHub...": "Redirecting to GitHub...",
|
||||
"请求超时,请刷新页面后重新发起 GitHub 登录": "Request timed out, please refresh and restart GitHub login"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@
|
||||
"Gemini思考适配设置": "Paramètres d'adaptation de la pensée Gemini",
|
||||
"Gemini版本设置": "Paramètres de version Gemini",
|
||||
"Gemini设置": "Paramètres Gemini",
|
||||
"启用FunctionCall思维签名填充": "Activer le remplissage de thoughtSignature pour FunctionCall",
|
||||
"仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature": "Remplit thoughtSignature uniquement pour les canaux Gemini/Vertex utilisant le format OpenAI",
|
||||
"GitHub": "GitHub",
|
||||
"GitHub Client ID": "ID client GitHub",
|
||||
"GitHub Client Secret": "Secret client GitHub",
|
||||
@@ -2089,6 +2091,8 @@
|
||||
"默认测试模型": "Modèle de test par défaut",
|
||||
"默认补全倍率": "Taux de complétion par défaut",
|
||||
"统一的": "La Passerelle",
|
||||
"大模型接口网关": "API LLM Unifiée"
|
||||
"大模型接口网关": "API LLM Unifiée",
|
||||
"正在跳转 GitHub...": "Redirection vers GitHub...",
|
||||
"请求超时,请刷新页面后重新发起 GitHub 登录": "Délai dépassé, veuillez actualiser la page puis relancer la connexion GitHub"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
"Gemini思考适配设置": "Gemini思考モード設定",
|
||||
"Gemini版本设置": "Geminiバージョン設定",
|
||||
"Gemini设置": "Gemini設定",
|
||||
"启用FunctionCall思维签名填充": "FunctionCall用のthoughtSignature自動付与を有効化",
|
||||
"仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature": "OpenAI形式を利用するGemini/VertexチャネルにのみthoughtSignatureを付与します",
|
||||
"GitHub": "GitHub",
|
||||
"GitHub Client ID": "GitHub Client ID",
|
||||
"GitHub Client Secret": "GitHub Client Secret",
|
||||
@@ -2080,6 +2082,8 @@
|
||||
"默认测试模型": "デフォルトテストモデル",
|
||||
"默认补全倍率": "デフォルト補完倍率",
|
||||
"统一的": "統合型",
|
||||
"大模型接口网关": "LLM APIゲートウェイ"
|
||||
"大模型接口网关": "LLM APIゲートウェイ",
|
||||
"正在跳转 GitHub...": "GitHub にリダイレクトしています...",
|
||||
"请求超时,请刷新页面后重新发起 GitHub 登录": "タイムアウトしました。ページをリロードして GitHub ログインをやり直してください"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,8 @@
|
||||
"Gemini思考适配设置": "Настройки адаптации мышления Gemini",
|
||||
"Gemini版本设置": "Настройки версии Gemini",
|
||||
"Gemini设置": "Настройки Gemini",
|
||||
"启用FunctionCall思维签名填充": "Включить автозаполнение thoughtSignature для FunctionCall",
|
||||
"仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature": "Заполнять thoughtSignature только для каналов Gemini/Vertex, использующих формат OpenAI",
|
||||
"GitHub": "GitHub",
|
||||
"GitHub Client ID": "ID клиента GitHub",
|
||||
"GitHub Client Secret": "Секрет клиента GitHub",
|
||||
@@ -2098,6 +2100,8 @@
|
||||
"默认测试模型": "Модель для тестирования по умолчанию",
|
||||
"默认补全倍率": "Коэффициент вывода по умолчанию",
|
||||
"统一的": "Единый",
|
||||
"大模型接口网关": "Шлюз API LLM"
|
||||
"大模型接口网关": "Шлюз API LLM",
|
||||
"正在跳转 GitHub...": "Перенаправление на GitHub...",
|
||||
"请求超时,请刷新页面后重新发起 GitHub 登录": "Время ожидания истекло, обновите страницу и снова запустите вход через GitHub"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@
|
||||
"Gemini思考适配设置": "Gemini思考适配设置",
|
||||
"Gemini版本设置": "Gemini版本设置",
|
||||
"Gemini设置": "Gemini设置",
|
||||
"启用FunctionCall思维签名填充": "启用FunctionCall思维签名填充",
|
||||
"仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature": "仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature",
|
||||
"GitHub": "GitHub",
|
||||
"GitHub Client ID": "GitHub Client ID",
|
||||
"GitHub Client Secret": "GitHub Client Secret",
|
||||
@@ -2071,6 +2073,8 @@
|
||||
"默认测试模型": "默认测试模型",
|
||||
"默认补全倍率": "默认补全倍率",
|
||||
"Creem 介绍": "Creem 是一个简单的支付处理平台,支持固定金额产品销售,以及订阅销售。",
|
||||
"Creem Setting Tips": "Creem 只支持预设的固定金额产品,这产品以及价格需要提前在Creem网站内创建配置,所以不支持自定义动态金额充值。在Creem端配置产品的名字以及价格,获取Product Id 后填到下面的产品,在new-api为该产品设置充值额度,以及展示价格。"
|
||||
"Creem Setting Tips": "Creem 只支持预设的固定金额产品,这产品以及价格需要提前在Creem网站内创建配置,所以不支持自定义动态金额充值。在Creem端配置产品的名字以及价格,获取Product Id 后填到下面的产品,在new-api为该产品设置充值额度,以及展示价格。",
|
||||
"正在跳转 GitHub...": "正在跳转 GitHub...",
|
||||
"请求超时,请刷新页面后重新发起 GitHub 登录": "请求超时,请刷新页面后重新发起 GitHub 登录"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,19 +39,22 @@ const GEMINI_VERSION_EXAMPLE = {
|
||||
default: 'v1beta',
|
||||
};
|
||||
|
||||
const DEFAULT_GEMINI_INPUTS = {
|
||||
'gemini.safety_settings': '',
|
||||
'gemini.version_settings': '',
|
||||
'gemini.supported_imagine_models': '',
|
||||
'gemini.thinking_adapter_enabled': false,
|
||||
'gemini.thinking_adapter_budget_tokens_percentage': 0.6,
|
||||
'gemini.function_call_thought_signature_enabled': true,
|
||||
};
|
||||
|
||||
export default function SettingGeminiModel(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputs, setInputs] = useState({
|
||||
'gemini.safety_settings': '',
|
||||
'gemini.version_settings': '',
|
||||
'gemini.supported_imagine_models': '',
|
||||
'gemini.thinking_adapter_enabled': false,
|
||||
'gemini.thinking_adapter_budget_tokens_percentage': 0.6,
|
||||
});
|
||||
const [inputs, setInputs] = useState(DEFAULT_GEMINI_INPUTS);
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
const [inputsRow, setInputsRow] = useState(DEFAULT_GEMINI_INPUTS);
|
||||
|
||||
async function onSubmit() {
|
||||
await refForm.current
|
||||
@@ -92,9 +95,9 @@ export default function SettingGeminiModel(props) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const currentInputs = {};
|
||||
const currentInputs = { ...DEFAULT_GEMINI_INPUTS };
|
||||
for (let key in props.options) {
|
||||
if (Object.keys(inputs).includes(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(DEFAULT_GEMINI_INPUTS, key)) {
|
||||
currentInputs[key] = props.options[key];
|
||||
}
|
||||
}
|
||||
@@ -166,6 +169,23 @@ export default function SettingGeminiModel(props) {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={16}>
|
||||
<Form.Switch
|
||||
label={t('启用FunctionCall思维签名填充')}
|
||||
field={'gemini.function_call_thought_signature_enabled'}
|
||||
extraText={t(
|
||||
'仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature',
|
||||
)}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
'gemini.function_call_thought_signature_enabled': value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||
<Form.TextArea
|
||||
|
||||
Reference in New Issue
Block a user