Compare commits

..

30 Commits

Author SHA1 Message Date
Seefs
aacdc395c8 Merge pull request #2013 from seefs001/fix/ci
feat: matrix ci
2025-10-11 13:47:54 +08:00
Seefs
37cc5d245e ci 2025-10-11 13:47:14 +08:00
Seefs
2842ae52c7 Merge pull request #2010 from seefs001/fix/ci
feat: matrix ci
2025-10-11 13:12:55 +08:00
Seefs
bea8c57f1d fix 2025-10-11 13:11:46 +08:00
Seefs
e603186dfa Merge pull request #2009 from seefs001/fix/ci
feat: matrix ci
2025-10-11 13:11:09 +08:00
Seefs
a8a0da5e3e fix 2025-10-11 13:10:06 +08:00
Seefs
4e0c911a06 Merge pull request #2008 from seefs001/fix/ci
feat: matrix ci
2025-10-11 13:02:37 +08:00
Seefs
24bc24abaa feat: matrix ci 2025-10-11 13:01:50 +08:00
Seefs
c948f85ee8 Merge pull request #2007 from QuantumNous/main
alpha -> mian
2025-10-11 13:00:17 +08:00
CaIon
0ff98b1dc1 feat: update Go version in CI configuration and add release workflow for multi-platform builds 2025-10-11 12:44:09 +08:00
CaIon
5d4a0757f7 fix: ensure error message is set when it is empty in error handling #1972 2025-10-11 12:12:41 +08:00
CaIon
07b099006c feat: add logging for model details and enhance action assignment in relay tasks 2025-10-11 11:56:44 +08:00
CaIon
5fbf860020 feat: enhance multipart validation with additional fields for model, seconds, and size 2025-10-11 11:33:59 +08:00
Calcium-Ion
eab768b4a0 Merge pull request #2006 from xyfacai/feat/sora-price
feat: sora 增加参数校验与计费
2025-10-11 11:22:08 +08:00
Calcium-Ion
1031f1ddf0 Merge pull request #2004 from feitianbubu/pr/openai-sdk-kling
支持可灵使用openai sdk生成视频
2025-10-11 11:05:11 +08:00
feitianbubu
5f36e32821 feat: add openai sdk create 2025-10-11 02:44:01 +08:00
feitianbubu
11e8e4e7a6 feat: add openai sdk for kling 2025-10-11 02:43:56 +08:00
feitianbubu
35422b316d refactor: openAI video use OpenAIVideoConverter 2025-10-11 02:43:43 +08:00
Seefs
df0ae9294d Merge pull request #2002 from qixing-jk/fix/dynamic-frequency-updates
fix(channel): handle dynamic frequency updates
2025-10-11 01:01:23 +08:00
anime
57e5d67f86 fix(channel): move log statement after sleep in auto-test loop 2025-10-11 00:59:13 +08:00
anime
7351480365 fix(channel): handle dynamic frequency updates 2025-10-11 00:53:26 +08:00
anime
e19e904179 fix(channel): handle dynamic frequency updates
- replace infinite sleep loop with time.Ticker to avoid goroutine leaks
- add immediate initial test execution before ticker starts
- implement frequency change detection and ticker recreation
- ensure proper ticker cleanup when loop exits or feature disabled
2025-10-11 00:34:15 +08:00
Xyfacai
a54baf4998 feat: sora 增加参数校验与计费 2025-10-10 23:56:36 +08:00
Calcium-Ion
68d975120d Merge pull request #1966 from QuantumNous/revert-1952-main
Revert "fix(topup): add currency symbol to amounts in RechargeCard"
2025-10-03 21:54:08 +08:00
Calcium-Ion
3473329524 Revert "fix(topup): add currency symbol to amounts in RechargeCard" 2025-10-03 21:53:55 +08:00
Calcium-Ion
5d33ec8473 Merge pull request #1952 from kyubibii/main
fix(topup): add currency symbol to amounts in RechargeCard
2025-10-03 21:39:02 +08:00
Seefs
8a01f09ef6 Merge pull request #1962 from QuantumNous/main
main -> alpha
2025-10-03 12:28:21 +08:00
キュビビイ
f44f68242e Merge branch 'main' of https://github.com/QuantumNous/new-api 2025-10-02 23:19:40 +08:00
キュビビイ
b0b6ab2ebc feat(web): add privacy policy and user agreement settings & pages
Closes #1858
2025-10-02 23:03:58 +08:00
キュビビイ
2290acc86c fix(topup): add currency symbol to amounts in RechargeCard
- What: 在充值卡显示的实付与节省金额前加入美元符号 `$`。
- Why: 满足 issue #1881,要求在金额前标注货币单位以减少歧义。
- Files: src/components/topup/RechargeCard.jsx
- Note: 这是局部修复。建议后续实现统一的 currency formatter(Intl.NumberFormat)并从后端/配置读取货币代码以支持本地化与多币种。

