Compare commits

...

27 Commits

Author SHA1 Message Date
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
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
28 changed files with 875 additions and 82 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"
@@ -192,6 +193,12 @@ 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)
}

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

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

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

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

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

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

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

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