Compare commits

...

44 Commits

Author SHA1 Message Date
Calcium-Ion
ef0647285c Merge pull request #2260 from seefs001/fix/multi-key-fetch-models
fix: When retrieving the model list with multiple keys, select the first enabled one.
2025-11-20 18:16:05 +08:00
Seefs
33b1fad5f8 fix: When retrieving the model list with multiple keys, select the first enabled one. 2025-11-20 18:02:17 +08:00
Calcium-Ion
b899122dfe Merge pull request #2256 from seefs001/feature/gemini-3-openai
feat: Fill thoughtSignature only for Gemini/Vertex channels using OpenAI format
2025-11-20 16:05:41 +08:00
Seefs
50c04a62f9 feat: Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format 2025-11-20 15:54:33 +08:00
Calcium-Ion
554b68484c Merge pull request #2250 from seefs001/fix/claude-cache-price-render
fix: claude cache price render
2025-11-20 15:13:16 +08:00
Calcium-Ion
6a1c046714 Merge pull request #2252 from QuantumNous/dependabot/go_modules/golang.org/x/crypto-0.45.0
chore(deps): bump golang.org/x/crypto from 0.42.0 to 0.45.0
2025-11-20 15:13:00 +08:00
dependabot[bot]
0b37bdddc6 chore(deps): bump golang.org/x/crypto from 0.42.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.42.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.42.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 02:46:07 +00:00
Seefs
563a426c00 fix: claude cache price render 2025-11-20 00:56:09 +08:00
Seefs
f6a5d9ef7e Merge pull request #2247 from feitianbubu/pr/channel-omit-key
feat: channel by tag omit key
2025-11-19 19:38:59 +08:00
feitianbubu
a7d2450704 feat: channel by tag omit key 2025-11-19 19:25:27 +08:00
Calcium-Ion
75fced3d9c Merge pull request #2243 from seefs001/feature/gemini-3
feat: gemini-3-pro
2025-11-19 14:52:00 +08:00
Calcium-Ion
5a1bbd1059 Merge pull request #2231 from QuantumNous/dependabot/npm_and_yarn/electron/js-yaml-4.1.1
chore(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 in /electron
2025-11-19 14:51:26 +08:00
Calcium-Ion
c133678cb1 fix: optimized the GitHub login copy and timeout. (#2244) 2025-11-19 14:50:56 +08:00
Seefs
1fc3c4b09d fix: optimized the GitHub login copy and timeout. 2025-11-19 14:34:30 +08:00
Seefs
77c4c3e804 feat: MediaResolution && VideoMetadata 2025-11-19 13:42:32 +08:00
Seefs
bc1f747418 feat: gemini-3-pro 2025-11-19 01:46:51 +08:00
CaIon
62edac7c7f fix: aws 2025-11-18 16:56:46 +08:00
Seefs
ff839df279 Merge pull request #2239 from QAbot-zh/modelCategories-update
update model categories' match rules
2025-11-17 16:08:04 +08:00
undefinedcodezhong
8b8511b19e update model categories' match rules 2025-11-17 14:54:12 +08:00
Seefs
7598753f4e Merge pull request #2238 from seefs001/feature/doubao-coding-plan
feat: support doubao coding plan
2025-11-16 23:49:35 +08:00
Calcium-Ion
68777bf05f Merge pull request #2237 from seefs001/feature/linux-do-settings
feat: support configuring the linuxdo endpoint via environment variables
2025-11-16 15:43:47 +08:00
Seefs
b6217b22b0 feat: linuxdo oauth endpoint -> environment 2025-11-16 14:50:59 +08:00
CaIon
196fa135fd feat(adaptor): Add support for Claude-specific headers in SetupRequestHeader 2025-11-16 14:28:41 +08:00
Calcium-Ion
ff3225ab44 Merge pull request #2236 from seefs001/feature/vertex-k2
feat: support vertex open source models
2025-11-16 14:24:15 +08:00
Seefs
ab36de3725 feature: support vertex open source models 2025-11-16 14:23:11 +08:00
Calcium-Ion
2b4617dc1b Merge pull request #2235 from seefs001/fix/boundary-parser-error
fix: boundary parser error (error parsing multipart NextPart: bufio: buffer full)
2025-11-16 14:12:46 +08:00
Seefs
e169818404 fix: boundary parser error (error parsing multipart form: multipart: NextPart: bufio: buffer full) 2025-11-16 14:09:10 +08:00
dependabot[bot]
c1a696e6f0 chore(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 in /electron
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-15 20:14:28 +00:00
Seefs
e07347ac53 feat: support gpt-5.1 prompt_cache_retention (#2228) 2025-11-15 13:32:24 +08:00
Seefs
fd38abd562 Merge pull request #2207 from QAbot-zh/reasoning
support reasoning field for playground
2025-11-15 13:26:57 +08:00
IcedTangerine
293c0277a8 Merge pull request #2227 from feitianbubu/pr/add-wan2.5-i2i-preview
增加wan2.5-i2i-preview图生图支持
2025-11-15 12:43:09 +08:00
feitianbubu
344a799fcf feat: add wan2.5-i2i-preview support 2025-11-14 20:30:18 +08:00
Seefs
35192e5675 Merge pull request #2226 from QuantumNous/omit-anthropic_beta-empty
fix(relay/channel/aws): 修复AnthropicBeta字段的omitempty处理
2025-11-14 16:55:20 +08:00
creamlike1024
9e80e4e7e5 fix(relay/channel/aws): 修复AnthropicBeta字段的omitempty处理 2025-11-14 15:54:12 +08:00
IcedTangerine
e7bef097dd Merge pull request #2225 from feitianbubu/pr/add-hailuo-video
新增MiniMax海螺视频模型支持
2025-11-14 14:48:59 +08:00
CaIon
41b2341b0b fix(adaptor): Add '-none' suffix to effortSuffixes for model parsing 2025-11-14 14:04:34 +08:00
CaIon
e1a52f1d5a feat(aws): Add support for anthropic-beta header in AwsClaudeRequest 2025-11-14 12:01:20 +08:00
feitianbubu
d8dc8029c0 feat: add hailuo i2v fl2v r2v 2025-11-14 11:55:43 +08:00
feitianbubu
87bc4ba419 feat: get hailuo video url 2025-11-14 11:55:43 +08:00
feitianbubu
850a553958 feat: add MiniMax Hailuo video 2025-11-14 11:55:43 +08:00
Calcium-Ion
974df5e7b9 Merge pull request #2222 from xyfacai/main
fix: 未设置价格模型不会被拉取,除非设置自用模式
2025-11-13 19:00:21 +08:00
Xyfacai
06cd774c10 fix: 未设置价格模型不会被拉取,除非设置自用模式 2025-11-13 18:44:18 +08:00
CaIon
4419be9c09 fix(claude): Prevent duplicate header values in WriteHeaders method 2025-11-13 16:49:40 +08:00
Q.A.zh
fb3b27a626 support reasoning field 2025-11-11 13:00:20 +00:00
47 changed files with 1084 additions and 138 deletions

View File

@@ -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
View File

@@ -16,6 +16,8 @@ new-api
tiktoken_cache
.eslintcache
.gocache
.cache
web/bun.lock
electron/node_modules
electron/dist

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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

View File

@@ -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))

View File

@@ -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 {

View File

@@ -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"`

View File

@@ -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 {

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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:

View File

@@ -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"`

View File

@@ -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 {

View 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")
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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))

View File

@@ -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, "-")

View 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
}

View 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
)

View 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,
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`

View File

@@ -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

View File

@@ -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)
}
}
}
}

View File

@@ -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,
}
// 全局实例

View File

@@ -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
}

View File

@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "react-template",

View File

@@ -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>
)}

View File

@@ -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>
)}

View File

@@ -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'
/>

View File

@@ -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;

View File

@@ -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');
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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 ログインをやり直してください"
}
}

View File

@@ -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"
}
}

View File

@@ -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 登录"
}
}

View File

@@ -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