Closes #1881
2025-10-02 12:25:07 +08:00
25 changed files with 409 additions and 336 deletions

View File

@@ -37,7 +37,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '>=1.18.0'
go-version: '>=1.25.1'
- name: Build frontend
env:

View File

@@ -1,60 +0,0 @@
name: Linux Release
permissions:
contents: write
on:
workflow_dispatch:
inputs:
name:
description: 'reason'
required: false
push:
tags:
- '*'
- '!*-alpha*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build Frontend
env:
CI: ""
run: |
cd web
bun install
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
cd ..
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '>=1.18.0'
- name: Build Backend (amd64)
run: |
go mod download
VERSION=$(git describe --tags)
go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-$VERSION
- name: Build Backend (arm64)
run: |
sudo apt-get update
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu
VERSION=$(git describe --tags)
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-arm64-$VERSION
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
new-api-*
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,52 +0,0 @@
name: macOS Release
permissions:
contents: write
on:
workflow_dispatch:
inputs:
name:
description: 'reason'
required: false
push:
tags:
- '*'
- '!*-alpha*'
jobs:
release:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build Frontend
env:
CI: ""
NODE_OPTIONS: "--max-old-space-size=4096"
run: |
cd web
bun install
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
cd ..
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '>=1.18.0'
- name: Build Backend
run: |
go mod download
VERSION=$(git describe --tags)
go build -ldflags "-X 'one-api/common.Version=$VERSION'" -o new-api-macos-$VERSION
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: new-api-macos-*
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

142
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,142 @@
name: Release (Linux, macOS, Windows)
permissions:
contents: write
on:
workflow_dispatch:
inputs:
name:
description: 'reason'
required: false
push:
tags:
- '*'
- '!*-alpha*'
jobs:
linux:
name: Linux Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build Frontend
env:
CI: ""
run: |
cd web
bun install
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
cd ..
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '>=1.25.1'
- name: Build Backend (amd64)
run: |
go mod download
VERSION=$(git describe --tags)
go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-$VERSION
- name: Build Backend (arm64)
run: |
sudo apt-get update
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu
VERSION=$(git describe --tags)
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-arm64-$VERSION
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
new-api-*
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
macos:
name: macOS Release
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build Frontend
env:
CI: ""
NODE_OPTIONS: "--max-old-space-size=4096"
run: |
cd web
bun install
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
cd ..
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '>=1.25.1'
- name: Build Backend
run: |
go mod download
VERSION=$(git describe --tags)
go build -ldflags "-X 'one-api/common.Version=$VERSION'" -o new-api-macos-$VERSION
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: new-api-macos-*
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
windows:
name: Windows Release
runs-on: windows-latest
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build Frontend
env:
CI: ""
run: |
cd web
bun install
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
cd ..
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '>=1.25.1'
- name: Build Backend
run: |
go mod download
VERSION=$(git describe --tags)
go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION'" -o new-api-$VERSION.exe
- name: Release
uses: softprops/action-gh-release@v2
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

@@ -1,17 +0,0 @@
# .github/workflows/sync.yml
name: Sync Fork
on:
push: # push 时触发, 主要是为了测试配置有没有问题
schedule:
- cron: '* */3 * * *' # 每3小时触发, 对于一些更新不那么频繁的项目可以设置为每天一次, 低碳一点
jobs:
repo-sync:
runs-on: ubuntu-latest
steps:
- uses: TG908/fork-sync@v1.6.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
owner: QuantumNous # fork 的上游仓库 user
head: main # fork 的上游仓库 branch
base: main # 本地仓库 branch

