Compare commits

...

17 Commits

Author SHA1 Message Date
skynono
826ef2e5a6 feat: jimeng images base64 limit (#2032) 2025-10-13 15:17:23 +08:00
Xyfacai
7311c18d52 fix: 修复视频任务不同分组可能导致补回额度计算错误 (#2030) 2025-10-13 15:17:06 +08:00
Seefs
4a4238d830 Merge pull request #2029 from feitianbubu/pr/jimeng-support-oai-files
feat: jimeng use openai sdk input_reference i2v
2025-10-13 14:14:43 +08:00
Xyfacai
9805b0f3b0 Merge pull request #2027 from xyfacai/refactor/openai-video
refactor: Openai video model 移动到 dto
2025-10-13 13:24:20 +08:00
feitianbubu
dfca9681c8 feat: jimeng use openai sdk input_reference i2v 2025-10-13 13:06:03 +08:00
Xyfacai
a6e6897f63 refactor: Openai video model 移动到 dto 2025-10-13 11:45:45 +08:00
CaIon
ec0633bdfb fix: update error messages for unsupported parameter names in Google extra body 2025-10-12 22:21:45 +08:00
CaIon
2d1534dc77 fix: 修复工作流重复创建release的问题 2025-10-12 15:40:22 +08:00
Seefs
eebd7ca0f3 Merge pull request #2025 from feitianbubu/pr/protect-increase-quota
feat: Add pre-status protection for IncreaseUserQuota
2025-10-12 15:06:12 +08:00
feitianbubu
98e3e5ca2c feat: Add pre-status protection for IncreaseUserQuota 2025-10-12 14:53:55 +08:00
Seefs
e5dde67272 Merge pull request #2024 from seefs001/fix/version
fix: empty version
2025-10-12 14:24:01 +08:00
Seefs
d2546cf9ec fix: empty version 2025-10-12 14:23:18 +08:00
CaIon
ede47ef014 feat: support free model setting 2025-10-12 13:31:03 +08:00
Seefs
6c7795238f Merge pull request #2023 from seefs001/fix/version
fix: version
2025-10-12 13:05:51 +08:00
Seefs
0baacb2686 fix: version 2025-10-12 13:05:13 +08:00
Seefs
c5aaee9f2f Merge pull request #2022 from seefs001/fix/ignore_ghcr
ignore ghcr
2025-10-12 12:40:18 +08:00
Seefs
1987c7e16c ignore ghcr 2025-10-12 12:38:44 +08:00
19 changed files with 227 additions and 108 deletions

View File

@@ -33,13 +33,14 @@ jobs:
- name: Resolve tag & write VERSION
run: |
git fetch --tags --force --depth=1
echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
TAG=${GITHUB_REF#refs/tags/}
echo "TAG=$TAG" >> $GITHUB_ENV
echo "$TAG" > VERSION
echo "Building tag: $TAG for ${{ matrix.arch }}"
- name: Normalize GHCR repository
run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
# - name: Normalize GHCR repository
# run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -50,12 +51,12 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# - name: Log in to GHCR
# uses: docker/login-action@v3
# with:
# registry: ghcr.io
# username: ${{ github.actor }}
# password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (labels)
id: meta
@@ -63,7 +64,7 @@ jobs:
with:
images: |
calciumion/new-api
ghcr.io/${{ env.GHCR_REPOSITORY }}
# ghcr.io/${{ env.GHCR_REPOSITORY }}
- name: Build & push single-arch (to both registries)
uses: docker/build-push-action@v6
@@ -74,8 +75,8 @@ jobs:
tags: |
calciumion/new-api:${{ env.TAG }}-${{ matrix.arch }}
calciumion/new-api:latest-${{ matrix.arch }}
ghcr.io/${{ env.GHCR_REPOSITORY }}:${{ env.TAG }}-${{ matrix.arch }}
ghcr.io/${{ env.GHCR_REPOSITORY }}:latest-${{ matrix.arch }}
# ghcr.io/${{ env.GHCR_REPOSITORY }}:${{ env.TAG }}-${{ matrix.arch }}
# ghcr.io/${{ env.GHCR_REPOSITORY }}:latest-${{ matrix.arch }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -83,16 +84,16 @@ jobs:
sbom: false
create_manifests:
name: Create multi-arch manifests (Docker Hub + GHCR)
name: Create multi-arch manifests (Docker Hub)
needs: [build_single_arch]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Extract tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Normalize GHCR repository
run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
#
# - name: Normalize GHCR repository
# run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
- name: Log in to Docker Hub
uses: docker/login-action@v3
@@ -115,23 +116,23 @@ jobs:
calciumion/new-api:latest-arm64
# ---- GHCR ----
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# - name: Log in to GHCR
# uses: docker/login-action@v3
# with:
# registry: ghcr.io
# username: ${{ github.actor }}
# password: ${{ secrets.GITHUB_TOKEN }}
- name: Create & push manifest (GHCR - version)
run: |
docker buildx imagetools create \
-t ghcr.io/${GHCR_REPOSITORY}:${TAG} \
ghcr.io/${GHCR_REPOSITORY}:${TAG}-amd64 \
ghcr.io/${GHCR_REPOSITORY}:${TAG}-arm64
- name: Create & push manifest (GHCR - latest)
run: |
docker buildx imagetools create \
-t ghcr.io/${GHCR_REPOSITORY}:latest \
ghcr.io/${GHCR_REPOSITORY}:latest-amd64 \
ghcr.io/${GHCR_REPOSITORY}:latest-arm64
# - name: Create & push manifest (GHCR - version)
# run: |
# docker buildx imagetools create \
# -t ghcr.io/${GHCR_REPOSITORY}:${TAG} \
# ghcr.io/${GHCR_REPOSITORY}:${TAG}-amd64 \
# ghcr.io/${GHCR_REPOSITORY}:${TAG}-arm64
#
# - name: Create & push manifest (GHCR - latest)
# run: |
# docker buildx imagetools create \
# -t ghcr.io/${GHCR_REPOSITORY}:latest \
# ghcr.io/${GHCR_REPOSITORY}:latest-amd64 \
# ghcr.io/${GHCR_REPOSITORY}:latest-arm64

View File

@@ -130,13 +130,10 @@ jobs:
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Create Release
- name: Upload to Release
uses: softprops/action-gh-release@v2
with:
files: |
windows-build/*
draft: false
prerelease: false
overwrite_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -54,8 +54,6 @@ jobs:
with:
files: |
new-api-*
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -93,8 +91,6 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
files: new-api-macos-*
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -134,8 +130,6 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
files: new-api-*.exe
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -23,7 +23,7 @@ RUN go mod download
COPY . .
COPY --from=builder /build/dist ./web/dist
RUN go build -ldflags "-s -w -X 'new-api/common.Version=$(cat VERSION)'" -o new-api
RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api
FROM alpine

View File

@@ -140,9 +140,13 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
// common.SetContextKey(c, constant.ContextKeyTokenCountMeta, meta)
newAPIError = service.PreConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
if newAPIError != nil {
return
if priceData.FreeModel {
logger.LogInfo(c, fmt.Sprintf("模型 %s 免费,跳过预扣费", relayInfo.OriginModelName))
} else {
newAPIError = service.PreConsumeQuota(c, priceData.QuotaToPreConsume, relayInfo)
if newAPIError != nil {
return
}
}
defer func() {

View File

@@ -137,14 +137,19 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
if modelName, ok := taskData["model"].(string); ok && modelName != "" {
// 获取模型价格和倍率
modelRatio, hasRatioSetting, _ := ratio_setting.GetModelRatio(modelName)
// 只有配置了倍率(非固定价格)时才按 token 重新计费
if hasRatioSetting && modelRatio > 0 {
// 获取用户和组的倍率信息
user, err := model.GetUserById(task.UserId, false)
if err == nil {
groupRatio := ratio_setting.GetGroupRatio(user.Group)
userGroupRatio, hasUserGroupRatio := ratio_setting.GetGroupGroupRatio(user.Group, user.Group)
group := task.Group
if group == "" {
user, err := model.GetUserById(task.UserId, false)
if err == nil {
group = user.Group
}
}
if group != "" {
groupRatio := ratio_setting.GetGroupRatio(group)
userGroupRatio, hasUserGroupRatio := ratio_setting.GetGroupGroupRatio(group, group)
var finalGroupRatio float64
if hasUserGroupRatio {
@@ -214,6 +219,7 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
}
}
case model.TaskStatusFailure:
preStatus := task.Status
task.Status = model.TaskStatusFailure
task.Progress = "100%"
if task.FinishTime == 0 {
@@ -222,12 +228,18 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
task.FailReason = taskResult.Reason
logger.LogInfo(ctx, fmt.Sprintf("Task %s failed: %s", task.TaskID, task.FailReason))
quota := task.Quota
taskResult.Progress = "100%"
if quota != 0 {
if err := model.IncreaseUserQuota(task.UserId, quota, false); err != nil {
logger.LogError(ctx, "Failed to increase user quota: "+err.Error())
if preStatus != model.TaskStatusFailure {
// 任务失败且之前状态不是失败才退还额度,防止重复退还
if err := model.IncreaseUserQuota(task.UserId, quota, false); err != nil {
logger.LogWarn(ctx, "Failed to increase user quota: "+err.Error())
}
logContent := fmt.Sprintf("Video async task failed %s, refund %s", task.TaskID, logger.LogQuota(quota))
model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
} else {
logger.LogWarn(ctx, fmt.Sprintf("Task %s already in failure status, skip refund", task.TaskID))
}
logContent := fmt.Sprintf("Video async task failed %s, refund %s", task.TaskID, logger.LogQuota(quota))
model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
}
default:
return fmt.Errorf("unknown task status %s for task %s", taskResult.Status, taskId)

View File

@@ -1,4 +1,4 @@
package common
package dto
import (
"strconv"

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
commonRelay "github.com/QuantumNous/new-api/relay/common"
)
@@ -15,15 +16,15 @@ func (t TaskStatus) ToVideoStatus() string {
var status string
switch t {
case TaskStatusQueued, TaskStatusSubmitted:
status = commonRelay.VideoStatusQueued
status = dto.VideoStatusQueued
case TaskStatusInProgress:
status = commonRelay.VideoStatusInProgress
status = dto.VideoStatusInProgress
case TaskStatusSuccess:
status = commonRelay.VideoStatusCompleted
status = dto.VideoStatusCompleted
case TaskStatusFailure:
status = commonRelay.VideoStatusFailed
status = dto.VideoStatusFailed
default:
status = commonRelay.VideoStatusUnknown // Default fallback
status = dto.VideoStatusUnknown // Default fallback
}
return status
}
@@ -45,6 +46,7 @@ type Task struct {
TaskID string `json:"task_id" gorm:"type:varchar(191);index"` // 第三方id不一定有/ song id\ Task id
Platform constant.TaskPlatform `json:"platform" gorm:"type:varchar(30);index"` // 平台
UserId int `json:"user_id" gorm:"index"`
Group string `json:"group" gorm:"type:varchar(50)"` // 修正计费用
ChannelId int `json:"channel_id" gorm:"index"`
Quota int `json:"quota"`
Action string `json:"action" gorm:"type:varchar(40);index"` // 任务类型, song, lyrics, description-mode
@@ -98,6 +100,7 @@ type SyncTaskQueryParams struct {
func InitTask(platform constant.TaskPlatform, relayInfo *commonRelay.RelayInfo) *Task {
t := &Task{
UserId: relayInfo.UserId,
Group: relayInfo.UsingGroup,
SubmitTime: time.Now().Unix(),
Status: TaskStatusNotStart,
Progress: "0%",

View File

@@ -53,5 +53,5 @@ type TaskAdaptor interface {
}
type OpenAIVideoConverter interface {
ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon.OpenAIVideo, error)
ConvertToOpenAIVideo(originTask *model.Task) (*dto.OpenAIVideo, error)
}

View File

@@ -211,7 +211,16 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
// eg. {"google":{"thinking_config":{"thinking_budget":5324,"include_thoughts":true}}}
if googleBody, ok := extraBody["google"].(map[string]interface{}); ok {
adaptorWithExtraBody = true
// check error param name like thinkingConfig, should be thinking_config
if _, hasErrorParam := googleBody["thinkingConfig"]; hasErrorParam {
return nil, errors.New("extra_body.google.thinkingConfig is not supported, use extra_body.google.thinking_config instead")
}
if thinkingConfig, ok := googleBody["thinking_config"].(map[string]interface{}); ok {
// check error param name like thinkingBudget, should be thinking_budget
if _, hasErrorParam := thinkingConfig["thinkingBudget"]; hasErrorParam {
return nil, errors.New("extra_body.google.thinking_config.thinkingBudget is not supported, use extra_body.google.thinking_config.thinking_budget instead")
}
if budget, ok := thinkingConfig["thinking_budget"].(float64); ok {
budgetInt := int(budget)
geminiRequest.GenerationConfig.ThinkingConfig = &dto.GeminiThinkingConfig{

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
@@ -64,6 +65,11 @@ type responseTask struct {
TimeElapsed string `json:"time_elapsed"`
}
const (
// 即梦限制单个文件最大4.7MB https://www.volcengine.com/docs/85621/1747301
MaxFileSize int64 = 4*1024*1024 + 700*1024 // 4.7MB (4MB + 724KB)
)
// ============================
// Adaptor implementation
// ============================
@@ -89,7 +95,6 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
// ValidateRequestAndSetAction parses body, validates fields and sets default action.
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) {
// Accept only POST /v1/video/generations as "generate" action.
return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate)
}
@@ -113,13 +118,49 @@ func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info
return nil
}
// BuildRequestBody converts request into Jimeng specific format.
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 := v.(relaycommon.TaskSubmitReq)
req, ok := v.(relaycommon.TaskSubmitReq)
if !ok {
return nil, fmt.Errorf("invalid request type in context")
}
// 支持openai sdk的图片上传方式
if mf, err := c.MultipartForm(); err == nil {
if files, exists := mf.File["input_reference"]; exists && len(files) > 0 {
if len(files) == 1 {
info.Action = constant.TaskActionGenerate
} else if len(files) > 1 {
info.Action = constant.TaskActionFirstTailGenerate
}
// 将上传的文件转换为base64格式
var images []string
for _, fileHeader := range files {
// 检查文件大小
if fileHeader.Size > MaxFileSize {
return nil, fmt.Errorf("文件 %s 大小超过限制,最大允许 %d MB", fileHeader.Filename, MaxFileSize/(1024*1024))
}
file, err := fileHeader.Open()
if err != nil {
continue
}
fileBytes, err := io.ReadAll(file)
file.Close()
if err != nil {
continue
}
// 将文件内容转换为base64
base64Str := base64.StdEncoding.EncodeToString(fileBytes)
images = append(images, base64Str)
}
req.Images = images
}
}
body, err := a.convertToRequestPayload(&req)
if err != nil {
@@ -158,7 +199,7 @@ func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *rela
return
}
ov := relaycommon.NewOpenAIVideo()
ov := dto.NewOpenAIVideo()
ov.ID = jResp.Data.TaskID
ov.TaskID = jResp.Data.TaskID
ov.CreatedAt = time.Now().Unix()
@@ -364,10 +405,10 @@ func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*
// 即梦视频3.0 ReqKey转换
// https://www.volcengine.com/docs/85621/1792707
if strings.Contains(r.ReqKey, "jimeng_v30") {
if len(r.ImageUrls) > 1 {
if len(req.Images) > 1 {
// 多张图片:首尾帧生成
r.ReqKey = strings.Replace(r.ReqKey, "jimeng_v30", "jimeng_i2v_first_tail_v30", 1)
} else if len(r.ImageUrls) == 1 {
} else if len(req.Images) == 1 {
// 单张图片:图生视频
r.ReqKey = strings.Replace(r.ReqKey, "jimeng_v30", "jimeng_i2v_first_v30", 1)
} else {
@@ -405,13 +446,13 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e
return &taskResult, nil
}
func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon.OpenAIVideo, error) {
func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) (*dto.OpenAIVideo, error) {
var jimengResp responseTask
if err := json.Unmarshal(originTask.Data, &jimengResp); err != nil {
return nil, errors.Wrap(err, "unmarshal jimeng task data failed")
}
openAIVideo := relaycommon.NewOpenAIVideo()
openAIVideo := dto.NewOpenAIVideo()
openAIVideo.ID = originTask.TaskID
openAIVideo.Status = originTask.Status.ToVideoStatus()
openAIVideo.SetProgressStr(originTask.Progress)
@@ -420,7 +461,7 @@ func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon
openAIVideo.CompletedAt = originTask.UpdatedAt
if jimengResp.Code != 10000 {
openAIVideo.Error = &relaycommon.OpenAIVideoError{
openAIVideo.Error = &dto.OpenAIVideoError{
Message: jimengResp.Message,
Code: fmt.Sprintf("%d", jimengResp.Code),
}

View File

@@ -188,7 +188,7 @@ func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *rela
taskErr = service.TaskErrorWrapperLocal(fmt.Errorf(kResp.Message), "task_failed", http.StatusBadRequest)
return
}
ov := relaycommon.NewOpenAIVideo()
ov := dto.NewOpenAIVideo()
ov.ID = kResp.Data.TaskId
ov.TaskID = kResp.Data.TaskId
ov.CreatedAt = time.Now().Unix()
@@ -367,13 +367,13 @@ func isNewAPIRelay(apiKey string) bool {
return strings.HasPrefix(apiKey, "sk-")
}
func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon.OpenAIVideo, error) {
func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) (*dto.OpenAIVideo, error) {
var klingResp responsePayload
if err := json.Unmarshal(originTask.Data, &klingResp); err != nil {
return nil, errors.Wrap(err, "unmarshal kling task data failed")
}
openAIVideo := relaycommon.NewOpenAIVideo()
openAIVideo := dto.NewOpenAIVideo()
openAIVideo.ID = originTask.TaskID
openAIVideo.Status = originTask.Status.ToVideoStatus()
openAIVideo.SetProgressStr(originTask.Progress)
@@ -391,7 +391,7 @@ func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon
}
if klingResp.Code != 0 && klingResp.Message != "" {
openAIVideo.Error = &relaycommon.OpenAIVideoError{
openAIVideo.Error = &dto.OpenAIVideoError{
Message: klingResp.Message,
Code: fmt.Sprintf("%d", klingResp.Code),
}

View File

@@ -186,8 +186,8 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e
return &taskResult, nil
}
func (a *TaskAdaptor) ConvertToOpenAIVideo(task *model.Task) (*relaycommon.OpenAIVideo, error) {
openAIVideo := &relaycommon.OpenAIVideo{}
func (a *TaskAdaptor) ConvertToOpenAIVideo(task *model.Task) (*dto.OpenAIVideo, error) {
openAIVideo := &dto.OpenAIVideo{}
err := json.Unmarshal(task.Data, openAIVideo)
if err != nil {
return nil, errors.Wrap(err, "unmarshal to OpenAIVideo failed")

View File

@@ -155,7 +155,7 @@ func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *rela
return
}
ov := relaycommon.NewOpenAIVideo()
ov := dto.NewOpenAIVideo()
ov.ID = vResp.TaskId
ov.TaskID = vResp.TaskId
ov.CreatedAt = time.Now().Unix()
@@ -263,13 +263,13 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e
return taskInfo, nil
}
func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon.OpenAIVideo, error) {
func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) (*dto.OpenAIVideo, error) {
var viduResp taskResultResponse
if err := json.Unmarshal(originTask.Data, &viduResp); err != nil {
return nil, errors.Wrap(err, "unmarshal vidu task data failed")
}
openAIVideo := relaycommon.NewOpenAIVideo()
openAIVideo := dto.NewOpenAIVideo()
openAIVideo.ID = originTask.TaskID
openAIVideo.Status = originTask.Status.ToVideoStatus()
openAIVideo.SetProgressStr(originTask.Progress)
@@ -281,7 +281,7 @@ func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon
}
if viduResp.State == "failed" && viduResp.ErrCode != "" {
openAIVideo.Error = &relaycommon.OpenAIVideoError{
openAIVideo.Error = &dto.OpenAIVideoError{
Message: viduResp.ErrCode,
Code: viduResp.ErrCode,
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/QuantumNous/new-api/common"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/QuantumNous/new-api/types"
@@ -55,6 +56,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
var cacheCreationRatio float64
var audioRatio float64
var audioCompletionRatio float64
var freeModel bool
if !usePrice {
preConsumedTokens := common.Max(promptTokens, common.PreConsumedQuota)
if meta.MaxTokens != 0 {
@@ -87,18 +89,35 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
}
// check if free model pre-consume is disabled
if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
// if model price or ratio is 0, do not pre-consume quota
if usePrice {
if modelPrice == 0 {
preConsumedQuota = 0
freeModel = true
}
} else {
if modelRatio == 0 {
preConsumedQuota = 0
freeModel = true
}
}
}
priceData := types.PriceData{
ModelPrice: modelPrice,
ModelRatio: modelRatio,
CompletionRatio: completionRatio,
GroupRatioInfo: groupRatioInfo,
UsePrice: usePrice,
CacheRatio: cacheRatio,
ImageRatio: imageRatio,
AudioRatio: audioRatio,
AudioCompletionRatio: audioCompletionRatio,
CacheCreationRatio: cacheCreationRatio,
ShouldPreConsumedQuota: preConsumedQuota,
FreeModel: freeModel,
ModelPrice: modelPrice,
ModelRatio: modelRatio,
CompletionRatio: completionRatio,
GroupRatioInfo: groupRatioInfo,
UsePrice: usePrice,
CacheRatio: cacheRatio,
ImageRatio: imageRatio,
AudioRatio: audioRatio,
AudioCompletionRatio: audioCompletionRatio,
CacheCreationRatio: cacheCreationRatio,
QuotaToPreConsume: preConsumedQuota,
}
if common.DebugEnabled {

View File

@@ -0,0 +1,21 @@
package operation_setting
import "github.com/QuantumNous/new-api/setting/config"
type QuotaSetting struct {
EnableFreeModelPreConsume bool `json:"enable_free_model_pre_consume"` // 是否对免费模型启用预消耗
}
// 默认配置
var quotaSetting = QuotaSetting{
EnableFreeModelPreConsume: true,
}
func init() {
// 注册到全局配置管理器
config.GlobalConfig.Register("quota_setting", &quotaSetting)
}
func GetQuotaSetting() *QuotaSetting {
return &quotaSetting
}

View File

@@ -9,18 +9,19 @@ type GroupRatioInfo struct {
}
type PriceData struct {
ModelPrice float64
ModelRatio float64
CompletionRatio float64
CacheRatio float64
CacheCreationRatio float64
ImageRatio float64
AudioRatio float64
AudioCompletionRatio float64
OtherRatios map[string]float64
UsePrice bool
ShouldPreConsumedQuota int
GroupRatioInfo GroupRatioInfo
FreeModel bool
ModelPrice float64
ModelRatio float64
CompletionRatio float64
CacheRatio float64
CacheCreationRatio float64
ImageRatio float64
AudioRatio float64
AudioCompletionRatio float64
OtherRatios map[string]float64
UsePrice bool
QuotaToPreConsume int // 预消耗额度
GroupRatioInfo GroupRatioInfo
}
type PerCallPriceData struct {
@@ -30,5 +31,5 @@ type PerCallPriceData struct {
}
func (p PriceData) ToSetting() string {
return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.ShouldPreConsumedQuota, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio)
return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, QuotaToPreConsume: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.QuotaToPreConsume, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio)
}

View File

@@ -35,6 +35,7 @@ const OperationSetting = () => {
PreConsumedQuota: 0,
QuotaForInviter: 0,
QuotaForInvitee: 0,
'quota_setting.enable_free_model_pre_consume': true,
/* 通用设置 */
TopUpLink: '',

View File

@@ -36,6 +36,7 @@ export default function SettingsCreditLimit(props) {
PreConsumedQuota: '',
QuotaForInviter: '',
QuotaForInvitee: '',
'quota_setting.enable_free_model_pre_consume': true,
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
@@ -166,6 +167,21 @@ export default function SettingsCreditLimit(props) {
/>
</Col>
</Row>
<Row>
<Col>
<Form.Switch
label={t('对免费模型启用预消耗')}
field={'quota_setting.enable_free_model_pre_consume'}
extraText={t('开启后对免费模型倍率为0或者价格为0的模型也会预消耗额度')}
onChange={(value) =>
setInputs({
...inputs,
'quota_setting.enable_free_model_pre_consume': value,
})
}
/>
</Col>
</Row>
<Row>
<Button size='default' onClick={onSubmit}>