View File

@@ -1,54 +0,0 @@
name: Windows Release
permissions:
contents: write
on:
workflow_dispatch:
inputs:
name:
description: 'reason'
required: false
push:
tags:
- '*'
- '!*-alpha*'
jobs:
release:
runs-on: windows-latest
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build Frontend
env:
CI: ""
run: |
cd web
bun install
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
cd ..
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '>=1.18.0'
- name: Build Backend
run: |
go mod download
VERSION=$(git describe --tags)
go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION'" -o new-api-$VERSION.exe
- name: Release
uses: softprops/action-gh-release@v2
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

@@ -9,10 +9,12 @@ COPY ./VERSION .
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
FROM golang:alpine AS builder2
ENV GO111MODULE=on CGO_ENABLED=0
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64}
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux
WORKDIR /build

View File

@@ -622,10 +622,10 @@ func AutomaticallyTestChannels() {
time.Sleep(10 * time.Minute)
continue
}
frequency := operation_setting.GetMonitorSetting().AutoTestChannelMinutes
common.SysLog(fmt.Sprintf("automatically test channels with interval %d minutes", frequency))
for {
frequency := operation_setting.GetMonitorSetting().AutoTestChannelMinutes
time.Sleep(time.Duration(frequency) * time.Minute)
common.SysLog(fmt.Sprintf("automatically test channels with interval %d minutes", frequency))
common.SysLog("automatically testing all channels")
_ = testAllChannels(false)
common.SysLog("automatically channel test finished")

View File

@@ -97,6 +97,7 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
taskResult.Url = t.FailReason
taskResult.Progress = t.Progress
taskResult.Reason = t.FailReason
task.Data = t.Data
} else if taskResult, err = adaptor.ParseTaskResult(responseBody); err != nil {
return fmt.Errorf("parseTaskResult failed for task %s: %w", taskId, err)
} else {

View File

@@ -174,14 +174,22 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
relayMode := relayconstant.RelayModeUnknown
if c.Request.Method == http.MethodPost {
relayMode = relayconstant.RelayModeVideoSubmit
form, err := common.ParseMultipartFormReusable(c)
if err != nil {
return nil, false, errors.New("无效的video请求, " + err.Error())
}
defer form.RemoveAll()
if form != nil {
if values, ok := form.Value["model"]; ok && len(values) > 0 {
modelRequest.Model = values[0]
contentType := c.Request.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "multipart/form-data") {
form, err := common.ParseMultipartFormReusable(c)
if err != nil {
return nil, false, errors.New("无效的video请求, " + err.Error())
}
defer form.RemoveAll()
if form != nil {
if values, ok := form.Value["model"]; ok && len(values) > 0 {
modelRequest.Model = values[0]
}
}
} else if strings.HasPrefix(contentType, "application/json") {
err = common.UnmarshalBodyReusable(c, &modelRequest)
if err != nil {
return nil, false, errors.New("无效的video请求, " + err.Error())
}
}
} else if c.Request.Method == http.MethodGet {

View File

@@ -4,6 +4,7 @@ import (
"io"
"net/http"
"one-api/dto"
"one-api/model"
relaycommon "one-api/relay/common"
"one-api/types"
@@ -49,3 +50,7 @@ type TaskAdaptor interface {
ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error)
}
type OpenAIVideoConverter interface {
ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon.OpenAIVideo, error)
}

View File

@@ -7,9 +7,11 @@ import (
"io"
"net/http"
"one-api/model"
"strconv"
"strings"
"time"
"github.com/bytedance/gopkg/util/logger"
"github.com/samber/lo"
"github.com/gin-gonic/gin"
@@ -303,14 +305,6 @@ func (a *TaskAdaptor) createJWTToken() (string, error) {
return a.createJWTTokenWithKey(a.apiKey)
}
//func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) {
// parts := strings.Split(apiKey, "|")
// if len(parts) != 2 {
// return "", fmt.Errorf("invalid API key format, expected 'access_key,secret_key'")
// }
// return a.createJWTTokenWithKey(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
//}
func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) {
if isNewAPIRelay(apiKey) {
return apiKey, nil // new api relay
@@ -369,3 +363,50 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e
func isNewAPIRelay(apiKey string) bool {
return strings.HasPrefix(apiKey, "sk-")
}
func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon.OpenAIVideo, error) {
var klingResp responsePayload
if err := json.Unmarshal(originTask.Data, &klingResp); err != nil {
return nil, errors.Wrap(err, "unmarshal kling task data failed")
}
convertProgress := func(progress string) int {
progress = strings.TrimSuffix(progress, "%")
p, err := strconv.Atoi(progress)
if err != nil {
logger.Warnf("convert progress failed, progress: %s, err: %v", progress, err)
}
return p
}
openAIVideo := &relaycommon.OpenAIVideo{
ID: klingResp.Data.TaskId,
Object: "video",
//Model: "kling-v1", //todo save model
Status: string(originTask.Status),
CreatedAt: klingResp.Data.CreatedAt,
CompletedAt: klingResp.Data.UpdatedAt,
Metadata: make(map[string]any),
Progress: convertProgress(originTask.Progress),
}
// 处理视频 URL
if len(klingResp.Data.TaskResult.Videos) > 0 {
video := klingResp.Data.TaskResult.Videos[0]
if video.Url != "" {
openAIVideo.Metadata["url"] = video.Url
}
if video.Duration != "" {
openAIVideo.Seconds = video.Duration
}
}
if klingResp.Code != 0 && klingResp.Message != "" {
openAIVideo.Error = &relaycommon.OpenAIVideoError{
Message: klingResp.Message,
Code: fmt.Sprintf("%d", klingResp.Code),
}
}
return openAIVideo, nil
}

View File

@@ -184,3 +184,12 @@ 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{}
err := json.Unmarshal(task.Data, openAIVideo)
if err != nil {
return nil, errors.Wrap(err, "unmarshal to OpenAIVideo failed")
}
return openAIVideo, nil
}

View File

@@ -550,3 +550,24 @@ func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOther
}
return jsonDataAfter, nil
}
type OpenAIVideo struct {
ID string `json:"id"`
TaskID string `json:"task_id,omitempty"` //兼容旧接口 待废弃
Object string `json:"object"`
Model string `json:"model"`
Status string `json:"status"`
Progress int `json:"progress"`
CreatedAt int64 `json:"created_at"`
CompletedAt int64 `json:"completed_at,omitempty"`
ExpiresAt int64 `json:"expires_at,omitempty"`
Seconds string `json:"seconds,omitempty"`
Size string `json:"size,omitempty"`
RemixedFromVideoID string `json:"remixed_from_video_id,omitempty"`
Error *OpenAIVideoError `json:"error,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}
type OpenAIVideoError struct {
Message string `json:"message"`
Code string `json:"code"`
}

View File

@@ -10,6 +10,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
)
type HasPrompt interface {
@@ -106,27 +107,97 @@ func validateMultipartTaskRequest(c *gin.Context, info *RelayInfo, action string
}
func ValidateMultipartDirect(c *gin.Context, info *RelayInfo) *dto.TaskError {
form, err := common.ParseMultipartFormReusable(c)
if err != nil {
return createTaskError(err, "invalid_multipart_form", http.StatusBadRequest, true)
}
defer form.RemoveAll()
contentType := c.GetHeader("Content-Type")
var prompt string
var model string
var seconds int
var size string
var hasInputReference bool
prompts, ok := form.Value["prompt"]
if !ok || len(prompts) == 0 {
return createTaskError(fmt.Errorf("prompt field is required"), "missing_prompt", http.StatusBadRequest, true)
if strings.HasPrefix(contentType, "multipart/form-data") {
form, err := common.ParseMultipartFormReusable(c)
if err != nil {
return createTaskError(err, "invalid_multipart_form", http.StatusBadRequest, true)
}
defer form.RemoveAll()
prompts, ok := form.Value["prompt"]
if !ok || len(prompts) == 0 {
return createTaskError(fmt.Errorf("prompt field is required"), "missing_prompt", http.StatusBadRequest, true)
}
prompt = prompts[0]
if _, ok := form.Value["model"]; !ok {
return createTaskError(fmt.Errorf("model field is required"), "missing_model", http.StatusBadRequest, true)
}
model = form.Value["model"][0]
if _, ok := form.File["input_reference"]; ok {
hasInputReference = true
}
if ss, ok := form.Value["seconds"]; ok {
sInt := common.String2Int(ss[0])
if sInt > seconds {
seconds = common.String2Int(ss[0])
}
}
if sz, ok := form.Value["size"]; ok {
size = sz[0]
}
} else {
var req TaskSubmitReq
if err := common.UnmarshalBodyReusable(c, &req); err != nil {
return createTaskError(err, "invalid_json", http.StatusBadRequest, true)
}
prompt = req.Prompt
model = req.Model
seconds = req.Duration
if strings.TrimSpace(req.Model) == "" {
return createTaskError(fmt.Errorf("model field is required"), "missing_model", http.StatusBadRequest, true)
}
if req.HasImage() {
hasInputReference = true
}
}
if taskErr := validatePrompt(prompts[0]); taskErr != nil {
if taskErr := validatePrompt(prompt); taskErr != nil {
return taskErr
}
if _, ok := form.Value["model"]; !ok {
return createTaskError(fmt.Errorf("model field is required"), "missing_model", http.StatusBadRequest, true)
}
action := constant.TaskActionTextGenerate
if _, ok := form.File["input_reference"]; ok {
if hasInputReference {
action = constant.TaskActionGenerate
}
if strings.HasPrefix(model, "sora-2") {
if size == "" {
size = "720x1280"
}
if seconds <= 0 {
seconds = 4
}
if model == "sora-2" && !lo.Contains([]string{"720x1280", "1280x720"}, size) {
return createTaskError(fmt.Errorf("sora-2 size is invalid"), "invalid_size", http.StatusBadRequest, true)
}
if model == "sora-2-pro" && !lo.Contains([]string{"720x1280", "1280x720", "1792x1024", "1024x1792"}, size) {
return createTaskError(fmt.Errorf("sora-2 size is invalid"), "invalid_size", http.StatusBadRequest, true)
}
info.PriceData.OtherRatios = map[string]float64{
"seconds": float64(seconds),
"size": 1,
}
if lo.Contains([]string{"1792x1024", "1024x1792"}, size) {
info.PriceData.OtherRatios["size"] = 1.666667
}
}
info.Action = action
return nil

View File

@@ -114,7 +114,7 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) types.
modelPrice, success := ratio_setting.GetModelPrice(info.OriginModelName, true)
// 如果没有配置价格,则使用默认价格
if !success {
defaultPrice, ok := ratio_setting.GetDefaultModelRatioMap()[info.OriginModelName]
defaultPrice, ok := ratio_setting.GetDefaultModelPriceMap()[info.OriginModelName]
if !ok {
modelPrice = 0.1
} else {

View File

@@ -11,6 +11,7 @@ import (
"one-api/constant"
"one-api/dto"
"one-api/model"
"one-api/relay/channel"
relaycommon "one-api/relay/common"
relayconstant "one-api/relay/constant"
"one-api/service"
@@ -53,7 +54,7 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.
}
modelPrice, success := ratio_setting.GetModelPrice(modelName, true)
if !success {
defaultPrice, ok := ratio_setting.GetDefaultModelRatioMap()[modelName]
defaultPrice, ok := ratio_setting.GetDefaultModelPriceMap()[modelName]
if !ok {
modelPrice = 0.1
} else {
@@ -70,6 +71,14 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.
} else {
ratio = modelPrice * groupRatio
}
if len(info.PriceData.OtherRatios) > 0 {
for _, ra := range info.PriceData.OtherRatios {
if 1.0 != ra {
ratio *= ra
}
}
}
println(fmt.Sprintf("model: %s, model_price: %.4f, group: %s, group_ratio: %.4f, final_ratio: %.4f", modelName, modelPrice, info.UsingGroup, groupRatio, ratio))
userQuota, err := model.GetUserQuota(info.UserId, false)
if err != nil {
taskErr = service.TaskErrorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
@@ -138,11 +147,22 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.
}
if quota != 0 {
tokenName := c.GetString("token_name")
gRatio := groupRatio
if hasUserGroupRatio {
gRatio = userGroupRatio
//gRatio := groupRatio
//if hasUserGroupRatio {
// gRatio = userGroupRatio
//}
logContent := fmt.Sprintf("操作 %s", info.Action)
if len(info.PriceData.OtherRatios) > 0 {
var contents []string
for key, ra := range info.PriceData.OtherRatios {
if 1.0 != ra {
contents = append(contents, fmt.Sprintf("%s: %.2f", key, ra))
}
}
if len(contents) > 0 {
logContent = fmt.Sprintf("%s, 计算参数:%s", logContent, strings.Join(contents, ", "))
}
}
logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %s", modelPrice, gRatio, info.Action)
other := make(map[string]interface{})
other["model_price"] = modelPrice
other["group_ratio"] = groupRatio
@@ -362,11 +382,34 @@ func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *d
}
}()
if len(respBody) == 0 {
respBody, err = json.Marshal(dto.TaskResponse[any]{
Code: "success",
Data: TaskModel2Dto(originTask),
})
if len(respBody) != 0 {
return
}
if strings.HasPrefix(c.Request.RequestURI, "/v1/videos/") {
adaptor := GetTaskAdaptor(originTask.Platform)
if adaptor == nil {
taskResp = service.TaskErrorWrapperLocal(fmt.Errorf("invalid channel id: %d", originTask.ChannelId), "invalid_channel_id", http.StatusBadRequest)
return
}
if converter, ok := adaptor.(channel.OpenAIVideoConverter); ok {
openAIVideo, err := converter.ConvertToOpenAIVideo(originTask)
if err != nil {
taskResp = service.TaskErrorWrapper(err, "convert_to_openai_video_failed", http.StatusInternalServerError)
return
}
respBody, _ = json.Marshal(openAIVideo)
return
}
taskResp = service.TaskErrorWrapperLocal(errors.New(fmt.Sprintf("not_implemented:%s", originTask.Platform)), "not_implemented", http.StatusNotImplemented)
return
}
respBody, err = json.Marshal(dto.TaskResponse[any]{
Code: "success",
Data: TaskModel2Dto(originTask),
})
if err != nil {
taskResp = service.TaskErrorWrapper(err, "marshal_response_failed", http.StatusInternalServerError)
}
return
}

View File

@@ -290,6 +290,8 @@ var defaultModelPrice = map[string]float64{
"mj_upscale": 0.05,
"swap_face": 0.05,
"mj_upload": 0.05,
"sora-2": 0.3,
"sora-2-pro": 0.5,
}
var defaultAudioRatio = map[string]float64{
@@ -452,6 +454,10 @@ func GetDefaultModelRatioMap() map[string]float64 {
return defaultModelRatio
}
func GetDefaultModelPriceMap() map[string]float64 {
return defaultModelPrice
}
func GetDefaultImageRatioMap() map[string]float64 {
return defaultImageRatio
}

View File

@@ -160,6 +160,9 @@ func (e *NewAPIError) ToOpenAIError() OpenAIError {
if e.errorCode != ErrorCodeCountTokenFailed {
result.Message = common.MaskSensitiveInfo(result.Message)
}
if result.Message == "" {
result.Message = string(e.errorType)
}
return result
}
@@ -186,6 +189,9 @@ func (e *NewAPIError) ToClaudeError() ClaudeError {
if e.errorCode != ErrorCodeCountTokenFailed {
result.Message = common.MaskSensitiveInfo(result.Message)
}
if result.Message == "" {
result.Message = string(e.errorType)
}
return result
}

View File

@@ -17,6 +17,7 @@ type PriceData struct {
ImageRatio float64
AudioRatio float64
AudioCompletionRatio float64
OtherRatios map[string]float64
UsePrice bool
ShouldPreConsumedQuota int
GroupRatioInfo GroupRatioInfo

View File

@@ -1,35 +0,0 @@
// functions/Pages_Functions.js
// 该函数会将所有请求转发到环境变量 `TARGET_URL` 指定的地址
export async function onRequest(context) {
// 从上下文中获取原始请求和环境变量
const { request, env } = context;
// 解析原始请求的 URL以获取路径和查询参数
const url = new URL(request.url);
const path = url.pathname;
const search = url.search;
// 从环境变量中获取目标 URL如果未设置则提供一个默认值
const target = env.TARGET_URL || 'http://172.0.0.1';
// 构建目标 URL
const targetUrl = `${target}${path}${search}`;
// 创建一个新的请求以转发到目标地址
// 复制原始请求的方法、头部和主体
const newRequest = new Request(targetUrl, {
method: request.method,
headers: request.headers,
body: request.body,
redirect: 'manual' // 防止 fetch 自动处理重定向
});
// 执行转发请求并返回响应
try {
return await fetch(newRequest);
} catch (error) {
// 如果目标服务器无法访问,返回一个错误信息
return new Response(`Error forwarding request: ${error.message}`, { status: 502 });
}
}

View File

@@ -1,35 +0,0 @@
// functions/Pages_Functions.js
// 该函数会将所有请求转发到环境变量 `TARGET_URL` 指定的地址
export async function onRequest(context) {
// 从上下文中获取原始请求和环境变量
const { request, env } = context;
// 解析原始请求的 URL以获取路径和查询参数
const url = new URL(request.url);
const path = url.pathname;
const search = url.search;
// 从环境变量中获取目标 URL如果未设置则提供一个默认值
const target = env.TARGET_URL || 'http://172.0.0.1';
// 构建目标 URL
const targetUrl = `${target}${path}${search}`;
// 创建一个新的请求以转发到目标地址
// 复制原始请求的方法、头部和主体
const newRequest = new Request(targetUrl, {
method: request.method,
headers: request.headers,
body: request.body,
redirect: 'manual' // 防止 fetch 自动处理重定向
});
// 执行转发请求并返回响应
try {
return await fetch(newRequest);
} catch (error) {
// 如果目标服务器无法访问,返回一个错误信息
return new Response(`Error forwarding request: ${error.message}`, { status: 502 });
}
}

View File

@@ -1,35 +0,0 @@
// functions/Pages_Functions.js
// 该函数会将所有请求转发到环境变量 `TARGET_URL` 指定的地址
export async function onRequest(context) {
// 从上下文中获取原始请求和环境变量
const { request, env } = context;
// 解析原始请求的 URL以获取路径和查询参数
const url = new URL(request.url);
const path = url.pathname;
const search = url.search;
// 从环境变量中获取目标 URL如果未设置则提供一个默认值
const target = env.TARGET_URL || 'http://172.0.0.1';
// 构建目标 URL
const targetUrl = `${target}${path}${search}`;
// 创建一个新的请求以转发到目标地址
// 复制原始请求的方法、头部和主体
const newRequest = new Request(targetUrl, {
method: request.method,
headers: request.headers,
body: request.body,
redirect: 'manual' // 防止 fetch 自动处理重定向
});
// 执行转发请求并返回响应
try {
return await fetch(newRequest);
} catch (error) {
// 如果目标服务器无法访问,返回一个错误信息
return new Response(`Error forwarding request: ${error.message}`, { status: 502 });
}
}

View File

@@ -6,7 +6,6 @@
"dependencies": {
"@douyinfe/semi-icons": "^2.63.1",
"@douyinfe/semi-ui": "^2.69.1",
"antd": "^5.25.2",
"@lobehub/icons": "^2.0.0",
"@visactor/react-vchart": "~1.8.8",
"@visactor/vchart": "~1.8.8",
@@ -45,7 +44,7 @@
},
"scripts": {
"dev": "vite",
"build": "node --max-old-space-size=4096 ./node_modules/vite/bin/vite.js build",
"build": "vite build",
"lint": "prettier . --check",
"lint:fix": "prettier . --write",
"eslint": "bunx eslint \"**/*.{js,jsx}\" --cache",

View File

@@ -377,6 +377,12 @@ export const useLogsData = () => {
other.file_search_call_count || 0,
),
});
if (logs[i]?.content) {
expandDataLocal.push({
key: t('其他详情'),
value: logs[i].content,
});
}
}
if (logs[i].type === 2) {
let modelMapped =