diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 7097fc3b1c2..acb7190aacc 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -3,7 +3,7 @@ name: Publish Docker image (Multi-arch) on: push: tags: - - '*' + - 'v*' - '!nightly*' workflow_dispatch: inputs: @@ -71,7 +71,9 @@ jobs: id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: - images: calciumion/new-api + images: | + ${{ secrets.DOCKERHUB_USERNAME }}/new-api + ghcr.io/${{ github.repository }} - name: Build & push id: build @@ -81,8 +83,10 @@ jobs: platforms: ${{ matrix.platform }} push: true tags: | - calciumion/new-api:${{ env.TAG }}-${{ matrix.arch }} - calciumion/new-api:latest-${{ matrix.arch }} + pzyyll/new-api:${{ env.TAG }}-${{ matrix.arch }} + pzyyll/new-api: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 @@ -93,13 +97,13 @@ jobs: uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Sign image with cosign - run: cosign sign --yes calciumion/new-api@${{ steps.build.outputs.digest }} + run: cosign sign --yes pzyyll/new-api@${{ steps.build.outputs.digest }} - name: Image summary run: | echo "### Docker Image Digest (${{ matrix.arch }})" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - echo "calciumion/new-api:${TAG}-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY + echo "pzyyll/new-api:${{ env.TAG }}-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY echo "${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY @@ -125,16 +129,16 @@ jobs: - name: Create & push manifest (version) run: | docker buildx imagetools create \ - -t calciumion/new-api:${TAG} \ - calciumion/new-api:${TAG}-amd64 \ - calciumion/new-api:${TAG}-arm64 + -t pzyyll/new-api:${TAG} \ + pzyyll/new-api:${TAG}-amd64 \ + pzyyll/new-api:${TAG}-arm64 - name: Create & push manifest (latest) run: | docker buildx imagetools create \ - -t calciumion/new-api:latest \ - calciumion/new-api:latest-amd64 \ - calciumion/new-api:latest-arm64 + -t pzyyll/new-api:latest \ + pzyyll/new-api:latest-amd64 \ + pzyyll/new-api:latest-arm64 - name: Install cosign uses: sigstore/cosign-installer@v3 @@ -148,5 +152,5 @@ jobs: run: | echo "### Multi-arch Manifest" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - docker buildx imagetools inspect calciumion/new-api:${TAG} >> $GITHUB_STEP_SUMMARY + docker buildx imagetools inspect pzyyll/new-api:${TAG} >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docker-image-branch.yml b/.github/workflows/docker-image-branch.yml index b5222468cdc..320d8a04977 100644 --- a/.github/workflows/docker-image-branch.yml +++ b/.github/workflows/docker-image-branch.yml @@ -1,6 +1,9 @@ -name: Publish Docker image (manual branch) +name: Publish Docker image (branch) on: + push: + branches: + - alpha workflow_dispatch: inputs: branch: @@ -8,6 +11,10 @@ on: required: true type: string +concurrency: + group: docker-branch-${{ github.event.inputs.branch || github.ref_name }} + cancel-in-progress: true + jobs: prepare: name: Prepare Docker tags @@ -24,12 +31,12 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 - ref: ${{ inputs.branch }} + ref: ${{ github.event.inputs.branch || github.ref_name }} - name: Resolve Docker tags id: version env: - BRANCH_NAME: ${{ inputs.branch }} + BRANCH_NAME: ${{ github.event.inputs.branch || github.ref_name }} run: | TAG_PREFIX=$(printf '%s' "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_.-]+/-/g; s/^[.-]+//; s/[.-]+$//') TAG_PREFIX=${TAG_PREFIX:0:105} @@ -92,7 +99,7 @@ jobs: uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: | - calciumion/new-api + pzyyll/new-api - name: Build & push single-arch id: build @@ -102,8 +109,8 @@ jobs: platforms: ${{ matrix.platform }} push: true tags: | - calciumion/new-api:${{ needs.prepare.outputs.tag_prefix }}-${{ matrix.arch }} - calciumion/new-api:${{ needs.prepare.outputs.version }}-${{ matrix.arch }} + pzyyll/new-api:${{ needs.prepare.outputs.tag_prefix }}-${{ matrix.arch }} + pzyyll/new-api:${{ needs.prepare.outputs.version }}-${{ matrix.arch }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max @@ -114,14 +121,14 @@ jobs: uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Sign image with cosign - run: cosign sign --yes calciumion/new-api@${{ steps.build.outputs.digest }} + run: cosign sign --yes pzyyll/new-api@${{ steps.build.outputs.digest }} - name: Output digest run: | echo "### Docker Image Digest (${{ matrix.arch }})" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - echo "calciumion/new-api:${{ needs.prepare.outputs.tag_prefix }}-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY - echo "calciumion/new-api:${{ needs.prepare.outputs.version }}-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY + echo "pzyyll/new-api:${{ needs.prepare.outputs.tag_prefix }}-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY + echo "pzyyll/new-api:${{ needs.prepare.outputs.version }}-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY echo "${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY @@ -141,30 +148,30 @@ jobs: - name: Create & push manifest (Docker Hub - branch) run: | docker buildx imagetools create \ - -t calciumion/new-api:${{ needs.prepare.outputs.tag_prefix }} \ - calciumion/new-api:${{ needs.prepare.outputs.tag_prefix }}-amd64 \ - calciumion/new-api:${{ needs.prepare.outputs.tag_prefix }}-arm64 + -t pzyyll/new-api:${{ needs.prepare.outputs.tag_prefix }} \ + pzyyll/new-api:${{ needs.prepare.outputs.tag_prefix }}-amd64 \ + pzyyll/new-api:${{ needs.prepare.outputs.tag_prefix }}-arm64 - name: Create & push manifest (Docker Hub - versioned) run: | docker buildx imagetools create \ - -t calciumion/new-api:${{ needs.prepare.outputs.version }} \ - calciumion/new-api:${{ needs.prepare.outputs.version }}-amd64 \ - calciumion/new-api:${{ needs.prepare.outputs.version }}-arm64 + -t pzyyll/new-api:${{ needs.prepare.outputs.version }} \ + pzyyll/new-api:${{ needs.prepare.outputs.version }}-amd64 \ + pzyyll/new-api:${{ needs.prepare.outputs.version }}-arm64 - name: Install cosign uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Sign manifests with cosign run: | - cosign sign --yes calciumion/new-api:${{ needs.prepare.outputs.tag_prefix }} - cosign sign --yes calciumion/new-api:${{ needs.prepare.outputs.version }} + cosign sign --yes pzyyll/new-api:${{ needs.prepare.outputs.tag_prefix }} + cosign sign --yes pzyyll/new-api:${{ needs.prepare.outputs.version }} - name: Output manifest digest run: | echo "### Multi-arch Manifest Digests" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - docker buildx imagetools inspect calciumion/new-api:${{ needs.prepare.outputs.tag_prefix }} >> $GITHUB_STEP_SUMMARY + docker buildx imagetools inspect pzyyll/new-api:${{ needs.prepare.outputs.tag_prefix }} >> $GITHUB_STEP_SUMMARY echo "---" >> $GITHUB_STEP_SUMMARY - docker buildx imagetools inspect calciumion/new-api:${{ needs.prepare.outputs.version }} >> $GITHUB_STEP_SUMMARY + docker buildx imagetools inspect pzyyll/new-api:${{ needs.prepare.outputs.version }} >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index c3afceb021f..f4a11a39993 100644 --- a/.gitignore +++ b/.gitignore @@ -18,11 +18,18 @@ new-api /__debug_bin* .DS_Store tiktoken_cache + +.aider.* +.claude +.sisyphus +.opencode +.pi + .eslintcache .gocache .gomodcache/ .cache -plans +# plans .claude .cursor @@ -32,6 +39,8 @@ data/ .gomodcache/ .gocache-temp .gopath +.worktrees + .test token_estimator_test.go skills-lock.json diff --git a/AGENTS.md b/AGENTS.md index 8f41dcf72c8..f01424ff2e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# AGENTS.md — Project Conventions for new-api +# Project Conventions for new-api DO NOT send optional commentary diff --git a/common/api_type.go b/common/api_type.go index c198ffc0e03..627e278e1c4 100644 --- a/common/api_type.go +++ b/common/api_type.go @@ -77,6 +77,8 @@ func ChannelType2APIType(channelType int) (int, bool) { apiType = constant.APITypeCodex case constant.ChannelTypeAdvancedCustom: apiType = constant.APITypeAdvancedCustom + case constant.ChannelTypeOpenCodeGo: + apiType = constant.APITypeOpenCodeGo } if apiType == -1 { return constant.APITypeOpenAI, false diff --git a/common/constants.go b/common/constants.go index 87d212f9973..242cbb5e787 100644 --- a/common/constants.go +++ b/common/constants.go @@ -115,6 +115,7 @@ var DebugEnabled bool var MemoryCacheEnabled bool var LogConsumeEnabled = true +var RequestDetailLogEnabled = false var TLSInsecureSkipVerify bool var InsecureTLSConfig = &tls.Config{InsecureSkipVerify: true} @@ -156,6 +157,16 @@ var PreConsumedQuota = 500 var RetryTimes = 0 +// SameChannelRetryTimes is the max number of extra attempts on the same channel +// with exponential backoff before switching to another channel. +var SameChannelRetryTimes = 0 + +// SameChannelRetryBaseDelayMs is the first same-channel backoff delay in milliseconds. +var SameChannelRetryBaseDelayMs = 200 + +// SameChannelRetryMaxDelayMs caps exponential same-channel backoff delays. +var SameChannelRetryMaxDelayMs = 2000 + //var RootUserEmail = "" var IsMasterNode bool diff --git a/common/retry_backoff.go b/common/retry_backoff.go new file mode 100644 index 00000000000..e6024b8107e --- /dev/null +++ b/common/retry_backoff.go @@ -0,0 +1,34 @@ +// ABOUTME: Computes exponential backoff delays for same-channel relay retries. +// ABOUTME: Caps delay growth using configured base and max milliseconds. +package common + +import "time" + +// SameChannelRetryBackoff returns the wait duration before the given same-channel +// retry attempt. attempt is 0-based (0 = first same-channel retry after a failure). +func SameChannelRetryBackoff(attempt int) time.Duration { + baseMs := SameChannelRetryBaseDelayMs + maxMs := SameChannelRetryMaxDelayMs + if baseMs <= 0 { + return 0 + } + if maxMs < baseMs { + maxMs = baseMs + } + if attempt < 0 { + attempt = 0 + } + + delayMs := baseMs + for i := 0; i < attempt; i++ { + if delayMs > maxMs/2 { + delayMs = maxMs + break + } + delayMs *= 2 + } + if delayMs > maxMs { + delayMs = maxMs + } + return time.Duration(delayMs) * time.Millisecond +} diff --git a/common/retry_backoff_test.go b/common/retry_backoff_test.go new file mode 100644 index 00000000000..4bb9d33b881 --- /dev/null +++ b/common/retry_backoff_test.go @@ -0,0 +1,32 @@ +// ABOUTME: Unit tests for same-channel exponential backoff delay calculation. +// ABOUTME: Verifies base growth, max capping, and non-positive base handling. +package common + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestSameChannelRetryBackoff(t *testing.T) { + originalBase := SameChannelRetryBaseDelayMs + originalMax := SameChannelRetryMaxDelayMs + t.Cleanup(func() { + SameChannelRetryBaseDelayMs = originalBase + SameChannelRetryMaxDelayMs = originalMax + }) + + SameChannelRetryBaseDelayMs = 200 + SameChannelRetryMaxDelayMs = 2000 + + assert.Equal(t, 200*time.Millisecond, SameChannelRetryBackoff(0)) + assert.Equal(t, 400*time.Millisecond, SameChannelRetryBackoff(1)) + assert.Equal(t, 800*time.Millisecond, SameChannelRetryBackoff(2)) + assert.Equal(t, 1600*time.Millisecond, SameChannelRetryBackoff(3)) + assert.Equal(t, 2000*time.Millisecond, SameChannelRetryBackoff(4)) + assert.Equal(t, 2000*time.Millisecond, SameChannelRetryBackoff(5)) + + SameChannelRetryBaseDelayMs = 0 + assert.Equal(t, time.Duration(0), SameChannelRetryBackoff(0)) +} diff --git a/constant/api_type.go b/constant/api_type.go index f3657a11679..94a287c29c1 100644 --- a/constant/api_type.go +++ b/constant/api_type.go @@ -37,5 +37,6 @@ const ( APITypeReplicate APITypeCodex APITypeAdvancedCustom + APITypeOpenCodeGo APITypeDummy // this one is only for count, do not add any channel after this ) diff --git a/constant/channel.go b/constant/channel.go index 45ec9a44e49..edde4c5c02d 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -56,6 +56,7 @@ const ( ChannelTypeReplicate = 56 ChannelTypeCodex = 57 ChannelTypeAdvancedCustom = 58 + ChannelTypeOpenCodeGo = 59 ChannelTypeDummy // this one is only for count, do not add any channel after this ) @@ -120,6 +121,7 @@ var ChannelBaseURLs = []string{ "https://api.replicate.com", //56 "https://chatgpt.com", //57 "", //58 + "https://opencode.ai/zen/go", //59 } var ChannelTypeNames = map[int]string{ @@ -178,6 +180,7 @@ var ChannelTypeNames = map[int]string{ ChannelTypeReplicate: "Replicate", ChannelTypeCodex: "ChatGPT Subscription (Codex)", ChannelTypeAdvancedCustom: "Advanced Custom", + ChannelTypeOpenCodeGo: "OpenCode Go", } func GetChannelTypeName(channelType int) string { diff --git a/controller/channel-test.go b/controller/channel-test.go index 4ba3698bd54..3d8e5b28838 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -957,7 +957,7 @@ func performChannelTests(ctx context.Context, channels []*model.Channel, testUse // disable channel if allowDisable && isChannelEnabled && shouldBanChannel && channel.GetAutoBan() { - processChannelError(result.context, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError) + processChannelError(result.context, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError, "") summary.Disabled++ } diff --git a/controller/log.go b/controller/log.go index ce9b4666fa5..a9b6a04c05b 100644 --- a/controller/log.go +++ b/controller/log.go @@ -1,13 +1,17 @@ package controller import ( + "errors" "net/http" "strconv" + "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/service" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) func GetAllLogs(c *gin.Context) { @@ -150,6 +154,83 @@ func GetLogsSelfStat(c *gin.Context) { return } +// GetLogRequestDetail returns request/response bodies for a request_id (admin). +// Bodies are read from disk; legacy DB columns are used as a fallback. +func GetLogRequestDetail(c *gin.Context) { + getLogRequestDetail(c, 0, true) +} + +// GetUserLogRequestDetail returns request/response bodies for a request_id owned by the current user. +func GetUserLogRequestDetail(c *gin.Context) { + getLogRequestDetail(c, c.GetInt("id"), false) +} + +func getLogRequestDetail(c *gin.Context, userId int, isAdmin bool) { + requestId := strings.TrimSpace(c.Query("request_id")) + if requestId == "" { + common.ApiErrorMsg(c, "request_id is required") + return + } + logRow, err := model.GetLogAccessByRequestId(requestId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + common.ApiErrorMsg(c, "log not found") + return + } + common.ApiError(c, err) + return + } + if !isAdmin && logRow.UserId != userId { + common.ApiErrorMsg(c, "log not found") + return + } + + bodies, err := service.ReadRequestDetailBodies(requestId) + if err != nil { + common.ApiError(c, err) + return + } + // Legacy fallback for rows written before file-backed storage. + if bodies.RequestBody == "" { + bodies.RequestBody = logRow.RequestBody + } + if bodies.ResponseBody == "" { + bodies.ResponseBody = logRow.ResponseBody + } + common.ApiSuccess(c, gin.H{ + "request_id": requestId, + "request_body": bodies.RequestBody, + "response_body": bodies.ResponseBody, + "is_stream": logRow.IsStream, + }) +} + +// GetRequestDetailStorageInfo returns disk usage for request-detail files (root). +func GetRequestDetailStorageInfo(c *gin.Context) { + dir, count, size, err := service.RequestDetailStorageInfo() + if err != nil { + common.ApiError(c, err) + return + } + common.ApiSuccess(c, gin.H{ + "dir": dir, + "file_count": count, + "total_size": size, + }) +} + +// ClearRequestDetailFiles deletes all request-detail body files on this node (root). +func ClearRequestDetailFiles(c *gin.Context) { + removed, err := service.ClearAllRequestDetailBodies() + if err != nil { + common.ApiError(c, err) + return + } + common.ApiSuccess(c, gin.H{ + "removed": removed, + }) +} + // DeleteHistoryLogs is the legacy synchronous log cleanup endpoint (DELETE /api/log/). // It deletes directly instead of going through the async system task. It is kept only // for the classic frontend; the default frontend uses POST /api/system-task/log-cleanup. diff --git a/controller/relay.go b/controller/relay.go index 6e91ccb6050..5b5bb001922 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -1,3 +1,5 @@ +// ABOUTME: Handles relay requests, retries failed upstream calls, and reports relay errors. +// ABOUTME: Same-channel backoff first, then exclude channel and prefer untried peers. package controller import ( @@ -188,6 +190,7 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) { relayInfo.RetryIndex = 0 relayInfo.LastError = nil +outer: for ; retryParam.GetRetry() <= common.RetryTimes; retryParam.IncreaseRetry() { relayInfo.RetryIndex = retryParam.GetRetry() channel, channelErr := getChannel(c, relayInfo, retryParam) @@ -196,42 +199,62 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) { newAPIError = channelErr break } + if channel == nil { + break + } addUsedChannel(c, channel.Id) - bodyStorage, bodyErr := common.GetBodyStorage(c) - if bodyErr != nil { - // Ensure consistent 413 for oversized bodies even when error occurs later (e.g., retry path) - if common.IsRequestBodyTooLargeError(bodyErr) || errors.Is(bodyErr, common.ErrRequestBodyTooLarge) { - newAPIError = types.NewErrorWithStatusCode(bodyErr, types.ErrorCodeReadRequestBodyFailed, http.StatusRequestEntityTooLarge, types.ErrOptionWithSkipRetry()) - } else { - newAPIError = types.NewErrorWithStatusCode(bodyErr, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest, types.ErrOptionWithSkipRetry()) + retryParam.ExcludeChannel(channel.Id) + + sameChannelAttempt := 0 + for { + bodyStorage, bodyErr := common.GetBodyStorage(c) + if bodyErr != nil { + // Ensure consistent 413 for oversized bodies even when error occurs later (e.g., retry path) + if common.IsRequestBodyTooLargeError(bodyErr) || errors.Is(bodyErr, common.ErrRequestBodyTooLarge) { + newAPIError = types.NewErrorWithStatusCode(bodyErr, types.ErrorCodeReadRequestBodyFailed, http.StatusRequestEntityTooLarge, types.ErrOptionWithSkipRetry()) + } else { + newAPIError = types.NewErrorWithStatusCode(bodyErr, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest, types.ErrOptionWithSkipRetry()) + } + break outer } - break - } - c.Request.Body = io.NopCloser(bodyStorage) - - switch relayFormat { - case types.RelayFormatOpenAIRealtime: - newAPIError = relay.WssHelper(c, relayInfo) - case types.RelayFormatClaude: - newAPIError = relay.ClaudeHelper(c, relayInfo) - case types.RelayFormatGemini: - newAPIError = geminiRelayHandler(c, relayInfo) - default: - newAPIError = relayHandler(c, relayInfo) - } + c.Request.Body = io.NopCloser(bodyStorage) - if newAPIError == nil { - relayInfo.LastError = nil - return - } + switch relayFormat { + case types.RelayFormatOpenAIRealtime: + newAPIError = relay.WssHelper(c, relayInfo) + case types.RelayFormatClaude: + newAPIError = relay.ClaudeHelper(c, relayInfo) + case types.RelayFormatGemini: + newAPIError = geminiRelayHandler(c, relayInfo) + default: + newAPIError = relayHandler(c, relayInfo) + } + + if newAPIError == nil { + relayInfo.LastError = nil + return + } + + newAPIError = service.NormalizeViolationFeeError(newAPIError) + relayInfo.LastError = newAPIError - newAPIError = service.NormalizeViolationFeeError(newAPIError) - relayInfo.LastError = newAPIError + processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError, service.CaptureResponseBodyForLog(relayInfo)) - processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError) + if sameChannelAttempt < common.SameChannelRetryTimes && shouldSameChannelRetry(c, newAPIError) { + delay := common.SameChannelRetryBackoff(sameChannelAttempt) + logger.LogInfo(c, fmt.Sprintf("same-channel retry %d/%d after %s (channel #%d, status %d)", + sameChannelAttempt+1, common.SameChannelRetryTimes, delay, channel.Id, newAPIError.StatusCode)) + if !sleepForSameChannelRetry(c, delay) { + break outer + } + sameChannelAttempt++ + continue + } - if !shouldRetry(c, newAPIError, common.RetryTimes-retryParam.GetRetry()) { + if !shouldRetry(c, newAPIError, common.RetryTimes-retryParam.GetRetry()) { + break outer + } break } } @@ -312,6 +335,9 @@ func getChannel(c *gin.Context, info *relaycommon.RelayInfo, retryParam *service return nil, types.NewError(fmt.Errorf("获取分组 %s 下模型 %s 的可用渠道失败(retry): %s", selectGroup, info.OriginModelName, err.Error()), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry()) } if channel == nil { + if retryParam.HasExcludedChannels() { + return nil, nil + } return nil, types.NewError(fmt.Errorf("分组 %s 下模型 %s 的可用渠道不存在(retry)", selectGroup, info.OriginModelName), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry()) } @@ -326,7 +352,9 @@ func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) b if openaiErr == nil { return false } - if service.ShouldSkipRetryAfterChannelAffinityFailure(c) { + softFail := service.IsSoftFailErrorCode(openaiErr.GetErrorCode()) + // Soft fails must override affinity SkipRetryOnFailure so sticky sessions can switch. + if service.ShouldSkipRetryAfterChannelAffinityFailure(c) && !softFail { return false } if types.IsChannelError(openaiErr) { @@ -335,12 +363,19 @@ func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) b if types.IsSkipRetryError(openaiErr) { return false } + // Committed client streams cannot switch channels mid-response. + if softFail && service.IsClientPayloadWritten(c, nil) { + return false + } if retryTimes <= 0 { return false } if _, ok := c.Get("specific_channel_id"); ok { return false } + if softFail { + return true + } code := openaiErr.StatusCode if code >= 200 && code < 300 { return false @@ -354,8 +389,87 @@ func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) b return operation_setting.ShouldRetryByStatusCode(code) } -func processChannelError(c *gin.Context, channelError types.ChannelError, err *types.NewAPIError) { +// shouldSameChannelRetry reports whether the failure is suitable for another +// attempt on the same channel with backoff (transient network/overloads only). +func shouldSameChannelRetry(c *gin.Context, openaiErr *types.NewAPIError) bool { + if openaiErr == nil { + return false + } + errorCode := openaiErr.GetErrorCode() + // Empty completed prefers another channel; capacity may recover on the same one. + if errorCode == types.ErrorCodeEmptyResponse { + return false + } + softFail := service.IsSoftFailErrorCode(errorCode) + if service.ShouldSkipRetryAfterChannelAffinityFailure(c) && !softFail { + return false + } + if types.IsSkipRetryError(openaiErr) { + return false + } + if operation_setting.IsAlwaysSkipRetryCode(errorCode) { + return false + } + // Upstream request transport failures are often wrapped as HTTP 500 with + // ErrorCodeDoRequestFailed; treat them as same-channel transient errors. + if errorCode == types.ErrorCodeDoRequestFailed { + return true + } + if errorCode == types.ErrorCodeUpstreamCapacity { + // Same-channel backoff is only safe before any client payload was flushed. + return !service.IsClientPayloadWritten(c, nil) + } + return isSameChannelRetryStatusCode(openaiErr.StatusCode) +} + +func shouldSameChannelRetryTask(c *gin.Context, taskErr *dto.TaskError) bool { + if taskErr == nil || taskErr.LocalError { + return false + } + if service.ShouldSkipRetryAfterChannelAffinityFailure(c) { + return false + } + if taskErr.Code == string(types.ErrorCodeDoRequestFailed) { + return true + } + return isSameChannelRetryStatusCode(taskErr.StatusCode) +} + +func isSameChannelRetryStatusCode(code int) bool { + // Non-HTTP failures (timeouts, connection reset, etc.) + if code < 100 || code > 599 { + return true + } + switch code { + case http.StatusTooManyRequests, http.StatusBadGateway, http.StatusServiceUnavailable: + return true + default: + return false + } +} + +func sleepForSameChannelRetry(c *gin.Context, delay time.Duration) bool { + if delay <= 0 { + return c == nil || c.Request == nil || c.Request.Context().Err() == nil + } + if c == nil || c.Request == nil { + time.Sleep(delay) + return true + } + timer := time.NewTimer(delay) + defer timer.Stop() + select { + case <-c.Request.Context().Done(): + return false + case <-timer.C: + return true + } +} + +func processChannelError(c *gin.Context, channelError types.ChannelError, err *types.NewAPIError, responseBody string) { logger.LogError(c, fmt.Sprintf("channel error (channel #%d, status code: %d): %s", channelError.ChannelId, err.StatusCode, common.LocalLogPreview(err.Error()))) + // Soft fails clear sticky affinity even when the handler path did not mark it first. + service.ApplySoftFailFromError(c, nil, err) // 不要使用context获取渠道信息,异步处理时可能会出现渠道信息不一致的情况 // do not use context to get channel info, there may be inconsistent channel info when processing asynchronously if service.ShouldDisableChannel(err) && channelError.AutoBan { @@ -390,13 +504,19 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t adminInfo["multi_key_index"] = common.GetContextKeyInt(c, constant.ContextKeyChannelMultiKeyIndex) } service.AppendChannelAffinityAdminInfo(c, adminInfo) + service.AppendSoftFailAdminInfo(c, nil, adminInfo) other["admin_info"] = adminInfo startTime := common.GetContextKeyTime(c, constant.ContextKeyRequestStartTime) if startTime.IsZero() { startTime = time.Now() } useTimeSeconds := int(time.Since(startTime).Seconds()) - model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.MaskSensitiveErrorWithStatusCode(), tokenId, useTimeSeconds, common.GetContextKeyBool(c, constant.ContextKeyIsStream), userGroup, other) + requestBody := service.CaptureRequestBodyForLog(c) + if !common.RequestDetailLogEnabled { + responseBody = "" + } + service.PersistRequestDetailBodies(c, requestBody, responseBody) + model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.MaskSensitiveErrorWithStatusCode(), tokenId, useTimeSeconds, common.GetContextKeyBool(c, constant.ContextKeyIsStream), userGroup, other, "", "") } } @@ -515,6 +635,7 @@ func RelayTask(c *gin.Context) { Retry: common.GetPointer(0), } +taskOuter: for ; retryParam.GetRetry() <= common.RetryTimes; retryParam.IncreaseRetry() { var channel *model.Channel @@ -534,33 +655,54 @@ func RelayTask(c *gin.Context) { taskErr = service.TaskErrorWrapperLocal(channelErr.Err, "get_channel_failed", http.StatusInternalServerError) break } + if channel == nil { + break + } } addUsedChannel(c, channel.Id) - bodyStorage, bodyErr := common.GetBodyStorage(c) - if bodyErr != nil { - if common.IsRequestBodyTooLargeError(bodyErr) || errors.Is(bodyErr, common.ErrRequestBodyTooLarge) { - taskErr = service.TaskErrorWrapperLocal(bodyErr, "read_request_body_failed", http.StatusRequestEntityTooLarge) - } else { - taskErr = service.TaskErrorWrapperLocal(bodyErr, "read_request_body_failed", http.StatusBadRequest) + retryParam.ExcludeChannel(channel.Id) + + sameChannelAttempt := 0 + for { + bodyStorage, bodyErr := common.GetBodyStorage(c) + if bodyErr != nil { + if common.IsRequestBodyTooLargeError(bodyErr) || errors.Is(bodyErr, common.ErrRequestBodyTooLarge) { + taskErr = service.TaskErrorWrapperLocal(bodyErr, "read_request_body_failed", http.StatusRequestEntityTooLarge) + } else { + taskErr = service.TaskErrorWrapperLocal(bodyErr, "read_request_body_failed", http.StatusBadRequest) + } + break taskOuter } - break - } - c.Request.Body = io.NopCloser(bodyStorage) + c.Request.Body = io.NopCloser(bodyStorage) - result, taskErr = relay.RelayTaskSubmit(c, relayInfo) - if taskErr == nil { - break - } + result, taskErr = relay.RelayTaskSubmit(c, relayInfo) + if taskErr == nil { + break taskOuter + } - if !taskErr.LocalError { - processChannelError(c, - *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, - common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), - types.NewOpenAIError(taskErr.Error, types.ErrorCodeBadResponseStatusCode, taskErr.StatusCode)) - } + if !taskErr.LocalError { + processChannelError(c, + *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, + common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), + types.NewOpenAIError(taskErr.Error, types.ErrorCodeBadResponseStatusCode, taskErr.StatusCode), + service.CaptureResponseBodyForLog(relayInfo)) + } - if !shouldRetryTaskRelay(c, channel.Id, taskErr, common.RetryTimes-retryParam.GetRetry()) { + if sameChannelAttempt < common.SameChannelRetryTimes && shouldSameChannelRetryTask(c, taskErr) { + delay := common.SameChannelRetryBackoff(sameChannelAttempt) + logger.LogInfo(c, fmt.Sprintf("same-channel retry %d/%d after %s (channel #%d, status %d)", + sameChannelAttempt+1, common.SameChannelRetryTimes, delay, channel.Id, taskErr.StatusCode)) + if !sleepForSameChannelRetry(c, delay) { + break taskOuter + } + sameChannelAttempt++ + continue + } + + if !shouldRetryTaskRelay(c, channel.Id, taskErr, common.RetryTimes-retryParam.GetRetry()) { + break taskOuter + } break } } diff --git a/controller/relay_retry_test.go b/controller/relay_retry_test.go new file mode 100644 index 00000000000..57f94347d57 --- /dev/null +++ b/controller/relay_retry_test.go @@ -0,0 +1,95 @@ +// ABOUTME: Unit tests for same-channel retry eligibility and backoff integration points. +// ABOUTME: Covers transient status codes, soft fails, and transport failure error codes. +package controller + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIsSameChannelRetryStatusCode(t *testing.T) { + assert.True(t, isSameChannelRetryStatusCode(0)) + assert.True(t, isSameChannelRetryStatusCode(429)) + assert.True(t, isSameChannelRetryStatusCode(http.StatusBadGateway)) + assert.True(t, isSameChannelRetryStatusCode(http.StatusServiceUnavailable)) + assert.False(t, isSameChannelRetryStatusCode(http.StatusBadRequest)) + assert.False(t, isSameChannelRetryStatusCode(http.StatusUnauthorized)) + assert.False(t, isSameChannelRetryStatusCode(http.StatusInternalServerError)) + assert.False(t, isSameChannelRetryStatusCode(http.StatusGatewayTimeout)) +} + +func TestShouldSameChannelRetryIncludesDoRequestFailed(t *testing.T) { + err := types.NewOpenAIError(errors.New("dial timeout"), types.ErrorCodeDoRequestFailed, http.StatusInternalServerError) + require.True(t, shouldSameChannelRetry(nil, err)) + + upstream500 := types.NewOpenAIError(errors.New("boom"), types.ErrorCodeBadResponseStatusCode, http.StatusInternalServerError) + require.False(t, shouldSameChannelRetry(nil, upstream500)) + + taskTransport := &dto.TaskError{Code: string(types.ErrorCodeDoRequestFailed), StatusCode: http.StatusInternalServerError} + require.True(t, shouldSameChannelRetryTask(nil, taskTransport)) + + taskUpstream500 := &dto.TaskError{Code: "upstream_error", StatusCode: http.StatusInternalServerError} + require.False(t, shouldSameChannelRetryTask(nil, taskUpstream500)) +} + +func TestSoftFailRetryPolicy(t *testing.T) { + oldMode := gin.Mode() + gin.SetMode(gin.TestMode) + t.Cleanup(func() { gin.SetMode(oldMode) }) + + capacity := types.NewOpenAIError(errors.New("at capacity"), types.ErrorCodeUpstreamCapacity, http.StatusServiceUnavailable) + empty := types.NewOpenAIError(errors.New("empty completed"), types.ErrorCodeEmptyResponse, http.StatusServiceUnavailable) + + base := httptest.NewRecorder() + cBase, _ := gin.CreateTestContext(base) + + require.True(t, shouldSameChannelRetry(cBase, capacity)) + require.False(t, shouldSameChannelRetry(cBase, empty)) + require.True(t, shouldRetry(cBase, capacity, 1)) + require.True(t, shouldRetry(cBase, empty, 1)) + require.False(t, shouldRetry(cBase, empty, 0)) + + // Affinity SkipRetryOnFailure is overridden for soft fails. + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Set("channel_affinity_skip_retry_on_failure", true) + require.True(t, service.ShouldSkipRetryAfterChannelAffinityFailure(c)) + require.True(t, shouldRetry(c, capacity, 1)) + require.True(t, shouldRetry(c, empty, 1)) + require.True(t, shouldSameChannelRetry(c, capacity)) + require.False(t, shouldSameChannelRetry(c, empty)) + + // Non-soft fail still respects affinity skip. + upstream500 := types.NewOpenAIError(errors.New("boom"), types.ErrorCodeBadResponseStatusCode, http.StatusInternalServerError) + require.False(t, shouldRetry(c, upstream500, 1)) + require.False(t, shouldSameChannelRetry(c, upstream500)) + + // Committed client payload blocks same-request soft-fail retries. + wCommitted := httptest.NewRecorder() + cCommitted, _ := gin.CreateTestContext(wCommitted) + cCommitted.Set("client_payload_written", true) + require.False(t, shouldRetry(cCommitted, capacity, 1)) + require.False(t, shouldRetry(cCommitted, empty, 1)) + require.False(t, shouldSameChannelRetry(cCommitted, capacity)) +} + +func TestSoftFailShouldNotDisableChannel(t *testing.T) { + old := common.AutomaticDisableChannelEnabled + common.AutomaticDisableChannelEnabled = true + t.Cleanup(func() { common.AutomaticDisableChannelEnabled = old }) + + capacity := types.NewOpenAIError(errors.New("at capacity"), types.ErrorCodeUpstreamCapacity, http.StatusServiceUnavailable) + empty := types.NewOpenAIError(errors.New("empty completed"), types.ErrorCodeEmptyResponse, http.StatusServiceUnavailable) + require.False(t, service.ShouldDisableChannel(capacity)) + require.False(t, service.ShouldDisableChannel(empty)) +} diff --git a/controller/topup_stripe.go b/controller/topup_stripe.go index bcae201ec4f..e827bcf38a1 100644 --- a/controller/topup_stripe.go +++ b/controller/topup_stripe.go @@ -367,6 +367,22 @@ func genStripeLink(referenceId string, customerId string, email string, amount i AllowPromotionCodes: stripe.Bool(setting.StripePromotionCodesEnabled), } + if setting.StripeAlipayEnabled || setting.StripeWechatPayEnabled { + params.PaymentMethodTypes = []*string{stripe.String("card")} + + if setting.StripeWechatPayEnabled { + params.PaymentMethodOptions = &stripe.CheckoutSessionPaymentMethodOptionsParams{ + WeChatPay: &stripe.CheckoutSessionPaymentMethodOptionsWeChatPayParams{ + Client: stripe.String("web"), + }, + } + params.PaymentMethodTypes = append(params.PaymentMethodTypes, stripe.String("wechat_pay")) + } + if setting.StripeAlipayEnabled { + params.PaymentMethodTypes = append(params.PaymentMethodTypes, stripe.String("alipay")) + } + } + if "" == customerId { if "" != email { params.CustomerEmail = stripe.String(email) diff --git a/docs/analysis/upstream-channel-retry-analysis.md b/docs/analysis/upstream-channel-retry-analysis.md new file mode 100644 index 00000000000..1623e30f450 --- /dev/null +++ b/docs/analysis/upstream-channel-retry-analysis.md @@ -0,0 +1,570 @@ +# 向上游渠道请求失败的重试逻辑分析 + +## 一、整体架构概览 + +系统采用**分层结构**处理上游 API 请求,重试逻辑贯穿于多个层级: + +``` +请求进入 → 中间件链 → Distribute(选择渠道) → Relay(重试循环) → 上游响应 + ↑ ↓ + 渠道选择与重试 失败时重试判断 + ↑ ↓ + CacheGetRandomSatisfiedChannel shouldRetry() +``` + +核心重试逻辑集中在以下文件中: + +| 文件 | 作用 | +|------|------| +| `controller/relay.go` | 主重试循环与重试判断逻辑 | +| `service/channel_select.go` | 渠道选择(含跨分组重试) | +| `model/channel_cache.go` / `model/ability.go` | 基于优先级的加权随机渠道选择 | +| `setting/operation_setting/status_code_ranges.go` | 可配置的重试/禁用状态码范围 | +| `types/error.go` | 错误类型体系与重试标记 | +| `service/channel.go` | 渠道自动禁用逻辑 | +| `service/channel_affinity.go` | 渠道亲和性与重试互操作 | + +--- + +## 二、重试配置体系 + +### 2.1 核心配置项 + +| 配置项 | 默认值 | 含义 | 配置方式 | +|--------|--------|------|----------| +| `RetryTimes` | **0** (不重试) | 最大重试次数 | UI: 设置 → 运维设置 → 通用设置 → 失败重试次数 | +| `RelayTimeout` | **0** (无超时) | 单次上游请求超时(秒) | 环境变量 `RELAY_TIMEOUT` | +| `StreamingTimeout` | **300** 秒 | 流式请求超时(秒) | 环境变量 `STREAMING_TIMEOUT` | +| `AutomaticDisableChannelEnabled` | **false** | 是否启用自动禁用渠道 | UI 运维设置 | +| `AutomaticEnableChannelEnabled` | **false** | 是否启用自动启用渠道 | UI 运维设置 | + +**存储路径**:`RetryTimes` 定义在 `common/constants.go:132`,通过 `model/option.go:502-503` 从数据库 `options` 表加载到运行时。 + +### 2.2 状态码重试范围 + +默认自动重试状态码范围定义在 `setting/operation_setting/status_code_ranges.go:21-29`: + +```go +var AutomaticRetryStatusCodeRanges = []StatusCodeRange{ + {Start: 100, End: 199}, // 1xx 信息类 + {Start: 300, End: 399}, // 3xx 重定向 + {Start: 401, End: 407}, // 认证/授权错误(不含400) + {Start: 409, End: 499}, // 客户端错误(不含408) + {Start: 500, End: 503}, // 服务端错误(不含504) + {Start: 505, End: 523}, // 服务端错误(不含524) + {Start: 525, End: 599}, // 服务端错误 +} +``` + +**永远不会重试的状态码**(硬编码,不可配置): + +| 状态码 | 含义 | +|--------|------| +| 400 | 请求格式错误 | +| 408 | Azure 处理超时 | +| 504 | 网关超时 | +| 524 | Cloudflare 超时 | + +**永远不会重试的错误码**(硬编码):`ErrorCodeBadResponseBody`(上游响应体解析失败)。 + +### 2.3 渠道自动禁用范围 + +默认自动禁用状态码范围: + +```go +var AutomaticDisableStatusCodeRanges = []StatusCodeRange{ + {Start: 401, End: 401}, // 仅 401 认证失败 +} +``` + +--- + +## 三、重试主循环 + +### 3.1 标准 Relay 重试 (`controller/relay.go`) + +入口函数 `Relay()` (第 67 行),重试循环位于第 189-235 行: + +```go +retryParam := &service.RetryParam{ + Ctx: c, + TokenGroup: relayInfo.TokenGroup, + ModelName: relayInfo.OriginModelName, + Retry: common.GetPointer(0), // 从 0 开始 +} + +for ; retryParam.GetRetry() <= common.RetryTimes; retryParam.IncreaseRetry() { + relayInfo.RetryIndex = retryParam.GetRetry() + + // 1. 获取渠道 + channel, channelErr := getChannel(c, relayInfo, retryParam) + if channelErr != nil { + break // 获取渠道失败,直接退出 + } + + // 2. 读取请求体(可重复读取) + addUsedChannel(c, channel.Id) + bodyStorage, bodyErr := common.GetBodyStorage(c) + + // 3. 执行转发 + switch relayFormat { + case RelayFormatOpenAIRealtime: newAPIError = relay.WssHelper(c, relayInfo) + case RelayFormatClaude: newAPIError = relay.ClaudeHelper(c, relayInfo) + case RelayFormatGemini: newAPIError = geminiRelayHandler(c, relayInfo) + default: newAPIError = relayHandler(c, relayInfo) + } + + // 4. 成功则直接返回 + if newAPIError == nil { + return + } + + // 5. 失败处理:记录错误、可能禁用渠道 + processChannelError(c, ...) + + // 6. 判断是否继续重试 + if !shouldRetry(c, newAPIError, common.RetryTimes - retryParam.GetRetry()) { + break + } +} +``` + +**关键特性**: +- 每次重试获取一个**不同的渠道**(通过 `RetryParam` 控制优先级层级) +- **无延迟重试**:重试之间没有 `sleep`/backoff,立即尝试下一个渠道 +- 如果 `common.RetryTimes = 0`(默认),循环体仅执行一次(`0 <= 0`),等效于不重试 + +### 3.2 Task 重试 (`controller/relay.go`) + +`RelayTask()` 函数处理异步任务类渠道(Midjourney、Suno、Video 等),重试循环位于第 510-558 行: + +```go +for ; retryParam.GetRetry() <= common.RetryTimes; retryParam.IncreaseRetry() { + // 支持锁定渠道(任务专属渠道) + if lockedCh, ok := relayInfo.LockedChannel.(*model.Channel); ok { + channel = lockedCh + // 仅第0次跳过 setup,后续重试需要重新 setup 上下文 + } else { + channel, channelErr = getChannel(c, relayInfo, retryParam) + } + + result, taskErr = relay.RelayTaskSubmit(c, relayInfo) + if taskErr == nil { + break // 成功 + } + + // 非本地错误时处理渠道错误 + if !taskErr.LocalError { + processChannelError(c, ...) + } + + if !shouldRetryTaskRelay(c, channel.Id, taskErr, ...) { + break + } +} +``` + +**与标准 Relay 的差异**: +- Task 重试使用独立的 `shouldRetryTaskRelay()` 判断函数 +- 支持 `LockedChannel`(任务锁定到特定渠道) +- `LocalError`(本地错误如请求体读取失败)不触发 `processChannelError` + +--- + +## 四、渠道选择与重试优先级 + +### 4.1 RetryParam 结构 + +```go +type RetryParam struct { + Ctx *gin.Context + TokenGroup string // 使用的分组 + ModelName string // 请求的模型名 + Retry *int // 当前重试次数(指针,支持跨循环修改) + resetNextTry bool // 是否跳过下次递增(跨分组切换时使用) +} +``` + +`IncreaseRetry()` 方法在每次循环迭代时自动递增 `Retry`,除非 `resetNextTry` 为 true。 + +### 4.2 优先级分层策略 + +渠道选择采用**优先级分层 + 加权随机**策略: + +1. 所有渠道按 `Priority` 降序排列,去重得到优先级层级列表 +2. 第 N 次重试 → 选择第 N 个优先级层级(N 超过层级数时使用最低优先级) +3. 在目标优先级层级内,按 `Weight` 加权随机选择一个渠道 +4. 权重为 0 的渠道也有均等机会(平滑因子 + 调整值 = 100) + +```go +// model/channel_cache.go 简化逻辑 +uniquePriorities := dedup(sortDesc(priorities)) +targetPriority := uniquePriorities[retry % len(uniquePriorities)] +targetChannels := filter(channels, priority == targetPriority) +selectedChannel := weightedRandom(targetChannels, weight) +``` + +**示例**:假设某分组下有 3 个优先级层级 [100, 50, 0],每个层级有若干渠道: +- 重试 #0 → 选择优先级 100 中的渠道 +- 重试 #1 → 选择优先级 50 中的渠道 +- 重试 #2 → 选择优先级 0 中的渠道 +- 重试 #3+ → 仍选择优先级 0 中的渠道(N ≥ 层级数,使用最低优先级) + +--- + +## 五、跨分组重试(Auto Group) + +当 Token 的分组设置为 `"auto"` 且启用了 `CrossGroupRetry`,系统会依次尝试多个自动分组。 + +### 5.1 状态跟踪 + +使用两个 Context Key 跟踪跨分组状态: + +| Context Key | 含义 | +|-------------|------| +| `ContextKeyAutoGroupIndex` | 当前正在重试的分组索引 | +| `ContextKeyAutoGroupRetryIndex` | 当前分组开始时的全局重试计数 | + +### 5.2 重试流程示例(2 个分组,各 2 个优先级,RetryTimes=3) + +``` +Retry=0: 分组A, 优先级0 → 失败 +Retry=1: 分组A, 优先级1 → 失败 +Retry=2: 分组A 优先级用完 → 切换到分组B, 优先级0 → 失败 +Retry=3: 分组B, 优先级1 → 失败/成功 +``` + +### 5.3 切换分组的关键逻辑 + +当 `priorityRetry >= common.RetryTimes` 时(当前分组所有优先级已用完): + +```go +// 准备切换到下一个分组 +common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, i+1) +param.SetRetry(0) // 重置重试计数器 +param.ResetRetryNextTry() // 跳过下次循环的递增(保持计数器为0) +``` + +--- + +## 六、重试判断逻辑(`shouldRetry`) + +### 6.1 标准 Relay 判断 (`controller/relay.go:318-348`) + +```go +func shouldRetry(c *gin.Context, err *types.NewAPIError, retryTimes int) bool +``` + +判断流程如下: + +``` + ┌─────────────┐ + │ 错误为 nil? │──是──→ 不重试 + └──────┬──────┘ + │否 + ┌──────▼──────┐ + │ 渠道亲和性 │──是──→ 不重试 + │ 跳过重试? │ + └──────┬──────┘ + │否 + ┌──────▼──────┐ + │ 渠道错误? │──是──→ 重试! + │(code前缀 │ + │ "channel:") │ + └──────┬──────┘ + │否 + ┌──────▼──────┐ + │ 标记跳过重试?│──是──→ 不重试 + │(skipRetry) │ + └──────┬──────┘ + │否 + ┌──────▼──────┐ + │ 剩余重试次数 │──≤0──→ 不重试 + │ > 0? │ + └──────┬──────┘ + │是 + ┌──────▼──────┐ + │ 指定了特定 │──是──→ 不重试 + │ 渠道ID? │ + └──────┬──────┘ + │否 + ┌──────▼──────┐ + │ 2xx 状态码? │──是──→ 不重试 + └──────┬──────┘ + │否 + ┌──────▼──────┐ + │ <100 或 │──是──→ 重试! + │ >599? │ + └──────┬──────┘ + │否 + ┌──────▼──────┐ + │ 总是跳过? │──是──→ 不重试 + │(504/524/ │ + │ BadResponse │ + │ Body) │ + └──────┬──────┘ + │否 + ┌──────▼──────┐ + │ 状态码在 │──是──→ 重试! + │ 重试范围内? │ + └──────┬──────┘ + │否 + ↓ + 不重试 +``` + +### 6.2 Task 重试判断 (`controller/relay.go:607-647`) + +`shouldRetryTaskRelay()` 的差异: + +| 条件 | 标准 Relay | Task Relay | +|------|-----------|------------| +| channel error | 重试 | N/A(Task 使用不同的错误结构) | +| 429 Too Many Requests | 通过状态码范围判断 | **明确重试** | +| 307 Temporary Redirect | 通过状态码范围判断 | **明确重试** | +| 5xx(除 504/524) | 通过状态码范围判断 | **明确重试** | +| 400 Bad Request | 不重试(不在范围内) | **明确不重试** | +| 408 Request Timeout | 不重试(不在范围内) | **明确不重试** | +| LocalError | N/A | **明确不重试** | + +--- + +## 七、错误类型体系 + +### 7.1 NewAPIError 结构 + +```go +type NewAPIError struct { + Err error // 底层错误 + RelayError any // 上游原始错误对象 + skipRetry bool // 是否跳过重试(关键标记) + recordErrorLog *bool // 是否记录错误日志 + errorType ErrorType // 错误类型 + errorCode ErrorCode // 错误码 + StatusCode int // HTTP 状态码 + Metadata json.RawMessage +} +``` + +### 7.2 关键判断方法 + +| 方法 | 判断逻辑 | 用途 | +|------|----------|------| +| `IsChannelError(err)` | `strings.HasPrefix(err.errorCode, "channel:")` | 判断是否为渠道级错误 | +| `IsSkipRetryError(err)` | `err.skipRetry == true` | 判断是否标记为不可重试 | +| `IsRecordErrorLog(err)` | `err.recordErrorLog != false` | 判断是否记录错误日志 | + +### 7.3 错误码分类与重试行为 + +| 错误类别 | 示例错误码 | 重试行为 | +|----------|-----------|----------| +| **渠道错误** (前缀 `channel:`) | `channel:no_available_key`, `channel:invalid_key`, `channel:response_time_exceeded`, `channel:param_override_invalid` 等 | **总是重试**(由 `IsChannelError()` 判断) | +| **中继错误** | `invalid_request`, `get_channel_failed`, `gen_relay_info_failed`, `do_request_failed`, `json_marshal_failed`, `model_price_error`, `count_token_failed` | **不重试**(均使用 `ErrOptionWithSkipRetry()` 标记) | +| **请求错误** | `read_request_body_failed`, `convert_request_failed`, `access_denied` | **不重试**(均使用 `ErrOptionWithSkipRetry()` 标记) | +| **配额错误** | `insufficient_user_quota`, `pre_consume_token_quota_failed` | **不重试** | +| **响应错误** | `bad_response_status_code`, `bad_response`, `bad_response_body`, `empty_response`, `prompt_blocked`, `model_not_found` | **取决于上游 HTTP 状态码**(如 429 则重试,400 则不重试) | + +### 7.4 上游错误处理流程 + +``` +上游 HTTP 响应 → service.RelayErrorHandler() → 解析响应体 + ├── 成功解析 → 提取 HTTP Status Code + │ └── 创建 NewAPIError(code="bad_response_status_code", statusCode=上游状态码) + └── 解析失败 → 创建 NewAPIError(code="bad_response_body") +``` + +上游返回的具体 HTTP 状态码决定了重试行为: +- 如上游返回 429 → 状态码在重试范围内 → **重试** +- 如上游返回 400 → 状态码不在重试范围内 → **不重试** +- 如上游返回 504 → 硬编码永远不重试 → **不重试** + +--- + +## 八、错误处理副作用 + +### 8.1 processChannelError (`controller/relay.go:350-395`) + +每次上游失败后调用,执行两项异步操作: + +1. **自动禁用渠道**(如果 `ShouldDisableChannel(err)` 且 `AutoBan` 为 true) +2. **记录错误日志**(如果 `ErrorLogEnabled` 为 true) + +### 8.2 渠道自动禁用判断 (`service/channel.go:45-65`) + +`ShouldDisableChannel()` 返回 true 的条件(需同时满足 `AutomaticDisableChannelEnabled = true`): + +``` + ┌─────────────────────┐ + │ 自动禁用功能已启用? │──否──→ 不禁用 + └──────────┬──────────┘ + │是 + ┌──────────▼──────────┐ + │ 渠道错误类型? │──是──→ 禁用! + │ ("channel:" 前缀) │ + └──────────┬──────────┘ + │否 + ┌──────────▼──────────┐ + │ 已标记跳过重试? │──是──→ 不禁用 + └──────────┬──────────┘ + │否 + ┌──────────▼──────────┐ + │ 状态码在禁用范围内? │──是──→ 禁用! + │ (默认仅 401) │ + └──────────┬──────────┘ + │否 + ┌──────────▼──────────┐ + │ 错误消息匹配禁用 │──是──→ 禁用! + │ 关键词?(AC自动机) │ + └──────────┬──────────┘ + │否 + ↓ + 不禁用 +``` + +### 8.3 DisableChannel 执行 + +```go +func DisableChannel(channelError types.ChannelError, reason string) { + // 只在 channelError.AutoBan == true 时执行 + model.UpdateChannelStatus(channelId, usingKey, common.ChannelStatusAutoDisabled, reason) + // 通知 root 用户 + NotifyRootUser(...) +} +``` + +渠道状态变更为 `ChannelStatusAutoDisabled = 3`(不同于手动禁用 `ChannelStatusManuallyDisabled = 2`)。 + +--- + +## 九、渠道亲和性(Channel Affinity)与重试 + +### 9.1 概念 + +渠道亲和性允许系统记住某类请求最近成功使用的渠道,并在后续同类请求中优先路由到同一渠道,提高缓存命中率。 + +### 9.2 与重试的互动 + +1. **分发阶段**:`Distribute()` 中间件先检查渠道亲和性缓存,如果命中且渠道可用,优先使用该渠道 +2. **禁用渠道时**:如果亲和渠道被禁用且规则中 `SkipRetryOnFailure = true`: + - **直接返回错误**,不进入重试循环 + - 由 `ShouldSkipRetryAfterChannelAffinityFailure()` 判断 +3. **重试循环中**:`shouldRetry()` 也检查 `ShouldSkipRetryAfterChannelAffinityFailure()`,防止在亲和性规则要求不重试时仍然重试 + +### 9.3 跳过重试的条件 + +```go +func ShouldSkipRetryAfterChannelAffinityFailure(c *gin.Context) bool { + // 1. 检查上下文标记 + if skipRetry, ok := c.Get(ginKeyChannelAffinitySkipRetry); ok { + return skipRetry.(bool) + } + // 2. 检查规则配置 + meta, ok := getChannelAffinityMeta(c) + return ok && meta.SkipRetry +} +``` + +--- + +## 十、请求体复用机制 + +在重试循环中,HTTP 请求体只能读取一次。系统通过 `common.GetBodyStorage(c)` 实现请求体复用: + +```go +func GetBodyStorage(c *gin.Context) (*BodyStorage, error) { + // 首次调用:读取请求体并缓存 + // 后续调用:返回缓存的副本 +} +``` + +重试时从缓存中恢复请求体: + +```go +c.Request.Body = io.NopCloser(bodyStorage) +``` + +如果请求体超过 `MAX_REQUEST_BODY_MB` 限制(默认 32MB),直接返回 413 错误且**不重试**。 + +--- + +## 十一、计费与退款 + +### 11.1 预扣费 + +在进入重试循环之前,系统执行预扣费: + +```go +newAPIError = service.PreConsumeBilling(c, priceData.QuotaToPreConsume, relayInfo) +if newAPIError != nil { return } // 预扣费失败,不进入重试 +``` + +### 11.2 失败退款 + +重试循环的 `defer` 块处理失败后退款: + +```go +defer func() { + if newAPIError != nil { + relayInfo.Billing.Refund(c) // 退还预扣额度 + } +}() +``` + +### 11.3 违规费用 + +如果上游返回特定错误(如 Grok CSAM),可能触发额外扣费: + +```go +service.ChargeViolationFeeIfNeeded(c, relayInfo, newAPIError) +``` + +--- + +## 十二、状态码范围配置解析 + +系统提供灵活的状态码范围配置语法,支持在 UI 中自定义: + +**格式**:逗号分隔的范围,如 `401-407,409-499,500-503` + +**解析规则** (`setting/operation_setting/status_code_ranges.go:117-169`): +1. 支持单个状态码:`401` +2. 支持范围:`401-407` +3. 自动合并相邻/重叠范围 +4. 自动排序 +5. 全角逗号 `,` 自动转换为半角 `,` +6. 状态码必须在 100-599 范围内 + +--- + +## 十三、总结与建议 + +### 13.1 当前重试机制特点 + +| 特性 | 描述 | +|------|------| +| **重试策略** | 基于优先级分层的多渠道重试,每次尝试不同渠道 | +| **重试延迟** | **无延迟**,立即重试下一个渠道 | +| **退避策略** | **无**(无 exponential backoff 或 jitter) | +| **熔断机制** | **无**(通过自动禁用渠道实现类似效果) | +| **渠道切换** | 每次重试切换到下一个优先级层级的渠道 | +| **跨分组** | auto 分组支持跨分组重试 | +| **亲和性** | 支持渠道亲和性,可配置跳过重试 | + +### 13.2 可改进的方向 + +1. **增加重试延迟**:当前无任何延迟,可能导致短时间内对多个上游同时发起请求。建议增加可配置的退避策略 +2. **增加熔断器**:考虑引入 Circuit Breaker 模式,当某个渠道连续失败时暂时跳过 +3. **增加重试延迟配置**:允许管理员配置重试间隔(ms),支持线性或指数退避 +4. **增强重试监控**:增加重试次数、重试原因的指标导出(Prometheus/OpenTelemetry) +5. **增加渠道健康度评分**:基于历史成功率动态调整渠道优先级 + +### 13.3 关键调试入口 + +| 目的 | 文件:行号 | 函数 | +|------|----------|------| +| 重试循环开始 | `controller/relay.go:189` | `Relay()` | +| 重试判断 | `controller/relay.go:318` | `shouldRetry()` | +| 渠道选择 | `service/channel_select.go:83` | `CacheGetRandomSatisfiedChannel()` | +| 渠道禁用判断 | `service/channel.go:45` | `ShouldDisableChannel()` | +| 状态码范围判断 | `setting/operation_setting/status_code_ranges.go:80` | `ShouldRetryByStatusCode()` | +| 上游错误处理 | `service/error.go` | `RelayErrorHandler()` | diff --git a/docs/plans/2026-07-19-empty-completed-affinity-failover-plan.md b/docs/plans/2026-07-19-empty-completed-affinity-failover-plan.md new file mode 100644 index 00000000000..94eb190a6e8 --- /dev/null +++ b/docs/plans/2026-07-19-empty-completed-affinity-failover-plan.md @@ -0,0 +1,392 @@ +# Soft Upstream Failure Failover (Empty Completed + Capacity/Null-Code + Affinity) Plan + +**Goal:** Make multi-channel failover work for **soft upstream failures** that today either look like success or fail without switching channels—especially empty completed answers, xAI capacity/`code: null` errors, and session affinity sticky binding. + +**Inputs:** + +1. **Empty completed:** Grok Responses stream ends `response.completed` with only a `reasoning` output (no `message` / tool call). Client sees empty/aborted answer; gateway treats as success. +2. **Capacity / null code:** + `Error Code null: The model is currently at capacity due to high demand. Please try again in a few minutes, or use a higher service tier for priority processing: https://docs.x.ai/developers/advanced-api-usage/priority-processing` + Flow aborts; multi-channel + affinity often do not recover the request. +3. Product need: multi-channel config must provide fault tolerance; affinity must not pin a session to a channel that just soft-failed. +4. Sibling plan: `docs/plans/2026-07-19-silent-upstream-termination-retry-plan.md` (mid-stream hang / scanner abort). **This plan absorbs its Task 3 capacity/null-code classification and affinity interaction**; hang detection remains in the sibling plan. + +**Assumptions:** + +- **Soft failure classes in scope (phase 1):** + | Class | Signal | Client-facing internal code | Retry shape | + |---|---|---|---| + | Empty completed | `status=completed` + no usable output | `ErrorCodeEmptyResponse` | Prefer **switch-channel** (503) | + | Capacity / overload | message keywords + null/empty/`null`/`unknown_error` code and/or 429/500/503 | `ErrorCodeUpstreamCapacity` (new) or normalized 503 | **Same-channel then switch** | + | Null-code non-capacity 5xx | code null + no capacity keywords + status 5xx | keep status; retry by status ranges | existing status retry | +- **Usable output** = non-empty assistant text **or** tool/function/custom-tool call. Reasoning-only is not usable. +- **In-request retry only if zero client payload flushed** for the attempt. Any flushed assistant SSE/JSON (incl. `reasoning_content`) commits the stream → no same-request switch. +- Soft failures **clear affinity** and **block affinity record**; they **override** `SkipRetryOnFailure` so sticky Codex/Claude-style rules cannot trap the session. +- Soft failures **must not auto-ban** the channel (capacity is transient; empty completed is model quirk). +- Prefer reusing `types.ErrorCodeEmptyResponse`. Add `types.ErrorCodeUpstreamCapacity = "upstream_capacity"`. +- Phase 1 paths: OpenAI Responses (+ chat-via-responses), shared `RelayErrorHandler` / stream error extraction, affinity middleware. Native chat empty stream follow-up if cheap. + +**Architecture:** Shared **soft-failure control plane** (client-commit flag, affinity unusable/clear, retry override, no auto-ban) plus two **classifiers**: + +1. **Usable-output / empty-completed** on final Responses payloads. +2. **Upstream error normalize** for OpenAI-style errors (`code: null` + capacity message → transient capacity). + +Handlers/error paths set the soft-failure error → controller retries (when zero-write) → affinity not sticky for next turn. + +**Tech Stack:** Go, Gin, `controller/relay.go`, `service/channel_affinity.go`, `service/error.go`, `service/relayconvert/internal/oai_responses/`, `types.NewAPIError`, testify. + +--- + +## Coverage matrix (what this plan supports) + +| Upstream symptom | Today (typical) | After this plan | +|---|---|---| +| Reasoning-only `completed` | Success; affinity sticky | Soft fail; clear affinity; switch if zero-write | +| xAI capacity, `code: null` | May surface error; affinity skip-retry can block switch; status/mapping edge cases | Classify capacity → 503-class retry; clear affinity; same-channel then switch | +| `code: null` + generic 500 body | Retry only if status in ranges; no message normalize | Keep status retry; optional message log; capacity keywords still classify | +| Mid-stream hang / EOF zero-chunk | Often `nil` error | **Sibling plan** (not this file) | +| True 400 validation | No retry | Unchanged | +| Usable tool_calls only | Success | Unchanged success | +| Capacity **after** client chunks written | Cannot safely switch | No in-request switch; clear affinity for **next** request | + +--- + +## File Map + +- Create: `service/relayconvert/internal/oai_responses/usable_output.go` (+ test) — empty-completed / usable output +- Create: `service/upstream_error_classify.go` (+ test) — capacity / null-code classification +- Modify: `types/error.go` — `ErrorCodeUpstreamCapacity` +- Modify: `relay/common/relay_info.go` — `ClientPayloadWritten`, `EmptyCompleted`, `AffinityUnusable` (and optional soft-fail reason) +- Modify: `relay/helper/common.go` and/or Responses stream send path — set client-commit on flush +- Modify: `relay/channel/openai/chat_via_responses.go` — empty completed + stream error capacity path +- Modify: `relay/channel/openai/relay_responses.go` / stream handlers that surface upstream error events +- Modify: `service/error.go` (`RelayErrorHandler`) — normalize null-code + capacity after parse +- Modify: `controller/relay.go` — retry eligibility; affinity skip override for soft-fail codes; no auto-ban +- Modify: `service/channel.go` (`ShouldDisableChannel`) — exclude empty/capacity soft codes +- Modify: `service/channel_affinity.go` + `middleware/distributor.go` — no record on unusable; clear on soft fail +- Modify: admin log generation — markers for empty/capacity/affinity +- Optional: `service/empty_response_guard.go` — short-window empty counter +- Test: `controller/relay_retry_test.go`, affinity tests, classify tests, handler tests + +## Out of Scope + +- TTFT weighted selection +- Full silent mid-stream hang implementation (sibling plan Tasks 1–2) +- Auto-ban on soft failure +- Buffer-until-usable for reasoning streams (later hardening) +- Client i18n copy redesign +- Changing default affinity rule definitions globally (only soft-fail overrides) + +--- + +## Desired behavior + +``` +attempt ends + ├─ transport / do_request_failed + │ → existing same-channel then switch + ├─ HTTP/SSE error body (OpenAI-style) + │ ├─ classify capacity (null code OK) → ErrorCodeUpstreamCapacity / 503 + │ │ → clear affinity; override SkipRetryOnFailure + │ │ → zero-write? same-channel then switch : no in-request switch + │ ├─ other 429/502/503 → existing (+ clear affinity on failure path) + │ └─ 400 validation → no retry + ├─ completed / 200 body or stream final + │ ├─ usable output → success; RecordChannelAffinity + │ └─ empty completed + │ → clear affinity; AffinityUnusable + │ → zero-write? ErrorCodeEmptyResponse 503 → switch-channel + │ → else committed stream; next request unbound + └─ mid-stream hang / zero-chunk EOF + → sibling silent-termination plan +``` + +--- + +## Tasks + +### Task 1: Usable-output classifier + +**Outcome:** Deterministic empty-completed detection for Responses payloads (Grok reasoning-only fixture). + +**Files:** + +- Create: `service/relayconvert/internal/oai_responses/usable_output.go` +- Create: `service/relayconvert/internal/oai_responses/usable_output_test.go` + +**Steps:** + +- [x] `IsUsableResponsesOutput(resp)` — non-empty extracted text **or** tool-like output items +- [x] `IsEmptyCompletedResponses(resp)` — status completed (or final completed event) **and** not usable +- [x] Incomplete/failed/cancelled ≠ empty-completed +- [x] Table tests: reasoning-only; message text; function_call only; empty message+reasoning; content_filter incomplete; nil + +**Validation:** + +- Run: `go test ./service/relayconvert/internal/oai_responses/ -count=1 -run 'Usable|EmptyCompleted'` +- Expected: PASS + +--- + +### Task 2: Upstream capacity / null-code classifier + +**Outcome:** xAI-style capacity messages with `code: null` become a stable retryable soft failure, not a dead-end “Error Code null” only. + +**Files:** + +- Create: `service/upstream_error_classify.go` +- Create: `service/upstream_error_classify_test.go` +- Modify: `types/error.go` — add `ErrorCodeUpstreamCapacity ErrorCode = "upstream_capacity"` +- Modify: `service/error.go` — apply classify in `RelayErrorHandler` after OpenAI error parse +- Modify: stream error branches that call `types.WithOpenAIError` (Responses `response.failed` / `response.error`, OpenAI chat stream error objects) to run the same normalize helper + +**Steps:** + +- [x] Named keyword constants (lowercase substring match), phase 1 fixed list at least: + - `at capacity` + - `high demand` + - `overloaded` + - `temporarily unavailable` + - `server is busy` + - `priority processing` (xAI capacity page hint; optional but include for this exact message) +- [x] `IsNullOrUnknownUpstreamCode(code any/string)` — true for nil, `""`, `"null"`, `"unknown_error"` +- [x] `ClassifyUpstreamOpenAIError(msg, code, httpStatus) SoftFailKind`: + - capacity if keywords match **and** (null/unknown code **or** status in {429,500,502,503} **or** status 0) + - else none +- [x] `NormalizeSoftUpstreamError(err *types.NewAPIError) *types.NewAPIError`: + - if capacity → set `errorCode=upstream_capacity`, prefer `StatusCode=503` (preserve original message) + - do not rewrite validation 400s that merely contain “try again” without capacity keywords + null/5xx context +- [x] Unit fixtures: + - exact user message + code null + status 500/503/429 → capacity + - code null + “invalid api key” + 401 → not capacity + - code null + empty message + 500 → not capacity (status retry only) + - normal 429 rate limit without keywords → unchanged (still retryable by status) + +**Validation:** + +- Run: `go test ./service/ -count=1 -run 'UpstreamError|Capacity|NullCode'` +- Expected: xAI capacity fixture classifies to `upstream_capacity` / 503 + +--- + +### Task 3: Client-commit + soft-fail flags on RelayInfo + +**Outcome:** Zero-write vs committed stream is explicit; affinity gate has a single unusable flag. + +**Files:** + +- Modify: `relay/common/relay_info.go` +- Modify: Responses→Chat send path / `relay/helper/common.go` as needed + +**Steps:** + +- [x] Fields: `ClientPayloadWritten bool`, `EmptyCompleted bool`, `AffinityUnusable bool`, optional `SoftFailReason string` +- [x] Do **not** use `ReceivedResponseCount` as client-commit (counts upstream events) +- [x] Set `ClientPayloadWritten` on successful client flush of converted assistant events +- [x] Helpers: `CanSoftFailRetry() bool` → `AffinityUnusable/soft fail && !ClientPayloadWritten` as needed by handlers + +**Validation:** + +- Run: package tests for relay/common or covered by handler tests +- Expected: existing tests pass + +--- + +### Task 4: Empty completed in Responses handlers + +**Outcome:** Reasoning-only completed is not a success for retry/affinity. + +**Files:** + +- Modify: `relay/channel/openai/chat_via_responses.go` (non-stream, buffered, stream) +- Modify: native responses handler if it finalizes full response objects +- Test: `relay/channel/openai/chat_via_responses_test.go` + +**Steps:** + +- [x] On final response: if empty completed → set flags, `ClearCurrentChannelAffinityCache`, admin reason +- [x] Zero-write → `return nil, NewOpenAIError(..., ErrorCodeEmptyResponse, 503)` +- [x] Already written → no in-request retry; still affinity clear/block; keep committed stream +- [x] Never “write error JSON then return nil” (avoid Gemini non-stream anti-pattern that blocks controller retry) + +**Validation:** + +- Run: `go test ./relay/channel/openai/ -count=1 -run 'EmptyCompleted|ResponsesToChat'` +- Expected: non-stream/buffered empty → 503 empty_response; committed stream does not double-write + +--- + +### Task 5: Wire capacity normalize into error + stream failure paths + +**Outcome:** Capacity/`code: null` errors always enter controller as retryable soft fails when HTTP/stream error is returned. + +**Files:** + +- Modify: `service/error.go` +- Modify: stream error handling in `relay/channel/openai/chat_via_responses.go`, `relay-openai.go` (if stream error object parsed), xAI adaptor only if it custom-parses errors +- Test: service + relay tests with capacity body fixtures + +**Steps:** + +- [x] After building `*types.NewAPIError` from upstream error JSON, call `NormalizeSoftUpstreamError` +- [x] Responses stream `response.failed` / `response.error` with capacity message → same normalize +- [x] Ensure showBodyWhenFail logging still works +- [x] If upstream returns capacity as **HTTP 200** with error object only (rare), detect in body handlers and convert to error (only if observed; otherwise document as follow-up) + +**Validation:** + +- Run: `go test ./service/ ./relay/channel/openai/ -count=1 -run 'Capacity|RelayError|Normalize'` +- Expected: fixture body produces `upstream_capacity` and 503 + +--- + +### Task 6: Controller retry policy for soft fails + no auto-ban + +**Outcome:** Soft fails retry across channels; affinity skip does not trap; channels not auto-disabled. + +**Files:** + +- Modify: `controller/relay.go` (`shouldRetry`, `shouldSameChannelRetry`) +- Modify: `service/channel.go` (`ShouldDisableChannel`) +- Test: `controller/relay_retry_test.go` + +**Steps:** + +- [x] Define soft-fail code set: `ErrorCodeEmptyResponse`, `ErrorCodeUpstreamCapacity` +- [x] `shouldRetry`: if soft-fail code and retry budget and no `specific_channel_id` → **true even when** `ShouldSkipRetryAfterChannelAffinityFailure` +- [x] Status: soft fails use 503 so existing ranges also match; keep explicit code branch as belt-and-suspenders +- [x] `shouldSameChannelRetry`: + - `upstream_capacity` → **true** (transient overload; backoff may help) + - `empty_response` → **false** (prefer other channel) +- [x] On soft-fail error path: `ClearCurrentChannelAffinityCache` if not already cleared by handler +- [x] `ShouldDisableChannel`: return false for soft-fail codes (even if status 503 would otherwise match disable rules—today disable is mostly 401/keywords; still hard-exclude) +- [x] Exclude failed channel for remaining attempts via existing retry exclude machinery + +**Validation:** + +- Run: `go test ./controller/ ./service/ -count=1 -run 'Retry|EmptyResponse|Capacity|Disable'` +- Expected: + - capacity → same-channel eligible + switch eligible + - empty → switch only + - affinity skip true + soft fail → still retry + - soft fail → no auto-disable + +--- + +### Task 7: Affinity record gate (session sticky fix) + +**Outcome:** Soft failures never refresh sticky binding; next request reselects. + +**Files:** + +- Modify: `middleware/distributor.go` post-`c.Next()` record condition +- Modify: `service/channel_affinity.go` defensive checks in `RecordChannelAffinity` +- Test: affinity unit tests + +**Steps:** + +- [x] Record only if HTTP < 400 **and not** `AffinityUnusable` / gin `channel_affinity_unusable` +- [x] Soft-fail handlers set gin key so middleware sees it after `c.Next()` +- [x] Failure path already may not record (status ≥ 400); capacity after normalize is 503 → no record; still **clear** sticky key so a previous success does not keep routing here +- [x] Admin info: `empty_completed` / `upstream_capacity` / `affinity_cleared` / `client_payload_written` + +**Validation:** + +- Run: `go test ./service/ -count=1 -run 'ChannelAffinity'` +- Expected: after soft fail, preferred affinity miss on next lookup + +--- + +### Task 8 (optional): Short-window empty/capacity counters + +**Outcome:** Repeated soft fails on same channel/model temporarily reduce selection pressure without auto-ban. + +**Files:** + +- Create: `service/soft_fail_guard.go` (+ test) +- Hook: retry exclude or preferred-affinity skip + +**Steps:** + +- [ ] Separate counters or shared window for empty vs capacity (capacity may recover faster) +- [ ] Named constants for window/threshold +- [ ] Skip if product wants minimal diff after Tasks 1–7 + +**Validation:** + +- Run: `go test ./service/ -count=1 -run 'SoftFailGuard'` +- Expected: threshold hit → temporary skip; TTL clears + +--- + +### Task 9: Observability + plan cross-links + +**Outcome:** Ops can grep soft-fail failover without full upstream dumps. + +**Files:** + +- Modify: log/admin info paths +- Modify: `docs/plans/2026-07-19-silent-upstream-termination-retry-plan.md` note that Task 3 capacity is implemented here (when done) + +**Steps:** + +- [x] Log line: `soft fail class=empty_completed|upstream_capacity channel=#... model=... client_written=bool` +- [x] Admin markers only enums/bools; no full reasoning text +- [x] Checklist update on both plans + +**Validation:** + +- Unit or manual assert keys present on fixtures + +--- + +## Final Validation + +- Run: `go test ./service/relayconvert/internal/oai_responses/ ./service/ ./relay/channel/openai/ ./controller/ -count=1` +- Expected: PASS + +**Scenario matrix:** + +| # | Setup | Expected | +|---|---|---| +| A | Channel A reasoning-only completed; B healthy; zero-write | Retry B; client gets B; affinity → B or empty until B success | +| B | Channel A xAI capacity `code: null`; B healthy; zero-write | Same-channel backoff optional then B; affinity cleared | +| C | Affinity SkipRetryOnFailure rule active + A soft-fails | Still switch (override) | +| D | Soft fail after client already received chunks | No in-request switch; affinity cleared; next request free | +| E | Tool-call-only success | No soft fail; affinity records | +| F | Real 400 validation | No retry | + +## Failure Behavior + +- Capacity keyword false positive → extra retries; mitigate with keyword+code/status gates. +- Capacity false negative → user still stuck; add keywords from production samples. +- Empty false positive (marks usable empty) → still broken failover; fixture locks reasoning-only. +- All channels soft-fail → exhaust retries; return final soft-fail error to client. +- Affinity cache delete fails → still block record via flag; log error. + +## Privacy and Security + +- Client error message can keep upstream capacity text (already user-visible) or genericize; do not add secrets. +- Admin markers: class enums only; no prompt dump beyond existing request-detail toggles. + +## Rollout Notes + +- No DB migration. +- Expect more multi-channel `use_channel` chains on free/capacity-limited providers (xAI free tier, etc.). +- Optional kill-switch later: `SoftFailFailoverEnabled` (default on) if ops need it. +- Implement **Tasks 2+5+6+7** even if empty-completed slips, if capacity is the higher prod pain—order by pain: capacity classify → affinity clear/override → empty completed. + +## Risks and Mitigations + +- **Affinity SkipRetryOnFailure** — explicit soft-fail override in `shouldRetry`. +- **503 auto-ban** — exclude soft-fail codes in `ShouldDisableChannel`. +- **Status mapping** rewriting 503→other — apply normalize **after** mapping or re-apply code-based retry branch. +- **Reasoning already streamed** — cannot switch mid-request; affinity clear is the session fix. +- **Overlap with silent-termination plan** — share client-commit flag naming; do not duplicate capacity Task 3 there after this ships. + +## Open Questions + +- Keep upstream capacity message text for clients vs generic `model at capacity`? **Default: keep upstream message, stable internal code.** +- Same-channel attempts for capacity when only one channel exists? **Default: yes, honor `SameChannelRetryTimes`.** +- Kill-switch for first deploy? **Default: no.** +- Is capacity ever returned as HTTP 200 + error object on your channels? If yes, add explicit body path in Task 5; if only non-2xx, HTTP handler path is enough. diff --git a/docs/plans/2026-07-19-silent-upstream-termination-retry-plan.md b/docs/plans/2026-07-19-silent-upstream-termination-retry-plan.md new file mode 100644 index 00000000000..749a4aa812e --- /dev/null +++ b/docs/plans/2026-07-19-silent-upstream-termination-retry-plan.md @@ -0,0 +1,241 @@ +# Silent Upstream Termination Retry Plan + +**Goal:** Make “upstream dies without a usable error code” (e.g. `code: null`, capacity message, mid-stream hang-up) correctly enter the existing retry path **when safe**, without double-writing streams or inventing credits. + +**Context:** Same-channel exponential backoff + exclude/switch-channel already exist. Gaps remain when: + +1. HTTP body has `error.code: null` (message still meaningful). +2. Stream opens as `200 + text/event-stream` then aborts; `DoResponse` often returns `nil` so controller never retries. +3. Stream already flushed bytes to the client — in-request channel switch is unsafe. + +**Out of scope (this plan):** + +- Changing priority / weight selection (TTFT plan is separate). +- Exponential backoff formula changes (already shipped on `feat/same-channel-backoff-retry`). +- Client-visible product copy rewrites beyond mapping to a stable internal error. + +**Assumptions:** + +- **Zero-write** means no successful write of upstream payload/SSE chunk to the client for this attempt (`ReceivedResponseCount == 0` and no equivalent “already flushed” flag). +- Once any chunk is written, **no same-request retry** (neither same-channel nor switch-channel). +- Capacity-like messages are treated as **503-class transient** for retry eligibility only; client status may still reflect upstream when already committed to a response. +- Defaults stay conservative: feature pieces can ship behind clear helpers; keyword list is small and optional to configure later. + +--- + +## Desired behavior + +``` +upstream attempt ends + ├─ transport failure (do_request_failed) + │ → same-channel backoff → then exclude + switch (existing) + ├─ HTTP error body (code null/unknown OK) + │ ├─ status 429/502/503 → same-channel then switch + │ ├─ capacity/overload keywords → treat as 503-class → same-channel then switch + │ └─ 400/401/etc → no retry + ├─ stream / body path + │ ├─ zero-write + abnormal end (timeout / scanner_error / panic / 0-chunk capacity) + │ │ → return NewAPIError to controller → same-channel then switch + │ └─ already wrote to client + │ → DO NOT retry; surface error event if possible; log stream_status + └─ success +``` + +--- + +## File Map (planned) + +| Area | Files | Change | +|---|---|---| +| Stream failure → error | `relay/channel/openai/relay-openai.go`, other stream handlers as needed, possibly small helper in `relay/helper/` | After `StreamScannerHandler`, if abnormal + zero-write → return `*types.NewAPIError` | +| Stream status | `relay/common/stream_status.go` | Optional helpers: `ShouldFailAttempt()`, `IsZeroWriteFailure()` | +| Capacity keywords | `setting/operation_setting/` or `service/` | Normalize message → transient retry class | +| Same-channel eligibility | `controller/relay.go` (`shouldSameChannelRetry*`) | Include capacity-normalized / stream-abort error codes | +| Switch eligibility | existing `shouldRetry` / status ranges | Prefer status 503 after normalize; avoid expanding ranges blindly | +| Tests | `relay/helper/stream_scanner_test.go`, new handler tests, `controller/relay_retry_test.go` | Zero-write fail; written-no-retry; capacity null code | +| Config (optional phase) | `common/constants.go`, `model/option.go`, Routing Reliability UI | Keyword list / enable flag if product wants tunability | +| Docs | this plan | Living checklist | + +--- + +## Tasks + +### Task 1: Zero-write stream failure surfaces as relay error + +**Outcome:** Abnormal stream ends that never wrote to the client return an error to `controller.Relay`, so same-channel backoff and channel switch can run. + +**Files:** + +- Modify: `relay/common/stream_status.go` (helpers) +- Modify: `relay/channel/openai/relay-openai.go` (`OaiStreamHandler` end path) +- Modify: other high-traffic stream handlers that currently always `return usage, nil` (Claude/Gemini/etc. only if same pattern) +- Test: stream scanner / openai stream tests + +**Steps:** + +- [ ] Define “abnormal end” using `StreamStatus`: + - fail: `timeout`, `scanner_error`, `panic`, `ping_fail` + - soft-normal today: `eof` without `done` — decide explicitly: + - **phase 1 recommend:** treat `eof` + `ReceivedResponseCount == 0` as failure + - `eof` + count > 0: no retry (already wrote or incomplete but committed) + - keep client disconnect (`client_gone`) as **non-retry** (local) +- [ ] Add helper e.g. `StreamAttemptFailed(info) (bool, errorCode, statusCode)` +- [ ] In `OaiStreamHandler` (and siblings): if failed + zero-write → `return nil, types.NewOpenAIError(..., ErrorCodeBadResponse / new ErrorCodeStreamAborted, 503 or 500)` +- [ ] Do **not** change success path when `done`/`eof` with content and no hard errors + +**Validation:** + +- Unit: zero-chunk timeout/EOF → non-nil error +- Unit: at least one chunk then timeout → nil or non-retry local error; controller would not switch if already written (Task 2) + +--- + +### Task 2: Already-written responses never switch channel + +**Outcome:** In-request retry is impossible after any client write of upstream stream data. + +**Files:** + +- Modify: `controller/relay.go` and/or stream handlers +- Possibly set a context flag when first SSE/object is written (`helper.StringData` / `ObjectData`) + +**Steps:** + +- [ ] Introduce a clear signal: `info.ReceivedResponseCount > 0` **or** new `info.ClientBytesCommitted` set on first successful client write +- [ ] `shouldRetry` / `shouldSameChannelRetry`: if committed → false +- [ ] Stream path: if committed + later failure → return error that carries `ErrOptionWithSkipRetry()` (or dedicated check) +- [ ] Log `stream_status` remains (already in log other) + +**Validation:** + +- Table test: committed=true + 503-class error → no same-channel, no switch +- Integration-style handler test: first chunk then abort → no second channel in `use_channel` + +--- + +### Task 3: Capacity / null-code message normalization + +**Status:** Implemented in `docs/plans/2026-07-19-empty-completed-affinity-failover-plan.md` (Tasks 2/5/6). Hang detection remains in this plan. + +**Outcome:** Messages like “model is currently at capacity… try again” retry as transient overload even when `code` is null. + +**Files:** + +- Create: `service/upstream_error_classify.go` (+ test) **or** under `setting/operation_setting/` +- Modify: `service/error.go` (`RelayErrorHandler`) and/or stream error extraction +- Modify: `controller/relay.go` eligibility if classification is error-code based + +**Steps:** + +- [x] Keyword list (lowercase match), phase 1 fixed constants, e.g.: + - `at capacity`, `high demand`, `overloaded`, `temporarily unavailable`, `server is busy`, `priority processing` +- [x] When OpenAI error code is null/empty/`null`/`unknown_error` **or** status is 500/503/429, and message matches → classify as **transient capacity** +- [x] Map classification to: + - internal `ErrorCode` e.g. `upstream_capacity` (new) **or** keep status 503 + - `shouldSameChannelRetry` true + - `shouldRetry` true (status 503 or explicit branch) +- [x] Do not disable channel on capacity (ensure not in auto-disable keywords/status) + +**Validation:** + +- `code: null` + capacity message + HTTP 500 → same-channel eligible +- `code: null` + validation message + HTTP 400 → not eligible + +--- + +### Task 4: Align same-channel eligibility with classified errors + +**Outcome:** Same-channel backoff covers transport + capacity + 429/502/503 without treating all 500s as same-channel. + +**Files:** + +- Modify: `controller/relay.go` (`shouldSameChannelRetry`, task variant) +- Modify: `controller/relay_retry_test.go` + +**Steps:** + +- [ ] Keep allowlist: network/`do_request_failed`, 429, 502, 503 +- [ ] Add: `upstream_capacity` / stream zero-write abort codes from Tasks 1–3 +- [ ] Explicitly **exclude** generic 500 without classification +- [ ] Task relay: mirror classification for `do_request_failed` + capacity + +**Validation:** + +- Existing `TestShouldSameChannelRetryIncludesDoRequestFailed` still passes +- New cases for capacity and stream-abort codes + +--- + +### Task 5: Regression tests and rollout checklist + +**Outcome:** Safe to enable defaults without behavior surprise when `SameChannelRetryTimes=0` (switch-only still gets zero-write errors). + +**Files:** + +- Tests from Tasks 1–4 +- Optional: admin docs / comment in Routing Reliability that stream retries only apply pre-write + +**Steps:** + +- [ ] Matrix: + + | Case | SameChannelRetryTimes=0 | >0 | + |---|---|---| + | zero-write stream abort | switch if RetryTimes allows | same-channel then switch | + | written stream abort | no retry | no retry | + | capacity null code HTTP 500 | switch (after classify) | same-channel then switch | + | plain 400 | no | no | + +- [ ] Manual smoke: xAI-style capacity message (non-stream + stream if reproducible) +- [ ] Confirm pre-consume refund still runs when controller receives error (existing defer) + +**Validation:** + +- `go test ./controller ./relay/... ./service -count=1` (scoped packages) +- No change when stream succeeds normally + +--- + +### Task 6 (optional later): Configurable keywords / UI + +**Outcome:** Ops can tune capacity keywords without deploy. + +**Only if product asks after phase 1.** + +- Option keys + Routing Reliability fields +- i18n via `add-missing-keys.mjs` workflow + +--- + +## Implementation order + +1. Task 1 — zero-write fail up +2. Task 2 — commit barrier (no retry after write) +3. Task 3 — capacity normalize +4. Task 4 — eligibility glue +5. Task 5 — tests / smoke +6. Task 6 — optional config UI + +**Do not implement in this session** (user request: plan + todos only). + +--- + +## Risks + +| Risk | Mitigation | +|---|---| +| False positive on incomplete but valid streams | Gate on zero-write + explicit end reasons | +| Double billing on stream retry | Only retry when zero-write; refund path already on controller error | +| Treating all 500 as same-channel | Classification required; generic 500 stays switch-only or no-retry per status ranges | +| Provider-specific capacity wording | Start small keyword set; optional Task 6 | +| Handlers other than OpenAI still swallow errors | Inventory stream handlers that always return nil | + +--- + +## Success criteria + +- [ ] Zero-write abnormal stream ends reach `controller` retry loop. +- [ ] Any client write for the attempt blocks further in-request retries. +- [ ] `code: null` capacity-style failures retry like 503-class transients. +- [ ] Successful streams and client disconnects unchanged. +- [ ] Defaults do not enlarge same-channel retries beyond existing allowlist + new classified codes. diff --git a/docs/plans/2026-07-19-ttft-weighted-channel-selection-plan.md b/docs/plans/2026-07-19-ttft-weighted-channel-selection-plan.md new file mode 100644 index 00000000000..ec4fec89e7d --- /dev/null +++ b/docs/plans/2026-07-19-ttft-weighted-channel-selection-plan.md @@ -0,0 +1,433 @@ +# TTFT-Weighted Channel Selection Implementation Plan + +**Goal:** Bias same-priority channel picks toward lower first-token latency (TTFT) by multiplying configured weight with a latency factor, without changing priority tiers or the existing retry/exclude semantics. + +**Inputs:** Product discussion: record per-channel average first-token latency and apply extra weight so faster channels receive higher selection share within the same priority. + +**Assumptions:** + +- Feature is **off by default** to avoid sudden traffic shifts. +- Phase 1 uses an **in-process EWMA** of successful stream TTFT keyed by **`(channel_id, origin_model_name)`** (e.g. `12` + `gpt-4o`). Model name means the client/origin model string (`OriginModelName`), not a numeric meta id. No new DB table in phase 1. +- Existing `perf_metrics` stays model+group (marketplace UI). Channel routing does **not** read that table hot-path. +- `channels.response_time` (channel test) is **not** used for routing in phase 1. +- Formula: `effectiveWeight = max(1, round(baseWeight * factor))` where `baseWeight` is current configured weight (same +10 / smoothing rules as today after the effective weight is computed). +- Cold start / insufficient samples: `factor = 1.0`. +- Only **successful stream** requests with a real first response contribute samples. +- Priority order, same-priority exhaustion, same-channel backoff, and auto/cross-group selection are **unchanged**. + +**Architecture:** On successful stream completion, record TTFT (`FirstResponseTime - StartTime`) into a small in-memory EWMA store keyed by channel (and model when present). During same-priority weighted random selection in cache and DB paths, convert each candidate’s configured weight into `effectiveWeight` via `factor = clamp(refTtft / avgTtft, minFactor, maxFactor)` using a reference TTFT (peer median or configured tau). Faster channels get `factor > 1`, slower get `factor < 1`. + +**Tech Stack:** Go, existing Gin relay path, `model` channel selection (`channel_cache.go`, `ability.go`), optional admin options via `model/option.go` + Routing Reliability UI. + +--- + +## File Map + +- Create: `service/channel_ttft.go` — EWMA store, observe API, factor calculation +- Create: `service/channel_ttft_test.go` — EWMA, cold start, clamp, effective weight +- Create: `setting/operation_setting/ttft_routing.go` — defaults and accessors for routing knobs (or place vars in `common/constants.go` if matching RetryTimes style) +- Modify: `pkg/perf_metrics/metrics.go` / `RecordRelaySample` callers path — pass channel id into a **routing** observer (prefer calling `service.ObserveChannelTtft` from success settle paths next to existing `RecordRelaySample`) +- Modify: `service/text_quota.go`, `service/quota.go` (and any other success `RecordRelaySample` sites) — after successful settle, observe TTFT for routing +- Modify: `model/channel_cache.go` — use effective weight when summing/selecting same-priority candidates +- Modify: `model/ability.go` — same for DB selection path +- Modify: `model/option.go` — load/persist config keys if admin-tunable +- Modify: `web/default/.../routing-reliability-section.tsx` (+ types/registry/defaults) — optional UI toggles +- Test: unit tests above; optional table tests around weight selection with fixed RNG seed if extractable + +--- + +## Tasks + +### Task 1: Config knobs and defaults + +**Outcome:** Runtime config exists with safe defaults; feature disabled unless enabled. + +**Files:** + +- Create: `setting/operation_setting/ttft_routing.go` (preferred) **or** Modify: `common/constants.go` +- Modify: `model/option.go` (if using OptionMap admin options) + +**Steps:** + +- [ ] Add: + - `TtftRoutingEnabled` default `false` + - `TtftRoutingMinSamples` default `20` + - `TtftRoutingEwmaAlpha` default `0.2` (or fixed constant if not exposed) + - `TtftRoutingMinFactor` default `0.5` + - `TtftRoutingMaxFactor` default `2.0` + - `TtftRoutingRefMs` default `500` (tau / reference TTFT when peer median unavailable) +- [ ] Wire OptionMap get/set like `RetryTimes` if exposed in admin UI +- [ ] Document: only affects **intra-priority** weight, not priority ladder + +**Validation:** + +- Run: `go test ./setting/operation_setting -count=1` (or package housing config) +- Expected: package builds; defaults load + +### Task 2: In-memory channel TTFT EWMA store + +**Outcome:** Process can observe TTFT and query avg/sample count per channel (and model). + +**Files:** + +- Create: `service/channel_ttft.go` +- Create: `service/channel_ttft_test.go` + +**Steps:** + +- [ ] Define store with `sync.Map` or sharded maps; entry fields: `ewmaMs float64`, `samples int64`, `updatedAt` +- [ ] Key: always **`channelId + "\x00" + originModelName`** when model is non-empty; channel-only key only as empty-model fallback +- [ ] `ObserveChannelTtft(channelId int, model string, ttftMs int64)`: + - ignore if `!TtftRoutingEnabled` (optional: still record when disabled for warm cache — **assume record only when enabled** to save CPU) + - ignore `ttftMs < 0` + - EWMA: first sample sets value; later `ewma = alpha*ttft + (1-alpha)*ewma` + - increment samples +- [ ] `GetChannelTtft(channelId int, model string) (avgMs float64, samples int64, ok bool)` +- [ ] `LatencyFactor(avgMs float64, samples int64, peerRefMs float64) float64`: + - if `samples < MinSamples` or `avgMs <= 0` → `1.0` + - `ref = peerRefMs` if `>0` else `TtftRoutingRefMs` + - `factor = ref / avgMs` + - clamp to `[MinFactor, MaxFactor]` +- [ ] `EffectiveWeight(baseWeight int, factor float64) int`: + - if baseWeight <= 0, treat as 0 before smoothing (preserve current zero-weight semantics) + - `out = int(math.Round(float64(baseWeight) * factor))` + - if baseWeight > 0 && out < 1 → `1` + +**Validation:** + +- Run: `go test ./service -run 'Ttft|LatencyFactor|EffectiveWeight' -count=1` +- Expected: cold start factor 1; faster than ref → factor > 1; clamp works; EWMA moves toward new samples + +### Task 3: Record TTFT on successful stream completion + +**Outcome:** Live traffic updates the store with real first-token latency. + +**Files:** + +- Modify: `service/text_quota.go` (success `RecordRelaySample`) +- Modify: `service/quota.go` (other success paths) +- Optionally Modify: `pkg/perf_metrics/metrics.go` only if centralizing; **prefer service-side observe** to avoid changing marketplace schema + +**Steps:** + +- [ ] Next to existing success `perfmetrics.RecordRelaySample(relayInfo, true, ...)`: + - if stream and `relayInfo.HasSendResponse()` (or equivalent) + - `ttft := FirstResponseTime.Sub(StartTime).Milliseconds()` + - `service.ObserveChannelTtft(relayInfo.ChannelId, relayInfo.OriginModelName, ttft)` +- [ ] Do **not** observe on failed requests in phase 1 +- [ ] Do **not** observe non-stream / no first response +- [ ] Guard nil `ChannelMeta` / zero channel id + +**Validation:** + +- Run: `go test ./service -count=1` +- Expected: existing tests pass; new unit test can call Observe + Get without relay + +### Task 4: Apply effective weight in channel selection (cache + DB) + +**Outcome:** Same-priority random pick prefers lower TTFT when enabled and samples sufficient. + +**Files:** + +- Modify: `model/channel_cache.go` — `GetRandomSatisfiedChannel` +- Modify: `model/ability.go` — `GetChannel` +- Test: `model/channel_user_agent_test.go` or new `model/channel_ttft_weight_test.go` + +**Steps:** + +- [ ] Extract helper in `model` or call `service` carefully (avoid import cycles): + - If `model` → `service` cycle: put store in `pkg/channellatency` or `common`/`setting` package used by both + - **Preferred package layout to avoid cycles:** `pkg/channellatency` (store + factor), imported by `service` (observe) and `model` (weight) +- [ ] For each same-priority candidate: + - `base := channel.GetWeight()` (cache) or `ability.Weight` (DB) + - `avg, n, _ := store.Get(...)` + - `factor := LatencyFactor(avg, n, ref)` where `ref` is peer median of candidates with enough samples, else `TtftRoutingRefMs` + - `eff := EffectiveWeight(base, factor)` when enabled; else `base` +- [ ] Sum / random selection uses `eff` instead of raw weight +- [ ] Keep existing smoothing (+10 on DB path, cache zero-weight smoothing) applied to **effective** weights consistently +- [ ] Peer median: among candidates with `samples >= MinSamples`, median of avgMs; if none, use `TtftRoutingRefMs` + +**Validation:** + +- Run: `go test ./model -run 'Ttft|Weight|GetRandomSatisfiedChannel|GetChannel' -count=1` +- Expected: with fixed store values, lower TTFT channel selected more often under equal base weights (statistical or deterministic by injecting factor via test hooks) + +### Task 5: Admin UI (optional but recommended) + +**Outcome:** Operators can enable and tune without rebuild. + +**Files:** + +- Modify: `web/default/src/features/system-settings/models/routing-reliability-section.tsx` +- Modify: types / section-registry / index defaults +- Locales via i18n script (en/zh/fr/ja/ru/vi) + +**Steps:** + +- [ ] Fields: enabled, min samples, min/max factor, ref ms +- [ ] Descriptions: English source keys; note only same-priority share is affected +- [ ] Classic UI optional parity under Operation settings + +**Validation:** + +- Run: `cd web/default && bun run i18n:sync` +- Expected: keys present in all locales; form validates ranges + +### Task 6: Observability + +**Outcome:** Debuggable in production. + +**Files:** + +- Modify: selection debug logs optional in `GetRandomSatisfiedChannel` when `DebugEnabled` +- Optional: admin-only metrics dump endpoint **out of scope** unless needed + +**Steps:** + +- [ ] When debug on: log channel id, base weight, avg TTFT, factor, effective weight for chosen channel +- [ ] No PII; only channel ids and numeric stats + +**Validation:** + +- Manual: enable debug + feature; confirm log line on request + +--- + +## Final Validation + +- Run: `go test ./pkg/channellatency ./service ./model -count=1` (adjust package names to final layout) +- Expected: all pass +- Run: `go test ./controller -run 'TestIsSameChannelRetryStatusCode|TestSameChannel' -count=1` +- Expected: pass (no regression on retry path) +- Manual smoke: + 1. Feature off → selection identical to weight-only + 2. Feature on, two equal-weight same-priority channels, seed A with low TTFT / B high → A receives more traffic over N requests + 3. Samples below min → no bias + 4. Retry exclude path still exhausts same priority before falling back + +--- + +## Failure Behavior + +- Store missing entry → factor 1.0, no error +- Negative / absurd TTFT → ignore sample +- All candidates cold → pure configured weights +- Feature disabled mid-flight → stop applying factors; EWMA may stop updating (per Task 2 choice) +- Import cycle risk → fix by extracting `pkg/channellatency` rather than coupling model↔service + +## Privacy and Security + +- TTFT is operational telemetry (latency only); no prompt/content +- Admin options require existing admin option permissions +- Do not expose per-channel TTFT store on public APIs in phase 1 + +## Rollout Notes + +1. Deploy with `TtftRoutingEnabled=false` +2. Optionally enable recording-only variant later; phase 1 can enable observe+route together +3. Enable on a canary node; compare channel mix and p95 TTFT +4. Start with mild clamps (`0.5–2.0`) and `MinSamples=20` +5. Multi-instance: EWMA is **per process** in phase 1 (each node learns locally). Phase 2 may share via Redis if needed + +## Risks and Mitigations + +| Risk | Mitigation | +|------|------------| +| Feedback loop (fast channel gets more load and slows down) | Clamp factors; optional periodic decay of EWMA; monitor | +| New channels starve or monopolize | Cold start factor=1 until MinSamples | +| Multi-instance inconsistent weights | Document local EWMA; Redis later | +| Stream-only bias ignores non-stream quality | Document scope; phase 2 may add total latency | +| Import cycles | `pkg/channellatency` isolation | + +## Out of Scope + +- Changing priority tiers based on TTFT +- Using marketplace `perf_metrics` aggregates for routing +- Using `channels.response_time` test field +- Persistence of EWMA across restarts (phase 1) +- Non-stream latency weighting +- Auto-disable based on TTFT + +## Open Questions + +- **Decided:** stats key is `(channel_id, origin_model_name)` where model is the string name (e.g. `gpt-4o`), not a numeric id. +- Whether to update EWMA when feature disabled (warm cache): plan assumes only when enabled. +- Redis shared store: deferred to phase 2. + +--- + +# 基于首字耗时(TTFT)的渠道加权选路 — 实现计划(中文) + +**目标:** 在**同一优先级内**,根据渠道平均首字耗时(TTFT)对配置权重做乘性修正,让更快的渠道获得更高被选概率;不改变优先级阶梯,也不改变现有排除/同级换渠/同渠退避语义。 + +**输入:** 产品讨论——记录渠道首字平均耗时,并在现有权重上额外加权,耗时越少占比越高。 + +**假设:** + +- 功能**默认关闭**,避免流量突变。 +- 第一期用**进程内 EWMA**,统计键为 **`(channel_id, origin_model_name)`**(如 `12` + `gpt-4o`)。此处「模型」指客户端原始模型名字符串,不是数字主键。不新建 DB 表。 +- 现有 `perf_metrics` 仍是 **model+group** 维度,仅供模型广场;选渠热路径**不读**该表。 +- 第一期**不使用** `channels.response_time`(渠道测试耗时)。 +- 公式:`effectiveWeight = max(1, round(baseWeight * factor))`(在现有 +10 / smoothing 规则之前或之后需统一,推荐先算 effective 再走现有 smoothing)。 +- 冷启动 / 样本不足:`factor = 1.0`。 +- 仅**成功且流式、已产生首包**的请求写入样本。 +- 优先级顺序、同优先级耗尽、同渠退避、auto/跨分组逻辑**保持不变**。 + +**架构:** 成功流式结算时,将 `FirstResponseTime - StartTime` 写入内存 EWMA。同优先级加权随机时,用 +`factor = clamp(refTtft / avgTtft, minFactor, maxFactor)` +把配置权重变为有效权重。更快的渠道 `factor > 1`,更慢 `factor < 1`。`ref` 优先用同批候选中足够样本的 TTFT 中位数,否则用配置参考值(如 500ms)。 + +**技术栈:** Go、现有 relay 结算路径、`model/channel_cache.go` 与 `model/ability.go` 选渠、可选运营配置与 Routing Reliability UI。 + +--- + +## 文件映射 + +- 新建:`pkg/channellatency/`(推荐)或 `service/channel_ttft.go` — EWMA 与 factor(**注意 model↔service 循环依赖,推荐独立 pkg**) +- 新建:对应 `*_test.go` +- 修改:成功结算处(`service/text_quota.go`、`service/quota.go` 等 `RecordRelaySample` 旁)— 调用 Observe +- 修改:`model/channel_cache.go`、`model/ability.go` — 使用 effective weight +- 修改:`model/option.go` + 前端 Routing Reliability — 可选开关与参数 +- 测试:unit + 选渠权重行为 + +--- + +## 任务 + +### 任务 1:配置项与默认值 + +**结果:** 运行时配置齐全,默认关闭。 + +**步骤:** + +- [ ] `TtftRoutingEnabled=false` +- [ ] `TtftRoutingMinSamples=20` +- [ ] `TtftRoutingMinFactor=0.5` / `MaxFactor=2.0` +- [ ] `TtftRoutingRefMs=500` +- [ ] `TtftRoutingEwmaAlpha=0.2`(可固定不暴露) +- [ ] 文档说明:只影响同优先级占比 + +**验证:** 配置包测试通过;默认关闭。 + +### 任务 2:内存 EWMA 存储 + +**结果:** 可 Observe / 查询 avg 与样本数;可算 factor 与 effectiveWeight。 + +**步骤:** + +- [ ] Key:固定为 **`channelId + originModelName`**(model 空时才退化为仅 channel) +- [ ] 首样本直接赋值;其后 EWMA 更新 +- [ ] 样本不足或无效 → factor=1 +- [ ] `factor = clamp(ref/avg, min, max)` +- [ ] `effectiveWeight` 对正 base 至少为 1 + +**验证:** `go test` 覆盖冷启动、夹紧、EWMA 收敛。 + +### 任务 3:成功流式请求写入 TTFT + +**结果:** 真实流量更新存储。 + +**步骤:** + +- [ ] 在成功 `RecordRelaySample` 旁:`HasTtft` 时 Observe +- [ ] 失败 / 非流式 / 无首包:不写 +- [ ] channelId 为 0:跳过 + +**验证:** service 测试通过。 + +### 任务 4:缓存与 DB 选渠应用有效权重 + +**结果:** 开启且样本足够时,同优先级更快渠道更容易被选中。 + +**步骤:** + +- [ ] 两处选渠路径统一 effective weight +- [ ] peer ref = 候选中足够样本的 avg 中位数,否则 RefMs +- [ ] 保持现有 smoothing 语义 +- [ ] 用 pkg 拆分避免 import cycle + +**验证:** model 测试证明等权下低 TTFT 渠道被选概率更高(或注入 factor 的确定性测试)。 + +### 任务 5:管理端 UI(建议) + +**结果:** 可开关与调参。 + +**步骤:** + +- [ ] Routing Reliability 增加字段 +- [ ] i18n 六语种脚本写入 + +**验证:** `bun run i18n:sync`;表单校验范围合法。 + +### 任务 6:可观测性 + +**结果:** Debug 日志可打印 base/avg/factor/eff。 + +**验证:** 打开 debug 后请求可见日志。 + +--- + +## 最终验证 + +- `go test` 覆盖 latency pkg、service、model +- 功能关:行为与纯 weight 一致 +- 功能开:等权同优先级,快渠道占比更高 +- 样本不足:无偏向 +- 重试排除:仍同级耗尽再降级 + +## 失败行为 + +- 无统计:factor=1,不报错 +- 非法样本:丢弃 +- 全员冷启动:退回配置权重 +- 循环依赖:抽到 `pkg/channellatency` + +## 隐私与安全 + +- 仅延迟数值,无内容 +- 管理配置沿用 option 权限 +- 第一期不对公网暴露 per-channel TTFT API + +## 上线说明 + +1. 默认关闭部署 +2. 金丝雀开启,对比渠道分布与 p95 TTFT +3. 先用温和夹紧与 MinSamples=20 +4. 第一期 EWMA **按进程本地**;多副本不一致可接受,二期再 Redis + +## 风险与缓解 + +| 风险 | 缓解 | +|------|------| +| 正反馈(越快越打越多再变慢) | factor 夹紧、监控、可衰减 | +| 新渠道样本不足 | 冷启动 factor=1 | +| 多实例权重不一致 | 文档说明;二期 Redis | +| 仅流式有 TTFT | 文档范围;二期可加总延迟 | + +## 不在范围 + +- 用 TTFT 改 priority +- 用模型广场 `perf_metrics` 选渠 +- 用渠道测试 `response_time` +- EWMA 持久化 +- 非流式延迟加权 +- 因 TTFT 自动禁用渠道 + +## 待决问题 + +- **已决:** 统计维度为 `(channel_id, origin_model_name)`(模型名为 `gpt-4o` 这类字符串) +- 关闭功能时是否仍采样(计划:仅开启时采样) +- 是否二期 Redis 共享 + +--- + +## Implementation Order (both languages) + +1. Config +2. `pkg/channellatency` store + tests +3. Observe on success stream +4. Wire effective weight into cache + DB selection +5. Admin UI + i18n +6. Debug logs + rollout + +**Estimated effort:** ~1–2 days for phase 1 backend; +0.5 day UI/i18n. diff --git a/docs/plans/2026-07-20-opencode-go-channel-plan.md b/docs/plans/2026-07-20-opencode-go-channel-plan.md new file mode 100644 index 00000000000..3bba269e209 --- /dev/null +++ b/docs/plans/2026-07-20-opencode-go-channel-plan.md @@ -0,0 +1,219 @@ +# OpenCode Go Channel Implementation Plan + +**Goal:** Add a new relay channel `OpenCode Go` (type 59) that proxies chat requests to the opencode-go subscription endpoint `https://opencode.ai/zen/go/v1`, routing per-model between its two upstream protocols (OpenAI Chat Completions and Anthropic Messages). + +**Inputs:** Analysis of the opencode source tree (`/home/julian/workspace/source/opencode`, esp. `packages/opencode/test/tool/fixtures/models-api.json` lines 33606+ and `packages/core/src/session/runner/model.ts`) and of new-api channel/adaptor architecture (`relay/channel/`, `constant/channel.go`, `relay/relay_adaptor.go`). + +**Assumptions:** + +- Auth is a plain API key (the `OPENCODE_API_KEY` from the opencode.ai console). Verified in opencode source: only the main `opencode` provider has an OAuth device flow; `opencode-go` is API-key-only. No OAuth/refresh logic is needed. +- The per-model protocol mapping is static for v1 (hardcoded set, mirroring the opencode remote config fixture). Opencode maintains it remotely; new-api cannot fetch it at runtime. +- Anthropic-protocol models are assumed to reject `/chat/completions` upstream (opencode's client routes them to `/messages` exclusively), so per-model routing is required rather than client-format routing. +- Out of scope for v1: OpenAI Responses API (`/v1/responses`), Gemini-format client requests, embeddings, images, audio, rerank, async tasks. The fixture shows chat-only models; these return `not implemented` errors, matching the moonshot adaptor precedent. +- No default entries in `setting/ratio_setting/model_ratio.go`: upstream is a flat-fee subscription, and unknown models fall back to the global default ratio. Admins price per their own policy. (Verified: none of the 19 model IDs collide with existing ratio entries.) + +**Architecture:** + +One new adaptor package `relay/channel/opencodego` implementing `channel.Adaptor`. For each request it resolves the upstream protocol from the mapped upstream model name (`info.UpstreamModelName`, falling back to the request model) against a hardcoded Anthropic-protocol model set, then delegates URL building, header setup, request conversion, and response handling to the existing `openai.Adaptor` or `claude.Adaptor`. This mirrors the moonshot/deepseek delegation pattern, except those route by client `RelayFormat` while opencode-go routes by model protocol. Response conversion back to the client's format is already handled by the delegated handlers (`info.FinalRequestRelayFormat` + `relayconvert.ConvertResponse`, see `relay/channel/openai/relay-openai.go:291` and `relay/channel/claude/adaptor.go:125`). + +Upstream facts (verified against opencode source): + +- Upstream endpoint prefix: `https://opencode.ai/zen/go/v1` +- Channel stored default base (platform convention, no `/v1`): `https://opencode.ai/zen/go` — adaptor and model-fetch append `/v1` +- OpenAI protocol: `POST {storedBase}/v1/chat/completions`, header `Authorization: Bearer `, SSE stream, supports `stream_options.include_usage` +- Anthropic protocol: `POST {storedBase}/v1/messages`, headers `x-api-key: ` + `anthropic-version`, SSE stream +- Model list: `GET {storedBase}/v1/models` with Bearer auth (generic new-api model-fetch path; no channel-specific case) +- Client headers the opencode TUI sends to zen/go (`packages/opencode/src/session/llm/request.ts:185-204`, consumed by the zen server `packages/console/app/src/routes/zen/util/handler.ts:108-111`): + - `x-opencode-session` — FUNCTIONAL: drives `stickyId` for sticky upstream-provider selection (`handler.ts:151`), i.e. same session → same provider → better prompt-cache locality. Server fallback when absent: workspaceID (per API key), then client IP. + - `x-opencode-request`, `x-opencode-client`, `x-opencode-project` — observability only (server `logger.metric`); the server strips all four `x-opencode-*` headers before forwarding to the real provider (`handler.ts:238-241`). + - `User-Agent: opencode/` — metrics only. All four headers are optional server-side (`?? ""`). +- Anthropic-protocol models: `minimax-m2.5`, `minimax-m2.7`, `minimax-m3`, `qwen3.5-plus`, `qwen3.6-plus`, `qwen3.7-plus`, `qwen3.7-max` +- OpenAI-protocol models: `deepseek-v4-flash`, `deepseek-v4-pro`, `glm-5`, `glm-5.1`, `glm-5.2`, `kimi-k2.5`, `kimi-k2.6`, `kimi-k2.7-code`, `mimo-v2-omni`, `mimo-v2-pro`, `mimo-v2.5`, `mimo-v2.5-pro` + +**Tech Stack:** Go 1.22+ backend (Gin, existing relay/channel framework), React 19 default frontend (`web/default`, Rsbuild), React 18 classic frontend (`web/classic`). + +--- + +## File Map + +- Create: `relay/channel/opencodego/constants.go` — model list, channel name, Anthropic-protocol model set +- Create: `relay/channel/opencodego/adaptor.go` — `Adaptor` implementing `channel.Adaptor` via per-model delegation +- Create: `relay/channel/opencodego/adaptor_test.go` — protocol routing, auth headers, conversion delegation tests +- Modify: `constant/channel.go` — `ChannelTypeOpenCodeGo = 59`, `ChannelBaseURLs` entry, `ChannelTypeNames[59]` +- Modify: `constant/api_type.go` — `APITypeOpenCodeGo` before `APITypeDummy` +- Modify: `common/api_type.go` — `ChannelTypeOpenCodeGo` → `APITypeOpenCodeGo` mapping case +- Modify: `relay/relay_adaptor.go` — import + `GetAdaptor` case returning `&opencodego.Adaptor{}` +- Modify: `relay/common/relay_info.go` — add type 59 to `streamSupportedChannels` (line ~345) +- Modify: `web/default/src/features/channels/constants.ts` — `CHANNEL_TYPES[59]`, `CHANNEL_TYPE_DISPLAY_ORDER`, `MODEL_FETCHABLE_TYPES` +- Modify: `web/default/src/features/channels/lib/channel-type-config.ts` — `CHANNEL_TYPE_CONFIGS[59]` entry +- Modify: `web/classic/src/constants/channel.constants.js` — channel option `{ value: 59, label: 'OpenCode Go' }` + +## Tasks + +### Task 1: Backend channel type registration + +**Outcome:** Type 59 exists end-to-end in backend constants and the adaptor factory; project compiles. + +**Files:** + +- Modify: `constant/channel.go` +- Modify: `constant/api_type.go` +- Modify: `common/api_type.go` +- Modify: `relay/relay_adaptor.go` +- Modify: `relay/common/relay_info.go` + +**Steps:** + +- [ ] `constant/channel.go`: add `ChannelTypeOpenCodeGo = 59` after `ChannelTypeAdvancedCustom = 58` (keep `ChannelTypeDummy` last — it is a count sentinel, line 59). Add `"OpenCode Go"` to `ChannelTypeNames` (map at line ~147). Append `https://opencode.ai/zen/go` (no `/v1`) to `ChannelBaseURLs` at index 59 (array at line ~68; verify length/index alignment — it is indexed by type ID). Aligns with other OpenAI-style channels whose stored base omits `/v1`. +- [ ] `constant/api_type.go`: add `APITypeOpenCodeGo` in the iota block immediately before `APITypeDummy` (line ~40). +- [ ] `common/api_type.go`: add `case constant.ChannelTypeOpenCodeGo: return constant.APITypeOpenCodeGo` to the `ChannelType2APIType` switch. +- [ ] `relay/common/relay_info.go`: add `constant.ChannelTypeOpenCodeGo: true` to `streamSupportedChannels` (map at line ~345). Rationale: the OpenAI-protocol endpoint supports `stream_options.include_usage`; this flag lets new-api auto-inject it. The Claude-protocol path reports usage natively per-event. +- [ ] `relay/relay_adaptor.go`: import `relay/channel/opencodego`, add `case constant.APITypeOpenCodeGo: return &opencodego.Adaptor{}` to `GetAdaptor` (switch at lines 54-100). This file will not compile until Task 2 lands; implement Tasks 1-2 together before building. + +**Validation:** + +- Run: `go build ./...` +- Expected: compiles (after Task 2 package exists). + +### Task 2: `opencodego` adaptor package + +**Outcome:** The adaptor relays OpenAI-format and Claude-format chat requests to the correct upstream protocol per model, with correct auth headers, streaming and non-streaming responses, and usage/billing intact. + +**Files:** + +- Create: `relay/channel/opencodego/constants.go` +- Create: `relay/channel/opencodego/adaptor.go` + +**Steps:** + +- [ ] Every new Go file starts with the required 2-line `ABOUTME:` header comment (per project convention). +- [ ] `constants.go`: + - `ChannelName = "opencodego"` + - `ModelList` = the 19 fixture model IDs listed in the header (order: group by vendor, e.g. deepseek, glm, kimi, mimo, minimax, qwen). + - `anthropicModels` = `map[string]bool` with the 7 Anthropic-protocol model IDs. Name the constant/set clearly, e.g. `anthropicProtocolModels`. + - Helper `isAnthropicProtocolModel(model string) bool` — exact match against the set. +- [ ] `adaptor.go` — `type Adaptor struct{}` implementing all `channel.Adaptor` methods (`relay/channel/adapter.go:14-38`): + - Model resolution helper: prefer `info.UpstreamModelName` when `info.ChannelMeta != nil && info.UpstreamModelName != ""`, else fall back to the request's model field — copy the pattern from `moonshot.getUpstreamModelName` (`relay/channel/moonshot/adaptor.go:93-99`). + - `Init(info)`: no-op (matches moonshot/deepseek). + - `GetRequestURL(info)`: if the resolved model is Anthropic-protocol → `{info.ChannelBaseUrl}/v1/messages`; else → `{info.ChannelBaseUrl}/v1/chat/completions`. (`info.ChannelBaseUrl` defaults to `https://opencode.ai/zen/go` via `ChannelBaseURLs` — no `/v1`; the adaptor appends `/v1` like other OpenAI-style channels. Frontend/admin only enter the host path without `/v1`.) + - `SetupRequestHeader(c, req, info)`: call `channel.SetupApiRequestHeader(info, c, req)`; then branch by resolved model protocol: + - Anthropic: `req.Set("x-api-key", info.ApiKey)`; set `anthropic-version` from the incoming client header, defaulting to `2023-06-01`; call `claude.CommonClaudeHeadersOperation(c, req, info)` for `anthropic-beta` passthrough (mirrors `claude/adaptor.go:84-94`). + - OpenAI: `req.Set("Authorization", "Bearer "+info.ApiKey)`. + - User-Agent (both protocols): passthrough of the incoming client header only — if `c.Request.Header.Get("User-Agent")` is non-empty, `req.Set("User-Agent", ...)` with that value; if empty, set nothing (do not impersonate `opencode/`, do not inject a `new-api/*` UA). + - Header override/passthrough needs no adaptor work: the channel-level `header_override` setting (`Channel.HeaderOverride`, JSON with `{api_key}` / `{client_header:}` placeholders, `*` and `re:`/`regex:` passthrough rules) is applied by `processHeaderOverride` inside the shared HTTP layer (`relay/channel/api_request.go:190`, called from the `DoApiRequest` family AFTER `SetupRequestHeader`, `api_request.go:319-325`), and explicit overrides win over passthrough and over adaptor-set headers. Using `channel.DoApiRequest` inherits it automatically, and the drawer's `header_override` field is type-agnostic (`channel-mutate-drawer.tsx:3945`) — no frontend change. Note the skip list (`api_request.go:67+`) never wildcard-passes credential headers (`authorization`, `x-api-key`), so auth stays adaptor-controlled. + - Session-affinity header (mirrors the opencode client behavior above): the adaptor carries a per-request `sessionAffinity` string field. Populate it in `ConvertOpenAIRequest` from `request.PromptCacheKey` (`dto/openai_request.go:76`) and in `ConvertClaudeRequest` from `metadata.user_id` (`dto/claude.go:14-15,230`; unmarshal `Metadata` only when non-empty). In `SetupRequestHeader`, resolve with priority: (1) `a.sessionAffinity` from the request body; (2) the incoming client's `x-session-id` header (`c.Request.Header.Get("x-session-id")`) — tried ONLY when (1) is empty; (3) if both empty, set nothing. When resolved non-empty, `req.Set("x-opencode-session", value)` for BOTH protocols. Never fabricate a value (a random per-request ID would defeat stickiness and is worse than the server's workspaceID fallback). The struct field works because the relay flow calls Convert* before `DoApiRequest` on the same adaptor instance (verified in `api_request.go:307-319`). Note the symmetry: the opencode client itself sends `X-Session-Id` when talking to non-opencode providers (`request.ts:197`), so opencode pointed at new-api gets its session affinity forwarded to zen/go automatically. Do NOT send `x-opencode-request` / `x-opencode-client` / `x-opencode-project` — they are metrics-only on the server. + - `ConvertOpenAIRequest`: Anthropic-protocol → delegate `claude.Adaptor{}.ConvertOpenAIRequest(c, info, request)` (converts to `dto.ClaudeRequest` via relayconvert). OpenAI-protocol → return `request, nil` unchanged. + - `ConvertClaudeRequest`: Anthropic-protocol → return `request, nil` (native passthrough). OpenAI-protocol → delegate `openai.Adaptor{}.ConvertClaudeRequest(c, info, request)` (converts to OpenAI chat via relayconvert; verified at `openai/adaptor.go:56` and registry converter `anthropic_messages_to_openai_chat_completions`, `service/relayconvert/request_registry.go:76`). + - `ConvertOpenAIResponsesRequest`, `ConvertGeminiRequest`, `ConvertAudioRequest`, `ConvertImageRequest`, `ConvertEmbeddingRequest`, `ConvertRerankRequest`: return `nil, errors.New("not implemented")` (moonshot precedent). Rationale: opencode-go exposes chat only; see Assumptions. + - `DoRequest`: `return channel.DoApiRequest(a, c, info, requestBody)`. + - `DoResponse`: branch by resolved model protocol (NOT `info.RelayFormat` — this is the key difference from moonshot): + - Anthropic → `claude.Adaptor{}.DoResponse(c, resp, info)` (sets `info.FinalRequestRelayFormat = types.RelayFormatClaude`; its handlers convert to the client format). + - OpenAI → `openai.Adaptor{}.DoResponse(c, resp, info)` (its handlers convert OpenAI responses/SSE to the client format, incl. Claude, via `relayconvert.ConvertResponse`, `relay-openai.go:291`). + - `GetModelList` / `GetChannelName`: return `ModelList` / `ChannelName`. +- [ ] Follow AGENTS.md backend rules: JSON via `common.*` wrappers (the adaptor itself should not need any), early returns, no single-use helpers beyond the two documented above (protocol resolution is a durable domain concept used by 5+ methods). + +**Validation:** + +- Run: `go build ./... && go vet ./relay/channel/opencodego/` +- Expected: clean build, no vet findings. + +### Task 3: Backend adaptor tests + +**Outcome:** Table-driven tests lock in the per-model routing contract and conversion delegation. + +**Files:** + +- Create: `relay/channel/opencodego/adaptor_test.go` + +**Steps:** + +- [ ] Use `testify/require` for setup/fatal, `testify/assert` for value checks (project convention). +- [ ] `GetRequestURL` table test: for each of the 7 Anthropic-protocol models expect `{base}/messages`; for a sample of OpenAI-protocol models (`glm-5.2`, `kimi-k2.6`, `deepseek-v4-pro`) and an unknown model expect `{base}/chat/completions`. Cover `UpstreamModelName` set vs. unset (fallback to request model). +- [ ] `SetupRequestHeader` test: Anthropic model → `x-api-key` present, `anthropic-version` defaults to `2023-06-01`, no `Authorization` header; OpenAI model → `Authorization: Bearer `, no `x-api-key`. +- [ ] Session-affinity tests: `ConvertOpenAIRequest` with `prompt_cache_key: "abc"` then `SetupRequestHeader` → `x-opencode-session: abc` (both protocols); `ConvertClaudeRequest` with `metadata.user_id` set → same header; body fields absent but client sends `x-session-id: sid` → header is `sid`; body fields present AND client sends `x-session-id` → body value wins; neither source present → header absent. +- [ ] User-Agent tests: client sends `User-Agent: my-agent/1.0` → upstream header is `my-agent/1.0` (both protocols); client sends no UA → upstream header unset (empty). +- [ ] `ConvertOpenAIRequest` test: for `minimax-m3` the result is `*dto.ClaudeRequest`; for `glm-5.2` the request passes through unchanged (same pointer). +- [ ] `ConvertClaudeRequest` test: for `minimax-m3` passthrough; for `glm-5.2` the result is `*dto.GeneralOpenAIRequest` with model preserved. +- [ ] Unsupported methods (`ConvertGeminiRequest`, `ConvertAudioRequest`, `ConvertImageRequest`, `ConvertEmbeddingRequest`, `ConvertOpenAIResponsesRequest`) return errors. +- [ ] Do NOT re-test response conversion internals (already covered by `claude`/`openai` adaptor tests); a delegation smoke test asserting `FinalRequestRelayFormat == types.RelayFormatClaude` after Anthropic-path `DoResponse` against an `httptest` server returning a minimal Claude JSON response is sufficient. + +**Validation:** + +- Run: `go test ./relay/channel/opencodego/... -count=1` +- Expected: all tests pass. + +### Task 4: Default frontend (`web/default`) channel UI + +**Outcome:** Admins can create/edit an OpenCode Go channel with correct defaults, icon, and the "fetch models" button works against `{base}/models`. + +**Files:** + +- Modify: `web/default/src/features/channels/constants.ts` +- Modify: `web/default/src/features/channels/lib/channel-type-config.ts` + +**Steps:** + +- [ ] `constants.ts`: add `59: 'OpenCode Go'` to `CHANNEL_TYPES` (map at lines 24-90); insert `59` into `CHANNEL_TYPE_DISPLAY_ORDER` near other subscription/coding channels (e.g. after 57 Codex); add `59` to `MODEL_FETCHABLE_TYPES` (set at line 382 — backend model fetch hits `{baseURL}/models` with Bearer, which opencode-go supports). +- [ ] `channel-type-config.ts`: add `CHANNEL_TYPE_CONFIGS[59]` with `name: CHANNEL_TYPES[59]`, a fitting existing icon key (check available icons in `channel-utils.ts`; reuse a generic/subscription-style icon if no opencode-specific one exists), `defaultBaseUrl: 'https://opencode.ai/zen/go/v1'`, and `hints.key` / `hints.baseUrl` text noting the key comes from the opencode.ai console (`OPENCODE_API_KEY`). +- [ ] No changes to `channel-mutate-drawer.tsx`: plain API-key channel, no special credential UI needed (unlike Codex). No `TYPE_TO_KEY_PROMPT` entry needed (single key, not a delimited format). +- [ ] If any new user-facing string is introduced in JSX, wrap in `t('...')` and run `bun run i18n:sync` per the i18n skill; prefer reusing existing hint mechanics so no new keys are required. + +**Validation:** + +- Run: `cd web/default && bun run build && bun run lint` +- Expected: build succeeds, no new lint errors. + +### Task 5: Classic frontend (`web/classic`) channel option + +**Outcome:** Classic theme also lists OpenCode Go in the channel type dropdown. + +**Files:** + +- Modify: `web/classic/src/constants/channel.constants.js` + +**Steps:** + +- [ ] Add `{ key: '59', value: 59, label: 'OpenCode Go', ... }` following the exact shape of the existing Codex entry (line ~188: verify surrounding fields such as `color`, `icon` before writing). + +**Validation:** + +- Run: `cd web/classic && bun run build` +- Expected: build succeeds. + +## Final Validation + +- Run: `go build ./... && go test ./relay/... -count=1` +- Expected: backend builds; relay tests pass. +- Run: `cd web/default && bun run build && cd ../classic && bun run build` +- Expected: both frontends build. +- Manual smoke (requires a real `OPENCODE_API_KEY`): create channel type 59, fetch models, then (1) `POST /v1/chat/completions` with `glm-5.2` stream+non-stream, (2) `POST /v1/messages` with `minimax-m3` stream+non-stream, (3) cross-format: `POST /v1/chat/completions` with `minimax-m3` and `POST /v1/messages` with `glm-5.2`. Expect correct answers, correct SSE framing per client format, and usage/quota deduction in logs. + +## Failure Behavior + +- Unknown/new model ID (not in either hardcoded set): routed to OpenAI Chat Completions by default. If opencode later adds an Anthropic-only model, requests fail with an upstream 4xx until the set is updated — surfaced to the client as a normal upstream error, no silent misbehavior. +- Invalid/expired API key: upstream 401/403 propagated through the standard error path. +- Requests for unsupported modes (embeddings/images/audio/rerank/responses/gemini): explicit `not implemented` error before any upstream call. +- Upstream protocol mismatch (e.g. Anthropic-protocol model removed upstream): standard relay error handling and retry/other-channel logic applies unchanged. + +## Privacy and Security + +- Credential is a single API key stored in `channel.Key`, same as most existing channels; no OAuth tokens, refresh flow, or extra secrets. Key is only sent to the channel's configured base URL over TLS. +- Admin-configured header overrides can inject arbitrary headers upstream (existing platform feature), but wildcard/regex passthrough cannot leak channel credentials: `authorization` and `x-api-key` are on the passthrough skip list (`relay/channel/api_request.go:67+`). +- No new logging of request/response bodies; billing/usage logs follow the existing pipeline. + +## Rollout Notes + +- No database migration: channel type is an integer column; GORM schema unchanged. +- After deploy, admins create a channel of type `OpenCode Go`, paste the opencode.ai console API key, use "fetch models" to populate the model list, then configure model ratios/prices per their own policy (no defaults shipped — see Assumptions). +- The hardcoded Anthropic-protocol set must be resynced when opencode publishes new models; source of truth: `GET https://opencode.ai/zen/go/v1/models` plus the opencode remote provider config. Possible follow-up (out of scope): a channel `other_settings` override list for Anthropic-protocol models. + +## Risks and Mitigations + +- Opencode changes the per-model protocol mapping remotely without notice — Mitigation: routing is centralized in one set + helper in `constants.go`; the failure mode is a visible upstream 4xx, and the model fetch button keeps the channel's model list current. +- Anthropic-protocol endpoint may require extra `anthropic-beta` headers for some models — Mitigation: header setup delegates to `claude.CommonClaudeHeadersOperation`, which passes through client `anthropic-beta` headers and channel Claude settings; flagged for the manual smoke test. +- Cross-format paths (OpenAI client → Anthropic upstream, Claude client → OpenAI upstream) depend on relayconvert fidelity — Mitigation: both directions are existing, tested converters used in production by other channels; Task 3 locks the delegation contract, and the smoke test covers all four combinations. + +## Open Questions + +- Does `https://opencode.ai/zen/go/v1` also expose the OpenAI Responses API (`/responses`)? Unverified; v1 returns not-implemented. Can be added later by delegating to `openai.Adaptor`'s Responses methods for OpenAI-protocol models. +- Should default model ratio entries be added for the 19 models despite the subscription being flat-fee? Current assumption: no; admins set prices. Confirm before implementation if reference pricing is desired. diff --git a/docs/plans/add-request-detail-logs-plan.md b/docs/plans/add-request-detail-logs-plan.md new file mode 100644 index 00000000000..feba0f3c9fa --- /dev/null +++ b/docs/plans/add-request-detail-logs-plan.md @@ -0,0 +1,252 @@ +# Add Request Detail Logs Implementation Plan + +## Goal + +Implement request JSON body logging for usage log details. + +The feature lets administrators enable or disable request body recording from Log Maintenance settings. When enabled, consume and error logs persist the full incoming JSON request body and expose it in the existing usage log details dialog. + +## Confirmed Decisions + +- Store the complete request JSON body without truncation. +- Display the request JSON body with `react-json-view`. +- Return `request_body` through the existing usage log list APIs. +- Keep the feature disabled by default to preserve current privacy and storage behavior. + +## Current State + +The existing `logs` table is represented by `model.Log` in `model/log.go`. + +Current persisted fields include: + +- `content`: summary text or masked error text. +- `other`: JSON metadata for billing, admin info, request path, conversion chain, stream status, and related details. +- `request_id` / `upstream_request_id`: request correlation identifiers. + +The full request JSON body is not persisted today. DEBUG mode logs raw request bodies through `middleware/debug_logger.go`, but that is application logging only and does not write to the database. + +## Backend Plan + +### 1. Add Runtime Option + +Files: + +- `common/constants.go` +- `model/option.go` + +Add a new global option: + +```go +var RequestDetailLogEnabled = false +``` + +Register it in option defaults: + +```go +common.OptionMap["RequestDetailLogEnabled"] = strconv.FormatBool(common.RequestDetailLogEnabled) +``` + +Handle updates in the boolean option switch: + +```go +case "RequestDetailLogEnabled": + common.RequestDetailLogEnabled = boolValue +``` + +### 2. Add Log Model Field + +File: + +- `model/log.go` + +Add a dedicated field instead of embedding the request body in `other`: + +```go +RequestBody string `json:"request_body,omitempty"` +``` + +`AutoMigrate(&Log{})` already runs for both main DB migration and `LOG_DB`, so the new column should be created automatically for SQLite, MySQL, and PostgreSQL. + +### 3. Extend Log Write Parameters + +File: + +- `model/log.go` + +Update: + +- `RecordConsumeLogParams` +- `RecordConsumeLog` +- `RecordErrorLog` + +Add `RequestBody string` to consume/error log writes and map it to `Log.RequestBody`. + +### 4. Add Request Body Capture Helper + +Recommended file: + +- `service/request_body_log.go` + +Add a helper: + +```go +func CaptureRequestBodyForLog(ctx *gin.Context) string +``` + +Behavior: + +- Return an empty string when `common.RequestDetailLogEnabled` is false. +- Use `common.GetBodyStorage(ctx)` to read the reusable request body. +- Reset storage position after reading. +- Store only JSON request bodies. +- Validate JSON with project JSON wrappers, e.g. `common.Unmarshal` into `any`. +- Return the original raw JSON string to preserve the exact request body. +- Do not truncate. + +### 5. Populate Consume Logs + +File: + +- `service/text_quota.go` + +In `PostTextConsumeQuota`, capture the request body before `model.RecordConsumeLog(...)` and pass it into `RecordConsumeLogParams`. + +### 6. Populate Error Logs + +File: + +- `controller/relay.go` + +In `processChannelError`, capture the request body and pass it into `model.RecordErrorLog(...)`. + +This ensures upstream failure logs can also expose request details when error logging and request detail logging are enabled. + +### 7. API Behavior + +Existing endpoints remain unchanged: + +- `GET /api/log/` +- `GET /api/log/self` + +Because `model.Log` is returned directly, adding `request_body` to the JSON model exposes it through the existing list payload. + +## Frontend Plan + +### 1. Add Dependency + +From `web/default`: + +```bash +bun add react-json-view +``` + +If TypeScript definitions are required: + +```bash +bun add -d @types/react-json-view +``` + +### 2. Update Usage Log Schema + +File: + +- `web/default/src/features/usage-logs/data/schema.ts` + +Add: + +```ts +request_body: z.string().default('') +``` + +### 3. Render Request Body in Details Dialog + +File: + +- `web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx` + +Add a `Request JSON Body` section. + +Behavior: + +- Parse `props.log.request_body` with `JSON.parse`. +- Render parsed JSON with `react-json-view`. +- Include copy-to-clipboard support using the existing dialog copy pattern. +- If parsing fails, fall back to raw body display. +- Keep the section visually consistent with existing detail sections. + +### 4. Add Log Maintenance Switch + +Files: + +- `web/default/src/features/system-settings/types.ts` +- `web/default/src/features/system-settings/operations/index.tsx` +- `web/default/src/features/system-settings/operations/section-registry.tsx` +- `web/default/src/features/system-settings/maintenance/log-settings-section.tsx` +- `web/default/src/features/system-settings/hooks/use-update-option.ts` + +Add `RequestDetailLogEnabled` next to `LogConsumeEnabled`. + +Suggested UI text: + +- Label: `Record request JSON body` +- Description: `Store full request JSON bodies for usage log details. This can significantly increase database size and may contain sensitive user-provided content.` + +### 5. Internationalization + +Add or sync translation keys for: + +- `Record request JSON body` +- `Store full request JSON bodies for usage log details. This can significantly increase database size and may contain sensitive user-provided content.` +- `Request JSON Body` +- `Invalid JSON; showing raw request body.` + +## Verification Plan + +### Backend + +Run: + +```bash +go test ./... +``` + +Manual checks: + +- Disable `RequestDetailLogEnabled`, send a request, and confirm `request_body` is empty. +- Enable `RequestDetailLogEnabled`, send a JSON request, and confirm `request_body` is persisted. +- Trigger an upstream error with error logging enabled and confirm the error log stores `request_body`. + +### Frontend + +From `web/default`: + +```bash +bun run typecheck +``` + +If available: + +```bash +bun run lint +``` + +Manual checks: + +- Open System Settings -> Operations -> Log Maintenance. +- Toggle request body logging and save. +- Send a request. +- Open Usage Logs -> Details. +- Confirm the JSON viewer renders the request body. +- Confirm copy-to-clipboard works. + +## Risks + +- Full request bodies can contain sensitive user-provided content. +- Full request bodies can significantly increase `logs` table size. +- Existing list APIs will return larger payloads when logs contain request bodies. +- Multipart/form-data requests are out of scope for this first implementation unless their body can be represented as valid JSON. +- Existing historical logs will not have `request_body`. + +## Rollout + +Default the option to disabled. Administrators must explicitly enable request body logging from Log Maintenance settings. diff --git a/docs/specs/add-request-detail-logs.md b/docs/specs/add-request-detail-logs.md new file mode 100644 index 00000000000..27b008bf337 --- /dev/null +++ b/docs/specs/add-request-detail-logs.md @@ -0,0 +1,31 @@ +# Add Request Json Body Detail Logs + +日志详情页新增请求 Json Body 的日志展示,方便用户查看请求的详细信息。 + +## 1. 需求背景 + +目前日志详情页仅展示了请求的基本信息,如请求 URL、请求方法、请求头等,但缺乏对请求 Json Body 的展示。对于一些需要查看请求内容的用户来说,这会带来不便。因此,我们计划在日志详情页新增请求 Json Body 的日志展示,以提供更全面的日志信息。 + +## 2. 需求分析 +### 2.1 功能需求 +- 在日志详情页新增一个区域用于展示请求 Json Body。 +- 该区域应支持格式化显示 Json 数据,便于用户阅读。 +- 新增的请求 Json Body 日志展示功能应与现有的日志详情页设计保持一致,确保用户体验的一致性。 +- 新增一个选项,允许用户选择是否记录请求日志详情,并根据该选项决定是否记录请求 Json Body 的内容。 + +### 2.2 非功能需求 +- 新增的请求 Json Body 日志展示功能应具有良好的性能,确保在日志详情页加载时不会显著增加加载时间。 +- 该功能应兼容主流浏览器,并适应不同屏幕尺寸的设备。 + +## 3. 实现方案 +### 3.1 前端实现 +- 在日志详情页的前端代码中,新增一个组件用于展示请求 Json Body +- 使用成熟的第三方库(如 `react-json-view`)来格式化和展示 Json 数据。 + +### 3.2 后端实现 +- 在后端日志记录逻辑中,新增一个字段用于存储请求 Json Body +- 确保在记录日志时正确捕获和存储请求 Json Body 的内容。 +- 在日志详情接口中,新增一个字段用于返回请求 Json Body 的内容。 +- 在设置 `日志维护` 中新增一个选项,允许用户选择是否记录请求日志详情,并根据该选项决定是否记录请求 Json Body 的内容。 +- 确保在日志详情接口中正确处理请求 Json Body 的内容,避免因内容过大或格式不正确导致接口响应失败。 +- 在日志详情接口中,新增一个字段用于返回请求 Json Body 的内容,并确保该字段的内容在日志详情页正确展示。 diff --git a/docs/superpowers/plans/2026-04-10-channel-user-agent-filter.md b/docs/superpowers/plans/2026-04-10-channel-user-agent-filter.md new file mode 100644 index 00000000000..b158dcfb4de --- /dev/null +++ b/docs/superpowers/plans/2026-04-10-channel-user-agent-filter.md @@ -0,0 +1,632 @@ +# Per-Channel User-Agent Filter Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Allow admins to configure per-channel User-Agent glob patterns so that a channel is only eligible for requests from matching clients. + +**Architecture:** New `user_agent` column on the `Channel` model stores comma-separated glob patterns. A `MatchUserAgent` method on `Channel` evaluates patterns (case-insensitive, `*`/`?` wildcards). `GetRandomSatisfiedChannel` filters candidate channels by calling `MatchUserAgent` before weighted-random selection. The affinity system also checks UA before accepting a cached channel. Frontend exposes the field in the channel edit modal. + +**Tech Stack:** Go 1.22+, Gin, GORM, React 18, Semi Design UI + +**Spec:** `docs/superpowers/specs/2026-04-10-channel-user-agent-filter-design.md` + +--- + +## File Map + +| Action | File | Responsibility | +|--------|------|----------------| +| Modify | `model/channel.go` | Add `UserAgent` field to `Channel` struct; add `MatchUserAgent` method; add `GetUserAgentPatterns` helper; add `globMatch` utility function | +| Create | `model/channel_user_agent_test.go` | Unit tests for glob matching and `MatchUserAgent` | +| Modify | `model/channel_cache.go` | Add `userAgent` param to `GetRandomSatisfiedChannel`; filter candidates | +| Modify | `model/ability.go` | Add `userAgent` param to `GetChannel` (DB fallback); filter after selection | +| Modify | `service/channel_select.go` | Extract UA from `param.Ctx` and pass to `model.GetRandomSatisfiedChannel` | +| Modify | `middleware/distributor.go` | Add UA check after affinity lookup returns a preferred channel | +| Modify | `web/src/components/table/channels/modals/EditChannelModal.jsx` | Add `user_agent` input field in advanced settings | +| Modify | `web/src/i18n/locales/en.json` | Add English translations for new UI strings | + +--- + +### Task 1: Add `UserAgent` Field and Glob Matching to Channel Model + +**Files:** +- Modify: `model/channel.go:21-57` (Channel struct and methods) +- Create: `model/channel_user_agent_test.go` + +- [ ] **Step 1: Write the failing tests** + +Create `model/channel_user_agent_test.go`: + +```go +// ABOUTME: Unit tests for per-channel User-Agent glob matching. +// ABOUTME: Tests the MatchUserAgent method and the underlying globMatch function. +package model + +import "testing" + +func TestGlobMatch(t *testing.T) { + tests := []struct { + pattern string + input string + want bool + }{ + // Basic wildcard + {"codex*", "codex-cli/1.0.0", true}, + {"codex*", "Codex-CLI/1.0.0", true}, // case-insensitive + {"codex*", "my-codex-tool", false}, // no match, codex not at start + {"*codex*", "my-codex-tool", true}, // match with surrounding wildcards + + // Question mark + {"codex-?", "codex-a", true}, + {"codex-?", "codex-ab", false}, + + // Exact match + {"codex-cli", "codex-cli", true}, + {"codex-cli", "Codex-CLI", true}, // case-insensitive + {"codex-cli", "codex-cli/1.0", false}, + + // Star matches slash (unlike path.Match) + {"codex*", "codex-cli/1.0.0/beta", true}, + + // Empty pattern and input + {"", "", true}, + {"*", "", true}, + {"*", "anything", true}, + {"?", "", false}, + + // Multiple stars + {"*codex*claude*", "my-codex-and-claude-tool", true}, + {"*codex*claude*", "my-claude-and-codex-tool", false}, + } + for _, tt := range tests { + t.Run(tt.pattern+"_"+tt.input, func(t *testing.T) { + got := globMatch(tt.pattern, tt.input) + if got != tt.want { + t.Errorf("globMatch(%q, %q) = %v, want %v", tt.pattern, tt.input, got, tt.want) + } + }) + } +} + +func TestChannelMatchUserAgent(t *testing.T) { + strPtr := func(s string) *string { return &s } + + tests := []struct { + name string + userAgent *string + clientUA string + want bool + }{ + {"nil field accepts all", nil, "anything", true}, + {"empty string accepts all", strPtr(""), "anything", true}, + {"whitespace-only accepts all", strPtr(" "), "anything", true}, + {"single pattern match", strPtr("codex*"), "codex-cli/1.0.0", true}, + {"single pattern no match", strPtr("codex*"), "claude-code/2.0", false}, + {"multiple patterns first matches", strPtr("codex*,claude-code*"), "codex-cli/1.0", true}, + {"multiple patterns second matches", strPtr("codex*,claude-code*"), "claude-code/2.0", true}, + {"multiple patterns none match", strPtr("codex*,claude-code*"), "curl/7.0", false}, + {"patterns with whitespace", strPtr(" codex* , claude-code* "), "codex-cli/1.0", true}, + {"empty client UA with pattern", strPtr("codex*"), "", false}, + {"empty client UA without pattern", nil, "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ch := &Channel{UserAgent: tt.userAgent} + got := ch.MatchUserAgent(tt.clientUA) + if got != tt.want { + t.Errorf("MatchUserAgent(%q) = %v, want %v", tt.clientUA, got, tt.want) + } + }) + } +} +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `cd /mnt/f/workspace/new-api && go test ./model/ -run "TestGlobMatch|TestChannelMatchUserAgent" -v` +Expected: Compilation errors — `globMatch` and `UserAgent` field don't exist yet. + +- [ ] **Step 3: Add the `UserAgent` field and implement matching** + +In `model/channel.go`, add the `UserAgent` field to the `Channel` struct, right after the `Remark` field (line 50): + +```go + Remark *string `json:"remark" gorm:"type:varchar(255)" validate:"max=255"` + UserAgent *string `json:"user_agent" gorm:"type:varchar(512);default:''"` + // add after v0.8.5 +``` + +Then add the following functions at the end of the file (before the closing of the file, after the `GetHeaderOverride` method around line 919): + +```go +// GetUserAgentPatterns returns the parsed list of User-Agent glob patterns. +// Returns nil if the field is empty (accept all clients). +func (channel *Channel) GetUserAgentPatterns() []string { + if channel.UserAgent == nil { + return nil + } + trimmed := strings.TrimSpace(*channel.UserAgent) + if trimmed == "" { + return nil + } + parts := strings.Split(trimmed, ",") + patterns := make([]string, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p != "" { + patterns = append(patterns, p) + } + } + if len(patterns) == 0 { + return nil + } + return patterns +} + +// MatchUserAgent returns true if the client's User-Agent matches at least one +// of the channel's configured User-Agent glob patterns. If no patterns are +// configured, all clients are accepted. +func (channel *Channel) MatchUserAgent(clientUA string) bool { + patterns := channel.GetUserAgentPatterns() + if patterns == nil { + return true + } + for _, pattern := range patterns { + if globMatch(pattern, clientUA) { + return true + } + } + return false +} + +// globMatch performs a case-insensitive glob match. +// Supported wildcards: * (matches any sequence of zero or more characters, +// including /), ? (matches exactly one character). +func globMatch(pattern, s string) bool { + pattern = strings.ToLower(pattern) + s = strings.ToLower(s) + return globMatchInner(pattern, s) +} + +func globMatchInner(pattern, s string) bool { + for len(pattern) > 0 { + switch pattern[0] { + case '*': + // Skip consecutive stars + for len(pattern) > 0 && pattern[0] == '*' { + pattern = pattern[1:] + } + if len(pattern) == 0 { + return true // trailing * matches everything + } + // Try matching the rest of the pattern at every position + for i := 0; i <= len(s); i++ { + if globMatchInner(pattern, s[i:]) { + return true + } + } + return false + case '?': + if len(s) == 0 { + return false + } + pattern = pattern[1:] + s = s[1:] + default: + if len(s) == 0 || pattern[0] != s[0] { + return false + } + pattern = pattern[1:] + s = s[1:] + } + } + return len(s) == 0 +} +``` + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `cd /mnt/f/workspace/new-api && go test ./model/ -run "TestGlobMatch|TestChannelMatchUserAgent" -v` +Expected: All tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add model/channel.go model/channel_user_agent_test.go +git commit -m "feat(channel): add UserAgent field and glob matching logic + +Add a UserAgent field to the Channel model for per-channel client +filtering. Implement case-insensitive glob matching with * and ? +wildcards." +``` + +--- + +### Task 2: Integrate UA Filtering into Channel Selection (Cache Path) + +**Files:** +- Modify: `model/channel_cache.go:96-191` (`GetRandomSatisfiedChannel`) +- Modify: `service/channel_select.go:83-162` (`CacheGetRandomSatisfiedChannel`) + +- [ ] **Step 1: Modify `GetRandomSatisfiedChannel` to accept and apply `userAgent` filter** + +In `model/channel_cache.go`, change the function signature at line 96 from: + +```go +func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel, error) { +``` + +to: + +```go +func GetRandomSatisfiedChannel(group string, model string, retry int, userAgent string) (*Channel, error) { +``` + +Then update the single-channel shortcut (line 118-123). Replace: + +```go + if len(channels) == 1 { + if channel, ok := channelsIDM[channels[0]]; ok { + return channel, nil + } + return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channels[0]) + } +``` + +with: + +```go + if len(channels) == 1 { + if channel, ok := channelsIDM[channels[0]]; ok { + if channel.MatchUserAgent(userAgent) { + return channel, nil + } + return nil, nil + } + return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channels[0]) + } +``` + +Then in the target channel building loop (lines 147-156), add a UA filter. Replace: + +```go + for _, channelId := range channels { + if channel, ok := channelsIDM[channelId]; ok { + if channel.GetPriority() == targetPriority { + sumWeight += channel.GetWeight() + targetChannels = append(targetChannels, channel) + } + } else { + return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId) + } + } +``` + +with: + +```go + for _, channelId := range channels { + if channel, ok := channelsIDM[channelId]; ok { + if channel.GetPriority() == targetPriority && channel.MatchUserAgent(userAgent) { + sumWeight += channel.GetWeight() + targetChannels = append(targetChannels, channel) + } + } else { + return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId) + } + } +``` + +Also, the priority collection loop (lines 125-132) must only consider channels that match the UA, so the priority tiers are correct. Replace: + +```go + uniquePriorities := make(map[int]bool) + for _, channelId := range channels { + if channel, ok := channelsIDM[channelId]; ok { + uniquePriorities[int(channel.GetPriority())] = true + } else { + return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId) + } + } +``` + +with: + +```go + uniquePriorities := make(map[int]bool) + for _, channelId := range channels { + if channel, ok := channelsIDM[channelId]; ok { + if channel.MatchUserAgent(userAgent) { + uniquePriorities[int(channel.GetPriority())] = true + } + } else { + return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId) + } + } + if len(uniquePriorities) == 0 { + return nil, nil + } +``` + +- [ ] **Step 2: Update `CacheGetRandomSatisfiedChannel` to pass the User-Agent** + +In `service/channel_select.go`, at line 118 (inside the auto-groups loop), change: + +```go + channel, _ = model.GetRandomSatisfiedChannel(autoGroup, param.ModelName, priorityRetry) +``` + +to: + +```go + userAgent := "" + if param.Ctx != nil && param.Ctx.Request != nil { + userAgent = param.Ctx.Request.UserAgent() + } + channel, _ = model.GetRandomSatisfiedChannel(autoGroup, param.ModelName, priorityRetry, userAgent) +``` + +At line 156 (the non-auto path), change: + +```go + channel, err = model.GetRandomSatisfiedChannel(param.TokenGroup, param.ModelName, param.GetRetry()) +``` + +to: + +```go + userAgent := "" + if param.Ctx != nil && param.Ctx.Request != nil { + userAgent = param.Ctx.Request.UserAgent() + } + channel, err = model.GetRandomSatisfiedChannel(param.TokenGroup, param.ModelName, param.GetRetry(), userAgent) +``` + +- [ ] **Step 3: Verify compilation** + +Run: `cd /mnt/f/workspace/new-api && go build ./...` +Expected: Compiles successfully with no errors. + +- [ ] **Step 4: Run existing tests to verify no regressions** + +Run: `cd /mnt/f/workspace/new-api && go test ./model/ ./service/ ./middleware/ -v -count=1 2>&1 | tail -30` +Expected: All existing tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add model/channel_cache.go service/channel_select.go +git commit -m "feat(channel): filter channels by User-Agent during selection + +GetRandomSatisfiedChannel now accepts a userAgent parameter and +excludes channels whose UA patterns don't match the client." +``` + +--- + +### Task 3: Integrate UA Filtering into DB Fallback Path + +**Files:** +- Modify: `model/ability.go:106-144` (`GetChannel`) + +- [ ] **Step 1: Add `userAgent` parameter to `GetChannel`** + +In `model/ability.go`, change the function signature at line 106 from: + +```go +func GetChannel(group string, model string, retry int) (*Channel, error) { +``` + +to: + +```go +func GetChannel(group string, model string, retry int, userAgent string) (*Channel, error) { +``` + +After the channel is fetched from DB (line 142), add a UA check before returning. Replace: + +```go + err = DB.First(&channel, "id = ?", channel.Id).Error + return &channel, err +``` + +with: + +```go + err = DB.First(&channel, "id = ?", channel.Id).Error + if err != nil { + return nil, err + } + if !channel.MatchUserAgent(userAgent) { + return nil, nil + } + return &channel, nil +``` + +- [ ] **Step 2: Update the call site in `GetRandomSatisfiedChannel`** + +In `model/channel_cache.go`, line 99, change: + +```go + return GetChannel(group, model, retry) +``` + +to: + +```go + return GetChannel(group, model, retry, userAgent) +``` + +- [ ] **Step 3: Verify compilation** + +Run: `cd /mnt/f/workspace/new-api && go build ./...` +Expected: Compiles successfully. + +- [ ] **Step 4: Commit** + +```bash +git add model/ability.go model/channel_cache.go +git commit -m "feat(channel): add UA filtering to DB fallback channel selection + +GetChannel (used when memory cache is disabled) now also filters by +User-Agent, consistent with the cache path." +``` + +--- + +### Task 4: Add UA Check to Channel Affinity Path + +**Files:** +- Modify: `middleware/distributor.go:102-128` (affinity block in `Distribute`) + +- [ ] **Step 1: Add UA check after affinity lookup** + +In `middleware/distributor.go`, inside the affinity block starting at line 102, after the status check and before the group matching. Find the block (around line 104): + +```go + if err == nil && preferred != nil { + if preferred.Status != common.ChannelStatusEnabled { +``` + +Add a UA check right after the status-enabled checks succeed. The entire affinity block (lines 102-128) should be updated so that the three branches that assign `channel = preferred` also verify UA match. The simplest approach is to add a single guard after the `preferred != nil` check. Replace the block: + +```go + if preferredChannelID, found := service.GetPreferredChannelByAffinity(c, modelRequest.Model, usingGroup); found { + preferred, err := model.CacheGetChannel(preferredChannelID) + if err == nil && preferred != nil { + if preferred.Status != common.ChannelStatusEnabled { + if service.ShouldSkipRetryAfterChannelAffinityFailure(c) { + abortWithOpenAiMessage(c, http.StatusForbidden, i18n.T(c, i18n.MsgDistributorChannelDisabled)) + return + } + } else if usingGroup == "auto" { +``` + +with: + +```go + if preferredChannelID, found := service.GetPreferredChannelByAffinity(c, modelRequest.Model, usingGroup); found { + preferred, err := model.CacheGetChannel(preferredChannelID) + if err == nil && preferred != nil && preferred.MatchUserAgent(c.Request.UserAgent()) { + if preferred.Status != common.ChannelStatusEnabled { + if service.ShouldSkipRetryAfterChannelAffinityFailure(c) { + abortWithOpenAiMessage(c, http.StatusForbidden, i18n.T(c, i18n.MsgDistributorChannelDisabled)) + return + } + } else if usingGroup == "auto" { +``` + +This way, if the preferred channel's UA filter doesn't match the client, the entire affinity block is skipped and normal channel selection proceeds. + +- [ ] **Step 2: Verify compilation** + +Run: `cd /mnt/f/workspace/new-api && go build ./...` +Expected: Compiles successfully. + +- [ ] **Step 3: Commit** + +```bash +git add middleware/distributor.go +git commit -m "feat(channel): check UA filter on affinity-cached channels + +Skip affinity-cached channel if its User-Agent filter doesn't match +the current client, falling through to normal selection." +``` + +--- + +### Task 5: Add Frontend User-Agent Field + +**Files:** +- Modify: `web/src/components/table/channels/modals/EditChannelModal.jsx` +- Modify: `web/src/i18n/locales/en.json` + +- [ ] **Step 1: Add default value in `originInputs`** + +In `EditChannelModal.jsx`, in the `originInputs` object (around line 172), add after the `settings` line (line 198): + +```javascript + user_agent: '', +``` + +So it reads: + +```javascript + settings: '', + user_agent: '', + // 仅 Vertex: 密钥格式(存入 settings.vertex_key_type) +``` + +- [ ] **Step 2: Add `user_agent` to the advanced settings auto-expand check** + +In `EditChannelModal.jsx`, in the `hasAdvancedValues` calculation (around line 1003), add a new condition. After the line: + +```javascript + (data.remark && data.remark.trim()) || +``` + +add: + +```javascript + (data.user_agent && data.user_agent.trim()) || +``` + +- [ ] **Step 3: Add the input field in the advanced settings form** + +In `EditChannelModal.jsx`, after the `remark` TextArea field (around line 2439), add the `user_agent` input: + +```jsx + handleInputChange('user_agent', value)} + extraText={t('逗号分隔的通配符模式。设置后,仅匹配的客户端 User-Agent 可使用此渠道。留空则允许所有客户端。')} + /> +``` + +- [ ] **Step 4: Add English translations** + +In `web/src/i18n/locales/en.json`, add these entries (in alphabetical order among existing keys): + +```json + "User-Agent 过滤": "User-Agent Filter", + "逗号分隔的通配符模式。设置后,仅匹配的客户端 User-Agent 可使用此渠道。留空则允许所有客户端。": "Comma-separated glob patterns. If set, only clients with matching User-Agent can use this channel. Leave empty to accept all clients.", +``` + +- [ ] **Step 5: Verify the frontend builds** + +Run: `cd /mnt/f/workspace/new-api/web && bun run build 2>&1 | tail -10` +Expected: Build succeeds. + +- [ ] **Step 6: Commit** + +```bash +git add web/src/components/table/channels/modals/EditChannelModal.jsx web/src/i18n/locales/en.json +git commit -m "feat(frontend): add User-Agent filter field to channel edit modal + +Adds a text input for comma-separated glob patterns in the advanced +settings section. Includes English translations." +``` + +--- + +### Task 6: Final Verification + +**Files:** None (verification only) + +- [ ] **Step 1: Run all Go tests** + +Run: `cd /mnt/f/workspace/new-api && go test ./... 2>&1 | tail -30` +Expected: All tests PASS. + +- [ ] **Step 2: Run the full frontend build** + +Run: `cd /mnt/f/workspace/new-api/web && bun run build 2>&1 | tail -10` +Expected: Build succeeds. + +- [ ] **Step 3: Verify Go compilation** + +Run: `cd /mnt/f/workspace/new-api && go build ./...` +Expected: Clean build, no errors. diff --git a/docs/superpowers/specs/2026-04-10-channel-user-agent-filter-design.md b/docs/superpowers/specs/2026-04-10-channel-user-agent-filter-design.md new file mode 100644 index 00000000000..6ff2df089e0 --- /dev/null +++ b/docs/superpowers/specs/2026-04-10-channel-user-agent-filter-design.md @@ -0,0 +1,130 @@ +# Per-Channel User-Agent Filtering + +**Date:** 2026-04-10 +**Status:** Approved + +## Goal + +Allow administrators to configure which clients (identified by `User-Agent` header) can use a specific channel. When a channel has a User-Agent allow-list configured, only requests from matching clients are routed to that channel. Channels without this field configured continue to accept all clients (backwards-compatible). + +## Use Case + +An admin wants to dedicate certain channels to specific tooling. For example: +- A channel configured with `codex*` only serves requests from Codex CLI clients. +- A channel configured with `claude-code*` only serves requests from Claude Code clients. +- Channels without a User-Agent filter serve all clients as before. + +## Design + +### 1. Data Model + +**File:** `model/channel.go` + +Add a new field to the `Channel` struct: + +```go +UserAgent *string `json:"user_agent" gorm:"type:varchar(512);default:''"` +``` + +- **Storage format:** Comma-separated glob patterns (e.g., `codex*,claude-code*`) +- **Semantics:** If nil or empty, accept all clients. If non-empty, at least one pattern must match the client's `User-Agent` for the channel to be eligible. +- **Migration:** Automatic via GORM `AutoMigrate` (already invoked for `Channel` in `model/main.go`). + +### 2. Matching Logic + +**File:** `model/channel.go` + +Add a method: + +```go +func (channel *Channel) MatchUserAgent(clientUA string) bool +``` + +**Algorithm:** +1. If `channel.UserAgent` is nil or the trimmed value is empty, return `true` (accept all). +2. Split by comma and trim whitespace from each pattern. +3. For each pattern, perform a case-insensitive glob match against `clientUA`. +4. Return `true` on first match; `false` if no pattern matches. + +**Glob semantics:** +- `*` matches any sequence of characters (including empty). +- `?` matches exactly one character. +- Case-insensitive: both the pattern and the UA are lowercased before matching. +- Use Go's `path.Match` with lowercased inputs. Note: `path.Match` requires `*` to not match path separators, but User-Agent strings don't contain `/` in the relevant prefix, so this is acceptable. Alternatively, implement a simple custom glob that treats `*` as matching everything. + +**Decision:** Use a simple custom glob function rather than `path.Match`, because `path.Match` treats `*` as not matching `/`, which could cause unexpected behavior with User-Agent strings like `codex-cli/1.0.0`. The custom function: +- Lowercases both pattern and input. +- `*` matches any sequence of zero or more characters (including `/`). +- `?` matches exactly one character. + +### 3. Channel Selection Integration + +**File:** `model/channel_cache.go` — `GetRandomSatisfiedChannel` + +**Change:** Add a `userAgent string` parameter. After collecting candidate channels for a priority tier, filter out channels where `MatchUserAgent(userAgent)` returns `false`. + +**Detailed flow:** +1. Existing logic collects `channels` (list of channel IDs) for the group+model. +2. Existing logic iterates priority tiers. +3. **New:** When building `targetChannels` for a priority tier, skip channels where `channelsIDM[channelId].MatchUserAgent(userAgent)` is `false`. +4. If `targetChannels` is empty after filtering, fall through to the next priority tier (increment retry internally). +5. Weighted random selection proceeds over the filtered set. + +**Also update:** `GetChannel` (the DB fallback when cache is disabled) to accept and apply the same `userAgent` filter. + +### 4. Caller Updates + +**File:** `service/channel_select.go` — `CacheGetRandomSatisfiedChannel` + +Extract the User-Agent from `param.Ctx.Request.UserAgent()` and pass it to `model.GetRandomSatisfiedChannel`. + +**File:** `middleware/distributor.go` — `Distribute()` + +The `RetryParam` already carries `Ctx`, so no changes needed here — the User-Agent is extracted inside `CacheGetRandomSatisfiedChannel`. + +### 5. Channel Affinity Check + +**File:** `middleware/distributor.go` — inside the affinity block + +After `GetPreferredChannelByAffinity` returns a preferred channel, verify `preferred.MatchUserAgent(c.Request.UserAgent())`. If it doesn't match, skip the affinity result and fall through to normal channel selection. + +### 6. Frontend + +**File:** `web/src/components/table/channels/modals/EditChannelModal.jsx` + +Add a `UserAgent` text input field in the advanced settings section: +- Label: `User-Agent 过滤` (with i18n) +- Placeholder: `codex*,claude-code*` +- Help text: "Comma-separated glob patterns. If set, only clients with matching User-Agent can use this channel. Leave empty to accept all clients." +- The field binds to `channel.user_agent`. + +### 7. Controller/API + +**Files:** `controller/channel.go` + +- `AddChannel` and `UpdateChannel`: The `UserAgent` field flows through naturally since it's on the `Channel` struct. No special handling needed. +- `SearchChannels` / `GetAllChannels`: No changes — the field is returned with the channel data. +- `EditChannelByTag`: Optionally support bulk-updating this field in a follow-up. + +### 8. I18n + +Add translation keys for the new UI label and help text to: +- `web/src/i18n/locales/zh-CN.json` +- `web/src/i18n/locales/en.json` +- Other locale files as needed. + +## Edge Cases + +1. **Empty User-Agent header:** If the client sends no User-Agent, `clientUA` will be `""`. A pattern like `codex*` won't match `""`, so channels with UA filters will be skipped. Channels without UA filters will still be available. + +2. **All channels filtered out:** If all channels for a group+model have UA filters and none match, the request fails with the existing "no available channel" error. This is correct behavior. + +3. **Affinity cache hit with UA mismatch:** If a cached affinity points to a channel whose UA filter doesn't match the current client, the affinity result is ignored and normal selection proceeds. + +4. **Multi-key channels:** UA filtering happens at the channel level, before key selection. No interaction with multi-key logic. + +## Non-Goals + +- Block-list (exclude-by-UA) is not implemented. Only allow-list. +- Regex patterns are not supported. Only glob (`*`, `?`). +- This does not affect channels selected via specific token-channel binding (when a token is bound to a specific channel ID). diff --git a/middleware/debug_headers.go b/middleware/debug_headers.go new file mode 100644 index 00000000000..2f69df1313e --- /dev/null +++ b/middleware/debug_headers.go @@ -0,0 +1,19 @@ +// ABOUTME: Middleware that logs the complete set of incoming request headers. +// ABOUTME: Only active when DEBUG=true to aid request debugging without leaking headers in production. +package middleware + +import ( + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/logger" + "github.com/gin-gonic/gin" +) + +func DebugHeaders() gin.HandlerFunc { + return func(c *gin.Context) { + if common.DebugEnabled { + headers, _ := common.Marshal(c.Request.Header) + logger.LogDebug(c, "request headers for %s %s: %s", c.Request.Method, c.Request.URL.String(), headers) + } + c.Next() + } +} diff --git a/middleware/distributor.go b/middleware/distributor.go index 4234011c9f7..11df90cd52b 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -1,3 +1,5 @@ +// ABOUTME: Selects relay channels and initializes request context before controller handling. +// ABOUTME: Preserves auto-group and affinity state so retries continue from the selected group. package middleware import ( @@ -104,15 +106,21 @@ func Distribute() func(c *gin.Context) { if preferredChannelID, found := service.GetPreferredChannelByAffinity(c, modelRequest.Model, usingGroup); found { affinityUsable := false preferred, err := model.CacheGetChannel(preferredChannelID) - if err == nil && preferred != nil && preferred.Status == common.ChannelStatusEnabled && + if err == nil && preferred != nil && preferred.MatchUserAgent(c.Request.UserAgent()) && channelSupportsRequestPath(preferred, c.Request.URL.Path, modelRequest.Model) { - if usingGroup == "auto" { + if preferred.Status != common.ChannelStatusEnabled { + if service.ShouldSkipRetryAfterChannelAffinityFailure(c) { + abortWithOpenAiMessage(c, http.StatusForbidden, i18n.T(c, i18n.MsgDistributorAffinityChannelDisabled)) + return + } + } else if usingGroup == "auto" { userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup) autoGroups := service.GetUserAutoGroup(userGroup) - for _, g := range autoGroups { + for groupIndex, g := range autoGroups { if model.IsChannelEnabledForGroupModel(g, modelRequest.Model, preferred.Id) { selectGroup = g common.SetContextKey(c, constant.ContextKeyAutoGroup, g) + common.SetContextKey(c, constant.ContextKeyAutoGroupIndex, groupIndex) channel = preferred affinityUsable = true service.MarkChannelAffinityUsed(c, g, preferred.Id) @@ -163,7 +171,9 @@ func Distribute() func(c *gin.Context) { common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now()) SetupContextForSelectedChannel(c, channel, modelRequest.Model) c.Next() - if channel != nil && c.Writer != nil && c.Writer.Status() < http.StatusBadRequest { + // Soft failures clear affinity and must not refresh sticky binding even on odd status paths. + if channel != nil && c.Writer != nil && c.Writer.Status() < http.StatusBadRequest && + !service.IsChannelAffinityUnusable(c) { service.RecordChannelAffinity(c, channel.Id) } } diff --git a/middleware/rate-limit.go b/middleware/rate-limit.go index d8dd15d9c5d..bd93af66ec1 100644 --- a/middleware/rate-limit.go +++ b/middleware/rate-limit.go @@ -201,5 +201,6 @@ func SearchRateLimit() func(c *gin.Context) { if !common.SearchRateLimitEnable { return defNext } + return userRateLimitFactory(common.SearchRateLimitNum, common.SearchRateLimitDuration, "SR") } diff --git a/model/ability.go b/model/ability.go index e67b28301e0..6f44cca60e7 100644 --- a/model/ability.go +++ b/model/ability.go @@ -1,14 +1,18 @@ +// ABOUTME: Manages model-to-channel abilities and database-backed channel selection. +// ABOUTME: Selects the highest-priority eligible channel while honoring retry exclusions. package model import ( "errors" "fmt" + "sort" "strings" "sync" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/pkg/channellatency" "github.com/samber/lo" "gorm.io/gorm" @@ -25,6 +29,13 @@ type Ability struct { Tag *string `json:"tag" gorm:"index"` } +func (ability Ability) PriorityValue() int64 { + if ability.Priority == nil { + return 0 + } + return *ability.Priority +} + type AbilityWithChannel struct { Ability ChannelType int `json:"channel_type"` @@ -60,90 +71,111 @@ func GetAllEnableAbilities() []Ability { return abilities } -func getPriority(group string, model string, retry int) (int, error) { - - var priorities []int - err := DB.Model(&Ability{}). - Select("DISTINCT(priority)"). - Where(commonGroupCol+" = ? and model = ? and enabled = ?", group, model, true). - Order("priority DESC"). // 按优先级降序排序 - Pluck("priority", &priorities).Error // Pluck用于将查询的结果直接扫描到一个切片中 - +func GetChannel(group string, model string, excludedChannelIds map[int]struct{}, allowMultiKeyFallback bool, userAgent string, requestPath string) (*Channel, error) { + var abilities []Ability + err := DB.Where(commonGroupCol+" = ? and model = ? and enabled = ?", group, model, true). + Order("priority DESC, weight DESC"). + Find(&abilities).Error if err != nil { - // 处理错误 - return 0, err + return nil, err } - - if len(priorities) == 0 { - // 如果没有查询到优先级,则返回错误 - return 0, errors.New("数据库一致性被破坏") + abilities = filterAbilitiesByRequestPathAndModel(abilities, requestPath, model) + if len(abilities) == 0 { + return nil, nil } - // 确定要使用的优先级 - var priorityToUse int - if retry >= len(priorities) { - // 如果重试次数大于优先级数,则使用最小的优先级 - priorityToUse = priorities[len(priorities)-1] - } else { - priorityToUse = priorities[retry] + channelIDs := make([]int, 0, len(abilities)) + for _, ability := range abilities { + channelIDs = append(channelIDs, ability.ChannelId) + } + var channels []Channel + if err := DB.Where("id in (?)", channelIDs).Find(&channels).Error; err != nil { + return nil, err + } + channelByID := make(map[int]*Channel, len(channels)) + for i := range channels { + channel := channels[i] + channelByID[channel.Id] = &channel } - return priorityToUse, nil -} -func getChannelQuery(group string, model string, retry int) (*gorm.DB, error) { - maxPrioritySubQuery := DB.Model(&Ability{}).Select("MAX(priority)").Where(commonGroupCol+" = ? and model = ? and enabled = ?", group, model, true) - channelQuery := DB.Where(commonGroupCol+" = ? and model = ? and enabled = ? and priority = (?)", group, model, true, maxPrioritySubQuery) - if retry != 0 { - priority, err := getPriority(group, model, retry) - if err != nil { - return nil, err - } else { - channelQuery = DB.Where(commonGroupCol+" = ? and model = ? and enabled = ? and priority = ?", group, model, true, priority) + matchedAbilities := make([]Ability, 0, len(abilities)) + multiKeyFallbackAbilities := make([]Ability, 0) + for _, ability := range abilities { + channel, ok := channelByID[ability.ChannelId] + if !ok { + return nil, fmt.Errorf("channel %d not found", ability.ChannelId) } + if !channel.MatchUserAgent(userAgent) { + continue + } + if _, excluded := excludedChannelIds[ability.ChannelId]; excluded { + if channel.ChannelInfo.IsMultiKey { + multiKeyFallbackAbilities = append(multiKeyFallbackAbilities, ability) + } + continue + } + matchedAbilities = append(matchedAbilities, ability) + } + if len(matchedAbilities) == 0 && allowMultiKeyFallback { + matchedAbilities = multiKeyFallbackAbilities + } + if len(matchedAbilities) == 0 { + return nil, nil } - return channelQuery, nil -} - -func GetChannel(group string, model string, retry int, requestPath string) (*Channel, error) { - var abilities []Ability + priorities := make([]int64, 0, len(matchedAbilities)) + seenPriorities := make(map[int64]struct{}, len(matchedAbilities)) + for _, ability := range matchedAbilities { + priority := ability.PriorityValue() + if _, ok := seenPriorities[priority]; ok { + continue + } + seenPriorities[priority] = struct{}{} + priorities = append(priorities, priority) + } + sort.Slice(priorities, func(i, j int) bool { return priorities[i] > priorities[j] }) + targetPriority := priorities[0] - var err error = nil - channelQuery, err := getChannelQuery(group, model, retry) - if err != nil { - return nil, err + targetAbilities := make([]Ability, 0, len(matchedAbilities)) + for _, ability := range matchedAbilities { + if ability.PriorityValue() == targetPriority { + targetAbilities = append(targetAbilities, ability) + } } - if common.UsingMainDatabase(common.DatabaseTypeSQLite) || common.UsingMainDatabase(common.DatabaseTypePostgreSQL) { - err = channelQuery.Order("weight DESC").Find(&abilities).Error - } else { - err = channelQuery.Order("weight DESC").Find(&abilities).Error + if len(targetAbilities) == 0 { + return nil, errors.New("no channel found") } - if err != nil { - return nil, err + + targetChannelIDs := make([]int, 0, len(targetAbilities)) + for _, ability := range targetAbilities { + targetChannelIDs = append(targetChannelIDs, ability.ChannelId) } - abilities = filterAbilitiesByRequestPathAndModel(abilities, requestPath, model) - channel := Channel{} - if len(abilities) > 0 { - // Randomly choose one - weightSum := uint(0) - for _, ability_ := range abilities { - weightSum += ability_.Weight + 10 - } - // Randomly choose one - weight := common.GetRandomInt(int(weightSum)) - for _, ability_ := range abilities { - weight -= int(ability_.Weight) + 10 - //log.Printf("weight: %d, ability weight: %d", weight, *ability_.Weight) - if weight <= 0 { - channel.Id = ability_.ChannelId - break - } + peerRefMs := channellatency.PeerRefMs(targetChannelIDs, model) + effectiveWeights := make([]int, len(targetAbilities)) + var weightSum uint + for i, ability := range targetAbilities { + eff := channellatency.WeightForSelection(ability.ChannelId, model, int(ability.Weight), peerRefMs) + effectiveWeights[i] = eff + weightSum += uint(eff) + 10 + } + + weight := common.GetRandomInt(int(weightSum)) + selectedChannelID := 0 + for i, ability := range targetAbilities { + weight -= effectiveWeights[i] + 10 + if weight <= 0 { + selectedChannelID = ability.ChannelId + break } - } else { - return nil, nil } - err = DB.First(&channel, "id = ?", channel.Id).Error - return &channel, err + if selectedChannelID == 0 { + return nil, errors.New("channel not found") + } + channel, ok := channelByID[selectedChannelID] + if !ok { + return nil, fmt.Errorf("channel %d not found", selectedChannelID) + } + return channel, nil } // filterAbilitiesByRequestPathAndModel restricts candidates by request path and diff --git a/model/channel.go b/model/channel.go index dbd1deaef4d..8809f030326 100644 --- a/model/channel.go +++ b/model/channel.go @@ -50,6 +50,7 @@ type Channel struct { ParamOverride *string `json:"param_override" gorm:"type:text"` HeaderOverride *string `json:"header_override" gorm:"type:text"` Remark *string `json:"remark" gorm:"type:varchar(255)" validate:"max=255"` + UserAgent *string `json:"user_agent" gorm:"type:varchar(512);default:''"` // add after v0.8.5 ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"` @@ -1123,3 +1124,87 @@ func CountChannelsGroupByType() (map[int64]int64, error) { } return counts, nil } + +// GetUserAgentPatterns returns the parsed list of User-Agent glob patterns. +// Returns nil if the field is empty (accept all clients). +func (channel *Channel) GetUserAgentPatterns() []string { + if channel.UserAgent == nil { + return nil + } + trimmed := strings.TrimSpace(*channel.UserAgent) + if trimmed == "" { + return nil + } + parts := strings.Split(trimmed, ",") + patterns := make([]string, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p != "" { + patterns = append(patterns, p) + } + } + if len(patterns) == 0 { + return nil + } + return patterns +} + +// MatchUserAgent returns true if the client's User-Agent matches at least one +// of the channel's configured User-Agent glob patterns. If no patterns are +// configured, all clients are accepted. +func (channel *Channel) MatchUserAgent(clientUA string) bool { + patterns := channel.GetUserAgentPatterns() + if patterns == nil { + return true + } + for _, pattern := range patterns { + if globMatch(pattern, clientUA) { + return true + } + } + return false +} + +// globMatch performs a case-insensitive glob match. +// Supported wildcards: * (matches any sequence of zero or more characters, +// including /), ? (matches exactly one character). +func globMatch(pattern, s string) bool { + pattern = strings.ToLower(pattern) + s = strings.ToLower(s) + return globMatchInner(pattern, s) +} + +func globMatchInner(pattern, s string) bool { + for len(pattern) > 0 { + switch pattern[0] { + case '*': + // Skip consecutive stars + for len(pattern) > 0 && pattern[0] == '*' { + pattern = pattern[1:] + } + if len(pattern) == 0 { + return true // trailing * matches everything + } + // Try matching the rest of the pattern at every position + for i := 0; i <= len(s); i++ { + if globMatchInner(pattern, s[i:]) { + return true + } + } + return false + case '?': + if len(s) == 0 { + return false + } + pattern = pattern[1:] + s = s[1:] + default: + if len(s) == 0 || pattern[0] != s[0] { + return false + } + pattern = pattern[1:] + s = s[1:] + } + } + return len(s) == 0 +} diff --git a/model/channel_cache.go b/model/channel_cache.go index 81923017d79..ea9e9fe8544 100644 --- a/model/channel_cache.go +++ b/model/channel_cache.go @@ -1,3 +1,5 @@ +// ABOUTME: Maintains the in-memory channel cache and performs cached channel selection. +// ABOUTME: Chooses untried channels by priority and weight for relay retries. package model import ( @@ -13,6 +15,7 @@ import ( "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" + "github.com/QuantumNous/new-api/pkg/channellatency" "github.com/QuantumNous/new-api/setting/ratio_setting" ) @@ -111,10 +114,10 @@ func SyncChannelCache(frequency int) { } } -func GetRandomSatisfiedChannel(group string, model string, retry int, requestPath string) (*Channel, error) { +func GetRandomSatisfiedChannel(group string, model string, excludedChannelIds map[int]struct{}, allowMultiKeyFallback bool, userAgent string, requestPath string) (*Channel, error) { // if memory cache is disabled, get channel directly from database if !common.MemoryCacheEnabled { - return GetChannel(group, model, retry, requestPath) + return GetChannel(group, model, excludedChannelIds, allowMultiKeyFallback, userAgent, requestPath) } channelSyncLock.RLock() @@ -133,48 +136,64 @@ func GetRandomSatisfiedChannel(group string, model string, retry int, requestPat return nil, nil } - if len(channels) == 1 { - if channel, ok := channelsIDM[channels[0]]; ok { - return channel, nil + eligibleChannels := make([]*Channel, 0, len(channels)) + multiKeyFallbackChannels := make([]*Channel, 0) + for _, channelId := range channels { + channel, ok := channelsIDM[channelId] + if !ok { + return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId) + } + if !channel.MatchUserAgent(userAgent) { + continue } - return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channels[0]) + if _, excluded := excludedChannelIds[channel.Id]; excluded { + if channel.ChannelInfo.IsMultiKey { + multiKeyFallbackChannels = append(multiKeyFallbackChannels, channel) + } + continue + } + eligibleChannels = append(eligibleChannels, channel) + } + if len(eligibleChannels) == 0 && allowMultiKeyFallback { + eligibleChannels = multiKeyFallbackChannels + } + if len(eligibleChannels) == 0 { + return nil, nil } uniquePriorities := make(map[int]bool) - for _, channelId := range channels { - if channel, ok := channelsIDM[channelId]; ok { - uniquePriorities[int(channel.GetPriority())] = true - } else { - return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId) - } + for _, channel := range eligibleChannels { + uniquePriorities[int(channel.GetPriority())] = true } var sortedUniquePriorities []int for priority := range uniquePriorities { sortedUniquePriorities = append(sortedUniquePriorities, priority) } sort.Sort(sort.Reverse(sort.IntSlice(sortedUniquePriorities))) + targetPriority := int64(sortedUniquePriorities[0]) - if retry >= len(uniquePriorities) { - retry = len(uniquePriorities) - 1 - } - targetPriority := int64(sortedUniquePriorities[retry]) - - // get the priority for the given retry number - var sumWeight = 0 var targetChannels []*Channel - for _, channelId := range channels { - if channel, ok := channelsIDM[channelId]; ok { - if channel.GetPriority() == targetPriority { - sumWeight += channel.GetWeight() - targetChannels = append(targetChannels, channel) - } - } else { - return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId) + for _, channel := range eligibleChannels { + if channel.GetPriority() == targetPriority { + targetChannels = append(targetChannels, channel) } } if len(targetChannels) == 0 { - return nil, errors.New(fmt.Sprintf("no channel found, group: %s, model: %s, priority: %d", group, model, targetPriority)) + return nil, fmt.Errorf("no channel found, group: %s, model: %s, priority: %d", group, model, targetPriority) + } + + channelIDs := make([]int, 0, len(targetChannels)) + for _, channel := range targetChannels { + channelIDs = append(channelIDs, channel.Id) + } + peerRefMs := channellatency.PeerRefMs(channelIDs, model) + effectiveWeights := make([]int, len(targetChannels)) + sumWeight := 0 + for i, channel := range targetChannels { + eff := channellatency.WeightForSelection(channel.Id, model, channel.GetWeight(), peerRefMs) + effectiveWeights[i] = eff + sumWeight += eff } // smoothing factor and adjustment @@ -198,8 +217,8 @@ func GetRandomSatisfiedChannel(group string, model string, retry int, requestPat randomWeight := rand.Intn(totalWeight) // Find a channel based on its weight - for _, channel := range targetChannels { - randomWeight -= channel.GetWeight()*smoothingFactor + smoothingAdjustment + for i, channel := range targetChannels { + randomWeight -= effectiveWeights[i]*smoothingFactor + smoothingAdjustment if randomWeight < 0 { return channel, nil } diff --git a/model/channel_ttft_weight_test.go b/model/channel_ttft_weight_test.go new file mode 100644 index 00000000000..f38ada3da4c --- /dev/null +++ b/model/channel_ttft_weight_test.go @@ -0,0 +1,91 @@ +// ABOUTME: Regression tests for TTFT-biased same-priority channel selection. +// ABOUTME: Verifies channel+model keys and that faster channels get higher pick share. +package model + +import ( + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/pkg/channellatency" + "github.com/QuantumNous/new-api/setting/operation_setting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetChannelPrefersFasterTtftWithinSamePriority(t *testing.T) { + origEnabled := operation_setting.TtftRoutingEnabled + origMin := operation_setting.TtftRoutingMinSamples + origAlpha := operation_setting.TtftRoutingEwmaAlpha + origRef := operation_setting.TtftRoutingRefMs + origMinF := operation_setting.TtftRoutingMinFactor + origMaxF := operation_setting.TtftRoutingMaxFactor + origCache := common.MemoryCacheEnabled + t.Cleanup(func() { + operation_setting.TtftRoutingEnabled = origEnabled + operation_setting.TtftRoutingMinSamples = origMin + operation_setting.TtftRoutingEwmaAlpha = origAlpha + operation_setting.TtftRoutingRefMs = origRef + operation_setting.TtftRoutingMinFactor = origMinF + operation_setting.TtftRoutingMaxFactor = origMaxF + common.MemoryCacheEnabled = origCache + channellatency.ResetForTest() + }) + + common.MemoryCacheEnabled = false + operation_setting.TtftRoutingEnabled = true + operation_setting.TtftRoutingMinSamples = 3 + operation_setting.TtftRoutingEwmaAlpha = 1.0 + operation_setting.TtftRoutingRefMs = 500 + operation_setting.TtftRoutingMinFactor = 0.5 + operation_setting.TtftRoutingMaxFactor = 2.0 + channellatency.ResetForTest() + truncateTables(t) + + priority := int64(100) + weight := uint(10) + fast := &Channel{ + Status: common.ChannelStatusEnabled, + Name: "fast-ttft", + Key: "fast-key", + Models: "gpt-4o", + Group: "default", + Priority: &priority, + Weight: &weight, + } + slow := &Channel{ + Status: common.ChannelStatusEnabled, + Name: "slow-ttft", + Key: "slow-key", + Models: "gpt-4o", + Group: "default", + Priority: &priority, + Weight: &weight, + } + require.NoError(t, fast.Insert()) + require.NoError(t, slow.Insert()) + + for i := 0; i < 3; i++ { + channellatency.Observe(fast.Id, "gpt-4o", 100) + channellatency.Observe(slow.Id, "gpt-4o", 2000) + } + + // Independent model key: samples on claude must not affect gpt-4o weights. + for i := 0; i < 3; i++ { + channellatency.Observe(slow.Id, "claude-3.5", 50) + } + assert.Equal(t, 20, channellatency.WeightForSelection(fast.Id, "gpt-4o", 10, 0)) + assert.Equal(t, 5, channellatency.WeightForSelection(slow.Id, "gpt-4o", 10, 0)) + + const trials = 200 + fastWins := 0 + for i := 0; i < trials; i++ { + selected, err := GetChannel("default", "gpt-4o", nil, true, "", "") + require.NoError(t, err) + require.NotNil(t, selected) + if selected.Id == fast.Id { + fastWins++ + } + } + // Expected share ~ 30/45 ≈ 0.67 with +10 smoothing; require a clear majority. + assert.Greater(t, fastWins, trials*3/5) +} diff --git a/model/channel_user_agent_test.go b/model/channel_user_agent_test.go new file mode 100644 index 00000000000..c73e85b56f2 --- /dev/null +++ b/model/channel_user_agent_test.go @@ -0,0 +1,268 @@ +// ABOUTME: Unit tests for per-channel User-Agent glob matching. +// ABOUTME: Tests the MatchUserAgent method and the underlying globMatch function. +package model + +import ( + "fmt" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGlobMatch(t *testing.T) { + tests := []struct { + pattern string + input string + want bool + }{ + // Basic wildcard + {"codex*", "codex-cli/1.0.0", true}, + {"codex*", "Codex-CLI/1.0.0", true}, // case-insensitive + {"codex*", "my-codex-tool", false}, // no match, codex not at start + {"*codex*", "my-codex-tool", true}, // match with surrounding wildcards + + // Question mark + {"codex-?", "codex-a", true}, + {"codex-?", "codex-ab", false}, + + // Exact match + {"codex-cli", "codex-cli", true}, + {"codex-cli", "Codex-CLI", true}, // case-insensitive + {"codex-cli", "codex-cli/1.0", false}, + + // Star matches slash (unlike path.Match) + {"codex*", "codex-cli/1.0.0/beta", true}, + + // Empty pattern and input + {"", "", true}, + {"*", "", true}, + {"*", "anything", true}, + {"?", "", false}, + + // Multiple stars + {"*codex*claude*", "my-codex-and-claude-tool", true}, + {"*codex*claude*", "my-claude-and-codex-tool", false}, + } + for _, tt := range tests { + t.Run(tt.pattern+"_"+tt.input, func(t *testing.T) { + got := globMatch(tt.pattern, tt.input) + if got != tt.want { + t.Errorf("globMatch(%q, %q) = %v, want %v", tt.pattern, tt.input, got, tt.want) + } + }) + } +} + +func TestChannelMatchUserAgent(t *testing.T) { + strPtr := func(s string) *string { return &s } + + tests := []struct { + name string + userAgent *string + clientUA string + want bool + }{ + {"nil field accepts all", nil, "anything", true}, + {"empty string accepts all", strPtr(""), "anything", true}, + {"whitespace-only accepts all", strPtr(" "), "anything", true}, + {"single pattern match", strPtr("codex*"), "codex-cli/1.0.0", true}, + {"single pattern no match", strPtr("codex*"), "claude-code/2.0", false}, + {"multiple patterns first matches", strPtr("codex*,claude-code*"), "codex-cli/1.0", true}, + {"multiple patterns second matches", strPtr("codex*,claude-code*"), "claude-code/2.0", true}, + {"multiple patterns none match", strPtr("codex*,claude-code*"), "curl/7.0", false}, + {"patterns with whitespace", strPtr(" codex* , claude-code* "), "codex-cli/1.0", true}, + {"empty client UA with pattern", strPtr("codex*"), "", false}, + {"empty client UA without pattern", nil, "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ch := &Channel{UserAgent: tt.userAgent} + got := ch.MatchUserAgent(tt.clientUA) + if got != tt.want { + t.Errorf("MatchUserAgent(%q) = %v, want %v", tt.clientUA, got, tt.want) + } + }) + } +} + +func TestGetChannel_DBFallbackSkipsHigherPriorityNonMatchingChannel(t *testing.T) { + require.NoError(t, DB.AutoMigrate(&Ability{})) + initCol() + truncateTables(t) + + priorityHigh := int64(200) + priorityLow := int64(100) + weightHigh := uint(100) + weightLow := uint(1) + highUA := "other-client*" + lowUA := "my-client*" + + high := &Channel{ + Status: common.ChannelStatusEnabled, + Name: "high-priority-non-matching", + Key: "high-key", + Models: "gpt-4o", + Group: "default", + Priority: &priorityHigh, + Weight: &weightHigh, + UserAgent: &highUA, + } + require.NoError(t, high.Insert()) + + low := &Channel{ + Status: common.ChannelStatusEnabled, + Name: "low-priority-matching", + Key: "low-key", + Models: "gpt-4o", + Group: "default", + Priority: &priorityLow, + Weight: &weightLow, + UserAgent: &lowUA, + } + require.NoError(t, low.Insert()) + + channel, err := GetChannel("default", "gpt-4o", nil, true, "my-client/1.0", "") + require.NoError(t, err) + require.NotNil(t, channel) + if channel.Id != low.Id { + t.Fatalf("GetChannel returned channel %d, want matching lower-priority channel %d", channel.Id, low.Id) + } +} + +func TestGetRandomSatisfiedChannelExhaustsPriorityBeforeFallback(t *testing.T) { + for _, memoryCacheEnabled := range []bool{false, true} { + t.Run(fmt.Sprintf("memory_cache_%t", memoryCacheEnabled), func(t *testing.T) { + originalMemoryCacheEnabled := common.MemoryCacheEnabled + common.MemoryCacheEnabled = memoryCacheEnabled + t.Cleanup(func() { + common.MemoryCacheEnabled = originalMemoryCacheEnabled + }) + truncateTables(t) + + highPriority := int64(200) + lowPriority := int64(100) + weight := uint(1) + channels := []*Channel{ + { + Status: common.ChannelStatusEnabled, + Name: "high-priority-a", + Key: "high-key-a", + Models: "gpt-4o", + Group: "default", + Priority: &highPriority, + Weight: &weight, + }, + { + Status: common.ChannelStatusEnabled, + Name: "high-priority-b", + Key: "high-key-b", + Models: "gpt-4o", + Group: "default", + Priority: &highPriority, + Weight: &weight, + }, + { + Status: common.ChannelStatusEnabled, + Name: "low-priority", + Key: "low-key", + Models: "gpt-4o", + Group: "default", + Priority: &lowPriority, + Weight: &weight, + }, + } + for _, channel := range channels { + require.NoError(t, channel.Insert()) + } + if memoryCacheEnabled { + InitChannelCache() + } + + excludedChannelIds := make(map[int]struct{}) + first, err := GetRandomSatisfiedChannel("default", "gpt-4o", excludedChannelIds, true, "", "") + require.NoError(t, err) + require.NotNil(t, first) + assert.Equal(t, highPriority, first.GetPriority()) + excludedChannelIds[first.Id] = struct{}{} + + second, err := GetRandomSatisfiedChannel("default", "gpt-4o", excludedChannelIds, true, "", "") + require.NoError(t, err) + require.NotNil(t, second) + assert.Equal(t, highPriority, second.GetPriority()) + assert.NotEqual(t, first.Id, second.Id) + excludedChannelIds[second.Id] = struct{}{} + + third, err := GetRandomSatisfiedChannel("default", "gpt-4o", excludedChannelIds, true, "", "") + require.NoError(t, err) + require.NotNil(t, third) + assert.Equal(t, lowPriority, third.GetPriority()) + excludedChannelIds[third.Id] = struct{}{} + + exhausted, err := GetRandomSatisfiedChannel("default", "gpt-4o", excludedChannelIds, true, "", "") + require.NoError(t, err) + assert.Nil(t, exhausted) + }) + } +} + +func TestGetRandomSatisfiedChannelFallsBackToMultiKeyAfterUntriedChannels(t *testing.T) { + for _, memoryCacheEnabled := range []bool{false, true} { + t.Run(fmt.Sprintf("memory_cache_%t", memoryCacheEnabled), func(t *testing.T) { + originalMemoryCacheEnabled := common.MemoryCacheEnabled + common.MemoryCacheEnabled = memoryCacheEnabled + t.Cleanup(func() { + common.MemoryCacheEnabled = originalMemoryCacheEnabled + }) + truncateTables(t) + + highPriority := int64(200) + lowPriority := int64(100) + weight := uint(1) + multiKey := &Channel{ + Status: common.ChannelStatusEnabled, + Name: "multi-key-high-priority", + Key: "key-a\nkey-b", + Models: "gpt-4o", + Group: "default", + Priority: &highPriority, + Weight: &weight, + ChannelInfo: ChannelInfo{ + IsMultiKey: true, + MultiKeySize: 2, + }, + } + require.NoError(t, multiKey.Insert()) + low := &Channel{ + Status: common.ChannelStatusEnabled, + Name: "single-key-low-priority", + Key: "low-key", + Models: "gpt-4o", + Group: "default", + Priority: &lowPriority, + Weight: &weight, + } + require.NoError(t, low.Insert()) + if memoryCacheEnabled { + InitChannelCache() + } + + excludedChannelIds := map[int]struct{}{multiKey.Id: {}} + selected, err := GetRandomSatisfiedChannel("default", "gpt-4o", excludedChannelIds, true, "", "") + require.NoError(t, err) + require.NotNil(t, selected) + assert.Equal(t, low.Id, selected.Id) + + excludedChannelIds[low.Id] = struct{}{} + selected, err = GetRandomSatisfiedChannel("default", "gpt-4o", excludedChannelIds, false, "", "") + require.NoError(t, err) + assert.Nil(t, selected) + + selected, err = GetRandomSatisfiedChannel("default", "gpt-4o", excludedChannelIds, true, "", "") + require.NoError(t, err) + require.NotNil(t, selected) + assert.Equal(t, multiKey.Id, selected.Id) + }) + } +} diff --git a/model/log.go b/model/log.go index 506bd504b68..d591a13698a 100644 --- a/model/log.go +++ b/model/log.go @@ -78,6 +78,8 @@ type Log struct { RequestId string `json:"request_id,omitempty" gorm:"type:varchar(64);index:idx_logs_request_id;default:''"` UpstreamRequestId string `json:"upstream_request_id,omitempty" gorm:"type:varchar(128);index:idx_logs_upstream_request_id;default:''"` Other string `json:"other"` + RequestBody string `json:"request_body,omitempty" gorm:"type:text"` + ResponseBody string `json:"response_body,omitempty" gorm:"type:text"` } // don't use iota, avoid change log type value @@ -128,9 +130,19 @@ func formatUserLogs(logs []*Log, startIdx int) { } logs[i].Other = common.MapToJsonStr(otherMap) } + stripLogDetailBodies(logs) assignDisplayLogIds(logs, startIdx) } +// stripLogDetailBodies drops large body fields from list payloads. Bodies are +// loaded on demand from disk (or legacy DB columns via the detail API). +func stripLogDetailBodies(logs []*Log) { + for i := range logs { + logs[i].RequestBody = "" + logs[i].ResponseBody = "" + } +} + func GetLogByTokenId(tokenId int) (logs []*Log, err error) { order := "id desc" if common.UsingLogDatabase(common.DatabaseTypeClickHouse) { @@ -280,7 +292,7 @@ func RecordTopupLog(userId int, content string, callerIp string, paymentMethod s } func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, tokenName string, content string, tokenId int, useTimeSeconds int, - isStream bool, group string, other map[string]interface{}) { + isStream bool, group string, other map[string]interface{}, requestBody string, responseBody string) { logger.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, common.LocalLogPreview(content))) username := c.GetString("username") requestId := c.GetString(common.RequestIdKey) @@ -318,6 +330,8 @@ func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, RequestId: requestId, UpstreamRequestId: upstreamRequestId, Other: otherStr, + // Request/response bodies are stored on disk (see service.WriteRequestDetailBodies), + // not in the logs table. } err := createLog(log) if err != nil { @@ -338,6 +352,8 @@ type RecordConsumeLogParams struct { IsStream bool `json:"is_stream"` Group string `json:"group"` Other map[string]interface{} `json:"other"` + RequestBody string `json:"request_body,omitempty"` + ResponseBody string `json:"response_body,omitempty"` } func RecordConsumeLog(c *gin.Context, userId int, params RecordConsumeLogParams) { @@ -382,6 +398,8 @@ func RecordConsumeLog(c *gin.Context, userId int, params RecordConsumeLogParams) RequestId: requestId, UpstreamRequestId: upstreamRequestId, Other: otherStr, + // Request/response bodies are stored on disk (see service.WriteRequestDetailBodies), + // not in the logs table. } err := createLog(log) if err != nil { @@ -556,9 +574,28 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName } } + stripLogDetailBodies(logs) return logs, total, err } +// GetLogAccessByRequestId returns a log row used to authorize request-detail access. +// Multiple rows may share a request_id (retries); any matching row is sufficient. +func GetLogAccessByRequestId(requestId string) (*Log, error) { + if strings.TrimSpace(requestId) == "" { + return nil, gorm.ErrRecordNotFound + } + var log Log + order := "id desc" + if common.UsingLogDatabase(common.DatabaseTypeClickHouse) { + order = clickHouseLogOrder("") + } + err := LOG_DB.Model(&Log{}).Where("request_id = ?", requestId).Order(order).Limit(1).First(&log).Error + if err != nil { + return nil, err + } + return &log, nil +} + const logSearchCountLimit = 10000 func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int, group string, requestId string, upstreamRequestId string) (logs []*Log, total int64, err error) { diff --git a/model/main.go b/model/main.go index 76f98a59c30..18e214a712d 100644 --- a/model/main.go +++ b/model/main.go @@ -448,7 +448,9 @@ CREATE TABLE IF NOT EXISTS logs ( ip String DEFAULT '', request_id String DEFAULT '', upstream_request_id String DEFAULT '', - other String DEFAULT '' + other String DEFAULT '', + request_body String DEFAULT '', + response_body String DEFAULT '' ) ENGINE = MergeTree() PARTITION BY toYYYYMM(toDateTime(created_at)) diff --git a/model/option.go b/model/option.go index 8e8587f271c..fb6758272ef 100644 --- a/model/option.go +++ b/model/option.go @@ -48,6 +48,7 @@ func InitOptionMap() { common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled) common.OptionMap["AutomaticEnableChannelEnabled"] = strconv.FormatBool(common.AutomaticEnableChannelEnabled) common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled) + common.OptionMap["RequestDetailLogEnabled"] = strconv.FormatBool(common.RequestDetailLogEnabled) common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled) common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled) common.OptionMap["DrawingEnabled"] = strconv.FormatBool(common.DrawingEnabled) @@ -89,6 +90,8 @@ func InitOptionMap() { common.OptionMap["StripePriceId"] = setting.StripePriceId common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(setting.StripeUnitPrice, 'f', -1, 64) common.OptionMap["StripePromotionCodesEnabled"] = strconv.FormatBool(setting.StripePromotionCodesEnabled) + common.OptionMap["StripeWechatPayEnabled"] = strconv.FormatBool(setting.StripeWechatPayEnabled) + common.OptionMap["StripeAlipayEnabled"] = strconv.FormatBool(setting.StripeAlipayEnabled) common.OptionMap["CreemApiKey"] = setting.CreemApiKey common.OptionMap["CreemProducts"] = setting.CreemProducts common.OptionMap["CreemTestMode"] = strconv.FormatBool(setting.CreemTestMode) @@ -155,6 +158,14 @@ func InitOptionMap() { //common.OptionMap["ChatLink2"] = common.ChatLink2 common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64) common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes) + common.OptionMap["SameChannelRetryTimes"] = strconv.Itoa(common.SameChannelRetryTimes) + common.OptionMap["SameChannelRetryBaseDelayMs"] = strconv.Itoa(common.SameChannelRetryBaseDelayMs) + common.OptionMap["SameChannelRetryMaxDelayMs"] = strconv.Itoa(common.SameChannelRetryMaxDelayMs) + common.OptionMap["TtftRoutingEnabled"] = strconv.FormatBool(operation_setting.TtftRoutingEnabled) + common.OptionMap["TtftRoutingMinSamples"] = strconv.FormatInt(operation_setting.TtftRoutingMinSamples, 10) + common.OptionMap["TtftRoutingMinFactor"] = strconv.FormatFloat(operation_setting.TtftRoutingMinFactor, 'f', -1, 64) + common.OptionMap["TtftRoutingMaxFactor"] = strconv.FormatFloat(operation_setting.TtftRoutingMaxFactor, 'f', -1, 64) + common.OptionMap["TtftRoutingRefMs"] = strconv.FormatInt(operation_setting.TtftRoutingRefMs, 10) common.OptionMap["DataExportInterval"] = strconv.Itoa(common.DataExportInterval) common.OptionMap["DataExportDefaultTime"] = common.DataExportDefaultTime common.OptionMap["DefaultCollapseSidebar"] = strconv.FormatBool(common.DefaultCollapseSidebar) @@ -172,6 +183,7 @@ func InitOptionMap() { common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString() common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength) common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString() + common.OptionMap["UpstreamCapacityKeywords"] = operation_setting.UpstreamCapacityKeywordsToString() common.OptionMap["AutomaticDisableStatusCodes"] = operation_setting.AutomaticDisableStatusCodesToString() common.OptionMap["AutomaticRetryStatusCodes"] = operation_setting.AutomaticRetryStatusCodesToString() common.OptionMap["ExposeRatioEnabled"] = strconv.FormatBool(ratio_setting.IsExposeRatioEnabled()) @@ -306,8 +318,12 @@ func updateOptionMap(key string, value string) (err error) { common.AutomaticDisableChannelEnabled = boolValue case "AutomaticEnableChannelEnabled": common.AutomaticEnableChannelEnabled = boolValue + case "TtftRoutingEnabled": + operation_setting.TtftRoutingEnabled = boolValue case "LogConsumeEnabled": common.LogConsumeEnabled = boolValue + case "RequestDetailLogEnabled": + common.RequestDetailLogEnabled = boolValue case "DisplayInCurrencyEnabled": // 兼容旧字段:同步到新配置 general_setting.quota_display_type(运行时生效) // true -> USD, false -> TOKENS @@ -416,6 +432,10 @@ func updateOptionMap(key string, value string) (err error) { setting.StripeMinTopUp, _ = strconv.Atoi(value) case "StripePromotionCodesEnabled": setting.StripePromotionCodesEnabled = value == "true" + case "StripeWechatPayEnabled": + setting.StripeWechatPayEnabled = value == "true" + case "StripeAlipayEnabled": + setting.StripeAlipayEnabled = value == "true" case "CreemApiKey": setting.CreemApiKey = value case "CreemProducts": @@ -520,6 +540,41 @@ func updateOptionMap(key string, value string) (err error) { err = setting.UpdateModelRequestRateLimitGroupByJSONString(value) case "RetryTimes": common.RetryTimes, _ = strconv.Atoi(value) + case "SameChannelRetryTimes": + common.SameChannelRetryTimes, _ = strconv.Atoi(value) + if common.SameChannelRetryTimes < 0 { + common.SameChannelRetryTimes = 0 + } + case "SameChannelRetryBaseDelayMs": + common.SameChannelRetryBaseDelayMs, _ = strconv.Atoi(value) + if common.SameChannelRetryBaseDelayMs < 0 { + common.SameChannelRetryBaseDelayMs = 0 + } + case "SameChannelRetryMaxDelayMs": + common.SameChannelRetryMaxDelayMs, _ = strconv.Atoi(value) + if common.SameChannelRetryMaxDelayMs < 0 { + common.SameChannelRetryMaxDelayMs = 0 + } + case "TtftRoutingMinSamples": + if n, errConv := strconv.ParseInt(value, 10, 64); errConv == nil { + operation_setting.TtftRoutingMinSamples = n + } + operation_setting.ClampTtftRoutingConfig() + case "TtftRoutingMinFactor": + if f, errConv := strconv.ParseFloat(value, 64); errConv == nil { + operation_setting.TtftRoutingMinFactor = f + } + operation_setting.ClampTtftRoutingConfig() + case "TtftRoutingMaxFactor": + if f, errConv := strconv.ParseFloat(value, 64); errConv == nil { + operation_setting.TtftRoutingMaxFactor = f + } + operation_setting.ClampTtftRoutingConfig() + case "TtftRoutingRefMs": + if n, errConv := strconv.ParseInt(value, 10, 64); errConv == nil { + operation_setting.TtftRoutingRefMs = n + } + operation_setting.ClampTtftRoutingConfig() case "DataExportInterval": common.DataExportInterval, _ = strconv.Atoi(value) case "DataExportDefaultTime": @@ -560,6 +615,8 @@ func updateOptionMap(key string, value string) (err error) { setting.SensitiveWordsFromString(value) case "AutomaticDisableKeywords": operation_setting.AutomaticDisableKeywordsFromString(value) + case "UpstreamCapacityKeywords": + operation_setting.UpstreamCapacityKeywordsFromString(value) case "AutomaticDisableStatusCodes": err = operation_setting.AutomaticDisableStatusCodesFromString(value) case "AutomaticRetryStatusCodes": diff --git a/pkg/channellatency/store.go b/pkg/channellatency/store.go new file mode 100644 index 00000000000..bcc1c0556da --- /dev/null +++ b/pkg/channellatency/store.go @@ -0,0 +1,158 @@ +// ABOUTME: Maintains per-channel (and model) EWMA of successful stream first-token latency. +// ABOUTME: Supplies latency factors used to bias same-priority channel weights. +package channellatency + +import ( + "math" + "sort" + "strconv" + "sync" + + "github.com/QuantumNous/new-api/setting/operation_setting" +) + +type entry struct { + mu sync.Mutex + ewmaMs float64 + samples int64 +} + +type store struct { + entries sync.Map // key string -> *entry +} + +var defaultStore = &store{} + +func entryKey(channelId int, model string) string { + id := strconv.Itoa(channelId) + if model == "" { + return id + } + return id + "\x00" + model +} + +// Observe records a successful stream TTFT sample for the channel (and model). +// No-op when routing is disabled, channelId is 0, or ttftMs is negative. +func Observe(channelId int, model string, ttftMs int64) { + if !operation_setting.TtftRoutingEnabled { + return + } + if channelId == 0 || ttftMs < 0 { + return + } + operation_setting.ClampTtftRoutingConfig() + alpha := operation_setting.TtftRoutingEwmaAlpha + + key := entryKey(channelId, model) + raw, _ := defaultStore.entries.LoadOrStore(key, &entry{}) + e := raw.(*entry) + e.mu.Lock() + defer e.mu.Unlock() + if e.samples == 0 { + e.ewmaMs = float64(ttftMs) + } else { + e.ewmaMs = alpha*float64(ttftMs) + (1-alpha)*e.ewmaMs + } + e.samples++ +} + +// Snapshot returns the EWMA average and sample count for the exact +// (channelId, model) key. No cross-model or channel-only fallback. +func Snapshot(channelId int, model string) (avgMs float64, samples int64, ok bool) { + if channelId == 0 { + return 0, 0, false + } + raw, loaded := defaultStore.entries.Load(entryKey(channelId, model)) + if !loaded { + return 0, 0, false + } + e := raw.(*entry) + e.mu.Lock() + defer e.mu.Unlock() + if e.samples == 0 { + return 0, 0, false + } + return e.ewmaMs, e.samples, true +} + +// LatencyFactor returns a multiplier for base weight. Cold start and invalid +// averages yield 1.0. Faster than ref => factor > 1 (clamped). +func LatencyFactor(avgMs float64, samples int64, peerRefMs float64) float64 { + operation_setting.ClampTtftRoutingConfig() + if samples < operation_setting.TtftRoutingMinSamples || avgMs <= 0 { + return 1.0 + } + ref := peerRefMs + if ref <= 0 { + ref = float64(operation_setting.TtftRoutingRefMs) + } + factor := ref / avgMs + minF := operation_setting.TtftRoutingMinFactor + maxF := operation_setting.TtftRoutingMaxFactor + if factor < minF { + return minF + } + if factor > maxF { + return maxF + } + return factor +} + +// EffectiveWeight multiplies base weight by factor. Positive bases stay at least 1. +// Zero base stays 0 so existing zero-weight smoothing can apply. +func EffectiveWeight(baseWeight int, factor float64) int { + if baseWeight <= 0 { + return 0 + } + if factor <= 0 { + factor = 1 + } + out := int(math.Round(float64(baseWeight) * factor)) + if out < 1 { + return 1 + } + return out +} + +// PeerRefMs returns the median avg TTFT among candidates that have enough samples. +// Falls back to 0 when no peer qualifies (caller uses TtftRoutingRefMs). +func PeerRefMs(channelIDs []int, model string) float64 { + operation_setting.ClampTtftRoutingConfig() + minSamples := operation_setting.TtftRoutingMinSamples + avgs := make([]float64, 0, len(channelIDs)) + for _, id := range channelIDs { + avg, n, ok := Snapshot(id, model) + if !ok || n < minSamples || avg <= 0 { + continue + } + avgs = append(avgs, avg) + } + if len(avgs) == 0 { + return 0 + } + sort.Float64s(avgs) + mid := len(avgs) / 2 + if len(avgs)%2 == 1 { + return avgs[mid] + } + return (avgs[mid-1] + avgs[mid]) / 2 +} + +// WeightForSelection returns the weight to use for random selection when TTFT +// routing is enabled. When disabled, returns baseWeight unchanged. +func WeightForSelection(channelId int, model string, baseWeight int, peerRefMs float64) int { + if !operation_setting.TtftRoutingEnabled { + return baseWeight + } + avg, samples, ok := Snapshot(channelId, model) + if !ok { + return baseWeight + } + factor := LatencyFactor(avg, samples, peerRefMs) + return EffectiveWeight(baseWeight, factor) +} + +// ResetForTest clears the default store. Tests only. +func ResetForTest() { + defaultStore.entries = sync.Map{} +} diff --git a/pkg/channellatency/store_test.go b/pkg/channellatency/store_test.go new file mode 100644 index 00000000000..23bd83918c0 --- /dev/null +++ b/pkg/channellatency/store_test.go @@ -0,0 +1,137 @@ +// ABOUTME: Unit tests for channel TTFT EWMA storage and weight factor math. +// ABOUTME: Covers cold start, clamping, peer median, and effective weights. +package channellatency + +import ( + "testing" + + "github.com/QuantumNous/new-api/setting/operation_setting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLatencyFactorColdStartAndClamp(t *testing.T) { + origMin := operation_setting.TtftRoutingMinSamples + origMinF := operation_setting.TtftRoutingMinFactor + origMaxF := operation_setting.TtftRoutingMaxFactor + origRef := operation_setting.TtftRoutingRefMs + t.Cleanup(func() { + operation_setting.TtftRoutingMinSamples = origMin + operation_setting.TtftRoutingMinFactor = origMinF + operation_setting.TtftRoutingMaxFactor = origMaxF + operation_setting.TtftRoutingRefMs = origRef + }) + + operation_setting.TtftRoutingMinSamples = 5 + operation_setting.TtftRoutingMinFactor = 0.5 + operation_setting.TtftRoutingMaxFactor = 2.0 + operation_setting.TtftRoutingRefMs = 500 + + assert.Equal(t, 1.0, LatencyFactor(100, 4, 0)) + assert.InDelta(t, 2.0, LatencyFactor(100, 5, 0), 1e-9) // 500/100=5 clamped to 2 + assert.InDelta(t, 0.5, LatencyFactor(2000, 5, 0), 1e-9) // 500/2000=0.25 clamped to 0.5 + assert.InDelta(t, 1.0, LatencyFactor(500, 5, 0), 1e-9) + assert.InDelta(t, 2.0, LatencyFactor(100, 5, 200), 1e-9) // peer ref 200 -> 2.0 +} + +func TestEffectiveWeight(t *testing.T) { + assert.Equal(t, 0, EffectiveWeight(0, 2)) + assert.Equal(t, 20, EffectiveWeight(10, 2)) + assert.Equal(t, 1, EffectiveWeight(1, 0.1)) + assert.Equal(t, 10, EffectiveWeight(10, 0)) +} + +func TestObserveAndWeightForSelection(t *testing.T) { + ResetForTest() + origEnabled := operation_setting.TtftRoutingEnabled + origMin := operation_setting.TtftRoutingMinSamples + origAlpha := operation_setting.TtftRoutingEwmaAlpha + origRef := operation_setting.TtftRoutingRefMs + t.Cleanup(func() { + operation_setting.TtftRoutingEnabled = origEnabled + operation_setting.TtftRoutingMinSamples = origMin + operation_setting.TtftRoutingEwmaAlpha = origAlpha + operation_setting.TtftRoutingRefMs = origRef + ResetForTest() + }) + + operation_setting.TtftRoutingEnabled = true + operation_setting.TtftRoutingMinSamples = 3 + operation_setting.TtftRoutingEwmaAlpha = 1.0 // last sample wins for determinism + operation_setting.TtftRoutingRefMs = 500 + operation_setting.TtftRoutingMinFactor = 0.5 + operation_setting.TtftRoutingMaxFactor = 2.0 + + for i := 0; i < 3; i++ { + Observe(7, "gpt-4o", 100) + } + avg, n, ok := Snapshot(7, "gpt-4o") + require.True(t, ok) + assert.Equal(t, int64(3), n) + assert.InDelta(t, 100.0, avg, 1e-9) + + // Fast channel should get higher effective weight than base 10. + assert.Equal(t, 20, WeightForSelection(7, "gpt-4o", 10, 0)) + + operation_setting.TtftRoutingEnabled = false + assert.Equal(t, 10, WeightForSelection(7, "gpt-4o", 10, 0)) +} + +func TestSnapshotNoChannelOnlyFallback(t *testing.T) { + ResetForTest() + origEnabled := operation_setting.TtftRoutingEnabled + origMin := operation_setting.TtftRoutingMinSamples + origAlpha := operation_setting.TtftRoutingEwmaAlpha + t.Cleanup(func() { + operation_setting.TtftRoutingEnabled = origEnabled + operation_setting.TtftRoutingMinSamples = origMin + operation_setting.TtftRoutingEwmaAlpha = origAlpha + ResetForTest() + }) + + operation_setting.TtftRoutingEnabled = true + operation_setting.TtftRoutingMinSamples = 1 + operation_setting.TtftRoutingEwmaAlpha = 1.0 + + // Channel-only samples must not leak into a named model key. + Observe(9, "", 100) + _, _, ok := Snapshot(9, "gpt-4o") + assert.False(t, ok) + + // Different models on the same channel stay independent. + Observe(9, "gpt-4o", 100) + Observe(9, "claude-3.5", 2000) + avg4o, n4o, ok4o := Snapshot(9, "gpt-4o") + avgClaude, nClaude, okClaude := Snapshot(9, "claude-3.5") + require.True(t, ok4o) + require.True(t, okClaude) + assert.Equal(t, int64(1), n4o) + assert.Equal(t, int64(1), nClaude) + assert.InDelta(t, 100.0, avg4o, 1e-9) + assert.InDelta(t, 2000.0, avgClaude, 1e-9) +} + +func TestPeerRefMsMedian(t *testing.T) { + ResetForTest() + origEnabled := operation_setting.TtftRoutingEnabled + origMin := operation_setting.TtftRoutingMinSamples + origAlpha := operation_setting.TtftRoutingEwmaAlpha + t.Cleanup(func() { + operation_setting.TtftRoutingEnabled = origEnabled + operation_setting.TtftRoutingMinSamples = origMin + operation_setting.TtftRoutingEwmaAlpha = origAlpha + ResetForTest() + }) + + operation_setting.TtftRoutingEnabled = true + operation_setting.TtftRoutingMinSamples = 2 + operation_setting.TtftRoutingEwmaAlpha = 1.0 + + for i := 0; i < 2; i++ { + Observe(1, "m", 100) + Observe(2, "m", 300) + Observe(3, "m", 500) + } + assert.InDelta(t, 300.0, PeerRefMs([]int{1, 2, 3}, "m"), 1e-9) + assert.Equal(t, 0.0, PeerRefMs([]int{99}, "m")) +} diff --git a/relay/channel/api_request.go b/relay/channel/api_request.go index 9fae7df078d..392796a1e6d 100644 --- a/relay/channel/api_request.go +++ b/relay/channel/api_request.go @@ -519,6 +519,8 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http c.Set(common2.UpstreamRequestIdKey, upID) } + service.MaybeCaptureUpstreamHTTPResponseForLog(info, resp) + _ = req.Body.Close() _ = c.Request.Body.Close() return resp, nil diff --git a/relay/channel/gemini/adaptor.go b/relay/channel/gemini/adaptor.go index e0ab48e2dbc..1ad067dd13a 100644 --- a/relay/channel/gemini/adaptor.go +++ b/relay/channel/gemini/adaptor.go @@ -67,6 +67,7 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf // convert size to aspect ratio but allow user to specify aspect ratio aspectRatio := "1:1" // default aspect ratio + size := strings.TrimSpace(request.Size) if size != "" { if strings.Contains(size, ":") { diff --git a/relay/channel/gemini/dto.go b/relay/channel/gemini/dto.go new file mode 100644 index 00000000000..15b7959a9ea --- /dev/null +++ b/relay/channel/gemini/dto.go @@ -0,0 +1,223 @@ +package gemini + +import "encoding/json" + +type GeminiChatRequest struct { + Contents []GeminiChatContent `json:"contents"` + SafetySettings []GeminiChatSafetySettings `json:"safetySettings,omitempty"` + GenerationConfig GeminiChatGenerationConfig `json:"generationConfig,omitempty"` + Tools []GeminiChatTool `json:"tools,omitempty"` + SystemInstructions *GeminiChatContent `json:"systemInstruction,omitempty"` +} + +type GeminiThinkingConfig struct { + IncludeThoughts bool `json:"includeThoughts,omitempty"` + ThinkingBudget *int `json:"thinkingBudget,omitempty"` +} + +func (c *GeminiThinkingConfig) SetThinkingBudget(budget int) { + c.ThinkingBudget = &budget +} + +type GeminiInlineData struct { + MimeType string `json:"mimeType"` + Data string `json:"data"` +} + +// UnmarshalJSON custom unmarshaler for GeminiInlineData to support snake_case and camelCase for MimeType +func (g *GeminiInlineData) UnmarshalJSON(data []byte) error { + type Alias GeminiInlineData // Use type alias to avoid recursion + var aux struct { + Alias + MimeTypeSnake string `json:"mime_type"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + *g = GeminiInlineData(aux.Alias) // Copy other fields if any in future + + // Prioritize snake_case if present + if aux.MimeTypeSnake != "" { + g.MimeType = aux.MimeTypeSnake + } else if aux.MimeType != "" { // Fallback to camelCase from Alias + g.MimeType = aux.MimeType + } + // g.Data would be populated by aux.Alias.Data + return nil +} + +type FunctionCall struct { + FunctionName string `json:"name"` + Arguments any `json:"args"` +} + +type FunctionResponse struct { + Name string `json:"name"` + Response map[string]interface{} `json:"response"` +} + +type GeminiPartExecutableCode struct { + Language string `json:"language,omitempty"` + Code string `json:"code,omitempty"` +} + +type GeminiPartCodeExecutionResult struct { + Outcome string `json:"outcome,omitempty"` + Output string `json:"output,omitempty"` +} + +type GeminiFileData struct { + MimeType string `json:"mimeType,omitempty"` + FileUri string `json:"fileUri,omitempty"` +} + +type GeminiPart struct { + Text string `json:"text,omitempty"` + Thought bool `json:"thought,omitempty"` + InlineData *GeminiInlineData `json:"inlineData,omitempty"` + FunctionCall *FunctionCall `json:"functionCall,omitempty"` + FunctionResponse *FunctionResponse `json:"functionResponse,omitempty"` + FileData *GeminiFileData `json:"fileData,omitempty"` + ExecutableCode *GeminiPartExecutableCode `json:"executableCode,omitempty"` + CodeExecutionResult *GeminiPartCodeExecutionResult `json:"codeExecutionResult,omitempty"` +} + +// UnmarshalJSON custom unmarshaler for GeminiPart to support snake_case and camelCase for InlineData +func (p *GeminiPart) UnmarshalJSON(data []byte) error { + // Alias to avoid recursion during unmarshalling + type Alias GeminiPart + var aux struct { + Alias + InlineDataSnake *GeminiInlineData `json:"inline_data,omitempty"` // snake_case variant + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + // Assign fields from alias + *p = GeminiPart(aux.Alias) + + // Prioritize snake_case for InlineData if present + if aux.InlineDataSnake != nil { + p.InlineData = aux.InlineDataSnake + } else if aux.InlineData != nil { // Fallback to camelCase from Alias + p.InlineData = aux.InlineData + } + // Other fields like Text, FunctionCall etc. are already populated via aux.Alias + + return nil +} + +type GeminiChatContent struct { + Role string `json:"role,omitempty"` + Parts []GeminiPart `json:"parts"` +} + +type GeminiChatSafetySettings struct { + Category string `json:"category"` + Threshold string `json:"threshold"` +} + +type GeminiChatTool struct { + GoogleSearch any `json:"googleSearch,omitempty"` + GoogleSearchRetrieval any `json:"googleSearchRetrieval,omitempty"` + CodeExecution any `json:"codeExecution,omitempty"` + FunctionDeclarations any `json:"functionDeclarations,omitempty"` +} + +type GeminiChatGenerationConfig struct { + Temperature *float64 `json:"temperature,omitempty"` + TopP float64 `json:"topP,omitempty"` + TopK float64 `json:"topK,omitempty"` + MaxOutputTokens uint `json:"maxOutputTokens,omitempty"` + CandidateCount int `json:"candidateCount,omitempty"` + StopSequences []string `json:"stopSequences,omitempty"` + ResponseMimeType string `json:"responseMimeType,omitempty"` + ResponseSchema any `json:"responseSchema,omitempty"` + Seed int64 `json:"seed,omitempty"` + ResponseModalities []string `json:"responseModalities,omitempty"` + ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"` + SpeechConfig json.RawMessage `json:"speechConfig,omitempty"` // RawMessage to allow flexible speech config +} + +type GeminiChatCandidate struct { + Content GeminiChatContent `json:"content"` + FinishReason *string `json:"finishReason"` + Index int64 `json:"index"` + SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"` +} + +type GeminiChatSafetyRating struct { + Category string `json:"category"` + Probability string `json:"probability"` +} + +type GeminiChatPromptFeedback struct { + SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"` +} + +type GeminiChatResponse struct { + Candidates []GeminiChatCandidate `json:"candidates"` + PromptFeedback GeminiChatPromptFeedback `json:"promptFeedback"` + UsageMetadata GeminiUsageMetadata `json:"usageMetadata"` +} + +type GeminiUsageMetadata struct { + PromptTokenCount int `json:"promptTokenCount"` + CandidatesTokenCount int `json:"candidatesTokenCount"` + TotalTokenCount int `json:"totalTokenCount"` + ThoughtsTokenCount int `json:"thoughtsTokenCount"` + PromptTokensDetails []GeminiPromptTokensDetails `json:"promptTokensDetails"` +} + +type GeminiPromptTokensDetails struct { + Modality string `json:"modality"` + TokenCount int `json:"tokenCount"` +} + +// Imagen related structs +type GeminiImageRequest struct { + Instances []GeminiImageInstance `json:"instances"` + Parameters GeminiImageParameters `json:"parameters"` +} + +type GeminiImageInstance struct { + Prompt string `json:"prompt"` +} + +type GeminiImageParameters struct { + SampleCount int `json:"sampleCount,omitempty"` + AspectRatio string `json:"aspectRatio,omitempty"` + PersonGeneration string `json:"personGeneration,omitempty"` + SampleImageSize string `json:"sampleImageSize,omitempty"` +} + +type GeminiImageResponse struct { + Predictions []GeminiImagePrediction `json:"predictions"` +} + +type GeminiImagePrediction struct { + MimeType string `json:"mimeType"` + BytesBase64Encoded string `json:"bytesBase64Encoded"` + RaiFilteredReason string `json:"raiFilteredReason,omitempty"` + SafetyAttributes any `json:"safetyAttributes,omitempty"` +} + +// Embedding related structs +type GeminiEmbeddingRequest struct { + Content GeminiChatContent `json:"content"` + TaskType string `json:"taskType,omitempty"` + Title string `json:"title,omitempty"` + OutputDimensionality int `json:"outputDimensionality,omitempty"` +} + +type GeminiEmbeddingResponse struct { + Embedding ContentEmbedding `json:"embedding"` +} + +type ContentEmbedding struct { + Values []float64 `json:"values"` +} diff --git a/relay/channel/minimax/constants.go b/relay/channel/minimax/constants.go index efdab04c34c..6e102cccaa2 100644 --- a/relay/channel/minimax/constants.go +++ b/relay/channel/minimax/constants.go @@ -23,6 +23,8 @@ var ModelList = []string{ "MiniMax-M2.5-highspeed", "image-01", "image-01-live", + "MiniMax-M2.7", + "MiniMax-M2.7-highspeed", } var ChannelName = "minimax" diff --git a/relay/channel/openai/chat_via_responses.go b/relay/channel/openai/chat_via_responses.go index 18758e728d4..bbce2f75b6b 100644 --- a/relay/channel/openai/chat_via_responses.go +++ b/relay/channel/openai/chat_via_responses.go @@ -37,8 +37,13 @@ func OaiResponsesToChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } - if oaiError := responsesResp.GetOpenAIError(); oaiError != nil && oaiError.Type != "" { - return nil, types.WithOpenAIError(*oaiError, resp.StatusCode) + if oaiError := responsesResp.GetOpenAIError(); openAIErrorUsable(oaiError) { + return nil, service.ApplySoftFailRetryPolicy(c, info, service.NormalizeSoftUpstreamError(types.WithOpenAIError(*oaiError, resp.StatusCode))) + } + + if relayconvert.IsEmptyCompletedResponses(&responsesResp) { + // Non-stream checks before any client write, so this remains switchable. + return nil, service.ApplySoftFailRetryPolicy(c, info, service.NewEmptyCompletedError()) } chatResult, err := relayconvert.ConvertResponse(c, info, types.RelayFormatOpenAI, &responsesResp) @@ -74,6 +79,7 @@ func OaiResponsesToChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp } service.IOCopyBytesGracefully(c, resp, responseBody) + service.MarkClientPayloadWrittenContext(c, info) return usage, nil } @@ -123,8 +129,8 @@ func OaiResponsesToChatBufferedStreamHandler(c *gin.Context, info *relaycommon.R } case "response.failed", "response.error": if streamResp.Response != nil { - if oaiErr := streamResp.Response.GetOpenAIError(); oaiErr != nil && oaiErr.Type != "" { - streamErr = types.WithOpenAIError(*oaiErr, http.StatusInternalServerError) + if oaiErr := streamResp.Response.GetOpenAIError(); openAIErrorUsable(oaiErr) { + streamErr = service.NormalizeSoftUpstreamError(types.WithOpenAIError(*oaiErr, http.StatusInternalServerError)) break } } @@ -135,7 +141,7 @@ func OaiResponsesToChatBufferedStreamHandler(c *gin.Context, info *relaycommon.R } } if streamErr != nil { - return nil, streamErr + return nil, service.ApplySoftFailRetryPolicy(c, info, streamErr) } if err := scanner.Err(); err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) @@ -150,6 +156,10 @@ func OaiResponsesToChatBufferedStreamHandler(c *gin.Context, info *relaycommon.R } accumulator.SupplementResponseOutput(finalResponse) + if relayconvert.IsEmptyCompletedResponses(finalResponse) { + return nil, service.ApplySoftFailRetryPolicy(c, info, service.NewEmptyCompletedError()) + } + chatResult, err := relayconvert.ConvertResponse(c, info, types.RelayFormatOpenAI, finalResponse) if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) @@ -182,6 +192,7 @@ func OaiResponsesToChatBufferedStreamHandler(c *gin.Context, info *relaycommon.R } service.IOCopyBytesGracefully(c, resp, responseBody) + service.MarkClientPayloadWrittenContext(c, info) return usage, nil } @@ -203,6 +214,7 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) } streamErr := (*types.NewAPIError)(nil) + sentAssistantRole := false if info.RelayFormat == types.RelayFormatClaude && info.ClaudeConvertInfo == nil { info.ClaudeConvertInfo = &relaycommon.ClaudeConvertInfo{LastMessagesType: relaycommon.LastMessageTypeNone} @@ -219,6 +231,7 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo } c.Render(-1, common.CustomEvent{Data: "data: " + string(geminiResponseStr)}) _ = helper.FlushWriter(c) + service.MarkClientPayloadWrittenContext(c, info) return true } @@ -228,25 +241,49 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo if len(value.Choices) == 0 && value.Usage == nil { return true } + // Skip role-only empty start chunks so empty-completed can still failover. + if !service.ChatCompletionsStreamHasClientPayload(&value) { + return true + } + if !sentAssistantRole { + if err := helper.ObjectData(c, helper.GenerateStartEmptyResponse(responseId, createAt, info.UpstreamModelName, nil)); err != nil { + streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) + return false + } + sentAssistantRole = true + } if err := helper.ObjectData(c, &value); err != nil { streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) return false } + service.MarkClientPayloadWrittenContext(c, info) return true case *dto.ChatCompletionsStreamResponse: if value == nil || (len(value.Choices) == 0 && value.Usage == nil) { return true } + if !service.ChatCompletionsStreamHasClientPayload(value) { + return true + } + if !sentAssistantRole { + if err := helper.ObjectData(c, helper.GenerateStartEmptyResponse(responseId, createAt, info.UpstreamModelName, nil)); err != nil { + streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) + return false + } + sentAssistantRole = true + } if err := helper.ObjectData(c, value); err != nil { streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) return false } + service.MarkClientPayloadWrittenContext(c, info) return true case dto.ClaudeResponse: if err := helper.ClaudeData(c, value); err != nil { streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) return false } + service.MarkClientPayloadWrittenContext(c, info) return true case *dto.ClaudeResponse: if value == nil { @@ -256,6 +293,7 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) return false } + service.MarkClientPayloadWrittenContext(c, info) return true case dto.GeminiChatResponse: return sendGeminiResponse(&value) @@ -282,8 +320,8 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo if streamResp.Type == "response.error" || streamResp.Type == "response.failed" { if streamResp.Response != nil { - if oaiErr := streamResp.Response.GetOpenAIError(); oaiErr != nil && oaiErr.Type != "" { - streamErr = types.WithOpenAIError(*oaiErr, http.StatusInternalServerError) + if oaiErr := streamResp.Response.GetOpenAIError(); openAIErrorUsable(oaiErr) { + streamErr = service.ApplySoftFailRetryPolicy(c, info, service.NormalizeSoftUpstreamError(types.WithOpenAIError(*oaiErr, http.StatusInternalServerError))) sr.Stop(streamErr) return } @@ -293,6 +331,20 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo return } + // Intercept empty completed before converting/writing terminal chunks so + // zero-write attempts can still switch channels. + // Note: many providers send usage-only completed events without re-embedding + // output; if we already flushed content/tools, do not reclassify as empty. + if streamResp.Type == "response.completed" || streamResp.Type == "response.done" { + if streamResp.Response != nil && + relayconvert.IsEmptyCompletedResponses(streamResp.Response) && + !service.IsClientPayloadWritten(c, info) { + streamErr = service.ApplySoftFailRetryPolicy(c, info, service.NewEmptyCompletedError()) + sr.Stop(streamErr) + return + } + } + results, err := relayconvert.ConvertStreamResponseChunk(c, info, state, &streamResp) if err != nil { streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) @@ -308,7 +360,7 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo }) if streamErr != nil { - return nil, streamErr + return nil, service.ApplySoftFailRetryPolicy(c, info, streamErr) } usage := state.Usage() @@ -340,3 +392,18 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo } return usage, nil } + +// openAIErrorUsable reports whether an upstream OpenAI error object has enough +// signal to surface (message, code, or type). Type-only empty objects are ignored. +func openAIErrorUsable(err *types.OpenAIError) bool { + if err == nil { + return false + } + if strings.TrimSpace(err.Message) != "" { + return true + } + if err.Code != nil && fmt.Sprintf("%v", err.Code) != "" && fmt.Sprintf("%v", err.Code) != "" { + return true + } + return strings.TrimSpace(err.Type) != "" +} diff --git a/relay/channel/openai/chat_via_responses_test.go b/relay/channel/openai/chat_via_responses_test.go index dd6d8e39456..6809fc51f19 100644 --- a/relay/channel/openai/chat_via_responses_test.go +++ b/relay/channel/openai/chat_via_responses_test.go @@ -178,3 +178,175 @@ func requireOrderedSubstrings(t *testing.T, s string, parts ...string) { offset += idx + len(part) } } + +func TestOaiResponsesToChatHandlerEmptyCompleted(t *testing.T) { + oldMode := gin.Mode() + gin.SetMode(gin.TestMode) + t.Cleanup(func() { gin.SetMode(oldMode) }) + + body := `{"id":"resp_empty","model":"gpt-test","status":"completed","output":[{"type":"reasoning","content":[{"type":"reasoning_text","text":"thinking only"}]}],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}` + c, recorder, resp, info := newResponsesChatTestContext(t, body, false) + resp.Header.Set("Content-Type", "application/json") + + usage, err := OaiResponsesToChatHandler(c, info, resp) + require.Nil(t, usage) + require.NotNil(t, err) + require.Equal(t, types.ErrorCodeEmptyResponse, err.GetErrorCode()) + require.Equal(t, http.StatusServiceUnavailable, err.StatusCode) + require.True(t, info.EmptyCompleted) + require.True(t, info.AffinityUnusable) + require.Empty(t, recorder.Body.String()) +} + +func TestOaiResponsesToChatBufferedStreamHandlerEmptyCompleted(t *testing.T) { + oldMode := gin.Mode() + gin.SetMode(gin.TestMode) + t.Cleanup(func() { gin.SetMode(oldMode) }) + + body := strings.Join([]string{ + `data: {"type":"response.completed","response":{"id":"resp_empty","model":"gpt-test","status":"completed","output":[{"type":"reasoning","content":[{"type":"reasoning_text","text":"thinking only"}]}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`, + `data: [DONE]`, + ``, + }, "\n") + + c, recorder, resp, info := newResponsesChatTestContext(t, body, false) + usage, err := OaiResponsesToChatBufferedStreamHandler(c, info, resp) + require.Nil(t, usage) + require.NotNil(t, err) + require.Equal(t, types.ErrorCodeEmptyResponse, err.GetErrorCode()) + require.Equal(t, http.StatusServiceUnavailable, err.StatusCode) + require.True(t, info.EmptyCompleted) + require.True(t, info.AffinityUnusable) + require.Empty(t, recorder.Body.String()) +} + +func TestOaiResponsesToChatStreamHandlerEmptyCompletedZeroWrite(t *testing.T) { + oldMode := gin.Mode() + gin.SetMode(gin.TestMode) + t.Cleanup(func() { gin.SetMode(oldMode) }) + + oldTimeout := constant.StreamingTimeout + constant.StreamingTimeout = 30 + t.Cleanup(func() { constant.StreamingTimeout = oldTimeout }) + + body := strings.Join([]string{ + `data: {"type":"response.completed","response":{"id":"resp_empty","model":"gpt-test","status":"completed","output":[{"type":"reasoning","content":[{"type":"reasoning_text","text":"thinking only"}]}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`, + `data: [DONE]`, + ``, + }, "\n") + + c, recorder, resp, info := newResponsesChatTestContext(t, body, true) + usage, err := OaiResponsesToChatStreamHandler(c, info, resp) + require.Nil(t, usage) + require.NotNil(t, err) + require.Equal(t, types.ErrorCodeEmptyResponse, err.GetErrorCode()) + require.Equal(t, http.StatusServiceUnavailable, err.StatusCode) + require.True(t, info.EmptyCompleted) + require.True(t, info.AffinityUnusable) + require.False(t, info.ClientPayloadWritten) + // Headers may be set for SSE, but no assistant payload should be committed. + require.NotContains(t, recorder.Body.String(), `"content"`) + require.NotContains(t, recorder.Body.String(), `thinking only`) +} + +func TestOaiResponsesToChatStreamHandlerEmptyCompletedAfterCreated(t *testing.T) { + oldMode := gin.Mode() + gin.SetMode(gin.TestMode) + t.Cleanup(func() { gin.SetMode(oldMode) }) + + oldTimeout := constant.StreamingTimeout + constant.StreamingTimeout = 30 + t.Cleanup(func() { constant.StreamingTimeout = oldTimeout }) + + // Real Grok-like sequence: created emits role-only start which must NOT commit the stream. + body := strings.Join([]string{ + `data: {"type":"response.created","response":{"id":"resp_empty","model":"gpt-test","created_at":1710000000}}`, + `data: {"type":"response.completed","response":{"id":"resp_empty","model":"gpt-test","status":"completed","output":[{"type":"reasoning","content":[{"type":"reasoning_text","text":"thinking only"}]}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`, + `data: [DONE]`, + ``, + }, "\n") + + c, recorder, resp, info := newResponsesChatTestContext(t, body, true) + usage, err := OaiResponsesToChatStreamHandler(c, info, resp) + require.Nil(t, usage) + require.NotNil(t, err) + require.Equal(t, types.ErrorCodeEmptyResponse, err.GetErrorCode()) + require.False(t, info.ClientPayloadWritten) + require.False(t, types.IsSkipRetryError(err)) + require.NotContains(t, recorder.Body.String(), `thinking only`) +} + +func TestOaiResponsesToChatStreamHandlerCapacityAfterReasoningSkipsRetry(t *testing.T) { + oldMode := gin.Mode() + gin.SetMode(gin.TestMode) + t.Cleanup(func() { gin.SetMode(oldMode) }) + + oldTimeout := constant.StreamingTimeout + constant.StreamingTimeout = 30 + t.Cleanup(func() { constant.StreamingTimeout = oldTimeout }) + + body := strings.Join([]string{ + `data: {"type":"response.created","response":{"id":"resp_1","model":"gpt-test","created_at":1710000000}}`, + `data: {"type":"response.reasoning_summary_text.delta","delta":"planning search"}`, + `data: {"type":"response.failed","response":{"error":{"message":"The model is currently at capacity due to high demand. Please use priority processing","type":"upstream_error","code":null}}}`, + `data: [DONE]`, + ``, + }, "\n") + + c, recorder, resp, info := newResponsesChatTestContext(t, body, true) + usage, err := OaiResponsesToChatStreamHandler(c, info, resp) + require.Nil(t, usage) + require.NotNil(t, err) + require.Equal(t, types.ErrorCodeUpstreamCapacity, err.GetErrorCode()) + require.True(t, info.ClientPayloadWritten) + require.True(t, types.IsSkipRetryError(err)) + require.Contains(t, recorder.Body.String(), `planning search`) +} + +func TestOaiResponsesToChatStreamHandlerCapacityFailedEvent(t *testing.T) { + oldMode := gin.Mode() + gin.SetMode(gin.TestMode) + t.Cleanup(func() { gin.SetMode(oldMode) }) + + oldTimeout := constant.StreamingTimeout + constant.StreamingTimeout = 30 + t.Cleanup(func() { constant.StreamingTimeout = oldTimeout }) + + body := strings.Join([]string{ + `data: {"type":"response.failed","response":{"error":{"message":"The model is currently at capacity due to high demand. Please use priority processing","type":"upstream_error","code":null}}}`, + `data: [DONE]`, + ``, + }, "\n") + + c, _, resp, info := newResponsesChatTestContext(t, body, true) + usage, err := OaiResponsesToChatStreamHandler(c, info, resp) + require.Nil(t, usage) + require.NotNil(t, err) + require.Equal(t, types.ErrorCodeUpstreamCapacity, err.GetErrorCode()) + require.Equal(t, http.StatusServiceUnavailable, err.StatusCode) + require.False(t, types.IsSkipRetryError(err)) + require.False(t, info.ClientPayloadWritten) +} + +func TestOaiResponsesToChatStreamHandlerCapacityNullCodeWithoutType(t *testing.T) { + oldMode := gin.Mode() + gin.SetMode(gin.TestMode) + t.Cleanup(func() { gin.SetMode(oldMode) }) + + oldTimeout := constant.StreamingTimeout + constant.StreamingTimeout = 30 + t.Cleanup(func() { constant.StreamingTimeout = oldTimeout }) + + body := strings.Join([]string{ + `data: {"type":"response.failed","response":{"error":{"message":"The model is currently at capacity due to high demand","code":null}}}`, + `data: [DONE]`, + ``, + }, "\n") + + c, _, resp, info := newResponsesChatTestContext(t, body, true) + usage, err := OaiResponsesToChatStreamHandler(c, info, resp) + require.Nil(t, usage) + require.NotNil(t, err) + require.Equal(t, types.ErrorCodeUpstreamCapacity, err.GetErrorCode()) + _ = info +} diff --git a/relay/channel/openai/relay-openai.go b/relay/channel/openai/relay-openai.go index 50415c8b353..36727af8a4a 100644 --- a/relay/channel/openai/relay-openai.go +++ b/relay/channel/openai/relay-openai.go @@ -122,12 +122,25 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re // 检查是否为音频模型 isAudioModel := strings.Contains(strings.ToLower(model), "audio") + var streamErr *types.NewAPIError helper.StreamScannerHandler(c, resp, info, func(data string, sr *helper.StreamResult) { + if streamErr != nil { + sr.Stop(streamErr) + return + } + // Detect OpenAI-style error objects in SSE before forwarding (capacity/null-code). + if softErr := softFailFromChatStreamData(data); softErr != nil { + streamErr = service.ApplySoftFailRetryPolicy(c, info, softErr) + sr.Stop(streamErr) + return + } if lastStreamData != "" { if err := HandleStreamFormat(c, info, lastStreamData, info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent); err != nil { common.SysLog("error handling stream format: " + err.Error()) sr.Error(err) + } else if chatStreamDataHasClientPayload(lastStreamData) { + service.MarkClientPayloadWrittenContext(c, info) } } if len(data) > 0 { @@ -144,6 +157,10 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re } }) + if streamErr != nil { + return nil, service.ApplySoftFailRetryPolicy(c, info, streamErr) + } + // 对音频模型,从倒数第二个stream data中提取usage信息 if isAudioModel && secondLastStreamData != "" { var streamResp struct { @@ -218,7 +235,7 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo } if oaiError := simpleResponse.GetOpenAIError(); oaiError != nil && oaiError.Type != "" { - return nil, types.WithOpenAIError(*oaiError, resp.StatusCode) + return nil, service.NormalizeSoftUpstreamError(types.WithOpenAIError(*oaiError, resp.StatusCode)) } for _, choice := range simpleResponse.Choices { @@ -297,3 +314,29 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo return &simpleResponse.Usage, nil } + +// softFailFromChatStreamData detects OpenAI-style error objects embedded in chat SSE payloads. +func softFailFromChatStreamData(data string) *types.NewAPIError { + if strings.TrimSpace(data) == "" { + return nil + } + var envelope struct { + Error *types.OpenAIError `json:"error"` + } + if err := common.UnmarshalJsonStr(data, &envelope); err != nil || envelope.Error == nil { + return nil + } + if !openAIErrorUsable(envelope.Error) { + return nil + } + return service.NormalizeSoftUpstreamError(types.WithOpenAIError(*envelope.Error, http.StatusInternalServerError)) +} + +// chatStreamDataHasClientPayload reports whether a raw chat SSE chunk carries assistant payload. +func chatStreamDataHasClientPayload(data string) bool { + var chunk dto.ChatCompletionsStreamResponse + if err := common.UnmarshalJsonStr(data, &chunk); err != nil { + return false + } + return service.ChatCompletionsStreamHasClientPayload(&chunk) +} diff --git a/relay/channel/openai/relay_responses.go b/relay/channel/openai/relay_responses.go index 9293183168e..4060589d8ce 100644 --- a/relay/channel/openai/relay_responses.go +++ b/relay/channel/openai/relay_responses.go @@ -12,6 +12,7 @@ import ( relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/service/relayconvert" "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" @@ -30,8 +31,12 @@ func OaiResponsesHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } - if oaiError := responsesResponse.GetOpenAIError(); oaiError != nil && oaiError.Type != "" { - return nil, types.WithOpenAIError(*oaiError, resp.StatusCode) + if oaiError := responsesResponse.GetOpenAIError(); openAIErrorUsable(oaiError) { + return nil, service.ApplySoftFailRetryPolicy(c, info, service.NormalizeSoftUpstreamError(types.WithOpenAIError(*oaiError, resp.StatusCode))) + } + + if relayconvert.IsEmptyCompletedResponses(&responsesResponse) { + return nil, service.ApplySoftFailRetryPolicy(c, info, service.NewEmptyCompletedError()) } if responsesResponse.HasImageGenerationCall() { @@ -42,6 +47,7 @@ func OaiResponsesHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http // 写入新的 response body service.IOCopyBytesGracefully(c, resp, responseBody) + service.MarkClientPayloadWrittenContext(c, info) // compute usage usage := dto.Usage{} @@ -79,8 +85,31 @@ func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp var usage = &dto.Usage{} var responseTextBuilder strings.Builder + var streamErr *types.NewAPIError + // Buffer lifecycle-only frames so empty/capacity soft fails can still zero-write retry. + var lifecycleBuf []string + + flushLifecycle := func() { + if len(lifecycleBuf) == 0 { + return + } + for _, raw := range lifecycleBuf { + var buffered dto.ResponsesStreamResponse + if err := common.UnmarshalJsonStr(raw, &buffered); err != nil { + continue + } + sendResponsesStreamData(c, buffered, raw) + } + lifecycleBuf = nil + // Once lifecycle frames are flushed the client stream is committed. + service.MarkClientPayloadWrittenContext(c, info) + } helper.StreamScannerHandler(c, resp, info, func(data string, sr *helper.StreamResult) { + if streamErr != nil { + sr.Stop(streamErr) + return + } // 检查当前数据是否包含 completed 状态和 usage 信息 var streamResponse dto.ResponsesStreamResponse @@ -89,9 +118,45 @@ func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp sr.Error(err) return } + + if streamResponse.Type == "response.error" || streamResponse.Type == "response.failed" { + if streamResponse.Response != nil { + if oaiErr := streamResponse.Response.GetOpenAIError(); openAIErrorUsable(oaiErr) { + streamErr = service.ApplySoftFailRetryPolicy(c, info, service.NormalizeSoftUpstreamError(types.WithOpenAIError(*oaiErr, http.StatusInternalServerError))) + sr.Stop(streamErr) + return + } + } + streamErr = types.NewOpenAIError(fmt.Errorf("responses stream error: %s", streamResponse.Type), types.ErrorCodeBadResponse, http.StatusInternalServerError) + sr.Stop(streamErr) + return + } + + if streamResponse.Type == "response.completed" || streamResponse.Type == "response.done" { + // Usage-only completed frames are common after deltas; only treat as empty + // when no client payload has been flushed yet. + if streamResponse.Response != nil && + relayconvert.IsEmptyCompletedResponses(streamResponse.Response) && + !service.IsClientPayloadWritten(c, info) { + // Drop buffered lifecycle frames so zero-write failover remains possible. + lifecycleBuf = nil + streamErr = service.ApplySoftFailRetryPolicy(c, info, service.NewEmptyCompletedError()) + sr.Stop(streamErr) + return + } + } + + switch streamResponse.Type { + case "response.created", "response.in_progress": + lifecycleBuf = append(lifecycleBuf, data) + return + } + + flushLifecycle() sendResponsesStreamData(c, streamResponse, data) + service.MarkClientPayloadWrittenContext(c, info) switch streamResponse.Type { - case "response.completed": + case "response.completed", "response.done": if streamResponse.Response != nil { if streamResponse.Response.Usage != nil { if streamResponse.Response.Usage.InputTokens != 0 { @@ -132,6 +197,10 @@ func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp } }) + if streamErr != nil { + return nil, service.ApplySoftFailRetryPolicy(c, info, streamErr) + } + if usage.CompletionTokens == 0 { // 计算输出文本的 token 数量 tempStr := responseTextBuilder.String() diff --git a/relay/channel/opencodego/adaptor.go b/relay/channel/opencodego/adaptor.go new file mode 100644 index 00000000000..cff0ead7a74 --- /dev/null +++ b/relay/channel/opencodego/adaptor.go @@ -0,0 +1,210 @@ +// ABOUTME: OpenCode Go channel adaptor: per-model dual-protocol relay to zen/go. +// ABOUTME: Routes Anthropic-protocol models to /messages, others to /chat/completions. + +package opencodego + +import ( + "errors" + "fmt" + "io" + "net/http" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/claude" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/QuantumNous/new-api/types" + + "github.com/gin-gonic/gin" + "github.com/tidwall/gjson" +) + +const defaultAnthropicVersion = "2023-06-01" + +type Adaptor struct { + // sessionAffinity is populated during Convert* from the request body and + // applied as x-opencode-session in SetupRequestHeader (same adaptor instance). + sessionAffinity string +} + +func (a *Adaptor) Init(info *relaycommon.RelayInfo) {} + +func getUpstreamModelName(info *relaycommon.RelayInfo, fallback string) string { + if info != nil && info.ChannelMeta != nil && info.UpstreamModelName != "" { + return info.UpstreamModelName + } + return fallback +} + +func isPassThroughEnabled(info *relaycommon.RelayInfo) bool { + if model_setting.GetGlobalSettings().PassThroughRequestEnabled { + return true + } + return info != nil && info.ChannelSetting.PassThroughBodyEnabled +} + +// sessionAffinityFromRequestBody extracts OpenAI prompt_cache_key or Claude +// metadata.user_id from the cached raw body without full DTO unmarshal. +// Used when pass-through skips Convert*, so a.sessionAffinity is never set. +func sessionAffinityFromRequestBody(c *gin.Context) string { + if c == nil { + return "" + } + storage, err := common.GetBodyStorage(c) + if err != nil || storage == nil { + return "" + } + body, err := storage.Bytes() + if err != nil || len(body) == 0 { + return "" + } + if v := gjson.GetBytes(body, "prompt_cache_key"); v.Exists() && v.Type == gjson.String { + if s := v.String(); s != "" { + return s + } + } + if v := gjson.GetBytes(body, "metadata.user_id"); v.Exists() && v.Type == gjson.String { + if s := v.String(); s != "" { + return s + } + } + return "" +} + +// resolveSessionAffinity: body field (Convert* or pass-through parse) → +// client x-opencode-session → client x-session-id → empty. +func (a *Adaptor) resolveSessionAffinity(c *gin.Context, info *relaycommon.RelayInfo) string { + if a.sessionAffinity != "" { + return a.sessionAffinity + } + if isPassThroughEnabled(info) { + if id := sessionAffinityFromRequestBody(c); id != "" { + return id + } + } + if c == nil || c.Request == nil { + return "" + } + if id := c.Request.Header.Get("x-opencode-session"); id != "" { + return id + } + return c.Request.Header.Get("x-session-id") +} + +func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + // Channel base is stored without /v1 (platform convention, e.g. + // https://opencode.ai/zen/go); append /v1 here like other OpenAI-style channels. + baseURL := info.ChannelBaseUrl + model := getUpstreamModelName(info, info.OriginModelName) + if isAnthropicProtocolModel(model) { + return fmt.Sprintf("%s/v1/messages", baseURL), nil + } + return fmt.Sprintf("%s/v1/chat/completions", baseURL), nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { + channel.SetupApiRequestHeader(info, c, req) + + model := getUpstreamModelName(info, info.OriginModelName) + if isAnthropicProtocolModel(model) { + req.Set("x-api-key", info.ApiKey) + anthropicVersion := c.Request.Header.Get("anthropic-version") + if anthropicVersion == "" { + anthropicVersion = defaultAnthropicVersion + } + req.Set("anthropic-version", anthropicVersion) + claude.CommonClaudeHeadersOperation(c, req, info) + } else { + req.Set("Authorization", "Bearer "+info.ApiKey) + } + + if ua := c.Request.Header.Get("User-Agent"); ua != "" { + req.Set("User-Agent", ua) + } + + if sessionID := a.resolveSessionAffinity(c, info); sessionID != "" { + req.Set("x-opencode-session", sessionID) + } + + return nil +} + +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + if request.PromptCacheKey != "" { + a.sessionAffinity = request.PromptCacheKey + } + + model := getUpstreamModelName(info, request.Model) + if isAnthropicProtocolModel(model) { + return (&claude.Adaptor{}).ConvertOpenAIRequest(c, info, request) + } + return request, nil +} + +func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + if len(request.Metadata) > 0 { + var meta dto.ClaudeMetadata + if err := common.Unmarshal(request.Metadata, &meta); err == nil && meta.UserId != "" { + a.sessionAffinity = meta.UserId + } + } + + model := getUpstreamModelName(info, request.Model) + if isAnthropicProtocolModel(model) { + return request, nil + } + return (&openai.Adaptor{}).ConvertClaudeRequest(c, info, request) +} + +func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) { + return nil, errors.New("not implemented") +} + +func (a *Adaptor) ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) { + return nil, errors.New("not implemented") +} + +func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { + return nil, errors.New("not implemented") +} + +func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { + return nil, errors.New("not implemented") +} + +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + return nil, errors.New("not implemented") +} + +func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { + return nil, errors.New("not implemented") +} + +func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { + return channel.DoApiRequest(a, c, info, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { + model := getUpstreamModelName(info, info.OriginModelName) + if isAnthropicProtocolModel(model) { + return (&claude.Adaptor{}).DoResponse(c, resp, info) + } + return (&openai.Adaptor{}).DoResponse(c, resp, info) +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return ChannelName +} diff --git a/relay/channel/opencodego/adaptor_test.go b/relay/channel/opencodego/adaptor_test.go new file mode 100644 index 00000000000..b22755fbe1d --- /dev/null +++ b/relay/channel/opencodego/adaptor_test.go @@ -0,0 +1,436 @@ +// ABOUTME: Tests for OpenCode Go per-model protocol routing and header setup. +// ABOUTME: Locks dual-protocol auth, session affinity, and conversion delegation. + +package opencodego + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/QuantumNous/new-api/types" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testBaseURL = "https://opencode.ai/zen/go" + +func init() { + gin.SetMode(gin.TestMode) +} + +func testRelayInfo(upstreamModel string) *relaycommon.RelayInfo { + info := &relaycommon.RelayInfo{ + ChannelMeta: &relaycommon.ChannelMeta{ + ChannelBaseUrl: testBaseURL, + ApiKey: "sk-test-key", + UpstreamModelName: upstreamModel, + }, + } + return info +} + +func testGinContext(headers map[string]string) *gin.Context { + recorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(recorder) + c.Request = httptest.NewRequest(http.MethodPost, "/", nil) + for k, v := range headers { + c.Request.Header.Set(k, v) + } + return c +} + +func TestGetRequestURL_AnthropicProtocolModels(t *testing.T) { + anthropicModels := []string{ + "minimax-m2.5", "minimax-m2.7", "minimax-m3", + "qwen3.5-plus", "qwen3.6-plus", "qwen3.7-plus", "qwen3.7-max", + } + adaptor := &Adaptor{} + for _, model := range anthropicModels { + t.Run(model, func(t *testing.T) { + url, err := adaptor.GetRequestURL(testRelayInfo(model)) + require.NoError(t, err) + assert.Equal(t, testBaseURL+"/v1/messages", url) + }) + } +} + +func TestGetRequestURL_OpenAIProtocolAndUnknown(t *testing.T) { + cases := []string{"glm-5.2", "kimi-k2.6", "deepseek-v4-pro", "unknown-model-xyz"} + adaptor := &Adaptor{} + for _, model := range cases { + t.Run(model, func(t *testing.T) { + url, err := adaptor.GetRequestURL(testRelayInfo(model)) + require.NoError(t, err) + assert.Equal(t, testBaseURL+"/v1/chat/completions", url) + }) + } +} + +func TestGetRequestURL_FallbackToOriginModelName(t *testing.T) { + adaptor := &Adaptor{} + info := &relaycommon.RelayInfo{ + OriginModelName: "minimax-m3", + ChannelMeta: &relaycommon.ChannelMeta{ + ChannelBaseUrl: testBaseURL, + ApiKey: "sk-test-key", + }, + } + url, err := adaptor.GetRequestURL(info) + require.NoError(t, err) + assert.Equal(t, testBaseURL+"/v1/messages", url) + + info.OriginModelName = "glm-5.2" + url, err = adaptor.GetRequestURL(info) + require.NoError(t, err) + assert.Equal(t, testBaseURL+"/v1/chat/completions", url) +} + +func TestSetupRequestHeader_AnthropicAuth(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("minimax-m3") + c := testGinContext(nil) + header := http.Header{} + + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Equal(t, "sk-test-key", header.Get("x-api-key")) + assert.Equal(t, defaultAnthropicVersion, header.Get("anthropic-version")) + assert.Empty(t, header.Get("Authorization")) +} + +func TestSetupRequestHeader_OpenAIAuth(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("glm-5.2") + c := testGinContext(nil) + header := http.Header{} + + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Equal(t, "Bearer sk-test-key", header.Get("Authorization")) + assert.Empty(t, header.Get("x-api-key")) +} + +func TestSessionAffinity_PromptCacheKey(t *testing.T) { + for _, model := range []string{"minimax-m3", "glm-5.2"} { + t.Run(model, func(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo(model) + c := testGinContext(nil) + + _, err := adaptor.ConvertOpenAIRequest(c, info, &dto.GeneralOpenAIRequest{ + Model: model, + PromptCacheKey: "abc", + }) + require.NoError(t, err) + + header := http.Header{} + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Equal(t, "abc", header.Get("x-opencode-session")) + }) + } +} + +func TestSessionAffinity_ClaudeMetadataUserID(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("minimax-m3") + c := testGinContext(nil) + + _, err := adaptor.ConvertClaudeRequest(c, info, &dto.ClaudeRequest{ + Model: "minimax-m3", + Metadata: []byte(`{"user_id":"user-from-meta"}`), + Messages: []dto.ClaudeMessage{{Role: "user", Content: "hi"}}, + }) + require.NoError(t, err) + + header := http.Header{} + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Equal(t, "user-from-meta", header.Get("x-opencode-session")) +} + +func TestSessionAffinity_ClientXSessionIdFallback(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("glm-5.2") + c := testGinContext(map[string]string{"x-session-id": "sid"}) + + _, err := adaptor.ConvertOpenAIRequest(c, info, &dto.GeneralOpenAIRequest{Model: "glm-5.2"}) + require.NoError(t, err) + + header := http.Header{} + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Equal(t, "sid", header.Get("x-opencode-session")) +} + +func TestSessionAffinity_ClientXOpencodeSessionFallback(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("glm-5.2") + c := testGinContext(map[string]string{"x-opencode-session": "native-sid"}) + + header := http.Header{} + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Equal(t, "native-sid", header.Get("x-opencode-session")) +} + +func TestSessionAffinity_XOpencodeSessionWinsOverXSessionId(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("glm-5.2") + c := testGinContext(map[string]string{ + "x-opencode-session": "native-sid", + "x-session-id": "generic-sid", + }) + + header := http.Header{} + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Equal(t, "native-sid", header.Get("x-opencode-session")) +} + +func TestSessionAffinity_BodyWinsOverClientHeader(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("glm-5.2") + c := testGinContext(map[string]string{ + "x-opencode-session": "native-sid", + "x-session-id": "sid", + }) + + _, err := adaptor.ConvertOpenAIRequest(c, info, &dto.GeneralOpenAIRequest{ + Model: "glm-5.2", + PromptCacheKey: "from-body", + }) + require.NoError(t, err) + + header := http.Header{} + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Equal(t, "from-body", header.Get("x-opencode-session")) +} + +func TestSessionAffinity_PassThroughPromptCacheKey(t *testing.T) { + origin := model_setting.GetGlobalSettings().PassThroughRequestEnabled + model_setting.GetGlobalSettings().PassThroughRequestEnabled = true + defer func() { + model_setting.GetGlobalSettings().PassThroughRequestEnabled = origin + }() + + adaptor := &Adaptor{} + info := testRelayInfo("glm-5.2") + c := testGinContext(map[string]string{ + "x-opencode-session": "native-sid", + "x-session-id": "sid", + }) + setRequestBody(t, c, `{"model":"glm-5.2","prompt_cache_key":"pass-through-key"}`) + + header := http.Header{} + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Equal(t, "pass-through-key", header.Get("x-opencode-session")) +} + +func TestSessionAffinity_PassThroughClaudeMetadataUserID(t *testing.T) { + origin := model_setting.GetGlobalSettings().PassThroughRequestEnabled + model_setting.GetGlobalSettings().PassThroughRequestEnabled = true + defer func() { + model_setting.GetGlobalSettings().PassThroughRequestEnabled = origin + }() + + adaptor := &Adaptor{} + info := testRelayInfo("minimax-m3") + c := testGinContext(nil) + setRequestBody(t, c, `{"model":"minimax-m3","metadata":{"user_id":"pass-through-user"}}`) + + header := http.Header{} + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Equal(t, "pass-through-user", header.Get("x-opencode-session")) +} + +func TestSessionAffinity_PassThroughDisabledSkipsBody(t *testing.T) { + origin := model_setting.GetGlobalSettings().PassThroughRequestEnabled + model_setting.GetGlobalSettings().PassThroughRequestEnabled = false + defer func() { + model_setting.GetGlobalSettings().PassThroughRequestEnabled = origin + }() + + adaptor := &Adaptor{} + info := testRelayInfo("glm-5.2") + // Channel-level pass-through also off (zero value). + c := testGinContext(map[string]string{"x-session-id": "sid"}) + setRequestBody(t, c, `{"model":"glm-5.2","prompt_cache_key":"should-be-ignored"}`) + + header := http.Header{} + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + // Convert* was not called; without pass-through body parse, header wins. + assert.Equal(t, "sid", header.Get("x-opencode-session")) +} + +func TestSessionAffinity_ChannelPassThroughBodyEnabled(t *testing.T) { + origin := model_setting.GetGlobalSettings().PassThroughRequestEnabled + model_setting.GetGlobalSettings().PassThroughRequestEnabled = false + defer func() { + model_setting.GetGlobalSettings().PassThroughRequestEnabled = origin + }() + + adaptor := &Adaptor{} + info := testRelayInfo("glm-5.2") + info.ChannelSetting.PassThroughBodyEnabled = true + c := testGinContext(nil) + setRequestBody(t, c, `{"model":"glm-5.2","prompt_cache_key":"channel-pass"}`) + + header := http.Header{} + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Equal(t, "channel-pass", header.Get("x-opencode-session")) +} + +func TestSessionAffinity_AbsentWhenNoSource(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("glm-5.2") + c := testGinContext(nil) + + header := http.Header{} + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Empty(t, header.Get("x-opencode-session")) +} + +func setRequestBody(t *testing.T, c *gin.Context, body string) { + t.Helper() + storage, err := common.CreateBodyStorage([]byte(body)) + require.NoError(t, err) + c.Set(common.KeyBodyStorage, storage) +} + +func TestUserAgent_Passthrough(t *testing.T) { + for _, model := range []string{"minimax-m3", "glm-5.2"} { + t.Run(model, func(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo(model) + c := testGinContext(map[string]string{"User-Agent": "my-agent/1.0"}) + header := http.Header{} + + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Equal(t, "my-agent/1.0", header.Get("User-Agent")) + }) + } +} + +func TestUserAgent_UnsetWhenClientEmpty(t *testing.T) { + for _, model := range []string{"minimax-m3", "glm-5.2"} { + t.Run(model, func(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo(model) + c := testGinContext(nil) + header := http.Header{} + + require.NoError(t, adaptor.SetupRequestHeader(c, &header, info)) + assert.Empty(t, header.Get("User-Agent")) + }) + } +} + +func TestConvertOpenAIRequest_AnthropicDelegatesToClaude(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("minimax-m3") + c := testGinContext(nil) + + converted, err := adaptor.ConvertOpenAIRequest(c, info, &dto.GeneralOpenAIRequest{ + Model: "minimax-m3", + Messages: []dto.Message{ + {Role: "user", Content: "hello"}, + }, + }) + require.NoError(t, err) + _, ok := converted.(*dto.ClaudeRequest) + assert.True(t, ok, "expected *dto.ClaudeRequest, got %T", converted) +} + +func TestConvertOpenAIRequest_OpenAIPassthrough(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("glm-5.2") + c := testGinContext(nil) + req := &dto.GeneralOpenAIRequest{Model: "glm-5.2"} + + converted, err := adaptor.ConvertOpenAIRequest(c, info, req) + require.NoError(t, err) + assert.Same(t, req, converted) +} + +func TestConvertClaudeRequest_AnthropicPassthrough(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("minimax-m3") + c := testGinContext(nil) + req := &dto.ClaudeRequest{ + Model: "minimax-m3", + Messages: []dto.ClaudeMessage{{Role: "user", Content: "hi"}}, + } + + converted, err := adaptor.ConvertClaudeRequest(c, info, req) + require.NoError(t, err) + assert.Same(t, req, converted) +} + +func TestConvertClaudeRequest_OpenAIDelegates(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("glm-5.2") + c := testGinContext(nil) + + converted, err := adaptor.ConvertClaudeRequest(c, info, &dto.ClaudeRequest{ + Model: "glm-5.2", + Messages: []dto.ClaudeMessage{{Role: "user", Content: "hello"}}, + }) + require.NoError(t, err) + openAIReq, ok := converted.(*dto.GeneralOpenAIRequest) + require.True(t, ok, "expected *dto.GeneralOpenAIRequest, got %T", converted) + assert.Equal(t, "glm-5.2", openAIReq.Model) +} + +func TestUnsupportedMethodsReturnNotImplemented(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("glm-5.2") + c := testGinContext(nil) + + _, err := adaptor.ConvertGeminiRequest(c, info, &dto.GeminiChatRequest{}) + assert.Error(t, err) + + _, err = adaptor.ConvertAudioRequest(c, info, dto.AudioRequest{}) + assert.Error(t, err) + + _, err = adaptor.ConvertImageRequest(c, info, dto.ImageRequest{}) + assert.Error(t, err) + + _, err = adaptor.ConvertEmbeddingRequest(c, info, dto.EmbeddingRequest{}) + assert.Error(t, err) + + _, err = adaptor.ConvertOpenAIResponsesRequest(c, info, dto.OpenAIResponsesRequest{}) + assert.Error(t, err) + + _, err = adaptor.ConvertRerankRequest(c, 0, dto.RerankRequest{}) + assert.Error(t, err) +} + +func TestDoResponse_AnthropicSetsFinalRequestRelayFormat(t *testing.T) { + adaptor := &Adaptor{} + info := testRelayInfo("minimax-m3") + info.RelayFormat = types.RelayFormatClaude + + recorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(recorder) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil) + + body := `{"id":"msg_1","type":"message","role":"assistant","content":[{"type":"text","text":"hi"}],"model":"minimax-m3","stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1}}` + resp := &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(body)), + } + + _, newAPIErr := adaptor.DoResponse(c, resp, info) + require.Nil(t, newAPIErr) + assert.Equal(t, types.RelayFormat(types.RelayFormatClaude), info.FinalRequestRelayFormat) +} + +func TestGetModelListAndChannelName(t *testing.T) { + adaptor := &Adaptor{} + assert.Equal(t, ChannelName, adaptor.GetChannelName()) + assert.Len(t, adaptor.GetModelList(), 19) +} diff --git a/relay/channel/opencodego/constants.go b/relay/channel/opencodego/constants.go new file mode 100644 index 00000000000..92dcba94910 --- /dev/null +++ b/relay/channel/opencodego/constants.go @@ -0,0 +1,51 @@ +// ABOUTME: OpenCode Go channel constants: model list and Anthropic-protocol set. +// ABOUTME: Per-model protocol routing decides OpenAI Chat vs Anthropic Messages upstream. + +package opencodego + +var ChannelName = "opencodego" + +// ModelList is the known opencode-go fixture model IDs (chat only). +var ModelList = []string{ + // deepseek + "deepseek-v4-flash", + "deepseek-v4-pro", + // glm + "glm-5", + "glm-5.1", + "glm-5.2", + // kimi + "kimi-k2.5", + "kimi-k2.6", + "kimi-k2.7-code", + // mimo + "mimo-v2-omni", + "mimo-v2-pro", + "mimo-v2.5", + "mimo-v2.5-pro", + // minimax (Anthropic protocol) + "minimax-m2.5", + "minimax-m2.7", + "minimax-m3", + // qwen (Anthropic protocol) + "qwen3.5-plus", + "qwen3.6-plus", + "qwen3.7-plus", + "qwen3.7-max", +} + +// anthropicProtocolModels are upstream models that must use Anthropic Messages +// (`/messages` + x-api-key). All other models use OpenAI Chat Completions. +var anthropicProtocolModels = map[string]bool{ + "minimax-m2.5": true, + "minimax-m2.7": true, + "minimax-m3": true, + "qwen3.5-plus": true, + "qwen3.6-plus": true, + "qwen3.7-plus": true, + "qwen3.7-max": true, +} + +func isAnthropicProtocolModel(model string) bool { + return anthropicProtocolModels[model] +} diff --git a/relay/channel/zhipu_4v/constants.go b/relay/channel/zhipu_4v/constants.go index c1c1f289523..25115f0a79a 100644 --- a/relay/channel/zhipu_4v/constants.go +++ b/relay/channel/zhipu_4v/constants.go @@ -1,7 +1,7 @@ package zhipu_4v var ModelList = []string{ - "glm-4", "glm-4v", "glm-3-turbo", "glm-4-alltools", "glm-4-plus", "glm-4-0520", "glm-4-air", "glm-4-airx", "glm-4-long", "glm-4-flash", "glm-4v-plus", "glm-4.6", "glm-4.6v", "glm-4.7", "glm-4.7-flash", "glm-5", + "glm-4", "glm-4v", "glm-3-turbo", "glm-4-alltools", "glm-4-plus", "glm-4-0520", "glm-4-air", "glm-4-airx", "glm-4-long", "glm-4-flash", "glm-4v-plus", "glm-4.6", "glm-4.6v", "glm-4.7", "glm-4.7-flash", "glm-5", "gml-5.1", } var ChannelName = "zhipu_4v" diff --git a/relay/common/override_test.go b/relay/common/override_test.go index 79688113cef..68d33cd29f7 100644 --- a/relay/common/override_test.go +++ b/relay/common/override_test.go @@ -2287,7 +2287,6 @@ func TestShouldAuditParamPathUsesFieldBoundaryPrefixMatching(t *testing.T) { func assertJSONEqual(t *testing.T, want, got string) { t.Helper() - var wantObj interface{} var gotObj interface{} diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 9f460ce5c6a..3be524639c1 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -6,6 +6,7 @@ import ( "fmt" "strconv" "strings" + "sync" "time" "github.com/QuantumNous/new-api/common" @@ -103,25 +104,38 @@ type RelayInfo struct { UsePrice bool RelayMode int OriginModelName string - RequestURLPath string - RequestHeaders map[string]string - ShouldIncludeUsage bool - DisablePing bool // 是否禁止向下游发送自定义 Ping - ClientWs *websocket.Conn - TargetWs *websocket.Conn - InputAudioFormat string - OutputAudioFormat string - RealtimeTools []dto.RealTimeTool - IsFirstRequest bool - AudioUsage bool - ReasoningEffort string - UserSetting dto.UserSetting - UserEmail string - UserQuota int - RelayFormat types.RelayFormat - SendResponseCount int - ReceivedResponseCount int - FinalPreConsumedQuota int // 最终预消耗的配额 + // ClientModelName is the immutable client-requested model for routing stats + // (channel+model TTFT). OriginModelName may be rewritten by mapping helpers. + ClientModelName string + RequestURLPath string + RequestHeaders map[string]string + ShouldIncludeUsage bool + DisablePing bool // 是否禁止向下游发送自定义 Ping + ClientWs *websocket.Conn + TargetWs *websocket.Conn + InputAudioFormat string + OutputAudioFormat string + RealtimeTools []dto.RealTimeTool + IsFirstRequest bool + AudioUsage bool + ReasoningEffort string + UserSetting dto.UserSetting + UserEmail string + UserQuota int + RelayFormat types.RelayFormat + SendResponseCount int + ReceivedResponseCount int + // ClientPayloadWritten is true after assistant payload (text/reasoning/tool + // chunks or non-stream body) has been flushed to the client for this attempt. + // Do not use ReceivedResponseCount as a client-commit proxy. + ClientPayloadWritten bool + // EmptyCompleted marks Responses status=completed with no usable output. + EmptyCompleted bool + // AffinityUnusable blocks channel-affinity record for this request after a soft fail. + AffinityUnusable bool + // SoftFailReason is an admin/log enum such as empty_completed or upstream_capacity. + SoftFailReason string + FinalPreConsumedQuota int // 最终预消耗的配额 // ForcePreConsume 为 true 时禁用 BillingSession 的信任额度旁路, // 强制预扣全额。用于异步任务(视频/音乐生成等),因为请求返回后任务仍在运行, // 必须在提交前锁定全额。 @@ -161,6 +175,12 @@ type RelayInfo struct { // *bytes.Reader/Buffer/strings.Reader). 0 means "let net/http decide". UpstreamRequestBodySize int64 + // responseBodyForLog holds upstream response content for usage-log details + // when RequestDetailLogEnabled is on. Streams append SSE data payloads; + // non-stream requests store the full upstream body. + responseBodyMu sync.Mutex + responseBodyForLog strings.Builder + PriceData types.PriceData // QuotaClamp is set (non-nil) when a quota conversion saturated at the @@ -342,6 +362,7 @@ var streamSupportedChannels = map[int]bool{ constant.ChannelTypeMiniMax: true, constant.ChannelTypeSiliconFlow: true, constant.ChannelTypeAdvancedCustom: true, + constant.ChannelTypeOpenCodeGo: true, } func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo { @@ -477,6 +498,7 @@ func genBaseRelayInfo(c *gin.Context, request dto.Request) *RelayInfo { UserEmail: common.GetContextKeyString(c, constant.ContextKeyUserEmail), OriginModelName: common.GetContextKeyString(c, constant.ContextKeyOriginalModel), + ClientModelName: common.GetContextKeyString(c, constant.ContextKeyOriginalModel), TokenId: common.GetContextKeyInt(c, constant.ContextKeyTokenId), TokenKey: common.GetContextKeyString(c, constant.ContextKeyTokenKey), @@ -672,6 +694,60 @@ func (info *RelayInfo) HasSendResponse() bool { return info.FirstResponseTime.After(info.StartTime) } +// CanSoftFailRetry reports whether a soft failure may still switch channels +// in-request (soft-fail marked and no assistant payload flushed yet). +func (info *RelayInfo) CanSoftFailRetry() bool { + if info == nil { + return false + } + return info.AffinityUnusable && !info.ClientPayloadWritten +} + +// ResetResponseBodyForLog clears any captured upstream response body for the +// current attempt (e.g. channel retry). +func (info *RelayInfo) ResetResponseBodyForLog() { + if info == nil { + return + } + info.responseBodyMu.Lock() + defer info.responseBodyMu.Unlock() + info.responseBodyForLog.Reset() +} + +// SetResponseBodyForLog replaces the captured upstream response body. +func (info *RelayInfo) SetResponseBodyForLog(body string) { + if info == nil { + return + } + info.responseBodyMu.Lock() + defer info.responseBodyMu.Unlock() + info.responseBodyForLog.Reset() + info.responseBodyForLog.WriteString(body) +} + +// AppendResponseBodyForLog appends one upstream stream data payload plus a newline. +func (info *RelayInfo) AppendResponseBodyForLog(chunk string) { + if info == nil || chunk == "" { + return + } + info.responseBodyMu.Lock() + defer info.responseBodyMu.Unlock() + info.responseBodyForLog.WriteString(chunk) + if !strings.HasSuffix(chunk, "\n") { + info.responseBodyForLog.WriteByte('\n') + } +} + +// GetResponseBodyForLog returns the captured upstream response body text. +func (info *RelayInfo) GetResponseBodyForLog() string { + if info == nil { + return "" + } + info.responseBodyMu.Lock() + defer info.responseBodyMu.Unlock() + return info.responseBodyForLog.String() +} + type TaskRelayInfo struct { Action string OriginTaskID string diff --git a/relay/helper/stream_scanner.go b/relay/helper/stream_scanner.go index f88bef5206f..83109a4c8ff 100644 --- a/relay/helper/stream_scanner.go +++ b/relay/helper/stream_scanner.go @@ -265,6 +265,7 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon if !strings.HasPrefix(data, "[DONE]") { info.SetFirstResponseTime() info.ReceivedResponseCount++ + service.AppendUpstreamStreamChunkForLog(info, data) select { case dataChan <- data: diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index 2922744654e..4a49b993f98 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -26,6 +26,7 @@ import ( "github.com/QuantumNous/new-api/relay/channel/moonshot" "github.com/QuantumNous/new-api/relay/channel/ollama" "github.com/QuantumNous/new-api/relay/channel/openai" + "github.com/QuantumNous/new-api/relay/channel/opencodego" "github.com/QuantumNous/new-api/relay/channel/palm" "github.com/QuantumNous/new-api/relay/channel/perplexity" "github.com/QuantumNous/new-api/relay/channel/replicate" @@ -123,6 +124,8 @@ func GetAdaptor(apiType int) channel.Adaptor { return &codex.Adaptor{} case constant.APITypeAdvancedCustom: return &advancedcustom.Adaptor{} + case constant.APITypeOpenCodeGo: + return &opencodego.Adaptor{} } return nil } diff --git a/router/api-router.go b/router/api-router.go index 83f9259b213..b20889a00db 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -275,6 +275,10 @@ func SetApiRouter(router *gin.Engine) { logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs) logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogs) logRoute.GET("/self/search", middleware.UserAuth(), middleware.SearchRateLimit(), controller.SearchUserLogs) + logRoute.GET("/request-detail", middleware.AdminAuth(), controller.GetLogRequestDetail) + logRoute.GET("/self/request-detail", middleware.UserAuth(), controller.GetUserLogRequestDetail) + logRoute.GET("/request-detail/storage", middleware.RootAuth(), controller.GetRequestDetailStorageInfo) + logRoute.DELETE("/request-detail", middleware.RootAuth(), controller.ClearRequestDetailFiles) systemTaskRoute := apiRouter.Group("/system-task") systemTaskRoute.Use(middleware.RootAuth()) diff --git a/router/relay-router.go b/router/relay-router.go index 17a13cad7fd..4342be6b639 100644 --- a/router/relay-router.go +++ b/router/relay-router.go @@ -15,6 +15,7 @@ func SetRelayRouter(router *gin.Engine) { router.Use(middleware.DecompressRequestMiddleware()) router.Use(middleware.BodyStorageCleanup()) // 清理请求体存储 router.Use(middleware.StatsMiddleware()) + router.Use(middleware.DebugHeaders()) // DEBUG=true 时打印完整请求头 // https://platform.openai.com/docs/api-reference/introduction modelsRouter := router.Group("/v1/models") modelsRouter.Use(middleware.RouteTag("relay")) diff --git a/service/channel.go b/service/channel.go index 856e2cde3b3..4b5fc6ad945 100644 --- a/service/channel.go +++ b/service/channel.go @@ -49,6 +49,10 @@ func ShouldDisableChannel(err *types.NewAPIError) bool { if err == nil { return false } + // Soft failures are transient model/provider quirks; never auto-ban. + if IsSoftFailErrorCode(err.GetErrorCode()) { + return false + } if types.IsChannelError(err) { return true } diff --git a/service/channel_affinity.go b/service/channel_affinity.go index 96ec13e248c..6d39d07b94e 100644 --- a/service/channel_affinity.go +++ b/service/channel_affinity.go @@ -303,10 +303,10 @@ func extractChannelAffinityValue(c *gin.Context, src operation_setting.ChannelAf } return strings.TrimSpace(c.GetString(src.Key)) case "request_header": - if c == nil || c.Request == nil || src.Key == "" { + if src.Key == "" { return "" } - return strings.TrimSpace(c.Request.Header.Get(src.Key)) + return strings.TrimSpace(c.GetHeader(src.Key)) case "gjson": if src.Path == "" { return "" @@ -714,6 +714,10 @@ func RecordChannelAffinity(c *gin.Context, channelID int) { if channelID <= 0 { return } + // Soft failures (empty completed / capacity) must never refresh sticky binding. + if IsChannelAffinityUnusable(c) { + return + } setting := operation_setting.GetChannelAffinitySetting() if setting == nil || !setting.Enabled { return diff --git a/service/channel_affinity_template_test.go b/service/channel_affinity_template_test.go index fb703a24e72..0eccf41f92e 100644 --- a/service/channel_affinity_template_test.go +++ b/service/channel_affinity_template_test.go @@ -332,3 +332,28 @@ func TestChannelAffinityHitCodexTemplatePassHeadersEffective(t *testing.T) { _, exists = info.RuntimeHeadersOverride["x-codex-turn-metadata"] require.False(t, exists) } + +func TestRecordChannelAffinitySkipsWhenUnusable(t *testing.T) { + gin.SetMode(gin.TestMode) + + cacheKeySuffix := fmt.Sprintf("soft-fail-unusable:%d", time.Now().UnixNano()) + cacheKeyFull := channelAffinityCacheNamespace + ":" + cacheKeySuffix + cache := getChannelAffinityCache() + t.Cleanup(func() { + _, _ = cache.DeleteMany([]string{cacheKeySuffix}) + }) + + ctx := buildChannelAffinityTemplateContextForTest(channelAffinityMeta{ + CacheKey: cacheKeyFull, + TTLSeconds: 60, + RuleName: "soft fail unusable", + SkipRetry: true, + }) + ctx.Set(ginKeyChannelAffinityUnusable, true) + + RecordChannelAffinity(ctx, 4242) + _, found, err := cache.Get(cacheKeySuffix) + require.NoError(t, err) + require.False(t, found) + require.True(t, IsChannelAffinityUnusable(ctx)) +} diff --git a/service/channel_select.go b/service/channel_select.go index 24c4e252bfb..146a2e51124 100644 --- a/service/channel_select.go +++ b/service/channel_select.go @@ -1,3 +1,5 @@ +// ABOUTME: Coordinates channel selection state across relay attempts and auto groups. +// ABOUTME: Excludes attempted channels so retries exhaust each priority before falling back. package service import ( @@ -12,12 +14,14 @@ import ( ) type RetryParam struct { - Ctx *gin.Context - TokenGroup string - ModelName string - RequestPath string - Retry *int - resetNextTry bool + Ctx *gin.Context + TokenGroup string + ModelName string + RequestPath string + Retry *int + ExcludedChannelIds map[int]struct{} + autoGroupStartIndex int + autoGroupStartIndexSet bool } func (p *RetryParam) GetRetry() int { @@ -27,137 +31,130 @@ func (p *RetryParam) GetRetry() int { return *p.Retry } -func (p *RetryParam) SetRetry(retry int) { - p.Retry = &retry -} - func (p *RetryParam) IncreaseRetry() { - if p.resetNextTry { - p.resetNextTry = false - return - } if p.Retry == nil { p.Retry = new(int) } *p.Retry++ } -func (p *RetryParam) ResetRetryNextTry() { - p.resetNextTry = true +func (p *RetryParam) ExcludeChannel(channelId int) { + if p.ExcludedChannelIds == nil { + p.ExcludedChannelIds = make(map[int]struct{}) + } + p.ExcludedChannelIds[channelId] = struct{}{} +} + +func (p *RetryParam) HasExcludedChannels() bool { + return len(p.ExcludedChannelIds) > 0 } -// CacheGetRandomSatisfiedChannel tries to get a random channel that satisfies the requirements. -// 尝试获取一个满足要求的随机渠道。 -// -// For "auto" tokenGroup with cross-group Retry enabled: -// 对于启用了跨分组重试的 "auto" tokenGroup: -// -// - Each group will exhaust all its priorities before moving to the next group. -// 每个分组会用完所有优先级后才会切换到下一个分组。 -// -// - Uses ContextKeyAutoGroupIndex to track current group index. -// 使用 ContextKeyAutoGroupIndex 跟踪当前分组索引。 -// -// - Uses ContextKeyAutoGroupRetryIndex to track the global Retry count when current group started. -// 使用 ContextKeyAutoGroupRetryIndex 跟踪当前分组开始时的全局重试次数。 -// -// - priorityRetry = Retry - startRetryIndex, represents the priority level within current group. -// priorityRetry = Retry - startRetryIndex,表示当前分组内的优先级级别。 -// -// - When GetRandomSatisfiedChannel returns nil (priorities exhausted), moves to next group. -// 当 GetRandomSatisfiedChannel 返回 nil(优先级用完)时,切换到下一个分组。 -// -// Example flow (2 groups, each with 2 priorities, RetryTimes=3): -// 示例流程(2个分组,每个有2个优先级,RetryTimes=3): -// -// Retry=0: GroupA, priority0 (startRetryIndex=0, priorityRetry=0) -// 分组A, 优先级0 -// -// Retry=1: GroupA, priority1 (startRetryIndex=0, priorityRetry=1) -// 分组A, 优先级1 -// -// Retry=2: GroupA exhausted → GroupB, priority0 (startRetryIndex=2, priorityRetry=0) -// 分组A用完 → 分组B, 优先级0 -// -// Retry=3: GroupB, priority1 (startRetryIndex=2, priorityRetry=1) -// 分组B, 优先级1 +// CacheGetRandomSatisfiedChannel returns the highest-priority eligible channel +// that has not already been attempted by this request. The outer relay loop +// still enforces RetryTimes as the total retry limit. func CacheGetRandomSatisfiedChannel(param *RetryParam) (*model.Channel, string, error) { - var channel *model.Channel - var err error selectGroup := param.TokenGroup - userGroup := common.GetContextKeyString(param.Ctx, constant.ContextKeyUserGroup) + userAgent := "" + if param.Ctx != nil && param.Ctx.Request != nil { + userAgent = param.Ctx.Request.UserAgent() + } - if param.TokenGroup == "auto" { - if len(setting.GetAutoGroups()) == 0 { - return nil, selectGroup, errors.New("auto groups is not enabled") - } - autoGroups := GetUserAutoGroup(userGroup) + if param.TokenGroup != "auto" { + channel, err := model.GetRandomSatisfiedChannel( + param.TokenGroup, + param.ModelName, + param.ExcludedChannelIds, + true, + userAgent, + param.RequestPath, + ) + return channel, selectGroup, err + } - // startGroupIndex: the group index to start searching from - // startGroupIndex: 开始搜索的分组索引 - startGroupIndex := 0 - crossGroupRetry := common.GetContextKeyBool(param.Ctx, constant.ContextKeyTokenCrossGroupRetry) + if len(setting.GetAutoGroups()) == 0 { + return nil, selectGroup, errors.New("auto groups is not enabled") + } - if lastGroupIndex, exists := common.GetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex); exists { - if idx, ok := lastGroupIndex.(int); ok { - startGroupIndex = idx + userGroup := common.GetContextKeyString(param.Ctx, constant.ContextKeyUserGroup) + autoGroups := GetUserAutoGroup(userGroup) + startGroupIndex := 0 + if lastGroupIndex, exists := common.GetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex); exists { + if index, ok := lastGroupIndex.(int); ok { + startGroupIndex = index + } + } else { + currentGroup := common.GetContextKeyString(param.Ctx, constant.ContextKeyAutoGroup) + for index, autoGroup := range autoGroups { + if autoGroup == currentGroup { + startGroupIndex = index + break } } + } - for i := startGroupIndex; i < len(autoGroups); i++ { - autoGroup := autoGroups[i] - // Calculate priorityRetry for current group - // 计算当前分组的 priorityRetry - priorityRetry := param.GetRetry() - // If moved to a new group, reset priorityRetry and update startRetryIndex - // 如果切换到新分组,重置 priorityRetry 并更新 startRetryIndex - if i > startGroupIndex { - priorityRetry = 0 - } - logger.LogDebug(param.Ctx, "Auto selecting group: %s, priorityRetry: %d", autoGroup, priorityRetry) - - channel, _ = model.GetRandomSatisfiedChannel(autoGroup, param.ModelName, priorityRetry, param.RequestPath) - if channel == nil { - // Current group has no available channel for this model, try next group - // 当前分组没有该模型的可用渠道,尝试下一个分组 - logger.LogDebug(param.Ctx, "No available channel in group %s for model %s at priorityRetry %d, trying next group", autoGroup, param.ModelName, priorityRetry) - // 重置状态以尝试下一个分组 - common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, i+1) - common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupRetryIndex, 0) - // Reset retry counter so outer loop can continue for next group - // 重置重试计数器,以便外层循环可以为下一个分组继续 - param.SetRetry(0) - continue - } - common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroup, autoGroup) - selectGroup = autoGroup - logger.LogDebug(param.Ctx, "Auto selected group: %s", autoGroup) - - // Prepare state for next retry - // 为下一次重试准备状态 - if crossGroupRetry && priorityRetry >= common.RetryTimes { - // Current group has exhausted all retries, prepare to switch to next group - // This request still uses current group, but next retry will use next group - // 当前分组已用完所有重试次数,准备切换到下一个分组 - // 本次请求仍使用当前分组,但下次重试将使用下一个分组 - logger.LogDebug(param.Ctx, "Current group %s retries exhausted (priorityRetry=%d >= RetryTimes=%d), preparing switch to next group for next retry", autoGroup, priorityRetry, common.RetryTimes) - common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, i+1) - // Reset retry counter so outer loop can continue for next group - // 重置重试计数器,以便外层循环可以为下一个分组继续 - param.SetRetry(0) - param.ResetRetryNextTry() - } else { - // Stay in current group, save current state - // 保持在当前分组,保存当前状态 - common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, i) - } - break + if !param.autoGroupStartIndexSet { + param.autoGroupStartIndex = startGroupIndex + param.autoGroupStartIndexSet = true + } + + endGroupIndex := len(autoGroups) + crossGroupRetry := common.GetContextKeyBool(param.Ctx, constant.ContextKeyTokenCrossGroupRetry) + if param.HasExcludedChannels() && !crossGroupRetry && startGroupIndex < endGroupIndex { + endGroupIndex = startGroupIndex + 1 + } + + for groupIndex := startGroupIndex; groupIndex < endGroupIndex; groupIndex++ { + autoGroup := autoGroups[groupIndex] + logger.LogDebug(param.Ctx, "Auto selecting untried channel in group: %s, retry: %d", autoGroup, param.GetRetry()) + channel, err := model.GetRandomSatisfiedChannel( + autoGroup, + param.ModelName, + param.ExcludedChannelIds, + false, + userAgent, + param.RequestPath, + ) + if err != nil { + return nil, autoGroup, err } - } else { - channel, err = model.GetRandomSatisfiedChannel(param.TokenGroup, param.ModelName, param.GetRetry(), param.RequestPath) + if channel == nil { + continue + } + + common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroup, autoGroup) + common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, groupIndex) + logger.LogDebug(param.Ctx, "Auto selected group: %s", autoGroup) + return channel, autoGroup, nil + } + + fallbackStartGroupIndex := param.autoGroupStartIndex + fallbackEndGroupIndex := len(autoGroups) + if param.HasExcludedChannels() && !crossGroupRetry && fallbackStartGroupIndex < fallbackEndGroupIndex { + fallbackEndGroupIndex = fallbackStartGroupIndex + 1 + } + for groupIndex := fallbackStartGroupIndex; groupIndex < fallbackEndGroupIndex; groupIndex++ { + autoGroup := autoGroups[groupIndex] + logger.LogDebug(param.Ctx, "Auto selecting multi-key fallback in group: %s, retry: %d", autoGroup, param.GetRetry()) + channel, err := model.GetRandomSatisfiedChannel( + autoGroup, + param.ModelName, + param.ExcludedChannelIds, + true, + userAgent, + param.RequestPath, + ) if err != nil { - return nil, param.TokenGroup, err + return nil, autoGroup, err + } + if channel == nil { + continue } + + common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroup, autoGroup) + common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, groupIndex) + logger.LogDebug(param.Ctx, "Auto selected multi-key fallback group: %s", autoGroup) + return channel, autoGroup, nil } - return channel, selectGroup, nil + + return nil, selectGroup, nil } diff --git a/service/channel_select_test.go b/service/channel_select_test.go new file mode 100644 index 00000000000..1778536394d --- /dev/null +++ b/service/channel_select_test.go @@ -0,0 +1,100 @@ +// ABOUTME: Verifies retry-aware channel selection across auto groups. +// ABOUTME: Covers untried-channel ordering and multi-key fallback after cross-group failures. +package service + +import ( + "net/http/httptest" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/setting" + "github.com/gin-gonic/gin" + "github.com/glebarez/sqlite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func TestCacheGetRandomSatisfiedChannelReturnsToEarlierMultiKeyAutoGroup(t *testing.T) { + originalDB := model.DB + originalMemoryCacheEnabled := common.MemoryCacheEnabled + originalAutoGroups := setting.AutoGroups2JsonString() + originalUserUsableGroups := setting.UserUsableGroups2JSONString() + t.Cleanup(func() { + model.DB = originalDB + common.MemoryCacheEnabled = originalMemoryCacheEnabled + require.NoError(t, setting.UpdateAutoGroupsByJsonString(originalAutoGroups)) + require.NoError(t, setting.UpdateUserUsableGroupsByJSONString(originalUserUsableGroups)) + }) + + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + require.NoError(t, err) + model.DB = db + require.NoError(t, db.AutoMigrate(&model.Channel{}, &model.Ability{})) + require.NoError(t, setting.UpdateAutoGroupsByJsonString(`["group-a","group-b"]`)) + require.NoError(t, setting.UpdateUserUsableGroupsByJSONString(`{"default":"Default","group-a":"A","group-b":"B"}`)) + + common.MemoryCacheEnabled = false + highPriority := int64(200) + lowPriority := int64(100) + weight := uint(1) + multiKey := &model.Channel{ + Status: common.ChannelStatusEnabled, + Name: "group-a-multi-key", + Key: "key-a\nkey-b", + Models: "gpt-4o", + Group: "group-a", + Priority: &highPriority, + Weight: &weight, + ChannelInfo: model.ChannelInfo{ + IsMultiKey: true, + MultiKeySize: 2, + }, + } + require.NoError(t, multiKey.Insert()) + groupB := &model.Channel{ + Status: common.ChannelStatusEnabled, + Name: "group-b-single-key", + Key: "key-c", + Models: "gpt-4o", + Group: "group-b", + Priority: &lowPriority, + Weight: &weight, + } + require.NoError(t, groupB.Insert()) + common.MemoryCacheEnabled = true + model.InitChannelCache() + + gin.SetMode(gin.TestMode) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + ctx.Request = httptest.NewRequest("POST", "/v1/chat/completions", nil) + common.SetContextKey(ctx, constant.ContextKeyUserGroup, "default") + common.SetContextKey(ctx, constant.ContextKeyAutoGroup, "group-a") + common.SetContextKey(ctx, constant.ContextKeyAutoGroupIndex, 0) + common.SetContextKey(ctx, constant.ContextKeyTokenCrossGroupRetry, true) + + retryParam := &RetryParam{ + Ctx: ctx, + TokenGroup: "auto", + ModelName: "gpt-4o", + RequestPath: ctx.Request.URL.Path, + Retry: common.GetPointer(1), + ExcludedChannelIds: map[int]struct{}{multiKey.Id: {}}, + } + + selected, selectedGroup, err := CacheGetRandomSatisfiedChannel(retryParam) + require.NoError(t, err) + require.NotNil(t, selected) + assert.Equal(t, groupB.Id, selected.Id) + assert.Equal(t, "group-b", selectedGroup) + + retryParam.ExcludeChannel(groupB.Id) + retryParam.IncreaseRetry() + selected, selectedGroup, err = CacheGetRandomSatisfiedChannel(retryParam) + require.NoError(t, err) + require.NotNil(t, selected) + assert.Equal(t, multiKey.Id, selected.Id) + assert.Equal(t, "group-a", selectedGroup) +} diff --git a/service/channel_ttft.go b/service/channel_ttft.go new file mode 100644 index 00000000000..26dece96a30 --- /dev/null +++ b/service/channel_ttft.go @@ -0,0 +1,31 @@ +// ABOUTME: Records successful stream first-token latency for channel routing bias. +// ABOUTME: Keys stats by channel id and origin model name (e.g. gpt-4o). +package service + +import ( + "github.com/QuantumNous/new-api/pkg/channellatency" + relaycommon "github.com/QuantumNous/new-api/relay/common" +) + +// ObserveChannelTtftFromRelay stores TTFT for (channel_id, client_model_name) +// after a successful stream response with a real first token. +// Uses ClientModelName so model mapping / compact rewrites of OriginModelName +// cannot poison the routing key. +func ObserveChannelTtftFromRelay(info *relaycommon.RelayInfo) { + if info == nil || !info.IsStream || !info.HasSendResponse() { + return + } + channelId := 0 + if info.ChannelMeta != nil { + channelId = info.ChannelId + } + if channelId == 0 { + return + } + modelName := info.ClientModelName + if modelName == "" { + modelName = info.OriginModelName + } + ttftMs := info.FirstResponseTime.Sub(info.StartTime).Milliseconds() + channellatency.Observe(channelId, modelName, ttftMs) +} diff --git a/service/channel_ttft_test.go b/service/channel_ttft_test.go new file mode 100644 index 00000000000..2aaf808a4ca --- /dev/null +++ b/service/channel_ttft_test.go @@ -0,0 +1,97 @@ +// ABOUTME: Tests TTFT observation gates for latency-aware channel routing. +// ABOUTME: Ensures only successful streams with first token update (channel, model) stats. +package service + +import ( + "testing" + "time" + + "github.com/QuantumNous/new-api/pkg/channellatency" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/setting/operation_setting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestObserveChannelTtftFromRelayGates(t *testing.T) { + channellatency.ResetForTest() + origEnabled := operation_setting.TtftRoutingEnabled + origMin := operation_setting.TtftRoutingMinSamples + origAlpha := operation_setting.TtftRoutingEwmaAlpha + t.Cleanup(func() { + operation_setting.TtftRoutingEnabled = origEnabled + operation_setting.TtftRoutingMinSamples = origMin + operation_setting.TtftRoutingEwmaAlpha = origAlpha + channellatency.ResetForTest() + }) + + operation_setting.TtftRoutingEnabled = true + operation_setting.TtftRoutingMinSamples = 1 + operation_setting.TtftRoutingEwmaAlpha = 1.0 + + start := time.Now().Add(-800 * time.Millisecond) + first := time.Now() + + // Disabled: no sample. + operation_setting.TtftRoutingEnabled = false + ObserveChannelTtftFromRelay(&relaycommon.RelayInfo{ + IsStream: true, + StartTime: start, + FirstResponseTime: first, + ClientModelName: "gpt-4o", + ChannelMeta: &relaycommon.ChannelMeta{ChannelId: 11}, + }) + _, _, ok := channellatency.Snapshot(11, "gpt-4o") + assert.False(t, ok) + + operation_setting.TtftRoutingEnabled = true + + // Non-stream: no sample. + ObserveChannelTtftFromRelay(&relaycommon.RelayInfo{ + IsStream: false, + StartTime: start, + FirstResponseTime: first, + ClientModelName: "gpt-4o", + ChannelMeta: &relaycommon.ChannelMeta{ChannelId: 11}, + }) + _, _, ok = channellatency.Snapshot(11, "gpt-4o") + assert.False(t, ok) + + // No first token (FirstResponseTime not after StartTime): no sample. + ObserveChannelTtftFromRelay(&relaycommon.RelayInfo{ + IsStream: true, + StartTime: start, + FirstResponseTime: start.Add(-time.Second), + ClientModelName: "gpt-4o", + ChannelMeta: &relaycommon.ChannelMeta{ChannelId: 11}, + }) + _, _, ok = channellatency.Snapshot(11, "gpt-4o") + assert.False(t, ok) + + // Zero channel: no sample. + ObserveChannelTtftFromRelay(&relaycommon.RelayInfo{ + IsStream: true, + StartTime: start, + FirstResponseTime: first, + ClientModelName: "gpt-4o", + ChannelMeta: &relaycommon.ChannelMeta{ChannelId: 0}, + }) + _, _, ok = channellatency.Snapshot(11, "gpt-4o") + assert.False(t, ok) + + // Success stream with ClientModelName: records under client model, not OriginModelName. + ObserveChannelTtftFromRelay(&relaycommon.RelayInfo{ + IsStream: true, + StartTime: start, + FirstResponseTime: first, + ClientModelName: "gpt-4o", + OriginModelName: "gpt-4o-mapped-upstream", + ChannelMeta: &relaycommon.ChannelMeta{ChannelId: 11}, + }) + avg, n, ok := channellatency.Snapshot(11, "gpt-4o") + require.True(t, ok) + assert.Equal(t, int64(1), n) + assert.Greater(t, avg, 0.0) + _, _, okMapped := channellatency.Snapshot(11, "gpt-4o-mapped-upstream") + assert.False(t, okMapped) +} diff --git a/service/error.go b/service/error.go index cf7325b6103..19df6924f29 100644 --- a/service/error.go +++ b/service/error.go @@ -117,6 +117,7 @@ func RelayErrorHandler(ctx context.Context, resp *http.Response, showBodyWhenFai oaiError := errResponse.TryToOpenAIError() if oaiError != nil { newApiErr = types.WithOpenAIError(*oaiError, resp.StatusCode) + newApiErr = NormalizeSoftUpstreamError(newApiErr) if showBodyWhenFail { newApiErr.Err = buildErrWithBody(newApiErr.Error()) } @@ -124,6 +125,8 @@ func RelayErrorHandler(ctx context.Context, resp *http.Response, showBodyWhenFai } } newApiErr = types.NewOpenAIError(errors.New(errResponse.ToMessage()), types.ErrorCodeBadResponseStatusCode, resp.StatusCode) + // Non-object error bodies can still carry capacity text (e.g. plain message fields). + newApiErr = NormalizeSoftUpstreamError(newApiErr) if showBodyWhenFail { newApiErr.Err = buildErrWithBody(newApiErr.Error()) } diff --git a/service/error_test.go b/service/error_test.go index 9f19bfbb4fe..d64e404fba9 100644 --- a/service/error_test.go +++ b/service/error_test.go @@ -122,6 +122,20 @@ func TestRelayErrorHandlerKeepsOpenAIErrorMessage(t *testing.T) { require.Equal(t, message, newAPIError.Error()) } +func TestRelayErrorHandlerNormalizesCapacityNullCode(t *testing.T) { + body := `{"error":{"message":"The model is currently at capacity due to high demand. Please try again in a few minutes, or use a higher service tier for priority processing","type":"upstream_error","code":null}}` + resp := &http.Response{ + StatusCode: http.StatusInternalServerError, + Body: io.NopCloser(strings.NewReader(body)), + } + + newAPIError := RelayErrorHandler(context.Background(), resp, false) + require.NotNil(t, newAPIError) + require.Equal(t, types.ErrorCodeUpstreamCapacity, newAPIError.GetErrorCode()) + require.Equal(t, http.StatusServiceUnavailable, newAPIError.StatusCode) + require.Contains(t, newAPIError.Error(), "at capacity") +} + func TestRelayErrorHandlerKeepsInvalidJSONBodyInDebugLog(t *testing.T) { withDebugEnabled(t, true) diff --git a/service/log_info_generate.go b/service/log_info_generate.go index 207b0af5bfe..8a03b6a9bbe 100644 --- a/service/log_info_generate.go +++ b/service/log_info_generate.go @@ -106,8 +106,14 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m } AppendChannelAffinityAdminInfo(ctx, adminInfo) + AppendSoftFailAdminInfo(ctx, relayInfo, adminInfo) other["admin_info"] = adminInfo + if ctx != nil && ctx.Request != nil { + if ua := ctx.Request.UserAgent(); ua != "" { + other["user_agent"] = ua + } + } appendRequestPath(ctx, relayInfo, other) appendRequestConversionChain(relayInfo, other) appendFinalRequestFormat(relayInfo, other) diff --git a/service/quota.go b/service/quota.go index e5d4ec7e233..daa5727c3b3 100644 --- a/service/quota.go +++ b/service/quota.go @@ -242,6 +242,9 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod InjectTieredBillingInfo(other, relayInfo, tieredResult) } attachQuotaSaturation(ctx, relayInfo, other) + requestBody := CaptureRequestBodyForLog(ctx) + responseBody := CaptureResponseBodyForLog(relayInfo) + PersistRequestDetailBodies(ctx, requestBody, responseBody) model.RecordConsumeLog(ctx, relayInfo.UserId, model.RecordConsumeLogParams{ ChannelId: relayInfo.ChannelId, PromptTokens: usage.InputTokens, @@ -365,6 +368,9 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, u InjectTieredBillingInfo(other, relayInfo, tieredResult) } attachQuotaSaturation(ctx, relayInfo, other) + requestBody := CaptureRequestBodyForLog(ctx) + responseBody := CaptureResponseBodyForLog(relayInfo) + PersistRequestDetailBodies(ctx, requestBody, responseBody) model.RecordConsumeLog(ctx, relayInfo.UserId, model.RecordConsumeLogParams{ ChannelId: relayInfo.ChannelId, PromptTokens: usage.PromptTokens, @@ -381,6 +387,7 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, u }) gopool.Go(func() { perfmetrics.RecordRelaySample(relayInfo, true, int64(usage.CompletionTokens)) + ObserveChannelTtftFromRelay(relayInfo) }) } diff --git a/service/relayconvert/internal/oai_responses/usable_output.go b/service/relayconvert/internal/oai_responses/usable_output.go new file mode 100644 index 00000000000..3b18dce5225 --- /dev/null +++ b/service/relayconvert/internal/oai_responses/usable_output.go @@ -0,0 +1,73 @@ +// ABOUTME: Classifies OpenAI Responses payloads for usable assistant output. +// ABOUTME: Detects empty-completed (reasoning-only) soft failures for multi-channel failover. +package oairesponses + +import ( + "strings" + + "github.com/QuantumNous/new-api/dto" +) + +// IsUsableResponsesOutput reports whether a Responses payload contains client-usable +// assistant output: non-empty assistant message text or a tool/function call. +// Reasoning-only content is not usable. +func IsUsableResponsesOutput(resp *dto.OpenAIResponsesResponse) bool { + if resp == nil { + return false + } + if assistantMessageTextFromResponses(resp) != "" { + return true + } + return hasResponsesToolCallOutput(resp) +} + +// IsEmptyCompletedResponses reports a soft failure: status is completed and there is +// no usable assistant text or tool call (e.g. reasoning-only Grok completions). +// Incomplete/failed/cancelled statuses are not empty-completed. +func IsEmptyCompletedResponses(resp *dto.OpenAIResponsesResponse) bool { + if resp == nil { + return false + } + if !strings.EqualFold(responseStatusString(resp), "completed") { + return false + } + return !IsUsableResponsesOutput(resp) +} + +func assistantMessageTextFromResponses(resp *dto.OpenAIResponsesResponse) string { + if resp == nil || len(resp.Output) == 0 { + return "" + } + var sb strings.Builder + for _, out := range resp.Output { + if out.Type != responsesOutputTypeMessage { + continue + } + if out.Role != "" && out.Role != "assistant" { + continue + } + for _, c := range out.Content { + if c.Type == "output_text" && c.Text != "" { + sb.WriteString(c.Text) + } + } + } + return strings.TrimSpace(sb.String()) +} + +func hasResponsesToolCallOutput(resp *dto.OpenAIResponsesResponse) bool { + if resp == nil || len(resp.Output) == 0 { + return false + } + for _, out := range resp.Output { + if isResponsesToolOutputType(out.Type) { + return true + } + // Image generation is a usable Responses tool output even though it is not + // mapped through the chat function-call converter path. + if out.Type == dto.ResponsesOutputTypeImageGenerationCall { + return true + } + } + return false +} diff --git a/service/relayconvert/internal/oai_responses/usable_output_test.go b/service/relayconvert/internal/oai_responses/usable_output_test.go new file mode 100644 index 00000000000..44203c46c49 --- /dev/null +++ b/service/relayconvert/internal/oai_responses/usable_output_test.go @@ -0,0 +1,177 @@ +// ABOUTME: Table tests for Responses usable-output and empty-completed classification. +// ABOUTME: Locks reasoning-only completed as a soft failure and tool-call-only as success. +package oairesponses + +import ( + "testing" + + "github.com/QuantumNous/new-api/dto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIsUsableResponsesOutputAndEmptyCompleted(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + resp *dto.OpenAIResponsesResponse + wantUsable bool + wantEmptyComp bool + }{ + { + name: "nil", + resp: nil, + wantUsable: false, + wantEmptyComp: false, + }, + { + name: "reasoning only completed", + resp: &dto.OpenAIResponsesResponse{ + Status: []byte(`"completed"`), + Output: []dto.ResponsesOutput{ + { + Type: responsesOutputTypeReasoning, + Content: []dto.ResponsesOutputContent{ + {Type: "reasoning_text", Text: "thinking hard"}, + }, + }, + }, + }, + wantUsable: false, + wantEmptyComp: true, + }, + { + name: "message text completed", + resp: &dto.OpenAIResponsesResponse{ + Status: []byte(`"completed"`), + Output: []dto.ResponsesOutput{ + { + Type: responsesOutputTypeMessage, + Role: "assistant", + Content: []dto.ResponsesOutputContent{ + {Type: "output_text", Text: "hello"}, + }, + }, + }, + }, + wantUsable: true, + wantEmptyComp: false, + }, + { + name: "function call only completed", + resp: &dto.OpenAIResponsesResponse{ + Status: []byte(`"completed"`), + Output: []dto.ResponsesOutput{ + { + Type: responsesOutputTypeFunctionCall, + Name: "lookup", + CallId: "call_1", + }, + }, + }, + wantUsable: true, + wantEmptyComp: false, + }, + { + name: "empty message plus reasoning completed", + resp: &dto.OpenAIResponsesResponse{ + Status: []byte(`"completed"`), + Output: []dto.ResponsesOutput{ + { + Type: responsesOutputTypeReasoning, + Content: []dto.ResponsesOutputContent{ + {Type: "reasoning_text", Text: "hmm"}, + }, + }, + { + Type: responsesOutputTypeMessage, + Role: "assistant", + Content: []dto.ResponsesOutputContent{ + {Type: "output_text", Text: " "}, + }, + }, + }, + }, + wantUsable: false, + wantEmptyComp: true, + }, + { + name: "content filter incomplete", + resp: &dto.OpenAIResponsesResponse{ + Status: []byte(`"incomplete"`), + IncompleteDetails: &dto.IncompleteDetails{ + Reason: responsesIncompleteReasonContentFilter, + }, + Output: []dto.ResponsesOutput{}, + }, + wantUsable: false, + wantEmptyComp: false, + }, + { + name: "custom tool call only completed", + resp: &dto.OpenAIResponsesResponse{ + Status: []byte(`"completed"`), + Output: []dto.ResponsesOutput{ + { + Type: responsesOutputTypeCustomToolCall, + Name: "custom_tool", + }, + }, + }, + wantUsable: true, + wantEmptyComp: false, + }, + { + name: "image generation call only completed", + resp: &dto.OpenAIResponsesResponse{ + Status: []byte(`"completed"`), + Output: []dto.ResponsesOutput{ + { + Type: dto.ResponsesOutputTypeImageGenerationCall, + }, + }, + }, + wantUsable: true, + wantEmptyComp: false, + }, + { + name: "failed status with no output", + resp: &dto.OpenAIResponsesResponse{ + Status: []byte(`"failed"`), + }, + wantUsable: false, + wantEmptyComp: false, + }, + { + name: "completed with empty output", + resp: &dto.OpenAIResponsesResponse{ + Status: []byte(`"completed"`), + Output: nil, + }, + wantUsable: false, + wantEmptyComp: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.wantUsable, IsUsableResponsesOutput(tt.resp)) + assert.Equal(t, tt.wantEmptyComp, IsEmptyCompletedResponses(tt.resp)) + }) + } +} + +func TestIsEmptyCompletedResponsesRequiresCompletedStatus(t *testing.T) { + t.Parallel() + resp := &dto.OpenAIResponsesResponse{ + Status: []byte(`"cancelled"`), + Output: []dto.ResponsesOutput{ + {Type: responsesOutputTypeReasoning}, + }, + } + require.False(t, IsEmptyCompletedResponses(resp)) + require.False(t, IsUsableResponsesOutput(resp)) +} diff --git a/service/relayconvert/response_compat.go b/service/relayconvert/response_compat.go index b42bed42f71..d2b361c9043 100644 --- a/service/relayconvert/response_compat.go +++ b/service/relayconvert/response_compat.go @@ -124,6 +124,14 @@ func ExtractReasoningTextFromResponses(resp *dto.OpenAIResponsesResponse) string return oairesponses.ExtractReasoningTextFromResponses(resp) } +func IsUsableResponsesOutput(resp *dto.OpenAIResponsesResponse) bool { + return oairesponses.IsUsableResponsesOutput(resp) +} + +func IsEmptyCompletedResponses(resp *dto.OpenAIResponsesResponse) bool { + return oairesponses.IsEmptyCompletedResponses(resp) +} + func NewResponsesToChatStreamState(model string, includeUsage bool) *ResponsesToChatStreamState { return oairesponses.NewResponsesToChatStreamState(model, includeUsage) } diff --git a/service/request_body_log.go b/service/request_body_log.go new file mode 100644 index 00000000000..4210b030c6d --- /dev/null +++ b/service/request_body_log.go @@ -0,0 +1,68 @@ +// ABOUTME: Capture the raw client request JSON body for usage log details. +// ABOUTME: Behavior is gated on common.RequestDetailLogEnabled and only stores valid JSON payloads. +package service + +import ( + "io" + "strings" + + "github.com/QuantumNous/new-api/common" + + "github.com/gin-gonic/gin" +) + +// CaptureRequestBodyForLog reads the current request's JSON body and returns its +// raw bytes as a string. It returns an empty string when: +// - request body logging is disabled, +// - the context is nil or has no request, +// - the content type is not JSON, +// - the body cannot be read or is not valid JSON. +// +// The underlying body storage position is reset so downstream readers are unaffected. +func CaptureRequestBodyForLog(ctx *gin.Context) string { + if !common.RequestDetailLogEnabled { + return "" + } + if ctx == nil || ctx.Request == nil { + return "" + } + if !isJSONContentType(ctx.Request.Header.Get("Content-Type")) { + return "" + } + + storage, err := common.GetBodyStorage(ctx) + if err != nil || storage == nil { + return "" + } + + data, err := storage.Bytes() + if err != nil { + return "" + } + // Always restore the storage position so the next reader sees a clean stream. + if _, seekErr := storage.Seek(0, io.SeekStart); seekErr != nil { + return "" + } + if len(data) == 0 { + return "" + } + + var probe any + if err := common.Unmarshal(data, &probe); err != nil { + return "" + } + + return string(data) +} + +func isJSONContentType(contentType string) bool { + if contentType == "" { + return false + } + // Strip any parameters such as `; charset=utf-8`. + if idx := strings.Index(contentType, ";"); idx >= 0 { + contentType = contentType[:idx] + } + contentType = strings.TrimSpace(strings.ToLower(contentType)) + return contentType == "application/json" || strings.HasSuffix(contentType, "+json") +} diff --git a/service/request_detail_file.go b/service/request_detail_file.go new file mode 100644 index 00000000000..81d51429a19 --- /dev/null +++ b/service/request_detail_file.go @@ -0,0 +1,211 @@ +// ABOUTME: Persist request/response detail bodies as files keyed by request_id. +// ABOUTME: Used for temporary debug logging without storing large bodies in the DB. +package service + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "unicode" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/logger" + + "github.com/gin-gonic/gin" +) + +const ( + requestDetailDirName = "request-detail" + requestDetailRequestFile = "request.json" + requestDetailResponseFile = "response.txt" + maxRequestDetailIDLength = 128 +) + +var ( + errInvalidRequestDetailID = errors.New("invalid request id") + errRequestDetailDisabled = errors.New("request detail log directory unavailable") +) + +// RequestDetailBodies is the on-disk payload for one request_id. +type RequestDetailBodies struct { + RequestBody string `json:"request_body,omitempty"` + ResponseBody string `json:"response_body,omitempty"` +} + +func requestDetailRoot() (string, error) { + if common.LogDir == nil || strings.TrimSpace(*common.LogDir) == "" { + return "", errRequestDetailDisabled + } + return filepath.Join(*common.LogDir, requestDetailDirName), nil +} + +func sanitizeRequestDetailID(requestID string) (string, error) { + requestID = strings.TrimSpace(requestID) + if requestID == "" || len(requestID) > maxRequestDetailIDLength { + return "", errInvalidRequestDetailID + } + for _, r := range requestID { + if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' || r == '_' || r == '.' { + continue + } + return "", errInvalidRequestDetailID + } + if requestID == "." || requestID == ".." { + return "", errInvalidRequestDetailID + } + return requestID, nil +} + +func requestDetailDir(requestID string) (string, error) { + root, err := requestDetailRoot() + if err != nil { + return "", err + } + safeID, err := sanitizeRequestDetailID(requestID) + if err != nil { + return "", err + } + // Ensure the resolved path stays under the root (defense in depth). + dir := filepath.Join(root, safeID) + absRoot, err := filepath.Abs(root) + if err != nil { + return "", err + } + absDir, err := filepath.Abs(dir) + if err != nil { + return "", err + } + sep := string(os.PathSeparator) + if absDir != absRoot && !strings.HasPrefix(absDir, absRoot+sep) { + return "", errInvalidRequestDetailID + } + return absDir, nil +} + +// PersistRequestDetailBodies writes request/response bodies to disk using the +// request id from the Gin context. Failures are logged and never block billing. +func PersistRequestDetailBodies(c *gin.Context, requestBody, responseBody string) { + if !common.RequestDetailLogEnabled { + return + } + if c == nil { + return + } + requestID := c.GetString(common.RequestIdKey) + if requestID == "" { + return + } + if err := WriteRequestDetailBodies(requestID, requestBody, responseBody); err != nil { + logger.LogError(c, "failed to persist request detail bodies: "+err.Error()) + } +} + +// WriteRequestDetailBodies stores request/response bodies under +// {log-dir}/request-detail/{request_id}/. Empty sides are skipped (existing +// files for a side are left untouched when that side is empty). +func WriteRequestDetailBodies(requestID, requestBody, responseBody string) error { + if !common.RequestDetailLogEnabled { + return nil + } + if strings.TrimSpace(requestBody) == "" && strings.TrimSpace(responseBody) == "" { + return nil + } + dir, err := requestDetailDir(requestID) + if err != nil { + return err + } + if err := os.MkdirAll(dir, 0o750); err != nil { + return fmt.Errorf("create request detail dir: %w", err) + } + if strings.TrimSpace(requestBody) != "" { + path := filepath.Join(dir, requestDetailRequestFile) + if err := os.WriteFile(path, []byte(requestBody), 0o640); err != nil { + return fmt.Errorf("write request body file: %w", err) + } + } + if strings.TrimSpace(responseBody) != "" { + path := filepath.Join(dir, requestDetailResponseFile) + if err := os.WriteFile(path, []byte(responseBody), 0o640); err != nil { + return fmt.Errorf("write response body file: %w", err) + } + } + return nil +} + +// ReadRequestDetailBodies loads bodies for requestID from disk. +// Missing files yield empty strings without error. +func ReadRequestDetailBodies(requestID string) (RequestDetailBodies, error) { + var out RequestDetailBodies + dir, err := requestDetailDir(requestID) + if err != nil { + return out, err + } + reqPath := filepath.Join(dir, requestDetailRequestFile) + if data, err := os.ReadFile(reqPath); err == nil { + out.RequestBody = string(data) + } else if !os.IsNotExist(err) { + return out, fmt.Errorf("read request body file: %w", err) + } + respPath := filepath.Join(dir, requestDetailResponseFile) + if data, err := os.ReadFile(respPath); err == nil { + out.ResponseBody = string(data) + } else if !os.IsNotExist(err) { + return out, fmt.Errorf("read response body file: %w", err) + } + return out, nil +} + +// ClearAllRequestDetailBodies removes the entire request-detail directory tree. +// Returns how many top-level request_id directories were removed. +func ClearAllRequestDetailBodies() (int, error) { + root, err := requestDetailRoot() + if err != nil { + return 0, err + } + entries, err := os.ReadDir(root) + if err != nil { + if os.IsNotExist(err) { + return 0, nil + } + return 0, err + } + removed := 0 + for _, entry := range entries { + path := filepath.Join(root, entry.Name()) + if err := os.RemoveAll(path); err != nil { + return removed, fmt.Errorf("remove %s: %w", path, err) + } + removed++ + } + return removed, nil +} + +// RequestDetailStorageInfo returns basic disk usage for the request-detail root. +func RequestDetailStorageInfo() (dir string, fileCount int, totalSize int64, err error) { + root, err := requestDetailRoot() + if err != nil { + return "", 0, 0, err + } + dir = root + if _, statErr := os.Stat(root); os.IsNotExist(statErr) { + return dir, 0, 0, nil + } + err = filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if d.IsDir() { + return nil + } + info, infoErr := d.Info() + if infoErr != nil { + return infoErr + } + fileCount++ + totalSize += info.Size() + return nil + }) + return dir, fileCount, totalSize, err +} diff --git a/service/request_detail_file_test.go b/service/request_detail_file_test.go new file mode 100644 index 00000000000..2a7fdcec52b --- /dev/null +++ b/service/request_detail_file_test.go @@ -0,0 +1,105 @@ +// ABOUTME: Unit tests for request-detail file storage keyed by request_id. +// ABOUTME: Covers write/read/clear, path sanitization, and disabled-switch behavior. +package service + +import ( + "path/filepath" + "testing" + + "github.com/QuantumNous/new-api/common" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func withTempLogDir(t *testing.T) string { + t.Helper() + dir := t.TempDir() + prev := common.LogDir + prevEnabled := common.RequestDetailLogEnabled + common.LogDir = &dir + common.RequestDetailLogEnabled = true + t.Cleanup(func() { + common.LogDir = prev + common.RequestDetailLogEnabled = prevEnabled + }) + return dir +} + +func TestWriteAndReadRequestDetailBodies(t *testing.T) { + withTempLogDir(t) + + err := WriteRequestDetailBodies("req123", `{"model":"gpt"}`, "line1\nline2\n") + require.NoError(t, err) + + got, err := ReadRequestDetailBodies("req123") + require.NoError(t, err) + assert.Equal(t, `{"model":"gpt"}`, got.RequestBody) + assert.Equal(t, "line1\nline2\n", got.ResponseBody) +} + +func TestWriteRequestDetailBodies_Disabled(t *testing.T) { + withTempLogDir(t) + common.RequestDetailLogEnabled = false + + err := WriteRequestDetailBodies("req123", `{"a":1}`, `{"b":2}`) + require.NoError(t, err) + + got, err := ReadRequestDetailBodies("req123") + require.NoError(t, err) + assert.Equal(t, "", got.RequestBody) + assert.Equal(t, "", got.ResponseBody) +} + +func TestWriteRequestDetailBodies_PartialUpdate(t *testing.T) { + withTempLogDir(t) + + require.NoError(t, WriteRequestDetailBodies("req123", `{"a":1}`, `{"b":2}`)) + require.NoError(t, WriteRequestDetailBodies("req123", "", `{"b":3}`)) + + got, err := ReadRequestDetailBodies("req123") + require.NoError(t, err) + assert.Equal(t, `{"a":1}`, got.RequestBody) + assert.Equal(t, `{"b":3}`, got.ResponseBody) +} + +func TestSanitizeRequestDetailID_RejectsTraversal(t *testing.T) { + _, err := sanitizeRequestDetailID("../etc/passwd") + assert.ErrorIs(t, err, errInvalidRequestDetailID) + + _, err = sanitizeRequestDetailID("a/b") + assert.ErrorIs(t, err, errInvalidRequestDetailID) + + _, err = sanitizeRequestDetailID("") + assert.ErrorIs(t, err, errInvalidRequestDetailID) +} + +func TestClearAllRequestDetailBodies(t *testing.T) { + root := withTempLogDir(t) + require.NoError(t, WriteRequestDetailBodies("reqA", `{"a":1}`, "")) + require.NoError(t, WriteRequestDetailBodies("reqB", "", "resp")) + + removed, err := ClearAllRequestDetailBodies() + require.NoError(t, err) + assert.Equal(t, 2, removed) + + got, err := ReadRequestDetailBodies("reqA") + require.NoError(t, err) + assert.Equal(t, "", got.RequestBody) + + // Root directory itself remains; children are gone. + entries, err := filepath.Glob(filepath.Join(root, requestDetailDirName, "*")) + require.NoError(t, err) + assert.Empty(t, entries) +} + +func TestRequestDetailStorageInfo(t *testing.T) { + withTempLogDir(t) + require.NoError(t, WriteRequestDetailBodies("req1", `{"x":1}`, "y")) + + dir, count, size, err := RequestDetailStorageInfo() + require.NoError(t, err) + assert.Contains(t, dir, requestDetailDirName) + assert.Equal(t, 2, count) + assert.Greater(t, size, int64(0)) +} diff --git a/service/response_body_log.go b/service/response_body_log.go new file mode 100644 index 00000000000..d23dd6fc5a2 --- /dev/null +++ b/service/response_body_log.go @@ -0,0 +1,133 @@ +// ABOUTME: Capture upstream response bodies for usage log details (debug). +// ABOUTME: Non-stream JSON bodies and stream SSE data payloads; gated by RequestDetailLogEnabled. +package service + +import ( + "bytes" + "io" + "net/http" + "strings" + + "github.com/QuantumNous/new-api/common" + relaycommon "github.com/QuantumNous/new-api/relay/common" + relayconstant "github.com/QuantumNous/new-api/relay/constant" +) + +// CaptureResponseBodyForLog returns the upstream response body stored on RelayInfo +// when request detail logging is enabled. +func CaptureResponseBodyForLog(info *relaycommon.RelayInfo) string { + if !common.RequestDetailLogEnabled || info == nil { + return "" + } + return info.GetResponseBodyForLog() +} + +// AppendUpstreamStreamChunkForLog appends one upstream SSE data payload (JSON text) +// after the stream ends it is stored as newline-delimited plain text. +func AppendUpstreamStreamChunkForLog(info *relaycommon.RelayInfo, data string) { + if !common.RequestDetailLogEnabled || info == nil { + return + } + if shouldSkipResponseBodyLogByMode(info.RelayMode) { + return + } + if data == "" { + return + } + info.AppendResponseBodyForLog(data) +} + +// MaybeCaptureUpstreamHTTPResponseForLog reads and re-wraps a non-stream (or +// stream-error) upstream HTTP body so later handlers still see the full body. +// Successful streams are left untouched; StreamScannerHandler records chunks. +func MaybeCaptureUpstreamHTTPResponseForLog(info *relaycommon.RelayInfo, resp *http.Response) { + if !common.RequestDetailLogEnabled || info == nil || resp == nil || resp.Body == nil { + return + } + // Always reset so retries do not keep a previous attempt's body. + info.ResetResponseBodyForLog() + + if shouldSkipResponseBodyLogByMode(info.RelayMode) { + return + } + // Successful streams are captured as SSE data lines in StreamScannerHandler. + if info.IsStream && resp.StatusCode < http.StatusBadRequest { + return + } + contentType := resp.Header.Get("Content-Type") + if !isLoggableUpstreamResponseContentType(contentType) { + return + } + + body, err := io.ReadAll(resp.Body) + _ = resp.Body.Close() + if err != nil { + resp.Body = io.NopCloser(bytes.NewReader(nil)) + return + } + if len(body) > 0 { + info.SetResponseBodyForLog(string(body)) + } + resp.Body = io.NopCloser(bytes.NewReader(body)) +} + +func shouldSkipResponseBodyLogByMode(relayMode int) bool { + switch relayMode { + case relayconstant.RelayModeImagesGenerations, + relayconstant.RelayModeImagesEdits, + relayconstant.RelayModeAudioSpeech, + relayconstant.RelayModeAudioTranscription, + relayconstant.RelayModeAudioTranslation, + relayconstant.RelayModeVideoFetchByID, + relayconstant.RelayModeVideoSubmit, + relayconstant.RelayModeRealtime, + relayconstant.RelayModeMidjourneyImagine, + relayconstant.RelayModeMidjourneyDescribe, + relayconstant.RelayModeMidjourneyBlend, + relayconstant.RelayModeMidjourneyChange, + relayconstant.RelayModeMidjourneySimpleChange, + relayconstant.RelayModeMidjourneyNotify, + relayconstant.RelayModeMidjourneyTaskFetch, + relayconstant.RelayModeMidjourneyTaskImageSeed, + relayconstant.RelayModeMidjourneyTaskFetchByCondition, + relayconstant.RelayModeMidjourneyAction, + relayconstant.RelayModeMidjourneyModal, + relayconstant.RelayModeMidjourneyShorten, + relayconstant.RelayModeSwapFace, + relayconstant.RelayModeMidjourneyUpload, + relayconstant.RelayModeMidjourneyVideo, + relayconstant.RelayModeMidjourneyEdits: + return true + default: + return false + } +} + +func isLoggableUpstreamResponseContentType(contentType string) bool { + if contentType == "" { + // Many upstreams omit Content-Type on JSON errors; still capture. + return true + } + if idx := strings.Index(contentType, ";"); idx >= 0 { + contentType = contentType[:idx] + } + contentType = strings.TrimSpace(strings.ToLower(contentType)) + switch { + case contentType == "application/json", + strings.HasSuffix(contentType, "+json"), + contentType == "text/json", + contentType == "text/plain", + contentType == "text/event-stream", + strings.HasPrefix(contentType, "text/"): + return true + case strings.HasPrefix(contentType, "image/"), + strings.HasPrefix(contentType, "audio/"), + strings.HasPrefix(contentType, "video/"), + strings.HasPrefix(contentType, "application/octet-stream"), + strings.HasPrefix(contentType, "multipart/"): + return false + default: + // Be conservative for unknown binary-ish types. + return strings.Contains(contentType, "json") || strings.HasPrefix(contentType, "text/") + } +} diff --git a/service/response_body_log_test.go b/service/response_body_log_test.go new file mode 100644 index 00000000000..e03407e037c --- /dev/null +++ b/service/response_body_log_test.go @@ -0,0 +1,128 @@ +// ABOUTME: Unit tests for upstream response body capture used by usage logs. +// ABOUTME: Covers switch gating, content-type/mode skips, and stream chunk append. +package service + +import ( + "io" + "net/http" + "strings" + "testing" + + "github.com/QuantumNous/new-api/common" + relaycommon "github.com/QuantumNous/new-api/relay/common" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCaptureResponseBodyForLog_Disabled(t *testing.T) { + prev := common.RequestDetailLogEnabled + t.Cleanup(func() { common.RequestDetailLogEnabled = prev }) + common.RequestDetailLogEnabled = false + + info := &relaycommon.RelayInfo{} + info.SetResponseBodyForLog(`{"ok":true}`) + assert.Equal(t, "", CaptureResponseBodyForLog(info)) +} + +func TestMaybeCaptureUpstreamHTTPResponseForLog_NonStreamJSON(t *testing.T) { + prev := common.RequestDetailLogEnabled + t.Cleanup(func() { common.RequestDetailLogEnabled = prev }) + common.RequestDetailLogEnabled = true + + info := &relaycommon.RelayInfo{IsStream: false} + body := `{"id":"chatcmpl-1","choices":[]}` + resp := &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(body)), + } + + MaybeCaptureUpstreamHTTPResponseForLog(info, resp) + assert.Equal(t, body, CaptureResponseBodyForLog(info)) + + // Body must remain readable for downstream handlers. + got, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, body, string(got)) +} + +func TestMaybeCaptureUpstreamHTTPResponseForLog_SkipsSuccessfulStream(t *testing.T) { + prev := common.RequestDetailLogEnabled + t.Cleanup(func() { common.RequestDetailLogEnabled = prev }) + common.RequestDetailLogEnabled = true + + info := &relaycommon.RelayInfo{IsStream: true} + resp := &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"text/event-stream"}}, + Body: io.NopCloser(strings.NewReader("data: {\"a\":1}\n\n")), + } + + MaybeCaptureUpstreamHTTPResponseForLog(info, resp) + assert.Equal(t, "", CaptureResponseBodyForLog(info)) + + // Stream body must not be consumed. + got, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Contains(t, string(got), "data:") +} + +func TestMaybeCaptureUpstreamHTTPResponseForLog_CapturesStreamErrorJSON(t *testing.T) { + prev := common.RequestDetailLogEnabled + t.Cleanup(func() { common.RequestDetailLogEnabled = prev }) + common.RequestDetailLogEnabled = true + + info := &relaycommon.RelayInfo{IsStream: true} + body := `{"error":{"message":"capacity"}}` + resp := &http.Response{ + StatusCode: http.StatusTooManyRequests, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(body)), + } + + MaybeCaptureUpstreamHTTPResponseForLog(info, resp) + assert.Equal(t, body, CaptureResponseBodyForLog(info)) +} + +func TestMaybeCaptureUpstreamHTTPResponseForLog_SkipsImageMode(t *testing.T) { + prev := common.RequestDetailLogEnabled + t.Cleanup(func() { common.RequestDetailLogEnabled = prev }) + common.RequestDetailLogEnabled = true + + info := &relaycommon.RelayInfo{ + IsStream: false, + RelayMode: relayconstant.RelayModeImagesGenerations, + } + resp := &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{"data":[{"b64_json":"xxx"}]}`)), + } + + MaybeCaptureUpstreamHTTPResponseForLog(info, resp) + assert.Equal(t, "", CaptureResponseBodyForLog(info)) +} + +func TestAppendUpstreamStreamChunkForLog(t *testing.T) { + prev := common.RequestDetailLogEnabled + t.Cleanup(func() { common.RequestDetailLogEnabled = prev }) + common.RequestDetailLogEnabled = true + + info := &relaycommon.RelayInfo{IsStream: true} + AppendUpstreamStreamChunkForLog(info, `{"delta":"a"}`) + AppendUpstreamStreamChunkForLog(info, `{"delta":"b"}`) + + assert.Equal(t, "{\"delta\":\"a\"}\n{\"delta\":\"b\"}\n", CaptureResponseBodyForLog(info)) +} + +func TestIsLoggableUpstreamResponseContentType(t *testing.T) { + assert.True(t, isLoggableUpstreamResponseContentType("application/json")) + assert.True(t, isLoggableUpstreamResponseContentType("application/json; charset=utf-8")) + assert.True(t, isLoggableUpstreamResponseContentType("text/event-stream")) + assert.True(t, isLoggableUpstreamResponseContentType("")) + assert.False(t, isLoggableUpstreamResponseContentType("image/png")) + assert.False(t, isLoggableUpstreamResponseContentType("audio/mpeg")) + assert.False(t, isLoggableUpstreamResponseContentType("application/octet-stream")) +} diff --git a/service/soft_fail.go b/service/soft_fail.go new file mode 100644 index 00000000000..c27613a1eb8 --- /dev/null +++ b/service/soft_fail.go @@ -0,0 +1,253 @@ +// ABOUTME: Shared soft-failure control plane for empty-completed and capacity errors. +// ABOUTME: Marks affinity unusable, clears sticky binding, and records admin soft-fail markers. +package service + +import ( + "fmt" + "net/http" + "strings" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" +) + +// MarkClientPayloadWritten records that assistant payload was flushed to the client. +// Once set, in-request soft-fail channel switching is no longer safe. +func MarkClientPayloadWritten(info *relaycommon.RelayInfo) { + if info == nil { + return + } + info.ClientPayloadWritten = true +} + +// MarkClientPayloadWrittenContext also mirrors the flag onto gin for middleware/log paths. +func MarkClientPayloadWrittenContext(c *gin.Context, info *relaycommon.RelayInfo) { + MarkClientPayloadWritten(info) + if c != nil { + c.Set(ginKeyClientPayloadWritten, true) + } +} + +// IsClientPayloadWritten reports whether assistant payload was flushed for this attempt. +func IsClientPayloadWritten(c *gin.Context, info *relaycommon.RelayInfo) bool { + if info != nil && info.ClientPayloadWritten { + return true + } + if c == nil { + return false + } + if v, ok := c.Get(ginKeyClientPayloadWritten); ok { + if b, ok := v.(bool); ok && b { + return true + } + } + return false +} + +// ChatCompletionsStreamHasClientPayload reports whether a chat SSE chunk carries +// assistant-facing payload (text, reasoning, or tool calls). Role-only / empty +// start chunks are not payload and must not block empty-completed failover. +func ChatCompletionsStreamHasClientPayload(resp *dto.ChatCompletionsStreamResponse) bool { + if resp == nil { + return false + } + if resp.Usage != nil { + return true + } + for _, choice := range resp.Choices { + if strings.TrimSpace(choice.Delta.GetContentString()) != "" { + return true + } + if strings.TrimSpace(choice.Delta.GetReasoningContent()) != "" { + return true + } + if len(choice.Delta.ToolCalls) > 0 { + return true + } + if choice.FinishReason != nil && strings.TrimSpace(*choice.FinishReason) != "" { + // Finish-only after prior empty role start is still a terminal commit for + // double-write safety once the client has observed a finished choice. + // Empty completed is intercepted before terminal conversion, so this mainly + // protects capacity/error paths that arrive after a finish frame. + return true + } + } + return false +} + +// ApplySoftFailRetryPolicy clears affinity markers and, when the client stream is +// already committed, marks the error skip-retry so controller will not switch channels. +func ApplySoftFailRetryPolicy(c *gin.Context, info *relaycommon.RelayInfo, err *types.NewAPIError) *types.NewAPIError { + if err == nil { + return nil + } + if !IsSoftFailErrorCode(err.GetErrorCode()) { + return err + } + ApplySoftFailFromError(c, info, err) + if IsClientPayloadWritten(c, info) { + return types.MarkSkipRetry(err) + } + return err +} + +// MarkSoftFailAffinity marks the current attempt as affinity-unusable, clears sticky +// binding when possible, and stores admin-facing soft-fail markers on gin context. +func MarkSoftFailAffinity(c *gin.Context, info *relaycommon.RelayInfo, class string) { + if info != nil { + info.AffinityUnusable = true + info.SoftFailReason = class + if class == SoftFailClassEmptyCompleted { + info.EmptyCompleted = true + } + } + if c != nil { + c.Set(ginKeyChannelAffinityUnusable, true) + if class != "" { + c.Set(ginKeySoftFailClass, class) + } + if info != nil && info.ClientPayloadWritten { + c.Set(ginKeyClientPayloadWritten, true) + } + } + + cleared := ClearCurrentChannelAffinityCache(c) + if c != nil && cleared { + c.Set(ginKeyAffinityCleared, true) + } + + if c != nil && class != "" { + channelID := 0 + modelName := "" + clientWritten := false + if info != nil { + if info.ChannelMeta != nil { + channelID = info.ChannelId + } + modelName = info.OriginModelName + if modelName == "" { + modelName = info.UpstreamModelName + } + clientWritten = info.ClientPayloadWritten + } + logger.LogWarn(c, fmt.Sprintf( + "soft fail class=%s channel=#%d model=%s client_written=%t affinity_cleared=%t", + class, channelID, modelName, clientWritten, cleared, + )) + } +} + +// MarkEmptyCompletedSoftFail applies empty-completed soft-fail flags and affinity clear. +func MarkEmptyCompletedSoftFail(c *gin.Context, info *relaycommon.RelayInfo) { + MarkSoftFailAffinity(c, info, SoftFailClassEmptyCompleted) +} + +// MarkCapacitySoftFail applies capacity soft-fail flags and affinity clear. +func MarkCapacitySoftFail(c *gin.Context, info *relaycommon.RelayInfo) { + MarkSoftFailAffinity(c, info, SoftFailClassUpstreamCapacity) +} + +// NewEmptyCompletedError builds the switch-channel empty-response soft fail. +func NewEmptyCompletedError() *types.NewAPIError { + return types.NewOpenAIError( + fmt.Errorf("empty completed response: no usable assistant output"), + types.ErrorCodeEmptyResponse, + http.StatusServiceUnavailable, + ) +} + +// IsChannelAffinityUnusable reports whether the request marked affinity as unusable. +func IsChannelAffinityUnusable(c *gin.Context) bool { + if c == nil { + return false + } + if v, ok := c.Get(ginKeyChannelAffinityUnusable); ok { + if b, ok := v.(bool); ok && b { + return true + } + } + return false +} + +// AppendSoftFailAdminInfo nests soft-fail markers under admin_info. +func AppendSoftFailAdminInfo(c *gin.Context, info *relaycommon.RelayInfo, adminInfo map[string]interface{}) { + if adminInfo == nil { + return + } + + class := "" + affinityCleared := false + clientWritten := false + + if c != nil { + if v, ok := c.Get(ginKeySoftFailClass); ok { + if s, ok := v.(string); ok { + class = s + } + } + if v, ok := c.Get(ginKeyAffinityCleared); ok { + if b, ok := v.(bool); ok { + affinityCleared = b + } + } + if v, ok := c.Get(ginKeyClientPayloadWritten); ok { + if b, ok := v.(bool); ok { + clientWritten = b + } + } + } + if info != nil { + if info.SoftFailReason != "" && class == "" { + class = info.SoftFailReason + } + if info.ClientPayloadWritten { + clientWritten = true + } + if info.AffinityUnusable && class == "" && info.EmptyCompleted { + class = SoftFailClassEmptyCompleted + } + } + + if class == "" && !affinityCleared && !clientWritten { + return + } + if class != "" { + adminInfo["soft_fail_class"] = class + } + if affinityCleared { + adminInfo["affinity_cleared"] = true + } + if clientWritten { + adminInfo["client_payload_written"] = true + } + if info != nil && info.EmptyCompleted { + adminInfo["empty_completed"] = true + } +} + +// ApplySoftFailFromError marks affinity/log state when the returned error is a soft fail. +// Idempotent for the same class on one request (avoids duplicate warn logs). +func ApplySoftFailFromError(c *gin.Context, info *relaycommon.RelayInfo, err *types.NewAPIError) { + if err == nil { + return + } + class := SoftFailClassFromErrorCode(err.GetErrorCode()) + if class == "" { + return + } + if c != nil { + if prev, ok := c.Get(ginKeySoftFailClass); ok { + if prevClass, ok := prev.(string); ok && prevClass == class { + // Still refresh client-written flag if it became true after first mark. + if info != nil && info.ClientPayloadWritten { + c.Set(ginKeyClientPayloadWritten, true) + } + return + } + } + } + MarkSoftFailAffinity(c, info, class) +} diff --git a/service/text_quota.go b/service/text_quota.go index 7da3391206d..54ad86e1395 100644 --- a/service/text_quota.go +++ b/service/text_quota.go @@ -488,6 +488,9 @@ func PostTextConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, us attachQuotaSaturation(ctx, relayInfo, other) + requestBody := CaptureRequestBodyForLog(ctx) + responseBody := CaptureResponseBodyForLog(relayInfo) + PersistRequestDetailBodies(ctx, requestBody, responseBody) model.RecordConsumeLog(ctx, relayInfo.UserId, model.RecordConsumeLogParams{ ChannelId: relayInfo.ChannelId, PromptTokens: summary.PromptTokens, @@ -504,5 +507,6 @@ func PostTextConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, us }) gopool.Go(func() { perfmetrics.RecordRelaySample(relayInfo, true, int64(summary.CompletionTokens)) + ObserveChannelTtftFromRelay(relayInfo) }) } diff --git a/service/upstream_error_classify.go b/service/upstream_error_classify.go new file mode 100644 index 00000000000..77727cf7a3c --- /dev/null +++ b/service/upstream_error_classify.go @@ -0,0 +1,148 @@ +// ABOUTME: Classifies OpenAI-style upstream errors for capacity/null-code soft failures. +// ABOUTME: Normalizes capacity messages into retryable upstream_capacity / HTTP 503. +package service + +import ( + "errors" + "fmt" + "net/http" + "strings" + + "github.com/QuantumNous/new-api/setting/operation_setting" + "github.com/QuantumNous/new-api/types" +) + +// SoftFailKind is the soft-failure class derived from upstream error payloads. +type SoftFailKind string + +const ( + SoftFailKindNone SoftFailKind = "" + SoftFailKindCapacity SoftFailKind = "upstream_capacity" + + // SoftFailClassEmptyCompleted is the admin/log marker for empty completed responses. + SoftFailClassEmptyCompleted = "empty_completed" + // SoftFailClassUpstreamCapacity is the admin/log marker for capacity soft fails. + SoftFailClassUpstreamCapacity = "upstream_capacity" + + ginKeyChannelAffinityUnusable = "channel_affinity_unusable" + ginKeySoftFailClass = "soft_fail_class" + ginKeyAffinityCleared = "affinity_cleared" + ginKeyClientPayloadWritten = "client_payload_written" +) + +// IsSoftFailErrorCode reports internal soft-failure codes that must retry across +// channels, clear affinity, and never auto-ban. +func IsSoftFailErrorCode(code types.ErrorCode) bool { + switch code { + case types.ErrorCodeEmptyResponse, types.ErrorCodeUpstreamCapacity: + return true + default: + return false + } +} + +// IsNullOrUnknownUpstreamCode reports codes that are effectively absent/unknown. +func IsNullOrUnknownUpstreamCode(code any) bool { + if code == nil { + return true + } + switch v := code.(type) { + case string: + s := strings.TrimSpace(strings.ToLower(v)) + return s == "" || s == "null" || s == "unknown_error" || s == "" + case types.ErrorCode: + return IsNullOrUnknownUpstreamCode(string(v)) + case fmt.Stringer: + return IsNullOrUnknownUpstreamCode(v.String()) + default: + s := strings.TrimSpace(strings.ToLower(fmt.Sprintf("%v", v))) + return s == "" || s == "null" || s == "unknown_error" || s == "" + } +} + +// HasUpstreamCapacityKeywords reports whether message text matches configured +// capacity keywords (option UpstreamCapacityKeywords; case-insensitive substrings). +func HasUpstreamCapacityKeywords(message string) bool { + lower := strings.ToLower(message) + if lower == "" { + return false + } + for _, keyword := range operation_setting.UpstreamCapacityKeywords { + if keyword == "" { + continue + } + if strings.Contains(lower, keyword) { + return true + } + } + return false +} + +func isCapacityRetryableHTTPStatus(status int) bool { + switch status { + case 0, http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable: + return true + default: + return false + } +} + +// ClassifyUpstreamOpenAIError classifies an OpenAI-style upstream error as a soft fail. +// Capacity requires keyword match plus null/unknown code and/or a retryable status. +func ClassifyUpstreamOpenAIError(message string, code any, httpStatus int) SoftFailKind { + if !HasUpstreamCapacityKeywords(message) { + return SoftFailKindNone + } + if IsNullOrUnknownUpstreamCode(code) || isCapacityRetryableHTTPStatus(httpStatus) { + return SoftFailKindCapacity + } + return SoftFailKindNone +} + +// NormalizeSoftUpstreamError rewrites capacity soft failures to a stable internal +// code and HTTP 503 while preserving the upstream message text. +func NormalizeSoftUpstreamError(err *types.NewAPIError) *types.NewAPIError { + if err == nil { + return nil + } + + message := err.Error() + var code any = err.GetErrorCode() + if openAIErr, ok := err.RelayError.(types.OpenAIError); ok { + if openAIErr.Message != "" { + message = openAIErr.Message + } + if openAIErr.Code != nil { + code = openAIErr.Code + } + } + + kind := ClassifyUpstreamOpenAIError(message, code, err.StatusCode) + if kind != SoftFailKindCapacity { + return err + } + + normalized := types.NewOpenAIError(errors.New(message), types.ErrorCodeUpstreamCapacity, http.StatusServiceUnavailable) + if openAIErr, ok := err.RelayError.(types.OpenAIError); ok { + openAIErr.Message = message + openAIErr.Code = string(types.ErrorCodeUpstreamCapacity) + if openAIErr.Type == "" { + openAIErr.Type = "upstream_error" + } + normalized.RelayError = openAIErr + normalized.Metadata = openAIErr.Metadata + } + return normalized +} + +// SoftFailClassFromErrorCode maps an error code to the soft-fail admin marker class. +func SoftFailClassFromErrorCode(code types.ErrorCode) string { + switch code { + case types.ErrorCodeEmptyResponse: + return SoftFailClassEmptyCompleted + case types.ErrorCodeUpstreamCapacity: + return SoftFailClassUpstreamCapacity + default: + return "" + } +} diff --git a/service/upstream_error_classify_test.go b/service/upstream_error_classify_test.go new file mode 100644 index 00000000000..a4a5dd4d2ff --- /dev/null +++ b/service/upstream_error_classify_test.go @@ -0,0 +1,144 @@ +// ABOUTME: Unit tests for capacity/null-code upstream error classification. +// ABOUTME: Covers the xAI capacity fixture and non-capacity false-positive guards. +package service + +import ( + "errors" + "net/http" + "testing" + + "github.com/QuantumNous/new-api/setting/operation_setting" + "github.com/QuantumNous/new-api/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const xaiCapacityMessage = "Error Code null: The model is currently at capacity due to high demand. Please try again in a few minutes, or use a higher service tier for priority processing: https://docs.x.ai/developers/advanced-api-usage/priority-processing" + +func TestIsNullOrUnknownUpstreamCode(t *testing.T) { + t.Parallel() + assert.True(t, IsNullOrUnknownUpstreamCode(nil)) + assert.True(t, IsNullOrUnknownUpstreamCode("")) + assert.True(t, IsNullOrUnknownUpstreamCode("null")) + assert.True(t, IsNullOrUnknownUpstreamCode("NULL")) + assert.True(t, IsNullOrUnknownUpstreamCode("unknown_error")) + assert.False(t, IsNullOrUnknownUpstreamCode("rate_limit_exceeded")) + assert.False(t, IsNullOrUnknownUpstreamCode("invalid_api_key")) +} + +func TestClassifyUpstreamOpenAIErrorCapacity(t *testing.T) { + // Not parallel: shares package-level UpstreamCapacityKeywords with config tests. + + tests := []struct { + name string + message string + code any + status int + wantKind SoftFailKind + }{ + { + name: "xai capacity null code status 500", + message: xaiCapacityMessage, + code: nil, + status: http.StatusInternalServerError, + wantKind: SoftFailKindCapacity, + }, + { + name: "xai capacity null string status 503", + message: xaiCapacityMessage, + code: "null", + status: http.StatusServiceUnavailable, + wantKind: SoftFailKindCapacity, + }, + { + name: "xai capacity unknown_error status 429", + message: xaiCapacityMessage, + code: "unknown_error", + status: http.StatusTooManyRequests, + wantKind: SoftFailKindCapacity, + }, + { + name: "capacity keywords with real code but 503", + message: "model is overloaded", + code: "server_error", + status: http.StatusServiceUnavailable, + wantKind: SoftFailKindCapacity, + }, + { + name: "null code invalid api key 401", + message: "invalid api key", + code: nil, + status: http.StatusUnauthorized, + wantKind: SoftFailKindNone, + }, + { + name: "null code empty message 500", + message: "", + code: nil, + status: http.StatusInternalServerError, + wantKind: SoftFailKindNone, + }, + { + name: "normal 429 rate limit without capacity keywords", + message: "Rate limit reached for requests", + code: "rate_limit_exceeded", + status: http.StatusTooManyRequests, + wantKind: SoftFailKindNone, + }, + { + name: "capacity keywords with non-null code and 400", + message: "at capacity", + code: "invalid_request", + status: http.StatusBadRequest, + wantKind: SoftFailKindNone, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.wantKind, ClassifyUpstreamOpenAIError(tt.message, tt.code, tt.status)) + }) + } +} + +func TestNormalizeSoftUpstreamErrorCapacity(t *testing.T) { + // Not parallel: shares package-level UpstreamCapacityKeywords with config tests. + + original := types.WithOpenAIError(types.OpenAIError{ + Message: xaiCapacityMessage, + Type: "upstream_error", + Code: nil, + }, http.StatusInternalServerError) + + normalized := NormalizeSoftUpstreamError(original) + require.NotNil(t, normalized) + assert.Equal(t, types.ErrorCodeUpstreamCapacity, normalized.GetErrorCode()) + assert.Equal(t, http.StatusServiceUnavailable, normalized.StatusCode) + assert.Contains(t, normalized.Error(), "at capacity") + assert.True(t, IsSoftFailErrorCode(normalized.GetErrorCode())) +} + +func TestNormalizeSoftUpstreamErrorLeavesNonCapacity(t *testing.T) { + t.Parallel() + + original := types.NewOpenAIError(errors.New("Rate limit reached"), types.ErrorCode("rate_limit_exceeded"), http.StatusTooManyRequests) + normalized := NormalizeSoftUpstreamError(original) + require.NotNil(t, normalized) + assert.Equal(t, types.ErrorCode("rate_limit_exceeded"), normalized.GetErrorCode()) + assert.Equal(t, http.StatusTooManyRequests, normalized.StatusCode) +} + +func TestUpstreamCapacityKeywordsAreConfigurable(t *testing.T) { + orig := append([]string(nil), operation_setting.UpstreamCapacityKeywords...) + t.Cleanup(func() { + operation_setting.UpstreamCapacityKeywords = orig + }) + + operation_setting.UpstreamCapacityKeywordsFromString("custom-overload-token\n") + require.True(t, HasUpstreamCapacityKeywords("provider says custom-overload-token now")) + require.False(t, HasUpstreamCapacityKeywords("at capacity due to high demand")) + require.Equal(t, SoftFailKindCapacity, ClassifyUpstreamOpenAIError( + "hit custom-overload-token", nil, http.StatusInternalServerError, + )) +} diff --git a/setting/operation_setting/operation_setting.go b/setting/operation_setting/operation_setting.go index ef330d1adb8..3153c1106b2 100644 --- a/setting/operation_setting/operation_setting.go +++ b/setting/operation_setting/operation_setting.go @@ -30,3 +30,29 @@ func AutomaticDisableKeywordsFromString(s string) { } } } + +// UpstreamCapacityKeywords are case-insensitive substrings used to classify +// transient capacity/overload soft failures (retryable, no auto-ban). +// Defaults cover common provider messages such as xAI high-demand capacity. +var UpstreamCapacityKeywords = []string{ + "at capacity", + "high demand", + "overloaded", + "temporarily unavailable", + "server is busy", + "priority processing", +} + +func UpstreamCapacityKeywordsToString() string { + return strings.Join(UpstreamCapacityKeywords, "\n") +} + +func UpstreamCapacityKeywordsFromString(s string) { + UpstreamCapacityKeywords = []string{} + for _, line := range strings.Split(s, "\n") { + k := strings.ToLower(strings.TrimSpace(line)) + if k != "" { + UpstreamCapacityKeywords = append(UpstreamCapacityKeywords, k) + } + } +} diff --git a/setting/operation_setting/ttft_routing.go b/setting/operation_setting/ttft_routing.go new file mode 100644 index 00000000000..da94a9ec4b0 --- /dev/null +++ b/setting/operation_setting/ttft_routing.go @@ -0,0 +1,39 @@ +// ABOUTME: Configures latency-aware (TTFT) weighting for same-priority channel picks. +// ABOUTME: Defaults keep the feature off until operators enable it. +package operation_setting + +// TtftRoutingEnabled multiplies configured channel weights by a TTFT factor +// within the same priority only. Off by default. +var TtftRoutingEnabled = false + +// TtftRoutingMinSamples is the minimum EWMA samples before a channel's TTFT +// biases selection. Below this, factor is 1.0. +var TtftRoutingMinSamples int64 = 20 + +// TtftRoutingEwmaAlpha is the EWMA smoothing factor in (0, 1]. Higher reacts faster. +var TtftRoutingEwmaAlpha = 0.2 + +// TtftRoutingMinFactor / MaxFactor clamp the latency multiplier. +var TtftRoutingMinFactor = 0.5 +var TtftRoutingMaxFactor = 2.0 + +// TtftRoutingRefMs is the fallback reference TTFT (ms) when peer median is unavailable. +var TtftRoutingRefMs int64 = 500 + +func ClampTtftRoutingConfig() { + if TtftRoutingMinSamples < 1 { + TtftRoutingMinSamples = 1 + } + if TtftRoutingEwmaAlpha <= 0 || TtftRoutingEwmaAlpha > 1 { + TtftRoutingEwmaAlpha = 0.2 + } + if TtftRoutingMinFactor <= 0 { + TtftRoutingMinFactor = 0.5 + } + if TtftRoutingMaxFactor < TtftRoutingMinFactor { + TtftRoutingMaxFactor = TtftRoutingMinFactor + } + if TtftRoutingRefMs < 1 { + TtftRoutingRefMs = 500 + } +} diff --git a/setting/payment_stripe.go b/setting/payment_stripe.go index d97120c8523..7453575977d 100644 --- a/setting/payment_stripe.go +++ b/setting/payment_stripe.go @@ -6,3 +6,5 @@ var StripePriceId = "" var StripeUnitPrice = 8.0 var StripeMinTopUp = 1 var StripePromotionCodesEnabled = false +var StripeWechatPayEnabled = false +var StripeAlipayEnabled = false diff --git a/types/error.go b/types/error.go index 9717401ae7b..17718aed1fd 100644 --- a/types/error.go +++ b/types/error.go @@ -74,6 +74,7 @@ const ( ErrorCodeBadResponse ErrorCode = "bad_response" ErrorCodeBadResponseBody ErrorCode = "bad_response_body" ErrorCodeEmptyResponse ErrorCode = "empty_response" + ErrorCodeUpstreamCapacity ErrorCode = "upstream_capacity" ErrorCodeAwsInvokeError ErrorCode = "aws_invoke_error" ErrorCodeModelNotFound ErrorCode = "model_not_found" ErrorCodePromptBlocked ErrorCode = "prompt_blocked" @@ -378,6 +379,16 @@ func IsSkipRetryError(err *NewAPIError) bool { return err.skipRetry } +// MarkSkipRetry prevents controller same-request channel retries for this error. +// Used when client payload was already flushed (committed stream). +func MarkSkipRetry(err *NewAPIError) *NewAPIError { + if err == nil { + return nil + } + err.skipRetry = true + return err +} + func ErrOptionWithSkipRetry() NewAPIErrorOptions { return func(e *NewAPIError) { e.skipRetry = true diff --git a/web/bun.lock b/web/bun.lock index d86f3a8b3ff..e87a479039d 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -18,6 +18,7 @@ "@visactor/vchart-semi-theme": "~1.8.8", "axios": "catalog:", "clsx": "catalog:", + "date-fns": "2.30.0", "dayjs": "catalog:", "highlight.js": "^11.11.1", "history": "^5.3.0", @@ -84,6 +85,7 @@ "@tanstack/react-router": "^1.170.17", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.14.5", + "@uiw/react-json-view": "^2.0.0-alpha.42", "@visactor/react-vchart": "^2.1.2", "@visactor/vchart": "^2.1.2", "ai": "^7.0.14", @@ -1188,6 +1190,8 @@ "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260702.3", "", { "os": "win32", "cpu": "x64" }, "sha512-kPqfivHuzzEK2NFEl2QeCCiWJ4qkGMJYFX9TmJ4ylh784XICgQLX3PJe7OCqZcUicfeMIIDu6m3flcCi+4tW4Q=="], + "@uiw/react-json-view": ["@uiw/react-json-view@2.0.0-alpha.43", "", { "peerDependencies": { "@babel/runtime": ">=7.10.0", "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-PMj+6xRDCPbVGccfLUDhx7yGrmVBSeFx3py6bqDds4QIYScXo9GEgtTbWXn3gXpoehsmOu//6dveYTf872wu3Q=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], @@ -1512,7 +1516,7 @@ "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], - "date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], + "date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="], "date-fns-tz": ["date-fns-tz@1.3.8", "", { "peerDependencies": { "date-fns": ">=2.0.0" } }, "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ=="], @@ -2926,6 +2930,8 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@base-ui/react/date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], + "@codemirror/autocomplete/@codemirror/view": ["@codemirror/view@6.43.3", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-MwEwCAr/o0agJefhC2+reBv5kfOQpMcDRUNQrRYZgWlhH8IwQcerMZrpqWyUFSyO0ebgN2cnh/w87F7G4BGSng=="], "@codemirror/lang-html/@codemirror/view": ["@codemirror/view@6.43.3", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-MwEwCAr/o0agJefhC2+reBv5kfOQpMcDRUNQrRYZgWlhH8IwQcerMZrpqWyUFSyO0ebgN2cnh/w87F7G4BGSng=="], @@ -2942,10 +2948,6 @@ "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], - "@douyinfe/semi-foundation/date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="], - - "@douyinfe/semi-ui/date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="], - "@emoji-mart/react/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "@emotion/babel-plugin/@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], @@ -3202,6 +3204,8 @@ "rc-menu/@rc-component/trigger": ["@rc-component/trigger@2.3.1", "", { "dependencies": { "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", "classnames": "^2.3.2", "rc-motion": "^2.0.0", "rc-resize-observer": "^1.3.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A=="], + "react-day-picker/date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], + "react-i18next/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "react-rnd/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="], @@ -3276,6 +3280,8 @@ "@lobehub/ui/@base-ui/react/@base-ui/utils": ["@base-ui/utils@0.2.9", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw=="], + "@lobehub/ui/@base-ui/react/date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], + "@lobehub/ui/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="], "@lobehub/ui/@shikijs/core/@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="], diff --git a/web/classic/package.json b/web/classic/package.json index ecc6971280b..a034b8b09c7 100644 --- a/web/classic/package.json +++ b/web/classic/package.json @@ -13,6 +13,7 @@ "@visactor/vchart-semi-theme": "~1.8.8", "axios": "catalog:", "clsx": "catalog:", + "date-fns": "2.30.0", "dayjs": "catalog:", "history": "^5.3.0", "highlight.js": "^11.11.1", diff --git a/web/classic/rsbuild.config.ts b/web/classic/rsbuild.config.ts index 3ccf1e96df8..902abac19b6 100644 --- a/web/classic/rsbuild.config.ts +++ b/web/classic/rsbuild.config.ts @@ -1,32 +1,33 @@ -import path from 'path' -import { createRequire } from 'module' -import { fileURLToPath } from 'url' -import { defineConfig, loadEnv } from '@rsbuild/core' -import { pluginReact } from '@rsbuild/plugin-react' +// ABOUTME: Configures the Rsbuild pipeline for the classic React frontend. +// ABOUTME: Defines entry points, dependency aliases, dev proxy, and build output. +import path from 'path'; +import { createRequire } from 'module'; +import { fileURLToPath } from 'url'; +import { defineConfig, loadEnv } from '@rsbuild/core'; +import { pluginReact } from '@rsbuild/plugin-react'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const require = createRequire(import.meta.url) +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); const semiUiDir = path.resolve( path.dirname(require.resolve('@douyinfe/semi-ui')), '../..', -) +); +const dateFnsDir = path.dirname(require.resolve('date-fns/package.json')); export default defineConfig(({ envMode }) => { - const env = loadEnv({ mode: envMode, prefixes: ['VITE_'] }) + const env = loadEnv({ mode: envMode, prefixes: ['VITE_'] }); const clientServerUrl = process.env.VITE_REACT_APP_SERVER_URL || env.rawPublicVars.VITE_REACT_APP_SERVER_URL || - '' - const proxyServerUrl = - clientServerUrl || - 'http://localhost:3000' - const isProd = envMode === 'production' + ''; + const proxyServerUrl = clientServerUrl || 'http://localhost:3000'; + const isProd = envMode === 'production'; const devProxy = Object.fromEntries( (['/api', '/mj', '/pg'] as const).map((key) => [ key, { target: proxyServerUrl, changeOrigin: true }, ]), - ) as Record + ) as Record; return { plugins: [pluginReact()], @@ -35,9 +36,8 @@ export default defineConfig(({ envMode }) => { index: './src/index.jsx', }, define: { - 'import.meta.env.VITE_REACT_APP_SERVER_URL': JSON.stringify( - clientServerUrl, - ), + 'import.meta.env.VITE_REACT_APP_SERVER_URL': + JSON.stringify(clientServerUrl), }, }, resolve: { @@ -47,6 +47,7 @@ export default defineConfig(({ envMode }) => { semiUiDir, 'dist/css/semi.css', ), + 'date-fns': dateFnsDir, }, }, html: { @@ -102,5 +103,5 @@ export default defineConfig(({ envMode }) => { }, }, }, - } -}) + }; +}); diff --git a/web/classic/src/components/settings/OperationSetting.jsx b/web/classic/src/components/settings/OperationSetting.jsx index 8585a3e9027..5dc05d85cf6 100644 --- a/web/classic/src/components/settings/OperationSetting.jsx +++ b/web/classic/src/components/settings/OperationSetting.jsx @@ -44,6 +44,9 @@ const OperationSetting = () => { QuotaPerUnit: 0, USDExchangeRate: 0, RetryTimes: 0, + SameChannelRetryTimes: 0, + SameChannelRetryBaseDelayMs: 200, + SameChannelRetryMaxDelayMs: 2000, 'general_setting.quota_display_type': 'USD', DisplayTokenStatEnabled: false, DefaultCollapseSidebar: false, diff --git a/web/classic/src/components/settings/OtherSetting.jsx b/web/classic/src/components/settings/OtherSetting.jsx index a9848551b59..d074c77b383 100644 --- a/web/classic/src/components/settings/OtherSetting.jsx +++ b/web/classic/src/components/settings/OtherSetting.jsx @@ -245,12 +245,12 @@ const OtherSetting = () => { // Option 1: Use a public CORS proxy service // const proxyUrl = 'https://cors-anywhere.herokuapp.com/'; // const res = await API.get( - // `${proxyUrl}https://api.github.com/repos/Calcium-Ion/new-api/releases/latest`, + // `${proxyUrl}https://api.github.com/repos/pzyyll/new-api/releases/latest`, // ); // Option 2: Use the JSON proxy approach which often works better with GitHub API const res = await fetch( - 'https://api.github.com/repos/Calcium-Ion/new-api/releases/latest', + 'https://api.github.com/repos/pzyyll/new-api/releases/latest', { headers: { Accept: 'application/json', diff --git a/web/classic/src/components/table/channels/modals/EditChannelModal.jsx b/web/classic/src/components/table/channels/modals/EditChannelModal.jsx index 2b07e1f8775..10f9f39e928 100644 --- a/web/classic/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/classic/src/components/table/channels/modals/EditChannelModal.jsx @@ -195,6 +195,7 @@ const EditChannelModal = (props) => { system_prompt: '', system_prompt_override: false, settings: '', + user_agent: '', // 仅 Vertex: 密钥格式(存入 settings.vertex_key_type) vertex_key_type: 'json', // 仅 AWS: 密钥格式和区域(存入 settings.aws_key_type 和 settings.aws_region) @@ -1027,6 +1028,7 @@ const EditChannelModal = (props) => { (data.header_override && data.header_override.trim()) || (data.tag && data.tag.trim()) || (data.remark && data.remark.trim()) || + (data.user_agent && data.user_agent.trim()) || (data.priority && data.priority !== 0) || (data.weight && data.weight !== 0) || (data.proxy && data.proxy.trim()) || @@ -2458,6 +2460,14 @@ const EditChannelModal = (props) => { showClear onChange={(value) => handleInputChange('remark', value)} /> + handleInputChange('user_agent', value)} + extraText={t('逗号分隔的通配符模式。设置后,仅匹配的客户端 User-Agent 可使用此渠道。留空则允许所有客户端。')} + /> diff --git a/web/classic/src/constants/channel.constants.js b/web/classic/src/constants/channel.constants.js index 278e756336b..a249e96b0f0 100644 --- a/web/classic/src/constants/channel.constants.js +++ b/web/classic/src/constants/channel.constants.js @@ -189,11 +189,16 @@ export const CHANNEL_OPTIONS = [ color: 'blue', label: 'ChatGPT Subscription (Codex)', }, + { + value: 59, + color: 'blue', + label: 'OpenCode Go', + }, ]; // Channel types that support upstream model list fetching in UI. export const MODEL_FETCHABLE_CHANNEL_TYPES = new Set([ - 1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43, + 1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43, 59, ]); export const MODEL_TABLE_PAGE_SIZE = 10; diff --git a/web/classic/src/helpers/api.js b/web/classic/src/helpers/api.js index 88122a564cf..7c57a636b62 100644 --- a/web/classic/src/helpers/api.js +++ b/web/classic/src/helpers/api.js @@ -36,7 +36,6 @@ export let API = axios.create({ }, }); - function redirectToOAuthUrl(url, options = {}) { const { openInNewTab = false } = options; const targetUrl = typeof url === 'string' ? url : url.toString(); @@ -49,7 +48,6 @@ function redirectToOAuthUrl(url, options = {}) { window.location.assign(targetUrl); } - function patchAPIInstance(instance) { const originalGet = instance.get.bind(instance); const inFlightGetRequests = new Map(); diff --git a/web/classic/src/i18n/locales/fr.json b/web/classic/src/i18n/locales/fr.json index 420206ff79f..cb22ccf0df8 100644 --- a/web/classic/src/i18n/locales/fr.json +++ b/web/classic/src/i18n/locales/fr.json @@ -11,13 +11,8 @@ " 吗?": " ?", " 秒": "s", " 秒。": " secondes.", - ",": ", ", - ",当前无生效订阅,将自动使用钱包": ", aucun abonnement actif, le portefeuille sera utilisé automatiquement.", - ",时间:": ", time:", - ",点击更新": ", cliquez sur Mettre à jour", - "、": ", ", - "(共 {{total}} 个,省略 {{omit}} 个)": "", - "(共 {{total}} 个)": "", + "$/1M tokens": "$/1M tokens", + "$/次": "", "(筛选后显示 {{count}} 条)_many": "(Affichage de {{count}} éléments après filtrage)", "(筛选后显示 {{count}} 条)_one": "(Showing {{count}} item after filtering)", "(筛选后显示 {{count}} 条)_other": "(Showing {{count}} items after filtering)", @@ -25,25 +20,6 @@ "(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(Entrée {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + Entrée audio {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}", "(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}": "(Entrée {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Cache {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}", "(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}": "(Entrée {{nonImageInput}} tokens + Entrée image {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}", - "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "La valeur maximale de [Nombre maximal de requêtes] et [Nombre maximal d'achèvements de requêtes] est 2147483647.", - "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[Nombre maximal de requêtes] doit être supérieur ou égal à 0, [Nombre maximal d'achèvements de requêtes] doit être supérieur ou égal à 1.", - "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", - "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", - "{{count}} 项操作_many": "", - "{{count}} 项操作_one": "", - "{{count}} 项操作_other": "", - "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", - "{{name}} ID": "{{name}} ID", - "{{ratioType}} {{ratio}}": "{{ratioType}} {{ratio}}", - "{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x", - "「用户可选」决定用户创建令牌时能否自主选择该分组。未勾选的分组只能由管理员在后台分配给用户,不会出现在用户的令牌创建页面中。": "\"User Selectable\" controls whether users can choose this group when creating tokens. Unchecked groups can only be assigned by admin and won't appear in the token creation page.", - "• 视频服务商的跨域限制": "• Des restrictions cross-origin imposées par le fournisseur vidéo", - "• 防盗链保护机制": "• Un mécanisme de protection anti-hotlink", - "• 需要特定的请求头或认证": "• Des en-têtes ou une authentification spécifiques sont requis", - "© {{currentYear}}": "© {{currentYear}}", - "| 基于": " | Basé sur ", - "$/1M tokens": "$/1M tokens", - "$/次": "", "0 - 最低": "0 - La plus basse", "0 表示不限": "0 signifie illimité", "0.002-1之间的小数": "Décimal entre 0,002-1", @@ -53,10 +29,10 @@ "1h缓存创建 {{price}} / 1M tokens": "Création de cache 1h {{price}} / 1M tokens", "1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Création de cache 1h {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "Création du cache 1h {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (ratio : {{ratio}})", - "1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "Création du cache 1h : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de création du cache 1h {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}", "1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Prix de création du cache 1h {{symbol}}{{price}} / 1M tokens", "1h缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (1h缓存创建倍率: {{cacheCreationRatio1h}})": "Prix de création de cache 1h : {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (ratio de création 1h : {{cacheCreationRatio1h}})", "1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Prix de création du cache 1h : {{symbol}}{{price}} / 1M tokens", + "1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "Création du cache 1h : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de création du cache 1h {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}", "2 - 低": "2 - Basse", "2. 管理员在用户管理中将用户分配到对应分组(如 vip)": "2. Admin assigns users to groups in User Management (e.g., vip)", "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "Après le 10 mai 2025, les canaux ajoutés n'ont plus besoin de supprimer le point dans le nom du modèle lors du déploiement", @@ -67,17 +43,16 @@ "5m缓存创建 {{price}} / 1M tokens": "Création de cache 5m {{price}} / 1M tokens", "5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Création de cache 5m {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "Création du cache 5m {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (ratio : {{ratio}})", - "5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "Création du cache 5m : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de création du cache 5m {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}", "5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Prix de création du cache 5m {{symbol}}{{price}} / 1M tokens", "5m缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (5m缓存创建倍率: {{cacheCreationRatio5m}})": "Prix de création de cache 5m : {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (ratio de création 5m : {{cacheCreationRatio5m}})", "5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Prix de création du cache 5m : {{symbol}}{{price}} / 1M tokens", + "5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "Création du cache 5m : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de création du cache 5m {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}", "8 - 高": "8 - Haute", "AGPL v3.0协议": "Licence AGPL v3.0", "AI 对话": "Conversation IA", "AI模型测试环境": "Environnement de test de modèle d'IA", "AI模型配置": "Configuration du modèle d'IA", "AK/SK 模式:使用 AccessKey 和 SecretAccessKey;API Key 模式:使用 API Key": "Mode AK/SK : utiliser AccessKey et SecretAccessKey ; mode API Key : utiliser API Key", - "anthropic-beta JSON 示例": "", "API Key": "API Key", "API Key 模式下不支持批量创建": "Création en lot non prise en charge en mode clé API", "API Key 验证失败": "API Key verification failed", @@ -86,7 +61,12 @@ "API 密钥": "Clé API", "API 密钥 (沙盒)": "", "API 密钥 (生产)": "", + "API 密钥(测试环境)": "", + "API 密钥(生产环境)": "", "API 文档": "Docs API", + "API 私钥": "", + "API 私钥(测试环境)": "", + "API 私钥(生产环境)": "", "API 配置": "Config. API", "API令牌管理": "Jetons API", "API使用记录": "Journaux d'API", @@ -95,13 +75,14 @@ "API地址": "URL de base", "API渠道配置": "Configuration du canal de l'API", "API端点": "Points de terminaison de l'API", - "Authorization callback URL 填": "Remplir l'URL de rappel d'autorisation", "Authorization Endpoint": "Point de terminaison d'autorisation", - "auto分组调用链路": "Chaîne d'appels de groupe auto", + "Authorization callback URL 填": "Remplir l'URL de rappel d'autorisation", "Bark推送URL": "URL de notification Bark", "Bark推送URL必须以http://或https://开头": "L'URL de notification Bark doit commencer par http:// ou https://", "Bark通知": "Notification Bark", "Basic Auth 头": "En-tête Basic Auth", + "CPU 使用率超过此值时拒绝请求": "Rejeter les requêtes lorsque l'utilisation du CPU dépasse cette valeur", + "CPU 阈值 (%)": "Seuil CPU (%)", "Cached tokens": "Cached tokens", "Cached tokens 占比口径由后端返回:Claude 语义按 cached/(prompt+cached),其余按 cached/prompt。": "Le ratio de cached tokens est renvoyé par le backend : la sémantique Claude calcule cached/(prompt+cached), les autres calculent cached/prompt.", "Changing batch type to:": "Changement du type de lot en :", @@ -116,20 +97,13 @@ "Client ID": "ID client", "Client Secret": "Secret client", "Codex 渠道不支持批量创建": "Le canal Codex ne prend pas en charge la création par lot", - "common.changeLanguage": "Changer de langue", "Completion tokens": "Completion tokens", "Configuration": "Configuration", - "context_int/context_string 从请求上下文读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "context_int/context_string lit depuis le contexte de la requête ; gjson lit depuis le body JSON de la requête d'entrée via le chemin gjson.", - "CPU 使用率超过此值时拒绝请求": "Rejeter les requêtes lorsque l'utilisation du CPU dépasse cette valeur", - "CPU 阈值 (%)": "Seuil CPU (%)", "Creem API 密钥,敏感信息不显示": "Clé API Creem, les informations sensibles ne sont pas affichées", "Creem Setting Tips": "Creem ne prend en charge que des produits à montant fixe préconfigurés. Ces produits et leurs prix doivent être créés et configurés à l'avance sur le site Creem, les recharges à montant dynamique ne sont donc pas prises en charge. Configurez le nom et le prix du produit sur Creem, récupérez l'identifiant du produit, puis remplissez-le ci-dessous. Définissez enfin le montant et le prix affiché dans new-api.", "Creem 介绍": "Présentation de Creem", "Creem 充值": "Recharge Creem", "Creem 设置": "Paramètres Creem", - "default 和 vip 只能由管理员在「用户管理」中分配给用户。适用于按用户等级定价、内部测试等不希望用户自主选择的场景。": "default and vip can only be assigned to users by admin in \"User Management\". Suitable for tiered pricing, internal testing, or other scenarios where user self-selection is not desired.", - "default为默认设置,可单独设置每个分类的安全等级": "\"default\" est le paramètre par défaut, et chaque catégorie peut être définie séparément", - "default为默认设置,可单独设置每个模型的版本": "\"default\" est le paramètre par défaut, et chaque modèle peut être défini séparément", "Dify渠道只适配chatflow和agent,并且agent不支持图片!": "Le canal Dify ne prend en charge que chatflow et agent, et l'agent ne prend pas en charge les images !", "Discord": "Discord", "Discord Client ID": "ID client Discord", @@ -139,10 +113,11 @@ "Discovery scopes": "Discovery scopes", "Discovery 建议 scopes:": "Scopes Discovery recommandés :", "EUR (欧元)": "EUR (Euro)", - "false": "faux", "GC 已执行": "GC exécuté", "GC 执行失败": "Échec de l'exécution du GC", "GC 次数": "Nombre de GC", + "GPU/容器": "GPU/Container", + "GPU数量": "Number of GPUs", "Gemini安全设置": "Paramètres de sécurité Gemini", "Gemini思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Adaptation de la pensée Gemini BudgetTokens = MaxTokens * BudgetTokens pourcentage", "Gemini思考适配设置": "Paramètres d'adaptation de la pensée Gemini", @@ -157,14 +132,10 @@ "Gotify服务器地址": "Adresse du serveur Gotify", "Gotify服务器地址必须以http://或https://开头": "L'adresse du serveur Gotify doit commencer par http:// ou https://", "Gotify通知": "Notification Gotify", - "GPU/容器": "GPU/Container", - "GPU数量": "Number of GPUs", "Grok设置": "Paramètres Grok", "Haiku 模型": "Modèle Haiku", "Homepage URL 填": "Remplir l'URL de la page d'accueil", "ID": "ID", - "include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护": "include_obfuscation contrôle les champs d'obfuscation dans le flux Responses. Désactivé par défaut pour éviter que les clients ne désactivent cette protection de sécurité", - "inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息": "Le champ inference_geo contrôle la région de résidence des données d'inférence de Claude. Désactivé par défaut pour éviter la transmission non autorisée d'informations géographiques", "IP": "IP", "IP白名单": "IP Whitelist", "IP白名单(支持CIDR表达式)": "Liste blanche d'adresses IP (prise en charge des expressions CIDR)", @@ -172,6 +143,7 @@ "IP黑名单": "Liste noire d'adresses IP", "JSON": "JSON", "JSON 已格式化": "JSON formaté", + "JSON 必须是对象": "", "JSON 文本": "Texte JSON", "JSON 无效": "JSON invalide", "JSON 格式参考": "JSON Reference", @@ -204,6 +176,7 @@ "Ollama 模型管理": "Ollama Model Management", "Ollama 版本信息": "Ollama Version Info", "Opus 模型": "Modèle Opus", + "POST 参数": "Paramètres POST", "Passkey": "Passkey", "Passkey 已解绑": "Passkey délié", "Passkey 已重置": "Le Passkey a été réinitialisé", @@ -214,71 +187,73 @@ "Pay Method Name": "", "Pay Method Type": "", "Ping间隔(秒)": "Intervalle de ping (secondes)", - "POST 参数": "Paramètres POST", - "price_xxx 的商品价格 ID,新建产品后可获得": "ID de prix du produit price_xxx, peut être obtenu après la création d'un nouveau produit", + "Product ID": "", "Prompt cache hit tokens": "Prompt cache hit tokens", "Prompt tokens": "Prompt tokens", - "Reasoning Effort": "Effort de raisonnement", - "Request ID": "Request ID", "RSA 私钥 (沙盒)": "", "RSA 私钥 (生产)": "", - "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "Le champ safety_identifier aide OpenAI à identifier les utilisateurs d'applications susceptibles de violer les politiques d'utilisation. Désactivé par défaut pour protéger la confidentialité des utilisateurs", - "Scopes(可选)": "Scopes (optionnel)", - "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "Le champ service_tier est utilisé pour spécifier le niveau de service. Permettre le passage peut entraîner une facturation plus élevée que prévu. Désactivé par défaut pour éviter des frais supplémentaires", - "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "Clé secrète Stripe sk_xxx ou rk_xxx, les informations sensibles ne sont pas affichées", + "Reasoning Effort": "Effort de raisonnement", + "Request ID": "Request ID", "SMTP 加密方式": "Chiffrement SMTP", "SMTP 发送者邮箱": "Adresse e-mail de l'expéditeur SMTP", "SMTP 服务器地址": "Adresse du serveur SMTP", "SMTP 端口": "Port SMTP", "SMTP 访问凭证": "Informations d'identification d'accès SMTP", "SMTP 账户": "Compte SMTP", - "Sonnet 模型": "Modèle Sonnet", - "speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "Le champ speed contrôle le mode de vitesse d'inférence de Claude. Désactivé par défaut pour éviter un passage involontaire au mode fast", "SSE 事件": "Événement SSE", "SSE数据流": "Flux de données SSE", "SSL/TLS": "SSL/TLS", "SSRF防护开关详细说明": "L'interrupteur principal contrôle si la protection SSRF est activée. Lorsqu'elle est désactivée, toutes les vérifications SSRF sont contournées, autorisant l'accès à n'importe quelle URL. ⚠️ Ne désactivez cette fonctionnalité que dans des environnements entièrement fiables.", "SSRF防护设置": "Protection SSRF", "SSRF防护详细说明": "La protection SSRF empêche les utilisateurs malveillants d'utiliser votre serveur pour accéder aux ressources du réseau interne. Configurez des listes blanches pour les domaines/IP de confiance et limitez les ports autorisés. S'applique aux téléchargements de fichiers, aux webhooks et aux notifications.", - "standard 已被移除,vip 用户看不到": "standard has been removed, vip users cannot see it", "STARTTLS": "STARTTLS", - "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "Le champ store autorise OpenAI à stocker les données de requête pour l'évaluation et l'optimisation du produit. Désactivé par défaut. L'activation peut causer un dysfonctionnement de Codex", + "Scopes(可选)": "Scopes (optionnel)", + "Sonnet 模型": "Modèle Sonnet", + "Store ID": "", "Stripe 设置": "Paramètres Stripe", "Stripe/Creem 商品ID(可选)": "ID produit Stripe/Creem (optionnel)", "Stripe/Creem 需在第三方平台创建商品并填入 ID": "Les produits Stripe/Creem doivent être créés sur la plateforme tierce et l'ID doit être renseigné", + "TTL(秒)": "TTL (secondes)", + "TTL(秒,0 表示默认)": "TTL (secondes, 0 pour la valeur par défaut)", "Telegram": "Telegram", "Telegram Bot Token": "Jeton du bot Telegram", "Telegram Bot 名称": "Nom du bot Telegram", "Telegram ID": "ID Telegram", "Token Endpoint": "Point de terminaison du jeton", - "token 会按倍率换算成“额度/次数”,请求结束后再做差额结算(补扣/返还)。": "Les tokens sont convertis en quota/nombre d'utilisations selon le ratio. Après la requête, la différence est réglée (déduction supplémentaire/remboursement).", + "Token 估算器": "", "Total tokens": "Total tokens", - "true": "vrai", - "TTL(秒,0 表示默认)": "TTL (secondes, 0 pour la valeur par défaut)", - "TTL(秒)": "TTL (secondes)", "Turnstile Secret Key": "Clé secrète Turnstile", "Turnstile Site Key": "Clé du site Turnstile", - "Unix时间戳": "Horodatage Unix", - "Uptime Kuma地址": "Adresse Uptime Kuma", - "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Catégories de surveillance Uptime Kuma, vous pouvez configurer plusieurs catégories de surveillance pour l'affichage de l'état du service (maximum 20)", "URL 标识,只能包含小写字母、数字和连字符": "Identifiant URL, uniquement lettres minuscules, chiffres et tirets autorisés", "URL链接": "Lien URL", "USD (美元)": "USD (Dollar US)", + "Unix时间戳": "Horodatage Unix", + "Uptime Kuma地址": "Adresse Uptime Kuma", + "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Catégories de surveillance Uptime Kuma, vous pouvez configurer plusieurs catégories de surveillance pour l'affichage de l'état du service (maximum 20)", "User Info Endpoint": "Point de terminaison des informations utilisateur", "User-Agent include(每行一个,可不写)": "User-Agent include (un par ligne, optionnel)", + "User-Agent 过滤": "", "Value 正则": "Regex de valeur", "Vertex AI 不支持 functionResponse.id 字段,开启后将自动移除该字段": "Vertex AI ne prend pas en charge le champ functionResponse.id. Lorsqu'il est activé, ce champ sera automatiquement supprimé", - "Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)": "", "Waffo API 参数,可空(最多64位)": "", + "Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)": "", + "Waffo Pancake 设置": "", "Waffo 充值": "", "Waffo 充值的最低数量,默认 1": "", "Waffo 公钥 (沙盒)": "", "Waffo 公钥 (生产)": "", + "Waffo 公钥(测试环境)": "", + "Waffo 公钥(生产环境)": "", "Waffo 商户 ID": "", + "Waffo 当前使用 USD 结算": "", "Waffo 是一个支付聚合平台,支持多种支付方式。": "", "Waffo 设置": "", - "Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Recherche Web : {{count}} / 1K * prix unitaire {{price}} * {{ratioType}} {{ratio}} = {{amount}}", + "WeChat Server 服务器地址": "Adresse du serveur WeChat Server", + "WeChat Server 访问凭证": "Informations d'identification d'accès au serveur WeChat", "Web 搜索调用 {{webSearchCallCount}} 次": "Recherche Web appelée {{webSearchCallCount}} fois", + "Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Recherche Web : {{count}} / 1K * prix unitaire {{price}} * {{ratioType}} {{ratio}} = {{amount}}", + "Webhook 公钥(测试环境)": "", + "Webhook 公钥(生产环境)": "", "Webhook 密钥": "Clé Webhook", "Webhook 签名密钥": "Clé de signature Webhook", "Webhook地址": "URL du Webhook", @@ -286,19 +261,57 @@ "Webhook请求结构说明": "Description de la structure de la requête Webhook", "Webhook通知": "Notification par Webhook", "Web搜索价格:{{symbol}}{{price}} / 1K 次": "Prix de recherche Web : {{symbol}}{{price}} / 1K fois", - "WeChat Server 服务器地址": "Adresse du serveur WeChat Server", - "WeChat Server 访问凭证": "Informations d'identification d'accès au serveur WeChat", "Well-Known URL": "URL bien connue", "Well-Known URL 必须以 http:// 或 https:// 开头": "L'URL bien connue doit commencer par http:// ou https://", - "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "Clé de signature Webhook whsec_xxx, les informations sensibles ne sont pas affichées", "Worker地址": "Adresse du Worker", "Worker密钥": "Clé du Worker", + "X 也可以小于 1,当折扣用。想做\"只给输出加价\"或\"额外加固定费用\",请直接写完整计费公式。": "", + "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "La valeur maximale de [Nombre maximal de requêtes] et [Nombre maximal d'achèvements de requêtes] est 2147483647.", + "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[Nombre maximal de requêtes] doit être supérieur ou égal à 0, [Nombre maximal d'achèvements de requêtes] doit être supérieur ou égal à 1.", + "anthropic-beta JSON 示例": "", + "auto分组调用链路": "Chaîne d'appels de groupe auto", + "common.changeLanguage": "Changer de langue", + "context_int/context_string 从请求上下文读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "context_int/context_string lit depuis le contexte de la requête ; gjson lit depuis le body JSON de la requête d'entrée via le chemin gjson.", + "context_int/context_string 从请求上下文读取;header 从 HTTP 请求头读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "", + "default 和 vip 只能由管理员在「用户管理」中分配给用户。适用于按用户等级定价、内部测试等不希望用户自主选择的场景。": "default and vip can only be assigned to users by admin in \"User Management\". Suitable for tiered pricing, internal testing, or other scenarios where user self-selection is not desired.", + "default为默认设置,可单独设置每个分类的安全等级": "\"default\" est le paramètre par défaut, et chaque catégorie peut être définie séparément", + "default为默认设置,可单独设置每个模型的版本": "\"default\" est le paramètre par défaut, et chaque modèle peut être défini séparément", + "false": "faux", + "include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护": "include_obfuscation contrôle les champs d'obfuscation dans le flux Responses. Désactivé par défaut pour éviter que les clients ne désactivent cette protection de sécurité", + "inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息": "Le champ inference_geo contrôle la région de résidence des données d'inférence de Claude. Désactivé par défaut pour éviter la transmission non autorisée d'informations géographiques", + "price_xxx 的商品价格 ID,新建产品后可获得": "ID de prix du produit price_xxx, peut être obtenu après la création d'un nouveau produit", + "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "Le champ safety_identifier aide OpenAI à identifier les utilisateurs d'applications susceptibles de violer les politiques d'utilisation. Désactivé par défaut pour protéger la confidentialité des utilisateurs", + "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "Le champ service_tier est utilisé pour spécifier le niveau de service. Permettre le passage peut entraîner une facturation plus élevée que prévu. Désactivé par défaut pour éviter des frais supplémentaires", + "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "Clé secrète Stripe sk_xxx ou rk_xxx, les informations sensibles ne sont pas affichées", + "speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "Le champ speed contrôle le mode de vitesse d'inférence de Claude. Désactivé par défaut pour éviter un passage involontaire au mode fast", + "standard 已被移除,vip 用户看不到": "standard has been removed, vip users cannot see it", + "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "Le champ store autorise OpenAI à stocker les données de requête pour l'évaluation et l'optimisation du produit. Désactivé par défaut. L'activation peut causer un dysfonctionnement de Codex", + "token 会按倍率换算成“额度/次数”,请求结束后再做差额结算(补扣/返还)。": "Les tokens sont convertis en quota/nombre d'utilisations selon le ratio. Après la requête, la différence est réglée (déduction supplémentaire/remboursement).", + "true": "vrai", + "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "Clé de signature Webhook whsec_xxx, les informations sensibles ne sont pas affichées", + "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", + "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "{{count}} 项操作_many": "", + "{{count}} 项操作_one": "", + "{{count}} 项操作_other": "", + "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", + "{{label}} {{price}} / 1M tokens": "", + "{{name}} ID": "{{name}} ID", + "{{ratioType}} {{ratio}}": "{{ratioType}} {{ratio}}", + "{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x", + "| 基于": " | Basé sur ", + "© {{currentYear}}": "© {{currentYear}}", + "• 视频服务商的跨域限制": "• Des restrictions cross-origin imposées par le fournisseur vidéo", + "• 防盗链保护机制": "• Un mécanisme de protection anti-hotlink", + "• 需要特定的请求头或认证": "• Des en-têtes ou une authentification spécifiques sont requis", + "、": ", ", + "「用户可选」决定用户创建令牌时能否自主选择该分组。未勾选的分组只能由管理员在后台分配给用户,不会出现在用户的令牌创建页面中。": "\"User Selectable\" controls whether users can choose this group when creating tokens. Unchecked groups can only be assigned by admin and won't appear in the token creation page.", "一个月": "Un mois", "一天": "Un jour", "一小时": "Une heure", "一次调用消耗多少刀,优先级大于模型倍率": "Combien de USD coûte un appel, priorité sur le ratio de modèle", - "一行一个,不区分大小写": "Un mot-clé par ligne, insensible à la casse", "一行一个屏蔽词,不需要符号分割": "Un mot sensible par ligne, aucun symbole n'est requis", + "一行一个,不区分大小写": "Un mot-clé par ligne, insensible à la casse", "一键填充到 FluentRead": "Remplissage en un clic vers FluentRead", "三种操作的区别:": "Differences between the three operations:", "上一个表单块": "Bloc de formulaire précédent", @@ -342,10 +355,10 @@ "两次输入的密码不一致": "Les deux mots de passe saisis ne correspondent pas", "两次输入的密码不一致!": "Les mots de passe saisis deux fois sont incohérents !", "两步验证": "Authentification à deux facteurs", - "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "L'authentification à deux facteurs (2FA) offre une protection de sécurité supplémentaire à votre compte. Après l'avoir activée, vous devez saisir votre mot de passe et le code de vérification généré par l'application d'authentification lorsque vous vous connectez.", "两步验证启用成功!": "Authentification à deux facteurs activée avec succès !", "两步验证已禁用": "L'authentification à deux facteurs a été désactivée", "两步验证设置": "Paramètres d'authentification à deux facteurs", + "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "L'authentification à deux facteurs (2FA) offre une protection de sécurité supplémentaire à votre compte. Après l'avoir activée, vous devez saisir votre mot de passe et le code de vérification généré par l'application d'authentification lorsque vous vous connectez.", "个": " individuel", "个GPU": " GPUs", "个人中心": "Centre personnel", @@ -380,8 +393,10 @@ "为了保护账户安全,请验证您的身份。": "Pour protéger la sécurité de votre compte, veuillez vérifier votre identité.", "为什么 default 不勾选「用户可选」?": "Why is default not set as \"User Selectable\"?", "为保证匹配准确,请确保客户端直连本站点(避免反向代理/网关改写 User-Agent)。": "Pour garantir un matching précis, assurez-vous que le client se connecte directement à ce site (évitez les proxys inversés/passerelles qui réécrivent le User-Agent).", + "为模型前缀覆盖": "", "为特定用户分组配置可用分组的增减规则。「添加」为该分组新增可用分组,「移除」移除默认可用分组,「追加」直接追加分组": "Configure add/remove rules for specific user groups. \"Add\" adds a new selectable group, \"Remove\" removes a default selectable group, \"Append\" directly appends a group", "为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https": "Si vide, l'adresse du serveur est utilisée par défaut, plusieurs Origines sont séparées par des virgules, par exemple https://newapi.pro,https://newapi.com, attention ne pas inclure [], utiliser https", + "为默认价格": "", "主模型": "Modèle principal", "主页链接填": "Remplir le lien de la page d'accueil", "之前的所有日志": "Tous les journaux précédents", @@ -433,9 +448,9 @@ "令牌": "Jeton", "令牌不存在": "", "令牌分组": "Regroupement de jetons", - "令牌分组,默认为用户的分组": "Groupe de jetons, par défaut le groupe de l'utilisateur", "令牌分组设为 auto 时,按以下顺序依次尝试选择可用分组,排在前面的优先级更高": "When token group is set to auto, groups are selected in order of priority, with higher priority groups listed first", "令牌分组设为 auto 时,系统按优先级顺序自动选择一个可用分组。": "When token group is set to auto, the system automatically selects an available group by priority.", + "令牌分组,默认为用户的分组": "Groupe de jetons, par défaut le groupe de l'utilisateur", "令牌创建成功,请在列表页面点击复制获取令牌!": "Jeton créé avec succès, veuillez cliquer sur copier sur la page de liste pour obtenir le jeton !", "令牌名称": "Nom du jeton", "令牌已重置并已复制到剪贴板": "Le jeton a été réinitialisé et copié dans le presse-papiers", @@ -447,10 +462,9 @@ "以及": "et", "仪表盘设置": "Tableau de bord", "价格": "Tarifs", - "价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}": "Price: {{symbol}}{{price}} * {{ratioType}}: {{ratio}}", - "价格:${{price}} * {{ratioType}}:{{ratio}}": "Prix : ${{price}} * {{ratioType}} : {{ratio}}", "价格摘要": "Résumé des prix", "价格暂时不可用,请稍后重试": "Price temporarily unavailable, please try again later", + "价格根据用量档位和请求条件动态调整": "", "价格模式": "", "价格模式(默认)": "Mode prix (par défaut)", "价格计算中...": "Calculating price...", @@ -460,6 +474,8 @@ "价格设置方式": "Méthode de configuration des prix", "价格重新计算中...": "Recalculating price...", "价格预估": "Price Estimate", + "价格:${{price}} * {{ratioType}}:{{ratio}}": "Prix : ${{price}} * {{ratioType}} : {{ratio}}", + "价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}": "Price: {{symbol}}{{price}} * {{ratioType}}: {{ratio}}", "任一满足(OR)": "Au moins un (OR)", "任务 ID": "ID de la tâche", "任务ID": "ID de tâche", @@ -493,7 +509,6 @@ "你还没有处理{{type}}模型({{count}}个)。是否仅提交当前已勾选内容?_many": "", "你还没有处理{{type}}模型({{count}}个)。是否仅提交当前已勾选内容?_one": "", "你还没有处理{{type}}模型({{count}}个)。是否仅提交当前已勾选内容?_other": "", - "使用 {{name}} 继续": "Continuer avec {{name}}", "使用 Discord 继续": "Continuer avec Discord", "使用 GitHub 继续": "Continuer avec GitHub", "使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}": "Utiliser le format d'objet JSON, au format : {\"nom du groupe\": [nombre maximal de requêtes, nombre maximal d'achèvements de requêtes]}", @@ -502,6 +517,7 @@ "使用 Passkey 实现免密且更安全的登录体验": "Utilisez Passkey pour une expérience de connexion sans mot de passe et plus sécurisée.", "使用 Passkey 登录": "Se connecter avec Passkey", "使用 Passkey 验证": "Vérifier avec Passkey", + "使用 {{name}} 继续": "Continuer avec {{name}}", "使用 微信 继续": "Continuer avec WeChat", "使用 用户名 注册": "S'inscrire avec un nom d'utilisateur", "使用 邮箱或用户名 登录": "Connectez-vous avec votre e-mail ou votre nom d'utilisateur", @@ -511,52 +527,63 @@ "使用日志": "Journaux", "使用模式": "Mode d'utilisation", "使用统计": "Statistiques d'utilisation", - "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "Utilisez une application d'authentification (telle que Google Authenticator, Microsoft Authenticator) pour scanner le code QR ci-dessous :", "使用认证器应用扫描二维码": "Scanner le code QR avec l'application d'authentification", + "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "Utilisez une application d'authentification (telle que Google Authenticator, Microsoft Authenticator) pour scanner le code QR ci-dessous :", "使用说明": "Guide", "例如 /var/cache/new-api": "ex. : /var/cache/new-api", - "例如 €, £, Rp, ₩, ₹...": "Par exemple, €, £, Rp, ₩, ₹...", + "例如 0.5 或 2": "", + "例如 anthropic-beta": "", "例如 https://docs.newapi.pro": "Par exemple, https://docs.newapi.pro", "例如 https://example.com/api/waffo/webhook": "", "例如 https://example.com/console/topup": "", - "例如:": "Par exemple :", + "例如 service_tier": "", + "例如 €, £, Rp, ₩, ₹...": "Par exemple, €, £, Rp, ₩, ₹...", "例如: /bin/bash -c \"python app.py\"": "e.g.: /bin/bash -c \"python app.py\"", "例如: nginx:latest": "e.g.: nginx:latest", "例如: socks5://user:pass@host:port": "par exemple : socks5://user:pass@host:port", + "例如发卡网站的购买链接": "Par exemple, lien d'achat sur un site d'émission de cartes", + "例如(全渠道):": "Exemple (tous les canaux) :", + "例如(指定渠道):": "Exemple (canaux spécifiques) :", + "例如:": "Par exemple :", "例如:-c": "e.g.: -c", "例如:/bin/bash": "e.g.: /bin/bash", "例如:0001": "Par exemple : 0001", "例如:1000": "Par exemple : 1000", "例如:100000": "Ex. : 100000", - "例如:2,就是最低充值2$": "Par exemple : 2, c'est-à-dire un minimum de 2$ de recharge", "例如:2000": "Par exemple : 2000", + "例如:2,就是最低充值2$": "Par exemple : 2, c'est-à-dire un minimum de 2$ de recharge", "例如:4.99": "Ex. : 4.99", "例如:401, 403, 429, 500-599": "ex. : 401, 403, 429, 500-599", "例如:7,就是7元/美金": "Par exemple : 7, c'est-à-dire 7 yuans/dollar", "例如:Credit Card": "", + "例如:GitHub Enterprise": "ex. : GitHub Enterprise", + "例如:MER_xxx": "", + "例如:PROD_xxx": "", + "例如:STO_xxx": "", "例如:email": "ex. : email", "例如:example.com": "ex: example.com", "例如:github / si:google / https://example.com/logo.png / 🐱": "ex. : github / si:google / https://example.com/logo.png / 🐱", - "例如:GitHub Enterprise": "ex. : GitHub Enterprise", "例如:github-enterprise": "ex. : github-enterprise", "例如:gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$": "", "例如:https://example.com/.well-known/openid-configuration": "ex. : https://example.com/.well-known/openid-configuration", + "例如:https://example.com/api/waffo/webhook": "", + "例如:https://example.com/console/topup": "", "例如:https://gitea.example.com": "ex. : https://gitea.example.com", "例如:https://yourdomain.com": "Par exemple : https://yourdomain.com", "例如:name、full_name": "ex. : name, full_name", "例如:nginx:latest": "e.g.: nginx:latest", "例如:preferred_username、login": "ex. : preferred_username, login", "例如:preview": "Par exemple : preview", + "例如:price_xxx": "", "例如:prod_6I8rBerHpPxyoiU9WK4kot": "Ex. : prod_6I8rBerHpPxyoiU9WK4kot", + "例如:sk_xxx 或 rk_xxx,留空表示保持当前不变": "", "例如:sub、id、data.user.id": "ex. : sub, id, data.user.id", + "例如:whsec_xxx,留空表示保持当前不变": "", "例如:基础套餐": "Ex. : forfait de base", "例如:清理工具参数,避免上游校验错误": "", "例如:该请求不满足准入策略": "ex. : Cette requête ne satisfait pas la politique d'admission", "例如:适合轻度使用": "Ex. : Convient à un usage léger", "例如:需要等级 {{required}},你当前等级 {{current}}": "ex. : Niveau requis {{required}}, votre niveau actuel est {{current}}", - "例如(全渠道):": "Exemple (tous les canaux) :", - "例如(指定渠道):": "Exemple (canaux spécifiques) :", - "例如发卡网站的购买链接": "Par exemple, lien d'achat sur un site d'émission de cartes", "供应商": "Fournisseur", "供应商介绍": "Présentation du fournisseur", "供应商信息:": "Informations sur le fournisseur :", @@ -581,11 +608,17 @@ "保存 WeChat Server 设置": "Enregistrer les paramètres du serveur WeChat", "保存分组倍率设置": "Enregistrer les paramètres de ratio de groupe", "保存分组相关设置": "Enregistrer les paramètres liés aux groupes", + "保存后不会回显,请填写当前环境对应的 API 私钥": "", + "保存后不会回显,请填写当前环境对应的 Stripe API 密钥": "", + "保存后不会回显,请填写测试环境对应的 API 密钥": "", + "保存后不会回显,请填写测试环境对应的 API 私钥": "", + "保存后不会回显,请填写生产环境对应的 API 密钥": "", + "保存后不会回显,请填写生产环境对应的 API 私钥": "", "保存备用码": "Enregistrer les codes de sauvegarde", "保存备用码以备不时之需": "Enregistrez les codes de sauvegarde pour les urgences", "保存失败": "Échec de l'enregistrement", - "保存失败,请重试": "Échec de l'enregistrement, veuillez réessayer", "保存失败:": "Échec de l'enregistrement :", + "保存失败,请重试": "Échec de l'enregistrement, veuillez réessayer", "保存屏蔽词过滤设置": "Enregistrer les paramètres de filtrage des mots sensibles", "保存性能设置": "Enregistrer les paramètres de performance", "保存成功": "Enregistré avec succès", @@ -603,11 +636,11 @@ "保存预览": "Aperçu avant enregistrement", "保存额度设置": "Enregistrer les paramètres de quota", "保留": "kept", - "保留最近N个文件": "Conserver les N derniers fichiers", - "保留最近N天": "Conserver les N derniers jours", "保留原值(目标已有值时不覆盖)": "Conserver la valeur originale (ne pas écraser si la cible a déjà une valeur)", "保留天数": "Jours à conserver", "保留文件数": "Fichiers à conserver", + "保留最近N个文件": "Conserver les N derniers fichiers", + "保留最近N天": "Conserver les N derniers jours", "修复数据库一致性": "Réparer la cohérence de la base de données", "修改为": "Modifier en", "修改子渠道优先级": "Modifier la priorité du sous-canal", @@ -629,12 +662,12 @@ "停用": "Désactiver", "允许 AccountFilter 参数": "Autoriser le paramètre AccountFilter", "允许 HTTP 协议图片请求(适用于自部署代理)": "Autoriser les requêtes d'images via le protocole HTTP (applicable aux proxies auto-déployés)", + "允许 Turnstile 用户校验": "Autoriser la vérification des utilisateurs Turnstile", "允许 inference_geo 透传": "Autoriser la transmission de inference_geo", "允许 safety_identifier 透传": "Autoriser le passage de safety_identifier", "允许 service_tier 透传": "Autoriser le passage de service_tier", "允许 speed 透传": "Autoriser la transmission de speed", "允许 stream_options.include_obfuscation 透传": "Autoriser la transmission de stream_options.include_obfuscation", - "允许 Turnstile 用户校验": "Autoriser la vérification des utilisateurs Turnstile", "允许不安全的 Origin(HTTP)": "Autoriser une origine non sécurisée (HTTP)", "允许回调(会泄露服务器 IP 地址)": "Autoriser le rappel (divulguera l'adresse IP du serveur)", "允许在 Stripe 支付中输入促销码": "Autoriser la saisie de codes promotionnels lors du paiement Stripe", @@ -655,13 +688,14 @@ "允许重试": "Autoriser les tentatives", "元": "CNY", "充值": "Recharger", - "充值价格(x元/美金)": "Prix de recharge (x yuans/dollar)", + "充值价格必须大于 0": "", "充值价格显示": "Prix de recharge", + "充值价格(x元/美金)": "Prix de recharge (x yuans/dollar)", "充值分组倍率": "Ratio de groupe de recharge", "充值分组倍率不是合法的 JSON 字符串": "Le ratio de groupe de recharge n'est pas une chaîne JSON valide", "充值数量": "Quantité de recharge", - "充值数量,最低 ": "Quantité de recharge, minimum ", "充值数量不能小于": "Le montant de la recharge ne peut pas être inférieur à", + "充值数量,最低 ": "Quantité de recharge, minimum ", "充值方式设置": "Méthodes recharge", "充值方式设置不是合法的 JSON 字符串": "Les paramètres de la méthode de recharge ne sont pas une chaîne JSON valide", "充值确认": "Confirmation de la recharge", @@ -677,14 +711,15 @@ "兑换成功!": "Échange réussi !", "兑换码充值": "Recharge par code d'échange", "兑换码创建成功": "Code d'échange créé", - "兑换码创建成功,是否下载兑换码?": "Code d'échange créé avec succès. Voulez-vous le télécharger ?", "兑换码创建成功!": "Code d'échange créé avec succès !", + "兑换码创建成功,是否下载兑换码?": "Code d'échange créé avec succès. Voulez-vous le télécharger ?", "兑换码功能已禁用,管理员需先确认合规声明。": "Redemption codes are disabled until the administrator confirms compliance terms.", "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "Le code d'échange sera téléchargé sous forme de fichier texte, le nom de fichier étant le nom du code d'échange.", "兑换码更新成功!": "Code d'échange mis à jour avec succès !", "兑换码生成管理": "Génération de codes", "兑换码管理": "Codes d'échange", "兑换额度": "Utiliser", + "兜底档": "", "全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用": "Contrôle global des zones et des fonctions de la barre latérale, les utilisateurs ne peuvent pas activer les fonctions masquées par les administrateurs", "全局设置": "Paramètres globaux", "全选": "Tout sélectionner", @@ -730,8 +765,8 @@ "关闭": "Fermer", "关闭侧边栏": "Fermer la barre latérale", "关闭公告": "Fermer l'avis", - "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "Après fermeture, ce modèle ne sera pas automatiquement remplacé ou créé par \"Synchroniser depuis la bibliothèque de modèles officielle\"", "关闭后将不再显示此提示(仅对当前浏览器生效)。确定要关闭吗?": "Après fermeture, cet avertissement ne sera plus affiché (uniquement pour ce navigateur). Voulez-vous vraiment le fermer ?", + "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "Après fermeture, ce modèle ne sera pas automatiquement remplacé ou créé par \"Synchroniser depuis la bibliothèque de modèles officielle\"", "关闭弹窗,已停止批量测试": "Fermer la fenêtre popup, le test par lots a été arrêté", "关闭提示": "Fermer l’avertissement", "其他": "Autre", @@ -749,20 +784,6 @@ "内置": "Intégré", "内置 Ollama 镜像": "Built-in Ollama Image", "再次输入部署名称": "Enter Deployment Name Again", - "最低": "Le plus bas", - "最低充值数量": "", - "最低充值美元数量": "Montant minimum de recharge en dollars", - "最低充值美元数量必须大于 0": "Le montant minimum de recharge en dollars doit être supérieur à 0", - "最后使用时间": "Dernière utilisation", - "最后更新": "Last Updated", - "最后请求": "Dernière requête", - "最大GPU数量": "Max Number of GPUs", - "最大可用": "Max Available", - "最大条目数": "Nombre max. d'entrées", - "最终抵扣": "Déduction finale", - "最近一次": "Dernière", - "最近事件": "Recent Events", - "最高优先级": "highest priority", "写": "Écriture", "准入策略": "Politique d'admission", "准入策略 JSON(可选)": "Politique d'admission JSON (optionnel)", @@ -770,6 +791,9 @@ "准备完成初始化": "Prêt à terminer l'initialisation", "减少": "Soustraire", "凭证已刷新": "Identifiants actualisés", + "函数": "", + "分时缓存 (Claude)": "", + "分档价格表": "", "分类名称": "Nom de la catégorie", "分组": "Groupe", "分组JSON设置": "Group JSON Settings", @@ -854,20 +878,20 @@ "刷新缓存统计": "Actualiser les statistiques du cache", "刷新缓存统计失败": "Échec de l'actualisation des statistiques du cache", "刷新页面": "Recharger la page", - "前:": "Avant :", "前往 io.net API Keys": "Go to io.net API Keys", "前往设置": "Go to Settings", "前往设置页面": "Go to Settings Page", "前缀": "Préfixe", "前置": "", - "剪贴板中未检测到连接信息": "Aucune info de connexion trouvée dans le presse-papiers", - "副本数量": "Number of Replicas", + "前:": "Avant :", "剩余": "Remaining", "剩余备用码:": "Codes de sauvegarde restants : ", "剩余时间": "Remaining Time", "剩余额度": "Quota restant", - "剩余额度/总额度": "Restant/Total", "剩余额度$": "Quota restant $", + "剩余额度/总额度": "Restant/Total", + "剪贴板中未检测到连接信息": "Aucune info de connexion trouvée dans le presse-papiers", + "副本数量": "Number of Replicas", "功能特性": "Fonctionnalités", "加入渠道": "Join Channel", "加入预填组": "Rejoindre un groupe pré-rempli", @@ -888,12 +912,14 @@ "加载详情中...": "Loading details...", "加载账单失败": "Échec du chargement des factures", "加载隐私政策内容失败...": "Échec du chargement du contenu de la politique de confidentialité...", + "动态计费": "", "勾选后,该分组会出现在用户创建令牌时的下拉菜单中。未勾选的分组只能由管理员分配,用户自己无法选择。": "When checked, this group appears in the dropdown when users create tokens. Unchecked groups can only be assigned by admin.", "包含": "Contient", "包含来自未知或未标明供应商的AI模型,这些模型可能来自小型供应商或开源项目。": "Comprend des modèles d'IA de fournisseurs inconnus ou non marqués, qui peuvent provenir de petits fournisseurs ou de projets open-source.", "包括失败请求的次数,0代表不限制": "Y compris les tentatives de requête échouées, 0 signifie aucune limite", "匹配值": "Valeur de correspondance", "匹配值(可选)": "Valeur de correspondance (optionnel)", + "匹配内容": "", "匹配方式": "Méthode de correspondance", "匹配类型": "Type de correspondance", "区域": "Région", @@ -904,6 +930,7 @@ "原价": "Prix original", "原价,和普通用户一样": "original price, same as regular users", "原因:": "Raison :", + "原始额度": "", "原密码": "Mot de passe original", "原生格式": "Format natif", "原生额度": "Quota brut", @@ -939,6 +966,7 @@ "变换": "Variation", "变更": "Modification", "变焦": "Zoom", + "变量": "", "变量值": "Variable Value", "变量名": "Variable Name", "只包括请求成功的次数": "N'inclure que les tentatives de requête réussies", @@ -961,22 +989,23 @@ "可用端点类型": "Types de points de terminaison pris en charge", "可用邀请额度": "Quota d'invitation disponible", "可留空;留空时会尝试使用 Issuer URL + /.well-known/openid-configuration": "Peut être laissé vide ; si vide, tentera d'utiliser Issuer URL + /.well-known/openid-configuration", + "可空": "", "可视化": "Visualisation", "可视化倍率设置": "Ratio visuel", "可视化编辑": "Édition visuelle", - "可空": "", - "可选,公告的补充说明": "Facultatif, informations supplémentaires pour l'avis", - "可选,用于复现结果": "Optionnel, pour des résultats reproductibles", - "可选:基于用户信息 JSON 做组合条件准入,条件不满足时返回自定义提示": "Optionnel : Admission basée sur des conditions combinées à partir du JSON des informations utilisateur ; renvoie un message personnalisé lorsque les conditions ne sont pas remplies", - "可选:用于自动生成端点或 Discovery URL": "Optionnel : Utilisé pour générer automatiquement les points de terminaison ou l'URL de découverte", "可选。匹配入口请求的 User-Agent;任意一行作为子串匹配(忽略大小写)即命中。": "Optionnel. Correspondance du User-Agent de la requête entrante ; toute ligne correspondant comme sous-chaîne (insensible à la casse) est considérée comme un hit.", "可选。对提取到的亲和 Key 做正则校验;不填表示不校验。": "Optionnel. Validation regex de la clé d'affinité extraite ; laisser vide pour ignorer la validation.", "可选。对请求路径进行匹配;不填表示匹配所有路径。": "Optionnel. Correspondance du chemin de la requête ; laisser vide pour correspondre à tous les chemins.", "可选值": "Valeur facultative", + "可选,公告的补充说明": "Facultatif, informations supplémentaires pour l'avis", + "可选,用于复现结果": "Optionnel, pour des résultats reproductibles", + "可选:基于用户信息 JSON 做组合条件准入,条件不满足时返回自定义提示": "Optionnel : Admission basée sur des conditions combinées à partir du JSON des informations utilisateur ; renvoie un message personnalisé lorsque les conditions ne sont pas remplies", + "可选:用于自动生成端点或 Discovery URL": "Optionnel : Utilisé pour générer automatiquement les points de terminaison ou l'URL de découverte", "合规声明已确认": "Compliance confirmed", "合规声明确认成功": "Compliance confirmed successfully", "合计:{{total}}": "Total : {{total}}", "合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "Total : partie texte {{textTotal}} + partie audio {{audioTotal}} = {{total}}", + "同时满足": "", "同时重置消息": "Réinitialiser également les messages", "同步": "Synchroniser", "同步到渠道": "Sync to Channel", @@ -992,14 +1021,16 @@ "名称+密钥": "Nom + clé", "名称不能为空": "Le nom ne peut pas être vide", "名称匹配类型": "Type de correspondance de nom", - "后:": "Apres :", "后端固定": "", "后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。": "Ratio fixé par le backend : {{ratio}}. Ce champ affiche uniquement le prix converti.", "后端请求失败": "Échec de la requête du backend", "后缀": "Suffixe", + "后:": "Apres :", "向右展开": "Développer à droite", "向左展开": "Développer à gauche", "否": "Non", + "含时间条件": "", + "含请求条件": "", "启动": "Start", "启动参数 (Args)": "Startup Args", "启动命令": "Startup Command", @@ -1009,11 +1040,14 @@ "启动部署失败": "Failed to start deployment", "启动配置": "Startup Configuration", "启用": "Activer", + "启用 Prompt 检查": "Activer la vérification de l'invite", + "启用 Stripe 微信支付": "", + "启用 Stripe 支付宝": "", + "启用 Waffo": "", + "启用 Waffo Pancake": "", "启用 io.net 部署": "Enable io.net Deployment", "启用 io.net 部署开关": "Enable io.net Deployment Switch", "启用 io.net 部署时必须填写 API Key": "API Key is required when enabling io.net deployment", - "启用 Prompt 检查": "Activer la vérification de l'invite", - "启用 Waffo": "", "启用2FA失败": "Échec de l'activation de 2FA", "启用Claude思考适配(-thinking后缀)": "Activer l'adaptation de la pensée Claude (suffixe -thinking)", "启用FunctionCall思维签名填充": "Activer le remplissage de thoughtSignature pour FunctionCall", @@ -1049,6 +1083,7 @@ "启用验证": "Activer l'authentification", "周": "semaine", "命中判定:usage 中存在 cached tokens(例如 cached_tokens/prompt_cache_hit_tokens)即视为命中。": "Détermination de hit : La présence de cached tokens dans usage (ex. cached_tokens/prompt_cache_hit_tokens) est considérée comme un hit.", + "命中档位": "", "命中率": "Taux de hits", "命中该亲和规则后,会把此模板合并到渠道参数覆盖中(同名键由模板覆盖)。": "Lorsque cette règle d'affinité est déclenchée, le modèle est fusionné dans les remplacements de paramètres du canal (les clés homonymes sont remplacées par le modèle).", "和": "et", @@ -1075,15 +1110,15 @@ "图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ": "L'icône utilise la bibliothèque @lobehub/icons, telle que : OpenAI, Claude.Color, prend en charge les paramètres de chaîne : OpenAI.Avatar.type={'platform'}, OpenRouter.Avatar.shape={'square'}, interroger toutes les icônes disponibles s'il vous plaît ", "图标文件不能超过 100KB,请压缩后重新上传": "", "图混合": "Mélanger", + "图片/音频价格(可选)": "", "图片倍率 {{imageRatio}}": "Ratio d'image {{imageRatio}}", "图片功能在自定义请求体模式下不可用": "La fonction image n'est pas disponible en mode requête personnalisée", "图片地址": "URL de l'image", "图片已添加": "Image ajoutée", - "图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Génération d'image : 1 appel * prix unitaire {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "图片生成调用:{{symbol}}{{price}} / 1次": "Appel de génération d'image : {{symbol}}{{price}} / 1 fois", + "图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Génération d'image : 1 appel * prix unitaire {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "图片输入 {{price}} / 1M tokens": "Entrée image {{price}} / 1M tokens", "图片输入: {{imageRatio}}": "Entrée d'image : {{imageRatio}}", - "图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée d'image : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio d'image {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "图片输入价格": "Prix d'entrée image", "图片输入价格 {{symbol}}{{price}} / 1M tokens": "Prix d'entrée image {{symbol}}{{price}} / 1M tokens", "图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})": "Prix d'entrée d'image : {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (ratio d'image : {{imageRatio}})", @@ -1092,8 +1127,10 @@ "图片输入倍率": "", "图片输入倍率(仅部分模型支持该计费)": "Ratio d'entrée d'image (seulement certains modèles prennent en charge cette facturation)", "图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费": "Paramètres de ratio liés à l'entrée d'image, la clé est le nom du modèle, la valeur est le ratio, seulement certains modèles prennent en charge cette facturation", + "图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée d'image : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio d'image {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "图生文": "Décrire", "图生视频": "Générer une vidéo à partir d'une image", + "在 Stripe 后台创建价格后获得": "", "在Gotify服务器创建应用后获得的令牌,用于发送通知": "Jeton obtenu après la création d'une application sur le serveur Gotify, utilisé pour envoyer des notifications", "在Gotify服务器的应用管理中创建新应用": "Créer une nouvelle application dans la gestion des applications du serveur Gotify", "在找兑换码?": "Vous cherchez un code d'échange ? ", @@ -1122,12 +1159,12 @@ "填充新模板": "Remplir le nouveau modèle", "填充旧模板": "Remplir l'ancien modèle", "填充模板": "Remplir le modèle", + "填充模板(全渠道)": "Remplir le modèle (tous les canaux)", + "填充模板(指定渠道)": "Remplir le modèle (canaux sélectionnés)", "填充模板:等级+激活": "Remplir le modèle : Niveau + Activation", "填充模板:等级提示": "Remplir le modèle : Invite de niveau", "填充模板:组织或角色": "Remplir le modèle : Organisation ou rôle", "填充模板:组织提示": "Remplir le modèle : Invite d'organisation", - "填充模板(全渠道)": "Remplir le modèle (tous les canaux)", - "填充模板(指定渠道)": "Remplir le modèle (canaux sélectionnés)", "填入": "Remplir", "填入 CC Switch": "Remplir CC Switch", "填入所有模型": "Remplir tous les modèles", @@ -1141,7 +1178,16 @@ "填写 Issuer URL 后自动生成:": "Généré automatiquement après avoir rempli l'Issuer URL :", "填写Gotify服务器的完整URL地址": "Remplir l'adresse URL complète du serveur Gotify", "填写后会自动拼接预设端点": "Les points de terminaison prédéfinis seront automatiquement ajoutés après la saisie", + "填写后覆盖当前测试环境 API 密钥,留空表示保持当前不变": "", + "填写后覆盖当前测试环境 Webhook 公钥,留空表示保持当前不变": "", + "填写后覆盖当前测试环境私钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境 API 密钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境 Webhook 公钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境私钥,留空表示保持当前不变": "", + "填写后覆盖当前私钥,留空表示保持当前不变": "", "填写带https的域名,逗号分隔": "Saisir les domaines avec https, séparés par des virgules", + "填写测试环境 Waffo 公钥,Base64 或 PEM 内容均可": "", + "填写生产环境 Waffo 公钥,Base64 或 PEM 内容均可": "", "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "Après avoir rempli le contenu de l'accord utilisateur, les utilisateurs devront cocher avoir lu l'accord utilisateur lors de l'inscription", "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "Après avoir rempli le contenu de la politique de confidentialité, les utilisateurs devront cocher avoir lu la politique de confidentialité lors de l'inscription", "填写音频补全价格前,需要先填写音频输入价格。": "Renseignez d'abord le prix d'entrée audio avant de définir le prix de complétion audio.", @@ -1184,6 +1230,8 @@ "多密钥管理": "Gestion multi-clés", "多模型统一接入,只需将基址替换为:": "Accès unifié multi-modèles, remplacez simplement l'URL de base par : ", "多种充值方式,安全便捷": "Plusieurs méthodes de recharge, sûres et pratiques", + "大于": "", + "大于等于": "", "大模型接口网关": "API LLM Unifiée", "天": "Jour", "天前": "il y a des jours", @@ -1200,14 +1248,14 @@ "套餐标题": "Titre du plan", "套餐标题不能为空": "Le titre du forfait ne peut pas être vide", "套餐的基本信息和定价": "Informations de base et tarification du plan", - "如:大带宽批量分析图片推荐": "par exemple, Recommandations d'analyse d'images par lots à large bande passante", - "如:香港线路": "par exemple, Ligne de Hong Kong", "如向中华人民共和国境内公众提供生成式人工智能服务,你将依法履行备案登记、安全评估、内容安全、投诉举报、生成合成内容标识、日志留存、个人信息保护等合规义务;": "If you provide generative AI services to the public in mainland China, you will fulfill legal compliance obligations.", "如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。": "Si le canal d'affinité échoue, après une nouvelle tentative réussie sur un autre canal, l'affinité sera mise à jour vers le canal réussi.", "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "Si vous vous connectez à des projets de redirection One API ou New API en amont, veuillez utiliser le type OpenAI. N'utilisez pas ce type, sauf si vous savez ce que vous faites.", "如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "Si la requête de l'utilisateur contient un prompt système, utilisez ce paramètre pour le concaténer avant le prompt système de l'utilisateur", "如果镜像为私有,请填写密码或Token": "If the image is private, please fill in the password or token", "如果镜像为私有,请填写用户名": "If the image is private, please fill in the username", + "如:大带宽批量分析图片推荐": "par exemple, Recommandations d'analyse d'images par lots à large bande passante", + "如:香港线路": "par exemple, Ligne de Hong Kong", "始终使用浅色主题": "Toujours utiliser le thème clair", "始终使用深色主题": "Toujours utiliser le thème sombre", "字段映射": "Mapping des champs", @@ -1215,6 +1263,7 @@ "字段路径": "Chemin du champ", "字段透传控制": "Contrôle du passage des champs", "字段速查": "Référence rapide des champs", + "存在": "", "存在惩罚,鼓励讨论新话题": "Pénalité de présence, encourage de nouveaux sujets", "存在重复的分组名称:": "Duplicate group names: ", "存在重复的键名:": "Il existe des noms de clés en double :", @@ -1276,7 +1325,6 @@ "密钥": "Clé API", "密钥 JSON 必须包含 access_token": "Le JSON de la clé doit inclure access_token", "密钥 JSON 必须包含 account_id": "Le JSON de la clé doit inclure account_id", - "密钥(编辑模式下,保存的密钥不会显示)": "Clé (en mode édition, les clés enregistrées ne sont pas affichées)", "密钥去重": "Suppression des doublons de clés", "密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性": "La clé sera ajoutée à l'en-tête de la requête en tant que Bearer pour vérifier la légitimité de la requête webhook", "密钥已删除": "La clé a été supprimée", @@ -1294,6 +1342,7 @@ "密钥获取成功": "Acquisition de la clé réussie", "密钥输入方式": "Méthode de saisie de la clé", "密钥预览": "Aperçu de la clé", + "密钥(编辑模式下,保存的密钥不会显示)": "Clé (en mode édition, les clés enregistrées ne sont pas affichées)", "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "Pour les canaux officiels, le new-api a une adresse intégrée. Sauf s'il s'agit d'un site proxy tiers ou d'une adresse d'accès Azure spéciale, il n'est pas nécessaire de la remplir", "对免费模型启用预消耗": "Activer la préconsommation pour les modèles gratuits", "对域名启用 IP 过滤(推荐开启)": "Activer le filtrage IP pour les domaines (recommandé)", @@ -1324,6 +1373,8 @@ "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "Effacera toutes les configurations enregistrées et rétablira les paramètres par défaut. Cette opération ne peut pas être annulée. Continuer ?", "将清除选定时间之前的所有日志": "Effacera tous les journaux avant l'heure sélectionnée", "将追加 2 条规则到现有规则列表。": "2 règles seront ajoutées à la liste de règles existante.", + "小于": "", + "小于等于": "", "小时": "Heure", "小时费率": "Hourly Rate", "尚未使用": "Pas encore utilisé", @@ -1334,11 +1385,13 @@ "展开更多": "Développer plus", "展示价格": "Prix affiché", "嵌套映射:用户分组 → 使用分组 → 倍率": "Nested mapping: user group → using group → ratio", + "工具标识": "", + "工具调用定价": "", "左侧边栏个人设置": "Paramètres personnels de la barre latérale gauche", + "已为 ${count} 个渠道设置标签!": "Étiquettes définies pour ${count} canaux !", "已为 {{count}} 个模型设置{{type}}_many": "{{type}} défini pour {{count}} modèles", "已为 {{count}} 个模型设置{{type}}_one": "{{type}} défini pour {{count}} modèle", "已为 {{count}} 个模型设置{{type}}_other": "{{type}} défini pour {{count}} modèles", - "已为 ${count} 个渠道设置标签!": "Étiquettes définies pour ${count} canaux !", "已从 Discovery 自动填充配置": "Configuration remplie automatiquement depuis Discovery", "已从 Discovery 获取配置,可继续手动修改所有字段。": "Configuration récupérée depuis Discovery. Vous pouvez continuer à modifier manuellement tous les champs.", "已作废": "Invalidé", @@ -1350,10 +1403,12 @@ "已分配内存": "Mémoire allouée", "已切换为Assistant角色": "Basculé vers le rôle Assistant", "已切换为System角色": "Basculé vers le rôle Système", + "已切换到新版前端,正在刷新页面": "Passage au nouveau frontend effectué, actualisation de la page", "已切换到新版前端,正在跳转首页": "Passage au nouveau frontend effectué, redirection vers l'accueil", "已切换至最优倍率视图,每个模型使用其最低倍率分组": "Passé à la vue de ratio optimal, chaque modèle utilise son groupe de ratio le plus bas", "已初始化": "Initialisé", "已删除": "Supprimé", + "已删除 ${data} 个通道!": "${data} canaux supprimés !", "已删除 {{count}} 个令牌!": "Supprimé {{count}} jetons !", "已删除 {{count}} 个令牌!_many": "Supprimé {{count}} jetons !", "已删除 {{count}} 个令牌!_one": "Supprimé {{count}} jeton !", @@ -1361,7 +1416,6 @@ "已删除 {{count}} 条失效兑换码_many": "{{count}} codes d'échange invalides supprimés", "已删除 {{count}} 条失效兑换码_one": "{{count}} code d'échange invalide supprimé", "已删除 {{count}} 条失效兑换码_other": "{{count}} codes d'échange invalides supprimés", - "已删除 ${data} 个通道!": "${data} canaux supprimés !", "已删除所有禁用渠道,共计 ${data} 个": "Tous les canaux désactivés ont été supprimés, au total ${data}", "已删除消息及其回复": "Message et ses réponses supprimés", "已勾选": "Sélectionné", @@ -1385,8 +1439,6 @@ "已复制": "Copié", "已复制 ${count} 个模型": "${count} modèles copiés", "已复制 ID 到剪贴板": "ID copied to clipboard", - "已复制:": "Copié :", - "已复制:{{name}}": "Copié : {{name}}", "已复制全部数据": "Toutes les données copiées", "已复制到剪切板": "Copié dans le presse-papiers", "已复制到剪贴板": "Copié dans le presse-papiers", @@ -1395,6 +1447,8 @@ "已复制模型名称": "Nom du modèle copié", "已复制版本号": "Version copied", "已复制自动生成的 API Key": "Auto-generated API Key copied", + "已复制:": "Copié :", + "已复制:{{name}}": "Copié : {{name}}", "已完成": "Completed", "已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型": "La configuration tarifaire du modèle {{name}} a été appliquée à {{count}} modèles en lot", "已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型_many": "", @@ -1443,11 +1497,11 @@ "已达到购买上限": "Limite d'achat atteinte", "已过期": "Expiré", "已运行时间": "Uptime", + "已选择 ${count} 个渠道": "${count} canaux sélectionnés", "已选择 {{count}} 个模型_many": "{{count}} modèles sélectionnés", "已选择 {{count}} 个模型_one": "{{count}} modèle sélectionné", "已选择 {{count}} 个模型_other": "{{count}} modèles sélectionnés", "已选择 {{selected}} / {{total}}": "{{selected}} / {{total}} sélectionnés", - "已选择 ${count} 个渠道": "${count} canaux sélectionnés", "已重置为默认配置": "Réinitialisé à la configuration par défaut", "已销毁": "Destroyed", "币种": "Devise", @@ -1471,16 +1525,21 @@ "延长容器时长将会产生额外费用,请确认您有足够的账户余额。": "Extending container duration will incur additional charges, please ensure you have sufficient account balance.", "延长操作一旦确认无法撤销,费用将立即扣除。": "Once confirmed, the extension operation cannot be undone, and charges will be deducted immediately.", "延长时长": "Extension Duration", - "延长时长(小时)": "Extension Duration (hours)", "延长时长不能超过720小时(30天)": "Extension duration cannot exceed 720 hours (30 days)", "延长时长失败": "Failed to extend duration", "延长时长至少为1小时": "Extension duration must be at least 1 hour", + "延长时长(小时)": "Extension Duration (hours)", "建立连接时发生错误": "Erreur lors de l'établissement de la connexion", "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "Il est recommandé d'utiliser les bases de données MySQL ou PostgreSQL dans les environnements de production, ou de s'assurer que le fichier de base de données SQLite est mappé sur le stockage persistant de la machine hôte.", "开": "Ouvert", "开启「默认使用 auto 分组」后,新建令牌和初始令牌都会自动设为 auto。": "After enabling \"Default to auto group\", new tokens and initial tokens will be set to auto.", "开启之后会清除用户提示词中的": "Après l'activation, l'invite de l'utilisateur sera effacée", "开启之后将上游地址替换为服务器地址": "Après l'activation, l'adresse en amont sera remplacée par l'adresse du serveur", + "开启后不限制:必须设置模型倍率": "Après l'activation, aucune limite : le ratio de modèle doit être défini", + "开启后创建令牌默认选择auto分组,初始令牌也将设为auto": "When enabled, new tokens default to auto group, initial tokens are also set to auto", + "开启后未登录用户无法访问模型广场": "Lorsqu'il est activé, les utilisateurs non authentifiés ne peuvent pas accéder à la place du marché des modèles", + "开启后检测到新增模型会自动加入当前渠道模型列表": "", + "开启后由后端定时任务检测该渠道上游模型变化": "", "开启后,using_group 会参与 cache key(不同分组隔离)。": "Une fois activé, using_group fera partie de la clé de cache (isolation par groupe).", "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。": "Lorsque cette option est activée, conserver l'entrée d'affinité même si le canal d'affinité est désactivé ou n'est plus utilisable pour le groupe/modèle actuel. Lorsqu'elle est désactivée, l'entrée sera supprimée et un autre canal sera sélectionné.", "开启后,仅\"消费\"和\"错误\"日志将记录您的客户端IP地址": "Après l'activation, seuls les journaux \"consommation\" et \"erreur\" enregistreront votre adresse IP client", @@ -1493,11 +1552,6 @@ "开启后,规则名称会参与 cache key(不同规则隔离)。": "Une fois activé, le nom de la règle fera partie de la clé de cache (isolation par règle).", "开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)": "Une fois activé, les requêtes à Claude via ce canal ajouteront automatiquement ?beta=true (pas besoin de le passer manuellement côté client)", "开启后,违规请求将额外扣费。": "Lorsqu'il est activé, les requêtes en violation entraîneront des frais supplémentaires.", - "开启后不限制:必须设置模型倍率": "Après l'activation, aucune limite : le ratio de modèle doit être défini", - "开启后创建令牌默认选择auto分组,初始令牌也将设为auto": "When enabled, new tokens default to auto group, initial tokens are also set to auto", - "开启后未登录用户无法访问模型广场": "Lorsqu'il est activé, les utilisateurs non authentifiés ne peuvent pas accéder à la place du marché des modèles", - "开启后检测到新增模型会自动加入当前渠道模型列表": "", - "开启后由后端定时任务检测该渠道上游模型变化": "", "开启批量操作": "Activer la sélection par lots", "开始": "Début", "开始同步": "Démarrer la synchronisation", @@ -1537,6 +1591,7 @@ "当前没有未设置定价的模型": "Il n'y a actuellement aucun modèle sans prix", "当前版本": "Version actuelle", "当前状态": "Current Status", + "当前环境共用同一商户 ID": "", "当前缓存大小": "Taille actuelle du cache", "当前编辑": "En cours d'édition", "当前规则不支持写入到该位置": "La règle actuelle ne prend pas en charge l'écriture à cet emplacement", @@ -1580,6 +1635,7 @@ "总收益": "revenu total", "总计": "Total", "总额度": "Quota total", + "恢复默认": "", "您可以个性化设置侧边栏的要显示功能": "Vous pouvez personnaliser les fonctions de la barre latérale à afficher", "您可以在上方拉取需要的模型": "You can pull the required models above", "您无权访问此页面,请联系管理员": "Vous n'êtes pas autorisé à accéder à cette page. Veuillez contacter l'administrateur.", @@ -1649,22 +1705,23 @@ "拉取进度": "Pull Progress", "拒绝提示模板(可选)": "Modèle d'invite de rejet (optionnel)", "拦截原因": "Raison du blocage", + "按 1 美元对应的站内价格填写": "", "按K显示单位": "Afficher en K", "按价格设置": "Définir par prix", "按倍率类型筛选": "Filtrer par type de ratio", "按倍率设置": "Définir par ratio", "按次": "Par requête", "按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Par requête {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "按次计费": "Paiement par requête", "按次:{{symbol}}{{price}}": "Par requête : {{symbol}}{{price}}", "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "Par requête : {{symbol}}{{price}} * {{ratioType}} : {{ratio}} = {{symbol}}{{total}}", - "按次计费": "Paiement par requête", "按照如下格式输入:AccessKey|SecretAccessKey|Region": "Entrez au format : AccessKey|SecretAccessKey|Region", "按量计费": "Paiement à l'utilisation", "按量计费下需要先填写输入价格,才能保存其它价格项。": "En facturation au volume, il faut d'abord renseigner le prix d'entrée avant d'enregistrer les autres prix.", "按顺序替换content中的变量占位符": "Remplacer les espaces réservés de variable dans le contenu dans l'ordre", "换脸": "Remplacement de visage", - "授权,需在遵守": " et doit être utilisé conformément au ", "授权失败": "Échec de l'autorisation", + "授权,需在遵守": " et doit être utilisé conformément au ", "排序": "Ordre", "排队中": "En file d'attente", "接受未设置价格模型": "Accepter les modèles sans prix défini", @@ -1676,8 +1733,8 @@ "控制输出的随机性和创造性": "Contrôle l'aléatoire et la créativité de la sortie", "控制顶栏模块显示状态,全局生效": "Contrôler l'état d'affichage du module d'en-tête, effet global", "推荐": "Recommandé", - "推荐:用户可以选择是否使用指纹等验证": "Recommandé : les utilisateurs peuvent choisir d'utiliser ou non la vérification par empreinte digitale", "推荐使用(用户可选)": "Recommandé (optionnel pour l'utilisateur)", + "推荐:用户可以选择是否使用指纹等验证": "Recommandé : les utilisateurs peuvent choisir d'utiliser ou non la vérification par empreinte digitale", "描述": "Description", "提交": "Soumettre", "提交时间": "Heure de soumission", @@ -1687,6 +1744,8 @@ "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "Invite {{input}} tokens / 1M tokens * {{symbol}}{{price}}", "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Invite {{input}} tokens / 1M tokens * {{symbol}}{{price}} + Complétion {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Invite {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Cache {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + Création de cache {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + Complétion {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "提示价格:{{symbol}}{{price}} / 1M tokens": "Prix d'invite : {{symbol}}{{price}} / 1M tokens", + "提示缓存倍率": "Ratio de cache d'invite", "提示:如果切换后页面无法正常渲染,请清空浏览器缓存后重试。": "Conseil : si la page ne s’affiche pas correctement après le changement, videz le cache du navigateur puis réessayez.", "提示:如需备份数据,只需复制上述目录即可": "Astuce : pour sauvegarder les données, il suffit de copier le répertoire ci-dessus", "提示:此处配置仅用于控制「模型广场」对用户的展示效果,不会影响模型的实际调用与路由。若需配置真实调用行为,请前往「渠道管理」进行设置。": "Remarque : cette configuration n'affecte que l'affichage des modèles dans la place de marché des modèles et n'a aucun impact sur l'invocation ou le routage réels. Pour configurer le comportement réel des appels, veuillez aller dans « Gestion des canaux ».", @@ -1694,8 +1753,6 @@ "提示:该功能为测试版,未来配置结构与功能行为可能发生变更,请勿在生产环境使用。": "Remarque : cette fonctionnalité est en version bêta. La structure de configuration et le comportement peuvent changer à l’avenir. Ne l’utilisez pas en production.", "提示:语言偏好会同步到您登录的所有设备,并影响API返回的错误消息语言。": "Conseil : La préférence linguistique sera synchronisée sur tous vos appareils connectés et affectera la langue des messages d'erreur renvoyés par l'API.", "提示:链接中的{key}将被替换为API密钥,{address}将被替换为服务器地址": "Astuce : {key} dans le lien sera remplacé par la clé API, {address} sera remplacé par l'adresse du serveur", - "提示价格:{{symbol}}{{price}} / 1M tokens": "Prix d'invite : {{symbol}}{{price}} / 1M tokens", - "提示缓存倍率": "Ratio de cache d'invite", "搜索供应商": "Rechercher un fournisseur", "搜索关键字": "Rechercher des mots-clés", "搜索失败": "Search failed", @@ -1730,6 +1787,7 @@ "支付方式名称": "Nom de méthode de paiement", "支付方式名称不能为空": "", "支付方式类型": "Type de méthode de paiement", + "支付方式设置": "", "支付渠道": "Canaux de paiement", "支付设置": "Paiement", "支付请求失败": "Échec de la demande de paiement", @@ -1775,9 +1833,9 @@ "数据看板默认时间粒度": "Granularité temporelle par défaut du tableau de bord des données", "数据管理和日志查看": "Données et journaux", "文件上传": "Téléchargement de fichier", - "文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Recherche de fichier : {{count}} / 1K * prix unitaire {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "文件搜索价格:{{symbol}}{{price}} / 1K 次": "Prix de recherche de fichier : {{symbol}}{{price}} / 1K fois", "文件搜索调用 {{fileSearchCallCount}} 次": "Recherche de fichier appelée {{fileSearchCallCount}} fois", + "文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Recherche de fichier : {{count}} / 1K * prix unitaire {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "Invite texte {{input}} tokens / 1M tokens * {{symbol}}{{price}} + Complétion texte {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Prompt texte {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + Complétion texte {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + Prompt audio {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + Complétion audio {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "Invite texte {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Cache {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + Complétion texte {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", @@ -1807,9 +1865,9 @@ "新建容器部署": "Create Container Deployment", "新建数量": "Nouvelle quantité", "新建组": "Nouveau groupe", + "新格式模板": "Modèle de nouveau format", "新格式(支持条件判断与json自定义):": "Nouveau format (prend en charge les conditions et la personnalisation JSON) :", "新格式(规则 + 条件)": "Nouveau format (Règles + Conditions)", - "新格式模板": "Modèle de nouveau format", "新版本": "Nouvelle version", "新用户使用邀请码奖励额度": "Quota de bonus de code d'invitation pour nouvel utilisateur", "新用户初始额度": "Quota initial pour les nouveaux utilisateurs", @@ -1823,6 +1881,7 @@ "无加密": "Aucun chiffrement", "无效的部署信息": "Invalid deployment information", "无效的重置链接,请重新发起密码重置请求": "Lien de réinitialisation non valide, veuillez lancer une nouvelle demande de réinitialisation de mot de passe", + "无条件(兜底档)": "", "无法发起 Passkey 注册": "Impossible de lancer l'inscription Passkey", "无法复制到剪贴板,请手动复制": "Impossible de copier dans le presse-papiers, veuillez copier manuellement", "无法添加图片": "Impossible d'ajouter l'image", @@ -1847,19 +1906,22 @@ "日志类型": "Type de journal", "日志设置": "Config. journaux", "日志详情": "Détails du journal", - "旧格式(JSON 对象)": "Ancien format (Objet JSON)", - "旧格式(直接覆盖):": "Ancien format (remplacement direct) :", "旧格式必须是 JSON 对象": "L'ancien format doit être un objet JSON", "旧格式模板": "Modèle d'ancien format", + "旧格式(JSON 对象)": "Ancien format (Objet JSON)", + "旧格式(直接覆盖):": "Ancien format (remplacement direct) :", "旧版前端即将停止维护": "Le frontend classique ne sera bientôt plus maintenu", "旧的备用码已失效,请保存新的备用码": "Les anciens codes de sauvegarde ont été invalidés, veuillez enregistrer les nouveaux codes de sauvegarde", "早上好": "Bonjour", + "时区": "", "时间": "Heure", "时间信息": "Time Information", + "时间条件": "", "时间粒度": "Granularité temporelle", "易支付": "Epay", "易支付商户ID": "ID marchand Epay", "易支付商户密钥": "Clé marchand Epay", + "易支付设置": "", "是": "Oui", "是否为企业账户": "Est-ce un compte d'entreprise ?", "是否同时重置对话消息?选择\"是\"将清空所有对话记录并恢复默认示例;选择\"否\"将保留当前对话记录。": "Voulez-vous également réinitialiser les messages de conversation ? Choisir \"Oui\" effacera tous les enregistrements de conversation et restaurera les exemples par défaut ; choisir \"Non\" conservera les enregistrements de conversation actuels.", @@ -1870,16 +1932,17 @@ "是否自动禁用": "Désactiver automatiquement", "是否要求指纹/面容等生物识别": "Exiger une reconnaissance biométrique par empreinte digitale/faciale", "显示倍率": "Afficher le ratio", - "显示最新20条": "Afficher les 20 dernières", "显示名称": "Nom d'affichage", "显示名称字段(可选)": "Champ du nom d'affichage (optionnel)", "显示完整内容": "Afficher le contenu complet", "显示操作项": "Afficher les actions", "显示更多": "Afficher plus", + "显示最新20条": "Afficher les 20 dernières", "显示第": "Affichage de", "显示设置": "Paramètres d'affichage", "显示调试": "Afficher le débogage", "晚上好": "Bonsoir", + "普通按量/按次直接填价格就行;如果价格要跟请求参数或请求头联动,请切到表达式/阶梯计费。": "", "普通环境变量": "Regular Environment Variables", "普通用户": "Utilisateur normal", "普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée standard : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}", @@ -1935,9 +1998,12 @@ "更多": "Développer plus", "更多信息请参考": "Pour plus d'informations, veuillez vous référer à", "更多参数请参考": "Pour plus de paramètres, veuillez vous référer à", + "更多模板...": "", + "更好的价格,更好的稳定性,只需要将模型基址替换为:": "Meilleur prix, meilleure stabilité, aucun abonnement requis, il suffit de remplacer l'URL de BASE du modèle par : ", "更新": "Mettre à jour", "更新 Creem 设置": "Mettre à jour les paramètres Creem", "更新 Stripe 设置": "Mettre à jour les paramètres Stripe", + "更新 Waffo Pancake 设置": "", "更新 Waffo 设置": "", "更新SSRF防护设置": "Mettre à jour les paramètres de protection SSRF", "更新Worker设置": "Mettre à jour les paramètres du worker", @@ -1953,6 +2019,7 @@ "更新所有已启用通道余额": "Mettre à jour le solde de tous les canaux activés", "更新支付设置": "Mettre à jour les paramètres de paiement", "更新时间": "Heure de mise à jour", + "更新易支付设置": "", "更新服务器地址": "Mettre à jour l'adresse du serveur", "更新模型信息": "Mettre à jour les informations du modèle", "更新渠道信息": "Mettre à jour les informations du canal", @@ -1962,6 +2029,20 @@ "更新配置失败": "Failed to update configuration", "更新预填组": "Mettre à jour le groupe pré-rempli", "替换": "", + "最低": "Le plus bas", + "最低充值数量": "", + "最低充值美元数量": "Montant minimum de recharge en dollars", + "最低充值美元数量必须大于 0": "Le montant minimum de recharge en dollars doit être supérieur à 0", + "最后使用时间": "Dernière utilisation", + "最后更新": "Last Updated", + "最后请求": "Dernière requête", + "最大GPU数量": "Max Number of GPUs", + "最大可用": "Max Available", + "最大条目数": "Nombre max. d'entrées", + "最终抵扣": "Déduction finale", + "最近一次": "Dernière", + "最近事件": "Recent Events", + "最高优先级": "highest priority", "月": "mois", "有 Reasoning": "A un raisonnement", "有序字符串数组": "Ordered string array", @@ -2018,8 +2099,8 @@ "本地数据存储": "Stockage de données locales", "本地计费": "Facturation locale", "本月获得": "Ce mois-ci", - "本设备:手机指纹/面容,外接:USB安全密钥": "Intégré : empreinte digitale/visage du téléphone, Externe : clé de sécurité USB", "本设备内置": "Intégré à cet appareil", + "本设备:手机指纹/面容,外接:USB安全密钥": "Intégré : empreinte digitale/visage du téléphone, Externe : clé de sécurité USB", "本项目根据": "Ce projet est sous licence ", "机密环境变量": "Secret Environment Variables", "机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。": "Secret environment variables will be stored encrypted, suitable for storing passwords, API keys and other sensitive information.", @@ -2028,13 +2109,15 @@ "权限设置": "Paramètres d'autorisation", "条": "éléments", "条 - 第": "à", - "条,共": "sur", + "条件": "", + "条件乘数": "", "条件取反": "Inverser la condition", "条件数": "Conditions", "条件规则": "Règles de condition", "条件项设置": "Paramètres des éléments de condition", "条日志已清理!": "les journaux ont été effacés !", "条规则": "rules", + "条,共": "sur", "来源": "Source", "来源于 IO.NET 部署": "From IO.NET Deployment", "来源端点": "Point de terminaison source", @@ -2068,11 +2151,15 @@ "核采样,控制词汇选择的多样性": "Échantillonnage nucléaire, contrôle la diversité de la sélection du vocabulaire", "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。": "Selon la convention Anthropic, les tokens d'entrée de /v1/messages ne comptent que les entrées non mises en cache et excluent les tokens de lecture/écriture du cache.", "根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含": "Rechercher les métadonnées du modèle en fonction du nom du modèle et des règles de correspondance, priorité : exact > préfixe > suffixe > contient", + "格式": "", "格式化": "Formater", "格式化 JSON": "Formater le JSON", "格式正确": "Format valide", "格式示例:": "Exemple de format :", "格式错误": "Format invalide", + "档": "", + "档位": "", + "档位名称": "", "检查更新": "Vérifier les mises à jour", "检测全部渠道上游更新": "", "检测到 FluentRead(流畅阅读)": "FluentRead détecté", @@ -2093,10 +2180,10 @@ "模型价格 {{symbol}}{{price}} / 次": "Prix du modèle {{symbol}}{{price}} / requête", "模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Prix du modèle {{symbol}}{{price}} / requête * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}": "Prix du modèle {{symbol}}{{price}}, {{ratioType}} {{ratio}}", + "模型价格未配置": "Prix du modèle non configuré", "模型价格:{{symbol}}{{price}}": "Prix du modèle : {{symbol}}{{price}}", "模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "Prix du modèle : {{symbol}}{{price}} * {{ratioType}} : {{ratio}} = {{symbol}}{{total}}", "模型价格:{{symbol}}{{price}} / 次": "Prix du modèle : {{symbol}}{{price}} / requête", - "模型价格未配置": "Prix du modèle non configuré", "模型倍率": "Ratio", "模型倍率 {{modelRatio}}": "Ratio du modèle {{modelRatio}}", "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}": "Ratio du modèle {{modelRatio}}, ratio de cache {{cacheRatio}}, ratio de complétion {{completionRatio}}, {{ratioType}} {{ratio}}", @@ -2121,8 +2208,8 @@ "模型名称已存在": "Le nom du modèle existe déjà", "模型固定价格": "Prix du modèle par appel", "模型图标": "Icône du modèle", - "模型定价,需要登录访问": "Tarification du modèle, nécessite une connexion pour y accéder", "模型定价设置": "Model Pricing", + "模型定价,需要登录访问": "Tarification du modèle, nécessite une connexion pour y accéder", "模型广场": "Marché des modèles", "模型拉取失败: {{error}}": "Failed to pull model: {{error}}", "模型排行": "Classement des modèles", @@ -2132,8 +2219,8 @@ "模型更新成功!": "Modèle mis à jour avec succès !", "模型未加入列表,可能无法调用": "Le modèle n'est pas dans la liste, il peut ne pas être disponible", "模型正则": "Regex de modèle", - "模型正则(每行一个)": "Regex de modèle (un par ligne)", "模型正则不能为空": "La regex de modèle ne peut pas être vide", + "模型正则(每行一个)": "Regex de modèle (un par ligne)", "模型消耗分布": "Distribution de la consommation des modèles", "模型消耗趋势": "Tendance de la consommation des modèles", "模型版本": "Version du modèle", @@ -2167,8 +2254,8 @@ "正则替换": "", "正在加载可用部署位置...": "Loading available deployment locations...", "正在加载签到状态...": "Chargement du statut d'enregistrement...", - "正在处理,请稍候": "", "正在处理大内容...": "Traitement de contenu volumineux...", + "正在处理,请稍候": "", "正在批量处理,请稍候": "", "正在批量检测,请稍候": "", "正在提交": "Envoi en cours", @@ -2210,6 +2297,7 @@ "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "Facultatif pour les appels d'API via une adresse d'API personnalisée, n'ajoutez pas /v1 et / à la fin", "每个充值单位对应的 USD 金额,默认 1.0": "", "每个分组代表一个价格档位。管理员创建分组后,可以选择哪些档位对用户开放自选。": "Each group represents a pricing tier. After creating groups, admins can choose which tiers are open for user self-selection.", + "每个档位可设置 0~2 个条件(对 p 和 c),最后一档为兜底档无需条件。": "", "每个用户最多可创建的令牌数量,默认 1000,设置过大可能会影响性能": "Nombre maximal de jetons que chaque utilisateur peut créer, par défaut 1000. Une valeur trop élevée peut affecter les performances", "每周": "Hebdomadaire", "每天": "Quotidien", @@ -2243,8 +2331,8 @@ "注册": "S'inscrire", "注册 Passkey": "Enregistrer un Passkey", "注意": "Remarque", - "注意:JSON中重复的键只会保留最后一个同名键的值": "Remarque : Dans JSON, pour les clés dupliquées, seule la valeur de la dernière clé du même nom sera conservée", "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Remarque : Pour les API non-Chat, assurez-vous de saisir l'adresse API correcte, sinon elle pourrait ne pas fonctionner", + "注意:JSON中重复的键只会保留最后一个同名键的值": "Remarque : Dans JSON, pour les clés dupliquées, seule la valeur de la dernière clé du même nom sera conservée", "注销": "Se déconnecter", "注销成功!": "Déconnexion réussie !", "活跃文件": "Fichiers actifs", @@ -2299,6 +2387,10 @@ "添加启动命令": "Add Startup Command", "添加密钥环境变量": "Add Secret Environment Variable", "添加成功": "Ajouté avec succès", + "添加时间条件": "", + "添加更多档位": "", + "添加条件": "", + "添加条件组": "", "添加模型": "Ajouter un modèle", "添加模型区域": "Ajouter une zone de modèle", "添加渠道": "Ajouter un canal", @@ -2325,8 +2417,8 @@ "渠道 ID": "ID du Canal", "渠道ID,名称,密钥,API地址": "ID du canal, nom, clé, URL de base", "渠道亲和性": "Affinité de canal", - "渠道亲和性:上游缓存命中": "Affinité de canal : hit du cache en amont", "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。": "L'affinité de canal réutilise le dernier canal réussi en fonction des clés extraites du contexte de la requête ou du body JSON.", + "渠道亲和性:上游缓存命中": "Affinité de canal : hit du cache en amont", "渠道优先级": "Priorité du canal", "渠道信息": "Informations sur le canal", "渠道创建成功!": "Canal créé avec succès !", @@ -2349,6 +2441,7 @@ "渠道额外设置": "Paramètres supplémentaires du canal", "源地址": "Adresse source", "满足任一条件(OR)": "Satisfaire une condition quelconque (OR)", + "满足条件时,整单价格乘以 X;如果有多条同时命中,会继续相乘。": "", "演示站点": "Site de démonstration", "演示站点模式": "Mode site de démonstration", "点击 + 按钮添加图片URL进行多模态对话": "Cliquez sur + pour ajouter des URL d'images pour une conversation multimodale", @@ -2386,7 +2479,13 @@ "生成音乐": "générer de la musique", "生效": "Actif", "用于API调用的身份验证令牌,请妥善保管": "Jeton d'authentification pour les appels d'API, veuillez le conserver en lieu sûr", + "用于切换当前下单和回调校验所使用的环境": "", "用于唯一标识用户的字段路径": "Chemin du champ pour identifier les utilisateurs de manière unique", + "用于校验 Stripe Webhook 签名,保存后不会回显": "", + "用于校验测试环境的 Waffo Pancake Webhook 签名": "", + "用于校验测试环境的 Waffo 回调签名": "", + "用于校验生产环境的 Waffo Pancake Webhook 签名": "", + "用于校验生产环境的 Waffo 回调签名": "", "用于配置网络代理,支持 socks5 协议": "Utilisé pour configurer le proxy réseau, prend en charge le protocole socks5", "用于验证回调 new-api 的 webhook 请求的密钥,敏感信息不显示": "Clé utilisée pour vérifier les requêtes webhook de rappel de new-api, les informations sensibles ne sont pas affichées.", "用以支持基于 WebAuthn 的无密码登录注册": "Prise en charge de la connexion et de l'enregistrement sans mot de passe basés sur WebAuthn", @@ -2401,13 +2500,13 @@ "用以防止恶意用户利用临时邮箱批量注册": "Pour empêcher les utilisateurs malveillants d'utiliser des e-mails temporaires pour s'inscrire en masse", "用户": "Utilisateurs", "用户 ID 字段(可选)": "Champ ID utilisateur (optionnel)", + "用户ID": "", "用户个人功能": "Fonctions personnelles de l'utilisateur", "用户主页,展示系统信息": "Page d'accueil de l'utilisateur, affichant les informations système", "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "Priorité de l'utilisateur : si l'utilisateur spécifie une invite système dans la requête, le paramètre de l'utilisateur sera utilisé en premier", "用户信息": "Informations utilisateur", "用户信息更新成功!": "Informations utilisateur mises à jour avec succès !", "用户信息缺失": "Informations utilisateur manquantes", - "用户最大令牌数量": "Nombre maximal de jetons par utilisateur", "用户分组": "Votre groupe par défaut", "用户分组和额度管理": "Groupes et quotas", "用户分组的联动作用": "How user group affects other features", @@ -2417,6 +2516,7 @@ "用户协议": "Accord utilisateur", "用户协议已更新": "L'accord utilisateur a été mis à jour", "用户协议更新失败": "Échec de la mise à jour de l'accord utilisateur", + "用户单次最少可充值的美元数量": "", "用户可选": "User Selectable", "用户可选分组": "Groupes sélectionnables par l'utilisateur", "用户可选分组的名称和描述(只包含勾选了用户可选的分组)": "Names and descriptions of user-selectable groups (only includes checked groups)", @@ -2427,6 +2527,7 @@ "用户在充值页面看到的支付方式名称,例如:Credit Card": "", "用户控制面板,管理账户": "Panneau de configuration de l'utilisateur pour la gestion du compte", "用户新建令牌时可选的分组,格式为 JSON 字符串,例如:{\"vip\": \"VIP 用户\", \"test\": \"测试\"},表示用户可以选择 vip 分组和 test 分组": "Groupes sélectionnables par l'utilisateur lors de la création d'un jeton, format de chaîne JSON, par exemple : {\"vip\": \"Utilisateur VIP\", \"test\": \"Test\"}, indiquant que l'utilisateur peut sélectionner le groupe vip et le groupe test", + "用户最大令牌数量": "Nombre maximal de jetons par utilisateur", "用户每周期最多请求完成次数": "Nombre maximal de requêtes utilisateur réussies par période", "用户每周期最多请求次数": "Nombre maximal de requêtes utilisateur par période", "用户注册时看到的网站名称,比如'我的网站'": "Nom du site Web que les utilisateurs voient lors de l'inscription, par exemple 'Mon site Web'", @@ -2448,8 +2549,10 @@ "留空则使用默认端点;支持 {path, method}": "Laissez vide pour utiliser le point de terminaison par défaut ; prend en charge {path, method}", "留空则保持原有密钥": "Laisser vide pour conserver la clé existante", "留空则自动使用 服务器地址 + /api/waffo/webhook": "", + "留空则自动使用当前站点的默认充值页地址": "", "留空则自动使用当前站点的默认回调地址": "Laissez vide pour utiliser l'adresse de rappel par défaut du site actuel", "留空则默认使用服务器地址,注意不能携带http://或者https://": "Laissez vide pour utiliser l'adresse du serveur par défaut, notez que vous ne pouvez pas inclure http:// ou https://", + "留空时默认使用服务器地址作为回调地址,填写后将覆盖默认值": "", "登 录": "Se connecter", "登录": "Se connecter", "登录成功!": "Connexion réussie !", @@ -2476,7 +2579,6 @@ "硬件类型": "Hardware Type", "硬件配置": "Hardware Configuration", "确定": "OK", - "确定?": "Sûr ?", "确定删除此组?": "Confirmer la suppression de ce groupe ?", "确定导入": "Confirmer l'importation", "确定是否要修复数据库一致性?": "Êtes-vous sûr de vouloir réparer la cohérence de la base de données ?", @@ -2522,6 +2624,7 @@ "确定要降级此用户吗?": "Êtes-vous sûr de vouloir rétrograder cet utilisateur ?", "确定重置": "Confirmer la réinitialisation", "确定重置模型倍率吗?": "Confirmer la réinitialisation du ratio de modèle ?", + "确定?": "Sûr ?", "确认": "Confirmer", "确认作废": "Confirmer l'invalidation", "确认关闭提示": "Confirmer la fermeture", @@ -2529,8 +2632,8 @@ "确认切换": "Confirmer le changement", "确认删除": "Confirmer la suppression", "确认删除模型": "Confirm Delete Model", - "确认删除该分组?": "Confirm delete this group?", "确认删除该分组的所有规则?": "Delete all rules for this group?", + "确认删除该分组?": "Confirm delete this group?", "确认删除该规则?": "Confirm delete this rule?", "确认前,支付、兑换码、订阅计划和邀请返利功能将保持锁定。": "Payment, redemption codes, subscription plans, and invitation rewards remain locked until confirmation.", "确认取消密码登录": "Confirmer l'annulation de la connexion par mot de passe", @@ -2563,16 +2666,13 @@ "磁盘使用率超过此值时拒绝请求": "Rejeter les requêtes lorsque l'utilisation du disque dépasse cette valeur", "磁盘可用空间小于缓存最大总量设置": "L'espace disque disponible est inférieur au paramètre de taille maximale du cache", "磁盘命中": "Hits disque", - "磁盘缓存最大总量 (MB)": "Taille maximale du cache disque (Mo)", "磁盘缓存占用的最大空间": "Espace maximal occupé par le cache disque", "磁盘缓存已清理": "Cache disque nettoyé", + "磁盘缓存最大总量 (MB)": "Taille maximale du cache disque (Mo)", "磁盘缓存设置(磁盘换内存)": "Paramètres du cache disque (échange disque/mémoire)", "磁盘缓存阈值 (MB)": "Seuil du cache disque (Mo)", "示例": "Exemple", "示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。": "Exemple : {\"default\": [200, 100], \"vip\": [0, 1000]}.", - "视频": "Vidéo", - "视频Remix": "Remix vidéo", - "视频无法在当前浏览器中播放,这可能是由于:": "La vidéo ne peut pas être lue dans ce navigateur, cela peut être dû à :", "禁用": "Désactiver", "禁用 store 透传": "Désactiver le passage de store", "禁用2FA失败": "Échec de la désactivation de 2FA", @@ -2594,8 +2694,8 @@ "移动请求头": "", "移除": "Remove", "移除 (-:)": "Remove (-:)", - "移除 functionResponse.id 字段": "Supprimer le champ functionResponse.id", "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "La suppression de la marque de copyright de One API doit d'abord être autorisée. La maintenance du projet demande beaucoup d'efforts. Si ce projet a du sens pour vous, veuillez le soutenir activement.", + "移除 functionResponse.id 字段": "Supprimer le champ functionResponse.id", "空": "Vide", "窗口处理": "gestion des fenêtres", "窗口等待": "attente de la fenêtre", @@ -2622,25 +2722,28 @@ "第 {{line}} 条操作缺少目标路径": "Règle n°{{line}} opération manque un chemin cible", "第 {{line}} 条请求头透传格式无效": "Règle n°{{line}} format de transmission d'en-tête invalide", "第 {{line}} 条请求头透传缺少请求头名称": "Règle n°{{line}} transmission d'en-tête manque le nom de l'en-tête", + "第 {{n}} 档": "", + "第 {{n}} 组": "", "第三方支付配置": "Configuration des paiements tiers", "第三方账户绑定状态(只读)": "État de la liaison du compte tiers (lecture seule)", + "等于": "", "等价金额:": "Montant équivalent : ", "等待中": "En attente", "等待获取邮箱信息...": "En attente d'obtenir des informations par e-mail...", "筛选": "Filtre", - "签到最大额度": "Quota maximum d'enregistrement", - "签到最小额度": "Quota minimum d'enregistrement", "签到功能允许用户每日签到获取随机额度奖励": "La fonction d'enregistrement permet aux utilisateurs de s'enregistrer quotidiennement pour recevoir des récompenses de quota aléatoires", "签到失败": "Échec de l'enregistrement", "签到奖励将直接添加到您的账户余额": "Les récompenses d'enregistrement seront directement ajoutées à votre solde de compte", "签到奖励的最大额度": "Quota maximum pour les récompenses d'enregistrement", "签到奖励的最小额度": "Quota minimum pour les récompenses d'enregistrement", "签到成功!获得": "Enregistrement réussi ! Reçu", + "签到最大额度": "Quota maximum d'enregistrement", + "签到最小额度": "Quota minimum d'enregistrement", "签到设置": "Paramètres d'enregistrement", "简单来说:同一个令牌分组,不同等级的用户可以享受不同的价格。": "In short: same token group, different user tiers can enjoy different prices.", "简洁": "Simple", - "简洁模式:按 type 全量清理对象,例如 redacted_thinking。": "Mode simple : Nettoyer tous les objets par type, ex. redacted_thinking.", "简洁模式仅返回 message;状态码和错误类型将使用系统默认值。": "Le mode simple renvoie uniquement le message ; le code d'état et le type d'erreur utiliseront les valeurs par défaut du système.", + "简洁模式:按 type 全量清理对象,例如 redacted_thinking。": "Mode simple : Nettoyer tous les objets par type, ex. redacted_thinking.", "管理": "Gérer", "管理 Ollama 模型的拉取和删除": "Manage Ollama model pulling and deletion", "管理你的 LinuxDO OAuth App": "Gérer votre application OAuth LinuxDO", @@ -2650,9 +2753,11 @@ "管理员区域": "Zone administrateur", "管理员暂时未设置任何关于内容": "L'administrateur n'a encore défini aucun contenu personnalisé \"À propos\".", "管理员未开启 Creem 充值!": "L'administrateur n'a pas activé la recharge Creem !", + "管理员未开启 Waffo Pancake 充值!": "", + "管理员未开启 Waffo 充值!": "", "管理员未开启Stripe充值!": "L'administrateur n'a pas activé la recharge Stripe !", - "管理员未开启在线充值!": "L'administrateur n'a pas activé la recharge en ligne !", "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "L'administrateur n'a pas activé la fonction de recharge en ligne, veuillez contacter l'administrateur pour l'activer ou recharger avec un code d'échange.", + "管理员未开启在线充值!": "L'administrateur n'a pas activé la recharge en ligne !", "管理员未开启在线支付功能,请联系管理员配置。": "Le paiement en ligne n'est pas activé par l'administrateur. Veuillez contacter l'administrateur.", "管理员未设置用户可选分组": "L'administrateur n'a pas défini de groupes sélectionnables par l'utilisateur", "管理员给用户分配的分组(如 vip)不仅决定用户身份,还会影响后续两个功能:": "The group assigned to a user (e.g., vip) not only determines their identity, but also affects the following two features:", @@ -2696,12 +2801,12 @@ "系统管理功能": "Fonctions de gestion du système", "系统设置": "Système", "系统访问令牌": "Jeton d'accès au système", - "约": "Environ", "索引": "Index", "紧凑列表": "Liste compacte", - "纯字符串会直接覆盖整条请求头,或者点击“查看 JSON 示例”按 token 规则处理。": "", "累计签到": "Total des enregistrements", "累计获得": "Total reçu", + "约": "Environ", + "纯字符串会直接覆盖整条请求头,或者点击“查看 JSON 示例”按 token 规则处理。": "", "线路描述": "Description de l'itinéraire", "组列表": "Liste des groupes", "组名": "Nom du groupe", @@ -2731,45 +2836,48 @@ "统计次数": "Nombre de statistiques", "统计额度": "Quota statistique", "继续": "Continuer", + "缓存 Tokens": "Jetons de cache", "缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Cache {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "Cache {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (ratio : {{ratio}})", - "缓存 Tokens": "Jetons de cache", "缓存: {{cacheRatio}}": "Cache : {{cacheRatio}}", "缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Prix du cache : {{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (taux de cache : {{cacheRatio}})", "缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Prix du cache : {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (taux de cache : {{cacheRatio}})", "缓存倍率": "Ratio de cache", "缓存倍率 {{cacheRatio}}": "Ratio de cache {{cacheRatio}}", "缓存写": "Écriture cache", + "缓存创建": "", + "缓存创建 Tokens": "Jetons de création de cache", "缓存创建 {{price}} / 1M tokens": "Création de cache {{price}} / 1M tokens", "缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Création de cache {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "Création de cache {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (ratio : {{ratio}})", - "缓存创建 Tokens": "Jetons de création de cache", - "缓存创建: {{cacheCreationRatio}}": "Création de cache : {{cacheCreationRatio}}", + "缓存创建-1小时": "", "缓存创建: 1h {{cacheCreationRatio1h}}": "Création de cache : 1h {{cacheCreationRatio1h}}", "缓存创建: 5m {{cacheCreationRatio5m}}": "Création de cache : 5m {{cacheCreationRatio5m}}", "缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "Création de cache : 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}", - "缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Création du cache : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de création du cache {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "缓存创建: {{cacheCreationRatio}}": "Création de cache : {{cacheCreationRatio}}", "缓存创建价格": "Prix de création du cache d'entrée", "缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Prix de création du cache {{symbol}}{{price}} / 1M tokens", + "缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens": "Total du prix de création de cache : 5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens", "缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "Prix de création du cache : {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (taux de création de cache : {{cacheCreationRatio}})", "缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Prix de création du cache : {{symbol}}{{price}} / 1M tokens", - "缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens": "Total du prix de création de cache : 5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens", "缓存创建倍率": "Ratio de création du cache", - "缓存创建倍率 {{cacheCreationRatio}}": "Ratio de création de cache {{cacheCreationRatio}}", "缓存创建倍率 1h {{cacheCreationRatio1h}}": "Multiplicateur de création de cache 1h {{cacheCreationRatio1h}}", "缓存创建倍率 5m {{cacheCreationRatio5m}}": "Multiplicateur de création de cache 5m {{cacheCreationRatio5m}}", "缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "Ratio de création du cache 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}", + "缓存创建倍率 {{cacheCreationRatio}}": "Ratio de création de cache {{cacheCreationRatio}}", + "缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Création du cache : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de création du cache {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "缓存条目数": "Nombre d'entrées en cache", "缓存目录": "Répertoire de cache", "缓存目录磁盘空间": "Espace disque du répertoire de cache", "缓存读": "Lecture cache", "缓存读 {{price}} / 1M tokens": "Lecture du cache {{price}} / 1M tokens", - "缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Lecture du cache : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio du cache {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "缓存读取": "", "缓存读取价格": "Prix de lecture du cache d'entrée", "缓存读取价格 {{symbol}}{{price}} / 1M tokens": "Prix de lecture du cache {{symbol}}{{price}} / 1M tokens", "缓存读取价格:{{symbol}}{{price}} / 1M tokens": "Prix de lecture du cache : {{symbol}}{{price}} / 1M tokens", "缓存读取价格:{{symbol}}{{total}} / 1M tokens": "Prix de lecture du cache : {{symbol}}{{total}} / 1M tokens", "缓存读取倍率": "", + "缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Lecture du cache : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio du cache {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée en cache : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio du cache {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "编辑": "Modifier", "编辑 OAuth 提供商": "Modifier le fournisseur OAuth", @@ -2851,10 +2959,10 @@ "若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置": "Si votre fournisseur OIDC prend en charge le Discovery Endpoint, vous pouvez simplement remplir l'URL OIDC Well-Known, le système obtiendra automatiquement la configuration OIDC", "获取 Discovery 配置": "Récupérer la configuration Discovery", "获取 Discovery 配置失败:": "Échec de la récupération de la configuration Discovery : ", - "获取 io.net API Key": "Get io.net API Key", "获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确": "Échec de l'obtention de la configuration OIDC, veuillez vérifier l'état du réseau et si l'URL Well-Known est correcte", "获取 OIDC 配置成功!": "Configuration OIDC obtenue avec succès !", "获取 Ollama 版本失败": "Failed to get Ollama version", + "获取 io.net API Key": "Get io.net API Key", "获取2FA状态失败": "Échec de l'obtention de l'état 2FA", "获取令牌密钥失败": "", "获取充值配置失败": "Échec de la récupération de la configuration de recharge", @@ -2888,9 +2996,9 @@ "补全 {{completion}} tokens * 输出倍率 {{completionRatio}}": "Complétion {{completion}} tokens * Ratio de sortie {{completionRatio}}", "补全 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}": "Complétion {{completion}} tokens / 1M tokens * {{symbol}}{{price}}", "补全价格": "Prix de complétion", + "补全价格已锁定": "Le prix de complétion est verrouillé", "补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "Prix de complétion : {{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (taux de complétion : {{completionRatio}})", "补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens": "Prix de complétion : {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens", - "补全价格已锁定": "Le prix de complétion est verrouillé", "补全倍率": "Ratio de complétion", "补全倍率 {{completionRatio}}": "Ratio de complétion {{completionRatio}}", "补全倍率值": "Valeur du ratio de complétion", @@ -2899,10 +3007,15 @@ "补单成功": "Commande complétée avec succès", "表单引用错误,请刷新页面重试": "Erreur de référence de formulaire, veuillez actualiser la page et réessayer", "表格视图": "Vue tableau", + "表达式/阶梯计费": "", + "表达式编辑": "", + "表达式计费": "", + "表达式错误": "", "覆盖": "Remplacer", "覆盖模式:将完全替换现有的所有密钥": "Mode de remplacement : remplacera complètement toutes les clés existantes", "覆盖模板": "Modèle de remplacement", "覆盖现有密钥": "Remplacer les clés existantes", + "见上方动态计费详情": "", "规则": "Règle", "规则 JSON": "JSON de règle", "规则 JSON 格式不正确": "Le format JSON de la règle est incorrect", @@ -2912,6 +3025,9 @@ "规则导航": "Navigation des règles", "规则描述(可选)": "", "规则未找到,请刷新后重试": "Règle non trouvée, veuillez actualiser et réessayer", + "视频": "Vidéo", + "视频Remix": "Remix vidéo", + "视频无法在当前浏览器中播放,这可能是由于:": "La vidéo ne peut pas être lue dans ce navigateur, cela peut être dû à :", "角色": "Rôle", "解析响应数据时发生错误": "Erreur lors de l'analyse des données de réponse", "解析密钥文件失败: {{msg}}": "Échec de l'analyse du fichier de clés : {{msg}}", @@ -2920,6 +3036,9 @@ "解绑 Passkey": "Supprimer le Passkey", "解绑后将无法使用 Passkey 登录,确定要继续吗?": "Après la dissociation, vous ne pourrez plus vous connecter avec Passkey. Êtes-vous sûr de vouloir continuer ?", "解绑成功": "Dissociation réussie", + "警告": "Avertissement", + "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "Avertissement : après l'activation du keep-alive, si une erreur de canal se produit après l'écriture des données de keep-alive, le système ne peut pas réessayer. Si vous devez l'activer, il est recommandé de définir un intervalle Ping aussi grand que possible", + "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "Avertissement : la désactivation de l'authentification à deux facteurs supprimera définitivement vos paramètres de vérification et tous les codes de sauvegarde. Cette action est irréversible !", "计价币种": "Pricing Currency", "计算中": "Calculating", "计算成本": "Calculate Cost", @@ -3011,10 +3130,10 @@ "语音输入": "Entrée vocale", "语音输出": "Sortie vocale", "说明": "Description", + "说明信息": "Description", "说明:": "Description :", "说明:本页测试为非流式请求;若渠道仅支持流式返回,可能出现测试失败,请以实际使用为准。": "Remarque : les tests sur cette page utilisent des requêtes non-streaming. Si un canal ne prend en charge que les réponses en streaming, les tests peuvent échouer. Veuillez vous référer à l’usage réel.", "说明:生成结果是可直接粘贴到渠道密钥里的 JSON(包含 access_token / refresh_token / account_id)。": "Note : Le résultat généré est un JSON qui peut être collé directement dans la clé du canal (inclut access_token / refresh_token / account_id).", - "说明信息": "Description", "请上传密钥文件": "Veuillez télécharger le fichier de clé", "请上传密钥文件!": "Veuillez télécharger le fichier de clé !", "请为渠道命名": "Veuillez nommer le canal", @@ -3048,6 +3167,9 @@ "请填写完整的产品信息": "Veuillez renseigner l'ensemble des informations produit", "请填写完整的管理员账号信息": "Veuillez remplir les informations complètes du compte administrateur", "请填写密钥": "Veuillez saisir la clé", + "请填写当前环境对应的 Product ID": "", + "请填写当前环境对应的 Store ID": "", + "请填写当前环境对应的商户 ID": "", "请填写渠道名称和渠道密钥!": "Veuillez saisir le nom et la clé du canal !", "请填写部署地区": "Veuillez remplir la région de déploiement", "请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "Conservez les informations de clé en lieu sûr, ne les divulguez pas à d'autres. En cas de problèmes de sécurité, veuillez changer la clé immédiatement.", @@ -3059,16 +3181,20 @@ "请求体内存缓存": "Cache mémoire du corps de requête", "请求体磁盘缓存": "Cache disque du corps de requête", "请求体超过此大小时使用磁盘缓存": "Utiliser le cache disque lorsque le corps de la requête dépasse cette taille", + "请求参数": "", "请求参数无效": "Invalid request parameters", "请求发生错误": "Une erreur s'est produite lors de la demande", "请求发生错误: ": "Une erreur s'est produite lors de la demande : ", "请求后端接口失败:": "Échec de la requête de l'interface backend : ", "请求失败": "Échec de la demande", + "请求头": "", "请求头覆盖": "Remplacement des en-têtes de demande", "请求并计费模型": "Modèle de demande et de facturation", "请求时长: ${time}s": "Durée de la requête : ${time}s", + "请求条件调价": "", "请求次数": "Nombre de demandes", "请求结束后多退少补": "Ajuster après la fin de la demande", + "请求规则": "", "请求超时,请刷新页面后重新发起 GitHub 登录": "Délai dépassé, veuillez actualiser la page puis relancer la connexion GitHub", "请求路径": "Chemin de requête", "请求转换": "Transformation de requête", @@ -3094,18 +3220,20 @@ "请至少选择一个渠道": "Veuillez sélectionner au moins un canal", "请输入 API Key,一行一个,格式:APIKey|Region": "Saisissez une API Key par ligne, format : APIKey|Region", "请输入 API Key,格式:APIKey|Region": "Saisissez l'API Key au format : APIKey|Region", - "请输入 Authorization Endpoint": "Veuillez saisir l'Authorization Endpoint", "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "Veuillez saisir AZURE_OPENAI_ENDPOINT, par exemple : https://docs-test-001.openai.azure.com", + "请输入 Authorization Endpoint": "Veuillez saisir l'Authorization Endpoint", "请输入 Client ID": "Veuillez saisir le Client ID", "请输入 Client Secret": "Veuillez saisir le Client Secret", - "请输入 io.net API Key": "Please enter io.net API Key", - "请输入 io.net API Key(敏感信息不显示)": "Please enter io.net API Key (sensitive information not displayed)", "请输入 JSON 格式的 OAuth 凭据,例如:\n{\n \"access_token\": \"...\",\n \"account_id\": \"...\" \n}": "Veuillez saisir les identifiants OAuth au format JSON, ex. :\n{\n \"access_token\": \"...\",\n \"account_id\": \"...\" \n}", "请输入 JSON 格式的密钥内容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}": "Veuillez saisir le contenu de la clé au format JSON, par exemple :\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}", "请输入 OIDC 的 Well-Known URL": "Veuillez saisir l'URL Well-Known de l'OIDC", + "请输入 Product ID": "", "请输入 Slug": "Veuillez saisir le Slug", + "请输入 Store ID": "", "请输入 Token Endpoint": "Veuillez saisir le Token Endpoint", "请输入 User Info Endpoint": "Veuillez saisir le User Info Endpoint", + "请输入 io.net API Key": "Please enter io.net API Key", + "请输入 io.net API Key(敏感信息不显示)": "Please enter io.net API Key (sensitive information not displayed)", "请输入6位验证码或8位备用码": "Veuillez saisir le code de vérification à 6 chiffres ou le code de sauvegarde à 8 chiffres", "请输入API地址": "Veuillez saisir l'adresse de l'API", "请输入API地址!": "Veuillez saisir l'adresse de l'API !", @@ -3115,9 +3243,9 @@ "请输入Gotify服务器地址": "Veuillez saisir l'adresse du serveur Gotify", "请输入Gotify服务器地址,例如: https://gotify.example.com": "Veuillez saisir l'adresse du serveur Gotify, par exemple : https://gotify.example.com", "请输入JSON数组,如 [\"model-a\",\"model-b\"]": "Saisissez un tableau JSON, par ex. [\"model-a\",\"model-b\"]", + "请输入URL链接": "Veuillez saisir le lien URL", "请输入Uptime Kuma地址": "Veuillez saisir l'adresse Uptime Kuma", "请输入Uptime Kuma服务地址,如:https://status.example.com": "Veuillez saisir l'adresse du service Uptime Kuma, telle que : https://status.example.com", - "请输入URL链接": "Veuillez saisir le lien URL", "请输入Webhook地址": "Veuillez saisir l'adresse du Webhook", "请输入Webhook地址,例如: https://example.com/webhook": "Veuillez saisir l'URL du Webhook, par exemple : https://example.com/webhook", "请输入以下文字以确认:": "Please type the following text to confirm:", @@ -3136,6 +3264,7 @@ "请输入原密码": "Veuillez saisir le mot de passe original", "请输入原密码!": "Veuillez saisir le mot de passe original !", "请输入名称": "Veuillez saisir un nom", + "请输入商户 ID": "", "请输入回答内容": "Veuillez saisir le contenu de la réponse", "请输入回答内容(支持 Markdown/HTML)": "Veuillez saisir le contenu de la réponse (prend en charge Markdown/HTML)", "请输入图标名称": "Veuillez saisir le nom de l'icône", @@ -3143,14 +3272,14 @@ "请输入备注(仅管理员可见)": "Veuillez saisir une remarque (visible uniquement par les administrateurs)", "请输入套餐标题": "Veuillez saisir le titre du plan", "请输入完整的 JSON 格式密钥内容": "Veuillez saisir le contenu complet de la clé au format JSON", - "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Veuillez saisir l'URL complète, par exemple : https://api.openai.com/v1/chat/completions", "请输入完整的URL链接": "Veuillez saisir le lien URL complet", + "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Veuillez saisir l'URL complète, par exemple : https://api.openai.com/v1/chat/completions", "请输入容器名称": "Please enter container name", "请输入密码": "Veuillez saisir un mot de passe", "请输入密钥": "Veuillez saisir la clé", + "请输入密钥!": "Veuillez saisir la clé !", "请输入密钥,一行一个": "Veuillez saisir la clé, une par ligne", "请输入密钥,一行一个,格式:AccessKey|SecretAccessKey|Region": "Saisissez les clés une par ligne, format : AccessKey|SecretAccessKey|Region", - "请输入密钥!": "Veuillez saisir la clé !", "请输入延长时长": "Please enter extension duration", "请输入总额度": "Veuillez saisir le quota total", "请输入您的密码": "Veuillez saisir votre mot de passe", @@ -3204,9 +3333,9 @@ "请输入认证器验证码或备用码": "Veuillez saisir le code de vérification de l'authentificateur ou le code de sauvegarde", "请输入说明": "Veuillez saisir la description", "请输入运行时长": "Please enter runtime duration", - "请输入邮箱!": "Veuillez saisir votre e-mail !", "请输入邮箱地址": "Veuillez saisir l'adresse e-mail", "请输入邮箱验证码!": "Veuillez saisir le code de vérification de l'e-mail !", + "请输入邮箱!": "Veuillez saisir votre e-mail !", "请输入部署名称": "Please enter deployment name", "请输入部署名称以完成二次确认": "Enter deployment name to complete secondary confirmation", "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Veuillez saisir la région de déploiement, par exemple : us-central1\nPrend en charge l'utilisation du format de mappage de modèle\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}", @@ -3258,9 +3387,6 @@ "调用趋势": "Tendance des appels", "调试信息": "Informations de débogage", "谨慎": "Prudent", - "警告": "Avertissement", - "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "Avertissement : après l'activation du keep-alive, si une erreur de canal se produit après l'écriture des données de keep-alive, le système ne peut pas réessayer. Si vous devez l'activer, il est recommandé de définir un intervalle Ping aussi grand que possible", - "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "Avertissement : la désactivation de l'authentification à deux facteurs supprimera définitivement vos paramètres de vérification et tous les codes de sauvegarde. Cette action est irréversible !", "豆包": "Doubao", "账单": "Factures", "账户充值": "Recharge de compte", @@ -3292,6 +3418,7 @@ "跨分组": "Inter-groupes", "跨分组特殊倍率": "Cross-Group Special Ratios", "跨分组重试": "Nouvelle tentative inter-groupes", + "跨夜范围": "", "路径正则": "Regex de chemin", "路径正则(每行一个)": "Regex de chemin (un par ligne)", "跳转": "Sauter", @@ -3302,12 +3429,15 @@ "轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能": "Le mode de sondage doit être utilisé avec les fonctionnalités Redis et cache mémoire, sinon les performances seront considérablement réduites et la fonctionnalité de sondage ne pourra pas être réalisée", "软错误": "", "输入": "Entrée", - "输入 {{price}} / 1M tokens": "Entrée {{price}} / 1M tokens", "输入 $/1M tokens": "", "输入 OIDC 的 Authorization Endpoint": "Saisir le point de terminaison d'autorisation OIDC", "输入 OIDC 的 Client ID": "Saisir l'ID client OIDC", "输入 OIDC 的 Token Endpoint": "Saisir le point de terminaison de jeton OIDC", "输入 OIDC 的 Userinfo Endpoint": "Saisir le point de terminaison des informations utilisateur OIDC", + "输入 Token": "", + "输入 Token 数": "", + "输入 Token 数量,查看按当前配置的预计费用(不含分组倍率)。": "", + "输入 {{price}} / 1M tokens": "Entrée {{price}} / 1M tokens", "输入IP地址后回车,如:8.8.8.8": "Saisissez l'adresse IP et appuyez sur Entrée, par exemple : 8.8.8.8", "输入JSON对象": "Saisir l'objet JSON", "输入价格": "Prix d'entrée", @@ -3333,6 +3463,7 @@ "输入补全价格": "Saisir le prix d'achèvement", "输入补全倍率": "Saisir le ratio d'achèvement", "输入要添加的邮箱域名": "Saisir le domaine e-mail à ajouter", + "输入计费表达式...": "", "输入认证器应用显示的6位数字验证码": "Saisissez le code de vérification à 6 chiffres affiché sur l'application d'authentification", "输入邮箱地址": "Saisir l'adresse e-mail", "输入金额": "Entrer le montant", @@ -3341,15 +3472,17 @@ "输入验证码": "Saisir le code de vérification", "输入验证码完成设置": "Saisissez le code de vérification pour terminer la configuration", "输出": "Sortie", + "输出 Token": "", + "输出 Token 数": "", "输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}": "Sortie {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}}", - "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de complétion {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", - "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de sortie {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "输出价格": "Prix de sortie", "输出价格 {{symbol}}{{price}} / 1M tokens": "Prix de sortie {{symbol}}{{price}} / 1M tokens", "输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "Prix de sortie : {{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (ratio d'achèvement : {{completionRatio}})", "输出价格:{{symbol}}{{price}} / 1M tokens": "Prix de sortie : {{symbol}}{{price}} / 1M tokens", "输出价格:{{symbol}}{{total}} / 1M tokens": "Prix de sortie : {{symbol}}{{total}} / 1M tokens", "输出倍率 {{completionRatio}}": "Ratio de sortie {{completionRatio}}", + "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de complétion {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de sortie {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "边栏设置": "Barre latérale", "过期于": "Expire le", "过期时间": "Date d'expiration", @@ -3365,12 +3498,14 @@ "返回修改": "Revenir pour modifier", "返回登录": "Retour à la connexion", "返回错误": "", + "这个公式比较复杂,下面的简化表单没法完整还原,请在表达式编辑模式下修改。": "", "这个界面默认按价格填写,保存时会自动换算回后端需要的倍率 JSON。": "Cette interface utilise les prix par défaut et les reconvertit automatiquement en JSON de ratios requis par le backend lors de l'enregistrement.", "这些价格都是可选项,不填也可以。": "Tous ces prix sont optionnels et peuvent être laissés vides.", "这将删除超过 10 分钟未使用的临时缓存文件": "Cela supprimera les fichiers de cache temporaires non utilisés depuis plus de 10 minutes", "这是基础金额,实际扣费 = 基础金额 x 系统分组倍率。": "Ceci est le montant de base. Déduction réelle = montant de base × ratio de groupe système.", "这是重复键中的最后一个,其值将被使用": "Ceci est la dernière clé dupliquée, sa valeur sera utilisée", "这里直接编辑 JSON 对象。适合简单覆盖参数的场景。": "Modifier l'objet JSON directement ici. Adapté aux scénarios de remplacement simple de paramètres.", + "这里配置 Waffo 下展示给用户的 Card、Apple Pay、Google Pay 等子支付方式。": "", "进度": "calendrier", "进行中": "En cours", "进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "Lors de cette opération, cela peut entraîner des erreurs d'accès au canal. Veuillez ne l'utiliser que lorsqu'il y a un problème avec la base de données.", @@ -3427,7 +3562,9 @@ "透传请求头": "", "递归": "Récursif", "递归策略": "Stratégie récursive", + "逗号分隔的通配符模式。设置后,仅匹配的客户端 User-Agent 可使用此渠道。留空则允许所有客户端。": "", "通义千问": "Qwen", + "通用缓存": "", "通用设置": "Général", "通知": "Avis", "通知、价格和隐私相关设置": "Paramètres de notification, de prix et de confidentialité", @@ -3497,7 +3634,7 @@ "配置 Turnstile": "Configurer Turnstile", "配置 Waffo 充值时可用的支付方式,保存后在充值页面展示给用户。": "", "配置 WeChat Server": "Configurer le serveur WeChat", - "配置:": "Configuration :", + "配置各工具的调用价格($/1K次调用)。按次计费模型不额外收取工具费用。": "", "配置后的效果:": "After configuration:", "配置和消息已全部重置": "La configuration et les messages ont été entièrement réinitialisés", "配置套餐的有效时长": "Configurer la durée de validité du plan", @@ -3516,6 +3653,7 @@ "配置自定义 OAuth 提供商,支持 GitHub Enterprise、GitLab、Gitea、Nextcloud、Keycloak、ORY 等兼容 OAuth 2.0 协议的身份提供商": "Configurer des fournisseurs OAuth personnalisés, prend en charge GitHub Enterprise, GitLab, Gitea, Nextcloud, Keycloak, ORY et d'autres fournisseurs d'identité compatibles OAuth 2.0", "配置说明": "Instructions de configuration", "配置邮箱域名白名单": "Configurer la liste blanche des domaines e-mail", + "配置:": "Configuration :", "重启部署失败": "Failed to restart deployment", "重命名部署": "Rename Deployment", "重复提交": "Soumission en double", @@ -3569,6 +3707,7 @@ "镜像配置": "Image Configuration", "问题标题": "Titre de la question", "队列中": "En file d'attente", + "阶梯计费": "", "阶梯计费(未匹配到对应阶梯)": "Facturation par paliers (aucun palier correspondant)", "阶梯计费(表达式解析失败)": "Facturation par paliers (échec de l'analyse de l'expression)", "附加条件": "Conditions supplémentaires", @@ -3578,15 +3717,15 @@ "限制周期统一使用上方配置的“限制周期”值。": "La période de limite utilise uniformément la valeur \"période de limite\" configurée ci-dessus.", "限流": "Limitation de débit", "限购": "Limite", + "随机": "Aléatoire", + "随机模式": "Mode aléatoire", + "随机种子 (留空为随机)": "Graine aléatoire (laisser vide pour aléatoire)", "隐私政策": "Politique de confidentialité", "隐私政策已更新": "La politique de confidentialité a été mise à jour", "隐私政策更新失败": "Échec de la mise à jour de la politique de confidentialité", "隐私设置": "Confidentialité", "隐藏操作项": "Masquer les actions", "隐藏调试": "Masquer le débogage", - "随机": "Aléatoire", - "随机模式": "Mode aléatoire", - "随机种子 (留空为随机)": "Graine aléatoire (laisser vide pour aléatoire)", "零一万物": "Yi", "需要安全验证": "Vérification de sécurité requise", "需要添加的额度(支持负数)": "Besoin d'ajouter un quota (prend en charge les nombres négatifs)", @@ -3608,13 +3747,13 @@ "音频补全价格:{{symbol}}{{price}} / 1M tokens": "Prix de complétion audio : {{symbol}}{{price}} / 1M tokens", "音频补全倍率": "", "音频补全倍率(仅部分模型支持该计费)": "Ratio d'achèvement audio (seuls certains modèles prennent en charge cette facturation)", - "音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée audio : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio audio {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "音频输入价格": "Prix d'entrée audio", "音频输入价格:{{symbol}}{{price}} / 1M tokens": "Prix d'entrée audio : {{symbol}}{{price}} / 1M tokens", "音频输入倍率": "", "音频输入相关的倍率设置,键为模型名称,值为倍率": "Paramètres de ratio liés à l'entrée audio, la clé est le nom du modèle, la valeur est le ratio", - "音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie audio : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio audio {{audioRatio}} * ratio de complétion audio {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée audio : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio audio {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "音频输出补全相关的倍率设置,键为模型名称,值为倍率": "Paramètres de ratio liés à l'achèvement de la sortie audio, la clé est le nom du modèle, la valeur est le ratio", + "音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie audio : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio audio {{audioRatio}} * ratio de complétion audio {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "页脚": "Pied de page", "页面未找到,请检查您的浏览器地址是否正确": "Page non trouvée, veuillez vérifier si l'adresse de votre navigateur est correcte", "页面渲染出错,请刷新页面重试": "Une erreur est survenue lors du rendu de la page. Veuillez rafraîchir et réessayer.", @@ -3632,10 +3771,11 @@ "预览更新": "Mise à jour de l'aperçu", "预览模板": "Aperçu du modèle", "预览请求体": "Aperçu du corps de la requête", + "预警阈值必须为正数": "Le seuil d'alerte doit être un nombre positif", "预计结束": "Estimated End", "预计结果": "Résultat estimé", + "预计费用": "", "预设模板": "Modèle prédéfini", - "预警阈值必须为正数": "Le seuil d'alerte doit être un nombre positif", "频率惩罚,减少重复词汇的出现": "Pénalité de fréquence, réduit la répétition des mots", "频率限制的周期(分钟)": "Période de limitation de débit (minutes)", "颜色": "Couleur", @@ -3675,6 +3815,7 @@ "默认 Responses API 版本,为空则使用上方版本": "Version de l'API Responses par défaut, utilise la version ci-dessus si vide", "默认 TTL(秒)": "TTL par défaut (secondes)", "默认为 5m 缓存创建倍率;1h 缓存创建倍率按固定乘法自动计算(当前为 1.6x)": "Par défaut, le ratio de création de cache 5m est utilisé ; le ratio de création de cache 1h est calculé via une multiplication fixe (actuellement 1.6x)", + "默认使用 USD 结算": "", "默认使用auto分组": "Default to auto group", "默认使用系统名称": "Le nom du système est utilisé par défaut", "默认助手消息": "Bonjour ! Comment puis-je vous aider aujourd'hui ?", @@ -3684,6 +3825,12 @@ "默认折叠侧边栏": "Réduire la barre latérale par défaut", "默认测试模型": "Modèle de test par défaut", "默认用户消息": "Bonjour", - "默认补全倍率": "Taux de complétion par défaut" + "默认补全倍率": "Taux de complétion par défaut", + "(共 {{total}} 个)": "", + "(共 {{total}} 个,省略 {{omit}} 个)": "", + ",": ", ", + ",当前无生效订阅,将自动使用钱包": ", aucun abonnement actif, le portefeuille sera utilisé automatiquement.", + ",时间:": ", time:", + ",点击更新": ", cliquez sur Mettre à jour" } } diff --git a/web/classic/src/i18n/locales/ja.json b/web/classic/src/i18n/locales/ja.json index d071b81deca..791fc73ffe9 100644 --- a/web/classic/src/i18n/locales/ja.json +++ b/web/classic/src/i18n/locales/ja.json @@ -9,35 +9,13 @@ " 吗?": "に変更しますか?", " 秒": " 秒", " 秒。": " 秒。", - ",": ", ", - ",当前无生效订阅,将自动使用钱包": "、有効なサブスクリプションがないため、自動的にウォレットを使用します", - ",时间:": "、時間:", - ",点击更新": "、クリックして更新してください", - "、": ", ", - "(共 {{total}} 个,省略 {{omit}} 个)": "", - "(共 {{total}} 个)": "", + "$/1M tokens": "$/1M tokens", + "$/次": "", "(筛选后显示 {{count}} 条)_other": "(Showing {{count}} items after filtering)", "(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(入力 {{input}} tokens / 1M tokens * {{symbol}}{{price}}", "(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(入力 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + オーディオ入力 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}", "(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}": "(入力 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + キャッシュ {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}", "(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}": "(入力 {{nonImageInput}} tokens + 画像入力 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}", - "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "[最大リクエスト数]と[最大成功リクエスト数]の最大値は2147483647です", - "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[最大リクエスト数]は0以上、[最大成功リクエスト数]は1以上である必要があります", - "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", - "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", - "{{count}} 项操作_other": "", - "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", - "{{name}} ID": "{{name}} ID", - "{{ratioType}} {{ratio}}": "{{ratioType}} {{ratio}}", - "{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x", - "「用户可选」决定用户创建令牌时能否自主选择该分组。未勾选的分组只能由管理员在后台分配给用户,不会出现在用户的令牌创建页面中。": "「ユーザー選択可」はトークン作成時にユーザーがこのグループを選択できるかを制御します。未チェックのグループは管理者のみが割り当て可能で、トークン作成画面には表示されません。", - "• 视频服务商的跨域限制": "• Cross-origin limitations from the video provider", - "• 防盗链保护机制": "• Hotlink protection mechanisms", - "• 需要特定的请求头或认证": "• Specific headers or authentication are required", - "© {{currentYear}}": "© {{currentYear}}", - "| 基于": "| ベース: ", - "$/1M tokens": "$/1M tokens", - "$/次": "", "0 - 最低": "0 - 最低", "0 表示不限": "0 は無制限を意味します", "0.002-1之间的小数": "0.002~1の小数", @@ -47,10 +25,10 @@ "1h缓存创建 {{price}} / 1M tokens": "1h キャッシュ作成 {{price}} / 1M tokens", "1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "1h キャッシュ作成 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "1h cache creation {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (ratio: {{ratio}})", - "1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h キャッシュ作成: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 1h キャッシュ作成倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}", "1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "1h キャッシュ作成価格 {{symbol}}{{price}} / 1M tokens", "1h缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (1h缓存创建倍率: {{cacheCreationRatio1h}})": "1h cache creation price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (1h cache creation ratio: {{cacheCreationRatio1h}})", "1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "1h キャッシュ作成価格:{{symbol}}{{price}} / 1M tokens", + "1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h キャッシュ作成: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 1h キャッシュ作成倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}", "2 - 低": "2 - 低", "2. 管理员在用户管理中将用户分配到对应分组(如 vip)": "2. 管理者がユーザー管理でユーザーをグループに割り当て(例:vip)", "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "2025年5月10日以降に追加されたチャネルでは、デプロイ時にモデル名から「.」を削除する必要はなくなりました。", @@ -61,17 +39,16 @@ "5m缓存创建 {{price}} / 1M tokens": "5m キャッシュ作成 {{price}} / 1M tokens", "5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "5m キャッシュ作成 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "5m cache creation {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (ratio: {{ratio}})", - "5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "5m キャッシュ作成: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 5m キャッシュ作成倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}", "5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "5m キャッシュ作成価格 {{symbol}}{{price}} / 1M tokens", "5m缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (5m缓存创建倍率: {{cacheCreationRatio5m}})": "5m cache creation price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (5m cache creation ratio: {{cacheCreationRatio5m}})", "5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "5m キャッシュ作成価格:{{symbol}}{{price}} / 1M tokens", + "5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "5m キャッシュ作成: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 5m キャッシュ作成倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}", "8 - 高": "8 - 高", "AGPL v3.0协议": "AGPL v3.0ライセンス", "AI 对话": "AIチャット", "AI模型测试环境": "AIモデルテスト環境", "AI模型配置": "AIモデル設定", "AK/SK 模式:使用 AccessKey 和 SecretAccessKey;API Key 模式:使用 API Key": "AK/SK mode uses AccessKey and SecretAccessKey; API Key mode uses an API Key", - "anthropic-beta JSON 示例": "", "API Key": "API Key", "API Key 模式下不支持批量创建": "APIキーモードでは一括作成はサポート対象外です", "API Key 验证失败": "API Key verification failed", @@ -80,7 +57,12 @@ "API 密钥": "APIキー", "API 密钥 (沙盒)": "", "API 密钥 (生产)": "", + "API 密钥(测试环境)": "", + "API 密钥(生产环境)": "", "API 文档": "APIドキュメント", + "API 私钥": "", + "API 私钥(测试环境)": "", + "API 私钥(生产环境)": "", "API 配置": "API設定", "API令牌管理": "APIトークン管理", "API使用记录": "API利用履歴", @@ -89,13 +71,14 @@ "API地址": "ベースURL", "API渠道配置": "APIチャネル設定", "API端点": "APIエンドポイント", - "Authorization callback URL 填": "Authorization callback URLを入力してください", "Authorization Endpoint": "Authorization Endpoint", - "auto分组调用链路": "自動グループ連携", + "Authorization callback URL 填": "Authorization callback URLを入力してください", "Bark推送URL": "BarkプッシュURL", "Bark推送URL必须以http://或https://开头": "BarkプッシュURLは、http://またはhttps://で始まることが必須です", "Bark通知": "Bark通知", "Basic Auth 头": "Basic Auth ヘッダー", + "CPU 使用率超过此值时拒绝请求": "CPU使用率がこの値を超えた場合にリクエストを拒否", + "CPU 阈值 (%)": "CPUしきい値 (%)", "Cached tokens": "Cached tokens", "Cached tokens 占比口径由后端返回:Claude 语义按 cached/(prompt+cached),其余按 cached/prompt。": "キャッシュトークン比率はバックエンドから返されます:Claudeのセマンティクスはcached/(prompt+cached)、その他はcached/promptで計算されます。", "Changing batch type to:": "Changing batch type to:", @@ -110,20 +93,13 @@ "Client ID": "Client ID", "Client Secret": "Client Secret", "Codex 渠道不支持批量创建": "Codexチャネルはバッチ作成をサポートしていません", - "common.changeLanguage": "common.changeLanguage", "Completion tokens": "Completion tokens", "Configuration": "Configuration", - "context_int/context_string 从请求上下文读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "context_int/context_stringはリクエストコンテキストから読み取り、gjsonはエントリリクエストのJSON bodyからgjsonパスで読み取ります。", - "CPU 使用率超过此值时拒绝请求": "CPU使用率がこの値を超えた場合にリクエストを拒否", - "CPU 阈值 (%)": "CPUしきい値 (%)", "Creem API 密钥,敏感信息不显示": "Creem API key, sensitive information not displayed", "Creem Setting Tips": "Creem only supports preset fixed-amount products. These products and their prices need to be created and configured in advance on the Creem website, so custom dynamic amount top-ups are not supported. Configure the product name and price on Creem, obtain the Product Id, and then fill it in for the product below. Set the top-up amount and display price for this product in the new API.", "Creem 介绍": "Creem is the payment partner you always deserved, we strive for simplicity and straightforwardness on our APIs.", "Creem 充值": "Creem Recharge", "Creem 设置": "Creem Setting", - "default 和 vip 只能由管理员在「用户管理」中分配给用户。适用于按用户等级定价、内部测试等不希望用户自主选择的场景。": "default と vip は管理者が「ユーザー管理」で割り当てるのみ可能です。ユーザー等級別の料金設定やテストなど、ユーザーに自主選択させたくないシーンに適しています。", - "default为默认设置,可单独设置每个分类的安全等级": "「default」はデフォルト設定で、各分類のセキュリティレベルを個別に設定できます", - "default为默认设置,可单独设置每个模型的版本": "「default」はデフォルト設定で、各モデルのバージョンを個別に設定できます", "Dify渠道只适配chatflow和agent,并且agent不支持图片!": "Difyチャネルはchatflowとagentのみに対応しており、agentは画像のサポート対象外です", "Discord": "Discord", "Discord Client ID": "Discord Client ID", @@ -133,10 +109,11 @@ "Discovery scopes": "Discovery scopes", "Discovery 建议 scopes:": "推奨Discovery scopes:", "EUR (欧元)": "EUR (Euro)", - "false": "false", "GC 已执行": "GC実行済み", "GC 执行失败": "GC実行失敗", "GC 次数": "GC回数", + "GPU/容器": "GPU/Container", + "GPU数量": "Number of GPUs", "Gemini安全设置": "Geminiセキュリティ設定", "Gemini思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Gemini思考モード:BudgetTokens = MaxTokens * BudgetTokensの割合", "Gemini思考适配设置": "Gemini思考モード設定", @@ -151,14 +128,10 @@ "Gotify服务器地址": "GotifyサーバーURL", "Gotify服务器地址必须以http://或https://开头": "GotifyサーバーURLは、http://またはhttps://で始まることが必須です", "Gotify通知": "Gotify通知", - "GPU/容器": "GPU/Container", - "GPU数量": "Number of GPUs", "Grok设置": "Grok設定", "Haiku 模型": "Haikuモデル", "Homepage URL 填": "ホームページURLを入力してください", "ID": "ID", - "include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护": "include_obfuscationはResponsesストリームの難読化フィールドを制御します。クライアントがこのセキュリティ保護を無効にするのを防ぐため、デフォルトで無効です", - "inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息": "inference_geoフィールドはClaudeのデータ常駐推論リージョンを制御します。未承認の地理情報のパススルーを防ぐため、デフォルトで無効です", "IP": "IP", "IP白名单": "IP Whitelist", "IP白名单(支持CIDR表达式)": "IPホワイトリスト(CIDR表記に対応)", @@ -166,6 +139,7 @@ "IP黑名单": "IPブラックリスト", "JSON": "JSON", "JSON 已格式化": "JSONフォーマット済み", + "JSON 必须是对象": "", "JSON 文本": "JSONテキスト", "JSON 无效": "無効なJSON", "JSON 格式参考": "JSONリファレンス", @@ -198,6 +172,7 @@ "Ollama 模型管理": "Ollama Model Management", "Ollama 版本信息": "Ollama Version Info", "Opus 模型": "Opusモデル", + "POST 参数": "POSTパラメータ", "Passkey": "Passkey", "Passkey 已解绑": "Passkeyが連携解除されました。", "Passkey 已重置": "Passkeyがリセットされました。", @@ -208,71 +183,73 @@ "Pay Method Name": "", "Pay Method Type": "", "Ping间隔(秒)": "Ping間隔(秒)", - "POST 参数": "POSTパラメータ", - "price_xxx 的商品价格 ID,新建产品后可获得": "price_xxx の料金ID。新規製品の作成後に取得できます", + "Product ID": "", "Prompt cache hit tokens": "Prompt cache hit tokens", "Prompt tokens": "Prompt tokens", - "Reasoning Effort": "Reasoning Effort", - "Request ID": "Request ID", "RSA 私钥 (沙盒)": "", "RSA 私钥 (生产)": "", - "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "safety_identifierフィールドは、OpenAIが利用ポリシーに違反する可能性のあるアプリユーザーを特定するために使用されます。ユーザーのプライバシーを保護するため、デフォルトでは無効です", - "Scopes(可选)": "Scopes(オプション)", - "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "service_tierフィールドはサービス階層の指定に使用されます。パススルーを許可すると実際の課金額が想定を上回る場合があるため、追加料金を避けるためにデフォルトでは無効になっています", - "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "sk_xxx または rk_xxx のStripe APIキー。機密情報は表示されません", + "Reasoning Effort": "Reasoning Effort", + "Request ID": "Request ID", "SMTP 加密方式": "SMTP 暗号化方式", "SMTP 发送者邮箱": "SMTP 送信元メールアドレス", "SMTP 服务器地址": "SMTP サーバーURL", "SMTP 端口": "SMTP ポート", "SMTP 访问凭证": "SMTP 認証情報", "SMTP 账户": "SMTP アカウント", - "Sonnet 模型": "Sonnetモデル", - "speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "speed フィールドは Claude の推論速度モードを制御します。意図せず fast モードへ切り替わるのを避けるため、デフォルトで無効です", "SSE 事件": "SSEイベント", "SSE数据流": "SSEデータストリーム", "SSL/TLS": "SSL/TLS", "SSRF防护开关详细说明": "SSRF保護スイッチの詳細説明", "SSRF防护设置": "SSRF保護設定", "SSRF防护详细说明": "SSRF保護の詳細説明", - "standard 已被移除,vip 用户看不到": "standard は削除され、vipユーザーには表示されません", "STARTTLS": "STARTTLS", - "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "storeフィールドは、製品の評価と最適化のためにOpenAIがリクエストデータを保存することを許可します。デフォルトでは無効です。有効にすると、Codexが正常に利用できなくなる場合があります", + "Scopes(可选)": "Scopes(オプション)", + "Sonnet 模型": "Sonnetモデル", + "Store ID": "", "Stripe 设置": "Stripe 設定", "Stripe/Creem 商品ID(可选)": "Stripe/Creem 商品ID(任意)", "Stripe/Creem 需在第三方平台创建商品并填入 ID": "Stripe/Creem の商品は外部プラットフォームで作成し、ID を入力してください", + "TTL(秒)": "TTL(秒)", + "TTL(秒,0 表示默认)": "TTL(秒、0はデフォルト)", "Telegram": "Telegram", "Telegram Bot Token": "Telegram Bot Token", "Telegram Bot 名称": "Telegram Bot 名称", "Telegram ID": "Telegram ID", "Token Endpoint": "Token Endpoint", - "token 会按倍率换算成“额度/次数”,请求结束后再做差额结算(补扣/返还)。": "トークンは比率に基づいて「クォータ/回数」に換算されます。リクエスト完了後に差額精算(追加控除/返金)が行われます。", + "Token 估算器": "", "Total tokens": "Total tokens", - "true": "true", - "TTL(秒,0 表示默认)": "TTL(秒、0はデフォルト)", - "TTL(秒)": "TTL(秒)", "Turnstile Secret Key": "Turnstile Secret Key", "Turnstile Site Key": "Turnstile Site Key", - "Unix时间戳": "Unixタイムスタンプ", - "Uptime Kuma地址": "Uptime Kumaアドレス", - "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Uptime Kumaの監視分類管理:サービスステータス表示用に、複数の監視分類を設定できます(最大20個)", "URL 标识,只能包含小写字母、数字和连字符": "URL識別子、小文字、数字、ハイフンのみ使用可能", "URL链接": "URL", "USD (美元)": "USD (US Dollar)", + "Unix时间戳": "Unixタイムスタンプ", + "Uptime Kuma地址": "Uptime Kumaアドレス", + "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Uptime Kumaの監視分類管理:サービスステータス表示用に、複数の監視分類を設定できます(最大20個)", "User Info Endpoint": "User Info Endpoint", "User-Agent include(每行一个,可不写)": "User-Agent include(1行に1つ、オプション)", + "User-Agent 过滤": "", "Value 正则": "値の正規表現", "Vertex AI 不支持 functionResponse.id 字段,开启后将自动移除该字段": "Vertex AIはfunctionResponse.idフィールドをサポートしていません。有効にすると、このフィールドは自動的に削除されます", - "Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)": "", "Waffo API 参数,可空(最多64位)": "", + "Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)": "", + "Waffo Pancake 设置": "", "Waffo 充值": "", "Waffo 充值的最低数量,默认 1": "", "Waffo 公钥 (沙盒)": "", "Waffo 公钥 (生产)": "", + "Waffo 公钥(测试环境)": "", + "Waffo 公钥(生产环境)": "", "Waffo 商户 ID": "", + "Waffo 当前使用 USD 结算": "", "Waffo 是一个支付聚合平台,支持多种支付方式。": "", "Waffo 设置": "", - "Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web 検索: {{count}} / 1K * 単価 {{price}} * {{ratioType}} {{ratio}} = {{amount}}", + "WeChat Server 服务器地址": "WeChat Server サーバーURL", + "WeChat Server 访问凭证": "WeChatサーバー認証情報", "Web 搜索调用 {{webSearchCallCount}} 次": "Web 検索呼び出し {{webSearchCallCount}} 回", + "Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web 検索: {{count}} / 1K * 単価 {{price}} * {{ratioType}} {{ratio}} = {{amount}}", + "Webhook 公钥(测试环境)": "", + "Webhook 公钥(生产环境)": "", "Webhook 密钥": "Webhook Secret", "Webhook 签名密钥": "Webhook署名シークレット", "Webhook地址": "Webhook URL", @@ -280,19 +257,55 @@ "Webhook请求结构说明": "Webhookリクエスト構造の説明", "Webhook通知": "Webhook通知", "Web搜索价格:{{symbol}}{{price}} / 1K 次": "Web検索料金:{{symbol}}{{price}} / 1K回", - "WeChat Server 服务器地址": "WeChat Server サーバーURL", - "WeChat Server 访问凭证": "WeChatサーバー認証情報", "Well-Known URL": "Well-Known URL", "Well-Known URL 必须以 http:// 或 https:// 开头": "Well-Known URLは、http://またはhttps://で始まることが必須です", - "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "whsec_xxx のWebhook署名シークレット。機密情報は表示されません", "Worker地址": "Workerアドレス", "Worker密钥": "WorkerAPIキー", + "X 也可以小于 1,当折扣用。想做\"只给输出加价\"或\"额外加固定费用\",请直接写完整计费公式。": "", + "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "[最大リクエスト数]と[最大成功リクエスト数]の最大値は2147483647です", + "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[最大リクエスト数]は0以上、[最大成功リクエスト数]は1以上である必要があります", + "anthropic-beta JSON 示例": "", + "auto分组调用链路": "自動グループ連携", + "common.changeLanguage": "common.changeLanguage", + "context_int/context_string 从请求上下文读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "context_int/context_stringはリクエストコンテキストから読み取り、gjsonはエントリリクエストのJSON bodyからgjsonパスで読み取ります。", + "context_int/context_string 从请求上下文读取;header 从 HTTP 请求头读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "", + "default 和 vip 只能由管理员在「用户管理」中分配给用户。适用于按用户等级定价、内部测试等不希望用户自主选择的场景。": "default と vip は管理者が「ユーザー管理」で割り当てるのみ可能です。ユーザー等級別の料金設定やテストなど、ユーザーに自主選択させたくないシーンに適しています。", + "default为默认设置,可单独设置每个分类的安全等级": "「default」はデフォルト設定で、各分類のセキュリティレベルを個別に設定できます", + "default为默认设置,可单独设置每个模型的版本": "「default」はデフォルト設定で、各モデルのバージョンを個別に設定できます", + "false": "false", + "include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护": "include_obfuscationはResponsesストリームの難読化フィールドを制御します。クライアントがこのセキュリティ保護を無効にするのを防ぐため、デフォルトで無効です", + "inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息": "inference_geoフィールドはClaudeのデータ常駐推論リージョンを制御します。未承認の地理情報のパススルーを防ぐため、デフォルトで無効です", + "price_xxx 的商品价格 ID,新建产品后可获得": "price_xxx の料金ID。新規製品の作成後に取得できます", + "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "safety_identifierフィールドは、OpenAIが利用ポリシーに違反する可能性のあるアプリユーザーを特定するために使用されます。ユーザーのプライバシーを保護するため、デフォルトでは無効です", + "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "service_tierフィールドはサービス階層の指定に使用されます。パススルーを許可すると実際の課金額が想定を上回る場合があるため、追加料金を避けるためにデフォルトでは無効になっています", + "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "sk_xxx または rk_xxx のStripe APIキー。機密情報は表示されません", + "speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "speed フィールドは Claude の推論速度モードを制御します。意図せず fast モードへ切り替わるのを避けるため、デフォルトで無効です", + "standard 已被移除,vip 用户看不到": "standard は削除され、vipユーザーには表示されません", + "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "storeフィールドは、製品の評価と最適化のためにOpenAIがリクエストデータを保存することを許可します。デフォルトでは無効です。有効にすると、Codexが正常に利用できなくなる場合があります", + "token 会按倍率换算成“额度/次数”,请求结束后再做差额结算(补扣/返还)。": "トークンは比率に基づいて「クォータ/回数」に換算されます。リクエスト完了後に差額精算(追加控除/返金)が行われます。", + "true": "true", + "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "whsec_xxx のWebhook署名シークレット。機密情報は表示されません", + "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", + "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "{{count}} 项操作_other": "", + "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", + "{{label}} {{price}} / 1M tokens": "", + "{{name}} ID": "{{name}} ID", + "{{ratioType}} {{ratio}}": "{{ratioType}} {{ratio}}", + "{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x", + "| 基于": "| ベース: ", + "© {{currentYear}}": "© {{currentYear}}", + "• 视频服务商的跨域限制": "• Cross-origin limitations from the video provider", + "• 防盗链保护机制": "• Hotlink protection mechanisms", + "• 需要特定的请求头或认证": "• Specific headers or authentication are required", + "、": ", ", + "「用户可选」决定用户创建令牌时能否自主选择该分组。未勾选的分组只能由管理员在后台分配给用户,不会出现在用户的令牌创建页面中。": "「ユーザー選択可」はトークン作成時にユーザーがこのグループを選択できるかを制御します。未チェックのグループは管理者のみが割り当て可能で、トークン作成画面には表示されません。", "一个月": "1ヶ月", "一天": "1日", "一小时": "1時間", "一次调用消耗多少刀,优先级大于模型倍率": "1コールあたりの消費ドル額。モデル倍率より優先されます", - "一行一个,不区分大小写": "1行に1つずつ(大文字・小文字の区別なし)", "一行一个屏蔽词,不需要符号分割": "NGワードを1行に1つずつ入力してください。記号による区切りは不要です", + "一行一个,不区分大小写": "1行に1つずつ(大文字・小文字の区別なし)", "一键填充到 FluentRead": "FluentReadにクイック入力", "三种操作的区别:": "3つの操作の違い:", "上一个表单块": "前のフォームブロック", @@ -336,10 +349,10 @@ "两次输入的密码不一致": "パスワードが一致しません", "两次输入的密码不一致!": "パスワードが一致しません", "两步验证": "2要素認証", - "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "2要素認証(2FA)は、アカウントのセキュリティを強化する追加の保護機能です。有効にすると、ログイン時にパスワードと、認証アプリによって生成された認証コードの入力が必要になります。", "两步验证启用成功!": "2要素認証の有効化に成功しました", "两步验证已禁用": "2要素認証は無効になっています", "两步验证设置": "2要素認証設定", + "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "2要素認証(2FA)は、アカウントのセキュリティを強化する追加の保護機能です。有効にすると、ログイン時にパスワードと、認証アプリによって生成された認証コードの入力が必要になります。", "个": "個", "个GPU": " GPUs", "个人中心": "アカウント", @@ -374,8 +387,10 @@ "为了保护账户安全,请验证您的身份。": "アカウント保護のため、本人確認を行ってください。", "为什么 default 不勾选「用户可选」?": "なぜdefaultに「ユーザー選択可」をチェックしないのか?", "为保证匹配准确,请确保客户端直连本站点(避免反向代理/网关改写 User-Agent)。": "正確なマッチングを保証するため、クライアントがこのサイトに直接接続していることを確認してください(リバースプロキシ/ゲートウェイによるUser-Agentの書き換えを避けてください)。", + "为模型前缀覆盖": "", "为特定用户分组配置可用分组的增减规则。「添加」为该分组新增可用分组,「移除」移除默认可用分组,「追加」直接追加分组": "特定のユーザーグループに対して利用可能グループの追加・削除ルールを設定します。「追加」は新しいグループを追加、「削除」はデフォルトのグループを削除、「付加」は直接付加します", "为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https": "空欄の場合は、デフォルトのサーバーURLが使用されます。複数のオリジンはカンマで区切ってください(例:https://newapi.pro,https://newapi.com)。ご注意:[]は含めず、httpsを使用してください。", + "为默认价格": "", "主模型": "メインモデル", "主页链接填": "ホームページURLを入力してください。", "之前的所有日志": "これまでのすべてのログ", @@ -427,9 +442,9 @@ "令牌": "トークン", "令牌不存在": "", "令牌分组": "トークングループ", - "令牌分组,默认为用户的分组": "トークングループ、デフォルトはユーザーのグループ", "令牌分组设为 auto 时,按以下顺序依次尝试选择可用分组,排在前面的优先级更高": "トークングループがautoの場合、以下の順序で利用可能なグループを選択します。上位のグループが優先されます", "令牌分组设为 auto 时,系统按优先级顺序自动选择一个可用分组。": "トークングループがautoの場合、システムは優先順位に従って利用可能なグループを自動選択します。", + "令牌分组,默认为用户的分组": "トークングループ、デフォルトはユーザーのグループ", "令牌创建成功,请在列表页面点击复制获取令牌!": "トークンの作成に成功しました。リストページでコピーをクリックしてトークンを取得してください", "令牌名称": "トークン名", "令牌已重置并已复制到剪贴板": "トークンはリセットされ、クリップボードにコピーされました", @@ -441,10 +456,9 @@ "以及": "および", "仪表盘设置": "ダッシュボード", "价格": "料金", - "价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}": "Price: {{symbol}}{{price}} * {{ratioType}}: {{ratio}}", - "价格:${{price}} * {{ratioType}}:{{ratio}}": "料金:${{price}} * {{ratioType}}:{{ratio}}", "价格摘要": "価格概要", "价格暂时不可用,请稍后重试": "Price temporarily unavailable, please try again later", + "价格根据用量档位和请求条件动态调整": "", "价格模式": "", "价格模式(默认)": "価格モード(デフォルト)", "价格计算中...": "Calculating price...", @@ -454,6 +468,8 @@ "价格设置方式": "料金設定方法", "价格重新计算中...": "Recalculating price...", "价格预估": "Price Estimate", + "价格:${{price}} * {{ratioType}}:{{ratio}}": "料金:${{price}} * {{ratioType}}:{{ratio}}", + "价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}": "Price: {{symbol}}{{price}} * {{ratioType}}: {{ratio}}", "任一满足(OR)": "いずれか一致(OR)", "任务 ID": "タスクID", "任务ID": "タスクID", @@ -485,7 +501,6 @@ "你理解并自行承担部署、运营和收费行为产生的法律责任。": "You understand and independently bear legal responsibility arising from deployment, operation, and charging behavior.", "你理解本合规提醒仅用于风险提示,不构成法律意见、合规审查结论或对你使用本系统行为合法性的保证;你应根据实际业务场景自行咨询专业法律或合规顾问。": "You understand this compliance reminder is only for risk notice and does not constitute legal advice, a compliance review conclusion, or a guarantee of the legality of your use of this system; you should consult professional legal or compliance advisors based on your actual business scenario.", "你还没有处理{{type}}模型({{count}}个)。是否仅提交当前已勾选内容?_other": "", - "使用 {{name}} 继续": "{{name}}で続行", "使用 Discord 继续": "Continue with Discord", "使用 GitHub 继续": "GitHubでログイン", "使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}": "JSONオブジェクト形式で入力してください。形式:{\"グループ名\": [最大リクエスト数, 最大成功リクエスト数]}", @@ -494,6 +509,7 @@ "使用 Passkey 实现免密且更安全的登录体验": "Passkeyで、より安全なパスワードレスログインを実現。", "使用 Passkey 登录": "Passkeyでログイン", "使用 Passkey 验证": "Passkeyで認証", + "使用 {{name}} 继续": "{{name}}で続行", "使用 微信 继续": "WeChatでログイン", "使用 用户名 注册": "ユーザー名でサインアップ", "使用 邮箱或用户名 登录": "メールアドレスまたはユーザー名でログイン", @@ -503,52 +519,63 @@ "使用日志": "利用履歴", "使用模式": "利用モード", "使用统计": "利用統計", - "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "Google Authenticator、Microsoft Authenticatorなどの認証アプリで、以下のQRコードをスキャンしてください:", "使用认证器应用扫描二维码": " 認証アプリスキャン", + "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "Google Authenticator、Microsoft Authenticatorなどの認証アプリで、以下のQRコードをスキャンしてください:", "使用说明": "ガイド", "例如 /var/cache/new-api": "例:/var/cache/new-api", - "例如 €, £, Rp, ₩, ₹...": "例:€, £, Rp, ₩, ₹...", + "例如 0.5 或 2": "", + "例如 anthropic-beta": "", "例如 https://docs.newapi.pro": "例:https://docs.newapi.pro", "例如 https://example.com/api/waffo/webhook": "", "例如 https://example.com/console/topup": "", - "例如:": "例:", + "例如 service_tier": "", + "例如 €, £, Rp, ₩, ₹...": "例:€, £, Rp, ₩, ₹...", "例如: /bin/bash -c \"python app.py\"": "e.g.: /bin/bash -c \"python app.py\"", "例如: nginx:latest": "e.g.: nginx:latest", "例如: socks5://user:pass@host:port": "(例:socks5://user:pass@host:port)", + "例如发卡网站的购买链接": "例:カード発行サイトの購入リンク", + "例如(全渠道):": "例(全チャネル):", + "例如(指定渠道):": "例(指定チャネル):", + "例如:": "例:", "例如:-c": "e.g.: -c", "例如:/bin/bash": "e.g.: /bin/bash", "例如:0001": "例:0001", "例如:1000": "例:1000", "例如:100000": "e.g.: 100000", - "例如:2,就是最低充值2$": "例:2(最低チャージ額$2)", "例如:2000": "例:2000", + "例如:2,就是最低充值2$": "例:2(最低チャージ額$2)", "例如:4.99": "e.g.: 4.99", "例如:401, 403, 429, 500-599": "例:401, 403, 429, 500-599", "例如:7,就是7元/美金": "例:7(1USDあたり7CNY)", "例如:Credit Card": "", + "例如:GitHub Enterprise": "例:GitHub Enterprise", + "例如:MER_xxx": "", + "例如:PROD_xxx": "", + "例如:STO_xxx": "", "例如:email": "例:email", "例如:example.com": "例:example.com", "例如:github / si:google / https://example.com/logo.png / 🐱": "例:github / si:google / https://example.com/logo.png / 🐱", - "例如:GitHub Enterprise": "例:GitHub Enterprise", "例如:github-enterprise": "例:github-enterprise", "例如:gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$": "", "例如:https://example.com/.well-known/openid-configuration": "例:https://example.com/.well-known/openid-configuration", + "例如:https://example.com/api/waffo/webhook": "", + "例如:https://example.com/console/topup": "", "例如:https://gitea.example.com": "例:https://gitea.example.com", "例如:https://yourdomain.com": "例:https://yourdomain.com", "例如:name、full_name": "例:name、full_name", "例如:nginx:latest": "e.g.: nginx:latest", "例如:preferred_username、login": "例:preferred_username、login", "例如:preview": "例:preview", + "例如:price_xxx": "", "例如:prod_6I8rBerHpPxyoiU9WK4kot": "e.g.: prod_6I8rBerHpPxyoiU9WK4kot", + "例如:sk_xxx 或 rk_xxx,留空表示保持当前不变": "", "例如:sub、id、data.user.id": "例:sub、id、data.user.id", + "例如:whsec_xxx,留空表示保持当前不变": "", "例如:基础套餐": "e.g.: Basic Package", "例如:清理工具参数,避免上游校验错误": "", "例如:该请求不满足准入策略": "例:このリクエストはアドミッションポリシーを満たしていません", "例如:适合轻度使用": "例:軽めの利用に最適", "例如:需要等级 {{required}},你当前等级 {{current}}": "例:レベル{{required}}が必要です。現在のレベルは{{current}}です", - "例如(全渠道):": "例(全チャネル):", - "例如(指定渠道):": "例(指定チャネル):", - "例如发卡网站的购买链接": "例:カード発行サイトの購入リンク", "供应商": "プロバイダー", "供应商介绍": "プロバイダー紹介", "供应商信息:": "プロバイダー情報:", @@ -573,11 +600,17 @@ "保存 WeChat Server 设置": "WeChatサーバー設定を保存", "保存分组倍率设置": "グループ倍率設定を保存", "保存分组相关设置": "グループ関連設定を保存", + "保存后不会回显,请填写当前环境对应的 API 私钥": "", + "保存后不会回显,请填写当前环境对应的 Stripe API 密钥": "", + "保存后不会回显,请填写测试环境对应的 API 密钥": "", + "保存后不会回显,请填写测试环境对应的 API 私钥": "", + "保存后不会回显,请填写生产环境对应的 API 密钥": "", + "保存后不会回显,请填写生产环境对应的 API 私钥": "", "保存备用码": "バックアップコード", "保存备用码以备不时之需": "万一に備え保存", "保存失败": "保存に失敗しました", - "保存失败,请重试": "保存に失敗しました。再試行してください", "保存失败:": "保存に失敗しました:", + "保存失败,请重试": "保存に失敗しました。再試行してください", "保存屏蔽词过滤设置": "NGワードフィルタリング設定を保存", "保存性能设置": "パフォーマンス設定を保存", "保存成功": "保存に成功しました", @@ -595,11 +628,11 @@ "保存预览": "保存プレビュー", "保存额度设置": "クォータ設定を保存", "保留": "保持", - "保留最近N个文件": "最新のN個のファイルを保持", - "保留最近N天": "最新のN日間を保持", "保留原值(目标已有值时不覆盖)": "元の値を保持(ターゲットに既に値がある場合は上書きしない)", "保留天数": "保持日数", "保留文件数": "保持ファイル数", + "保留最近N个文件": "最新のN個のファイルを保持", + "保留最近N天": "最新のN日間を保持", "修复数据库一致性": "データベースの整合性を修復", "修改为": "変更先:", "修改子渠道优先级": "サブチャネルの優先度を変更", @@ -621,12 +654,12 @@ "停用": "無効", "允许 AccountFilter 参数": "AccountFilterパラメータを許可する", "允许 HTTP 协议图片请求(适用于自部署代理)": "HTTPプロトコルによる画像リクエストを許可する(セルフホストプロキシ向け)", + "允许 Turnstile 用户校验": "Turnstileによるユーザー検証を許可する", "允许 inference_geo 透传": "inference_geoパススルーを許可", "允许 safety_identifier 透传": "safety_identifierのパススルーを許可する", "允许 service_tier 透传": "service_tierのパススルーを許可する", "允许 speed 透传": "speed パススルーを許可", "允许 stream_options.include_obfuscation 透传": "stream_options.include_obfuscationパススルーを許可", - "允许 Turnstile 用户校验": "Turnstileによるユーザー検証を許可する", "允许不安全的 Origin(HTTP)": "安全でないオリジン(HTTP)を許可する", "允许回调(会泄露服务器 IP 地址)": "コールバックを許可する(サーバーIPアドレスが漏洩します)", "允许在 Stripe 支付中输入促销码": "Stripe決済でのプロモーションコード入力を許可する", @@ -647,13 +680,14 @@ "允许重试": "リトライを許可", "元": "CNY", "充值": "チャージ", - "充值价格(x元/美金)": "チャージ料金(x CNY/USD)", + "充值价格必须大于 0": "", "充值价格显示": "チャージ料金表示", + "充值价格(x元/美金)": "チャージ料金(x CNY/USD)", "充值分组倍率": "チャージグループ倍率", "充值分组倍率不是合法的 JSON 字符串": "チャージグループ倍率は有効なJSON文字列ではありません", "充值数量": "チャージ額", - "充值数量,最低 ": "チャージ額、最低", "充值数量不能小于": "チャージ額は次を下回れません:", + "充值数量,最低 ": "チャージ額、最低", "充值方式设置": "チャージ方法設定", "充值方式设置不是合法的 JSON 字符串": "チャージ方法設定は有効なJSON文字列ではありません", "充值确认": "チャージの確認", @@ -669,14 +703,15 @@ "兑换成功!": "引き換えに成功しました", "兑换码充值": "引き換えコードによるチャージ", "兑换码创建成功": "引き換えコードの作成に成功しました", - "兑换码创建成功,是否下载兑换码?": "引き換えコードの作成に成功しました。ダウンロードしますか?", "兑换码创建成功!": "引き換えコードの作成に成功しました", + "兑换码创建成功,是否下载兑换码?": "引き換えコードの作成に成功しました。ダウンロードしますか?", "兑换码功能已禁用,管理员需先确认合规声明。": "Redemption codes are disabled until the administrator confirms compliance terms.", "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "引き換えコードはテキストファイルとしてダウンロードされ、ファイル名は引き換えコードの名称になります。", "兑换码更新成功!": "引き換えコードの更新に成功しました", "兑换码生成管理": "引き換えコード生成管理", "兑换码管理": "引き換えコード", "兑换额度": "引き換え", + "兜底档": "", "全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用": "サイドバーの表示項目と機能をグローバルに制御します。管理者が非表示にした機能は、ユーザーは有効にできません", "全局设置": "グローバル設定", "全选": "すべて選択", @@ -717,8 +752,8 @@ "关闭": "閉じる", "关闭侧边栏": "サイドバー折りたたみ", "关闭公告": "お知らせを閉じる", - "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "オフにすると、このモデルは「公式から同期」機能によって自動的に上書き・作成されなくなります", "关闭后将不再显示此提示(仅对当前浏览器生效)。确定要关闭吗?": "閉じると、このお知らせは今後表示されません(このブラウザのみ)。閉じてもよろしいですか?", + "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "オフにすると、このモデルは「公式から同期」機能によって自動的に上書き・作成されなくなります", "关闭弹窗,已停止批量测试": "ポップアップを閉じたため、一括テストは停止されました", "关闭提示": "お知らせを閉じる", "其他": "その他", @@ -736,20 +771,6 @@ "内置": "組み込み", "内置 Ollama 镜像": "Built-in Ollama Image", "再次输入部署名称": "Enter Deployment Name Again", - "最低": "最低", - "最低充值数量": "", - "最低充值美元数量": "最低チャージUSD額", - "最低充值美元数量必须大于 0": "最低チャージUSD額は 0 より大きい必要があります", - "最后使用时间": "最終利用日時", - "最后更新": "Last Updated", - "最后请求": "最終リクエスト日時", - "最大GPU数量": "Max Number of GPUs", - "最大可用": "Max Available", - "最大条目数": "最大エントリ数", - "最终抵扣": "最終控除", - "最近一次": "最新", - "最近事件": "Recent Events", - "最高优先级": "最高優先", "写": "書込", "准入策略": "アドミッションポリシー", "准入策略 JSON(可选)": "アドミッションポリシー JSON(オプション)", @@ -757,6 +778,9 @@ "准备完成初始化": "初期化準備完了", "减少": "減少", "凭证已刷新": "資格情報が更新されました", + "函数": "", + "分时缓存 (Claude)": "", + "分档价格表": "", "分类名称": "分類名称", "分组": "グループ", "分组JSON设置": "グループJSON設定", @@ -841,20 +865,20 @@ "刷新缓存统计": "キャッシュ統計を更新", "刷新缓存统计失败": "キャッシュ統計の更新に失敗しました", "刷新页面": "ページを更新", - "前:": "前:", "前往 io.net API Keys": "Go to io.net API Keys", "前往设置": "Go to Settings", "前往设置页面": "Go to Settings Page", "前缀": "プレフィックス", "前置": "", - "剪贴板中未检测到连接信息": "クリップボードに接続情報が見つかりません", - "副本数量": "Number of Replicas", + "前:": "前:", "剩余": "Remaining", "剩余备用码:": "残りバックアップコード:", "剩余时间": "Remaining Time", "剩余额度": "残りクォータ", - "剩余额度/总额度": "残りクォータ/総クォータ", "剩余额度$": "残高 ($)", + "剩余额度/总额度": "残りクォータ/総クォータ", + "剪贴板中未检测到连接信息": "クリップボードに接続情報が見つかりません", + "副本数量": "Number of Replicas", "功能特性": "機能", "加入渠道": "Join Channel", "加入预填组": "事前入力グループへの参加", @@ -875,12 +899,14 @@ "加载详情中...": "Loading details...", "加载账单失败": "請求情報の読み込みに失敗しました", "加载隐私政策内容失败...": "プライバシーポリシーのコンテンツの読み込みに失敗しました", + "动态计费": "", "勾选后,该分组会出现在用户创建令牌时的下拉菜单中。未勾选的分组只能由管理员分配,用户自己无法选择。": "チェックすると、このグループはトークン作成時のドロップダウンに表示されます。未チェックのグループは管理者のみが割り当て可能です。", "包含": "含む", "包含来自未知或未标明供应商的AI模型,这些模型可能来自小型供应商或开源项目。": "プロバイダーが不明または明記されていないAIモデルが含まれています。これらのモデルは、小規模なプロバイダーやオープンソースプロジェクト由来の場合があります。", "包括失败请求的次数,0代表不限制": "失敗したリクエストの回数を含みます。0は無制限を意味します", "匹配值": "マッチ値", "匹配值(可选)": "マッチ値(オプション)", + "匹配内容": "", "匹配方式": "マッチ方法", "匹配类型": "マッチングタイプ", "区域": "リージョン", @@ -891,6 +917,7 @@ "原价": "定価", "原价,和普通用户一样": "定価、一般ユーザーと同じ", "原因:": "原因:", + "原始额度": "", "原密码": "現在のパスワード", "原生格式": "ネイティブ形式", "原生额度": "生クォータ", @@ -926,6 +953,7 @@ "变换": "バリエーション", "变更": "変更", "变焦": "ズーム", + "变量": "", "变量值": "Variable Value", "变量名": "Variable Name", "只包括请求成功的次数": "成功したリクエストの回数のみを含みます", @@ -948,22 +976,23 @@ "可用端点类型": "利用可能なエンドポイントタイプ", "可用邀请额度": "利用可能な招待クォータ", "可留空;留空时会尝试使用 Issuer URL + /.well-known/openid-configuration": "空欄可。空欄の場合、Issuer URL + /.well-known/openid-configurationの使用を試みます", + "可空": "", "可视化": "可視化", "可视化倍率设置": "倍率設定の可視化", "可视化编辑": "ビジュアル編集", - "可空": "", - "可选,公告的补充说明": "(オプション)お知らせの補足説明", - "可选,用于复现结果": "オプション、結果の再現用", - "可选:基于用户信息 JSON 做组合条件准入,条件不满足时返回自定义提示": "オプション:ユーザー情報JSONに基づく複合条件アドミッション。条件が満たされない場合、カスタムメッセージを返します", - "可选:用于自动生成端点或 Discovery URL": "オプション:エンドポイントまたはDiscovery URLの自動生成に使用", "可选。匹配入口请求的 User-Agent;任意一行作为子串匹配(忽略大小写)即命中。": "オプション。入力リクエストのUser-Agentをマッチ。いずれかの行がサブストリングとして一致(大文字小文字無視)すればヒットです。", "可选。对提取到的亲和 Key 做正则校验;不填表示不校验。": "オプション。抽出されたアフィニティキーを正規表現で検証。空欄は検証をスキップします。", "可选。对请求路径进行匹配;不填表示匹配所有路径。": "オプション。リクエストパスをマッチ。空欄はすべてのパスにマッチします。", "可选值": "選択可能な値", + "可选,公告的补充说明": "(オプション)お知らせの補足説明", + "可选,用于复现结果": "オプション、結果の再現用", + "可选:基于用户信息 JSON 做组合条件准入,条件不满足时返回自定义提示": "オプション:ユーザー情報JSONに基づく複合条件アドミッション。条件が満たされない場合、カスタムメッセージを返します", + "可选:用于自动生成端点或 Discovery URL": "オプション:エンドポイントまたはDiscovery URLの自動生成に使用", "合规声明已确认": "Compliance confirmed", "合规声明确认成功": "Compliance confirmed successfully", "合计:{{total}}": "合計: {{total}}", "合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "合計: テキスト部分 {{textTotal}} + 音声部分 {{audioTotal}} = {{total}}", + "同时满足": "", "同时重置消息": "メッセージも同時にリセット", "同步": "同期", "同步到渠道": "Sync to Channel", @@ -979,14 +1008,16 @@ "名称+密钥": "名称+APIキー", "名称不能为空": "名称は空にできません", "名称匹配类型": "名称マッチングタイプ", - "后:": "後:", "后端固定": "", "后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。": "バックエンド固定倍率: {{ratio}}。この項目は変換後の価格表示のみです。", "后端请求失败": "バックエンドリクエストに失敗しました", "后缀": "サフィックス", + "后:": "後:", "向右展开": "右に展開", "向左展开": "左に展開", "否": "いいえ", + "含时间条件": "", + "含请求条件": "", "启动": "Start", "启动参数 (Args)": "Startup Args", "启动命令": "Startup Command", @@ -996,11 +1027,14 @@ "启动部署失败": "Failed to start deployment", "启动配置": "Startup Configuration", "启用": "有効にする", + "启用 Prompt 检查": "プロンプトチェックを有効にする", + "启用 Stripe 微信支付": "", + "启用 Stripe 支付宝": "", + "启用 Waffo": "", + "启用 Waffo Pancake": "", "启用 io.net 部署": "Enable io.net Deployment", "启用 io.net 部署开关": "Enable io.net Deployment Switch", "启用 io.net 部署时必须填写 API Key": "API Key is required when enabling io.net deployment", - "启用 Prompt 检查": "プロンプトチェックを有効にする", - "启用 Waffo": "", "启用2FA失败": "2要素認証の有効化に失敗しました", "启用Claude思考适配(-thinking后缀)": "Claude思考モードを有効にする(-thinkingサフィックス)", "启用FunctionCall思维签名填充": "FunctionCall用のthoughtSignature自動付与を有効化", @@ -1036,6 +1070,7 @@ "启用验证": "認証を有効にする", "周": "週", "命中判定:usage 中存在 cached tokens(例如 cached_tokens/prompt_cache_hit_tokens)即视为命中。": "ヒット判定:usageにキャッシュトークン(例:cached_tokens/prompt_cache_hit_tokens)が存在する場合、ヒットとみなされます。", + "命中档位": "", "命中率": "ヒット率", "命中该亲和规则后,会把此模板合并到渠道参数覆盖中(同名键由模板覆盖)。": "このアフィニティルールがヒットすると、テンプレートがチャネルパラメータオーバーライドにマージされます(同名キーはテンプレートで上書きされます)。", "和": "と", @@ -1062,15 +1097,15 @@ "图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ": "アイコンは `@lobehub/icons` ライブラリを使用しています(例:OpenAI、Claude.Color)。`OpenAI.Avatar.type={'platform'}`、`OpenRouter.Avatar.shape={'square'}` のようなチェーンパラメータに対応しています。利用可能なすべてのアイコンは、こちらで確認できます。", "图标文件不能超过 100KB,请压缩后重新上传": "", "图混合": "Blend", + "图片/音频价格(可选)": "", "图片倍率 {{imageRatio}}": "画像倍率 {{imageRatio}}", "图片功能在自定义请求体模式下不可用": "カスタムリクエストモードでは画像機能は利用できません", "图片地址": "画像URL", "图片已添加": "画像が追加されました", - "图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "画像生成: 1 回 * 単価 {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "图片生成调用:{{symbol}}{{price}} / 1次": "画像生成APIコール:{{symbol}}{{price}} / 1回", + "图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "画像生成: 1 回 * 単価 {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "图片输入 {{price}} / 1M tokens": "画像入力 {{price}} / 1M tokens", "图片输入: {{imageRatio}}": "画像入力:{{imageRatio}}", - "图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "画像入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 画像倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "图片输入价格": "画像入力価格", "图片输入价格 {{symbol}}{{price}} / 1M tokens": "画像入力価格 {{symbol}}{{price}} / 1M tokens", "图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})": "画像入力料金:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens(画像倍率:{{imageRatio}})", @@ -1079,8 +1114,10 @@ "图片输入倍率": "", "图片输入倍率(仅部分模型支持该计费)": "画像入力倍率(一部のモデルのみこの課金に対応)", "图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费": "画像入力に関する倍率設定です。キー:モデル名、値:倍率。この課金方法は一部のモデルのみ対応しています", + "图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "画像入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 画像倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "图生文": "ディスクライブ", "图生视频": "画像からの動画生成", + "在 Stripe 后台创建价格后获得": "", "在Gotify服务器创建应用后获得的令牌,用于发送通知": "Gotifyサーバーでアプリを作成後に取得する、通知送信用トークンです。", "在Gotify服务器的应用管理中创建新应用": "Gotifyサーバーのアプリ管理で新規アプリを作成します。", "在找兑换码?": "引き換えコードをお探しですか?", @@ -1109,12 +1146,12 @@ "填充新模板": "新しいテンプレートを入力", "填充旧模板": "古いテンプレートを入力", "填充模板": "テンプレートを入力", + "填充模板(全渠道)": "テンプレートを入力(全チャネル)", + "填充模板(指定渠道)": "テンプレートを入力(指定チャネル)", "填充模板:等级+激活": "テンプレート入力:レベル+アクティベーション", "填充模板:等级提示": "テンプレート入力:レベルプロンプト", "填充模板:组织或角色": "テンプレート入力:組織またはロール", "填充模板:组织提示": "テンプレート入力:組織プロンプト", - "填充模板(全渠道)": "テンプレートを入力(全チャネル)", - "填充模板(指定渠道)": "テンプレートを入力(指定チャネル)", "填入": "入力", "填入 CC Switch": "CC Switchを入力", "填入所有模型": "すべてのモデルを入力", @@ -1128,7 +1165,16 @@ "填写 Issuer URL 后自动生成:": "Issuer URL入力後に自動生成:", "填写Gotify服务器的完整URL地址": "Gotifyサーバーの完全なURLを入力してください", "填写后会自动拼接预设端点": "入力後にプリセットエンドポイントが自動的に追加されます", + "填写后覆盖当前测试环境 API 密钥,留空表示保持当前不变": "", + "填写后覆盖当前测试环境 Webhook 公钥,留空表示保持当前不变": "", + "填写后覆盖当前测试环境私钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境 API 密钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境 Webhook 公钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境私钥,留空表示保持当前不变": "", + "填写后覆盖当前私钥,留空表示保持当前不变": "", "填写带https的域名,逗号分隔": "https://を含むドメインをカンマ区切りで入力してください", + "填写测试环境 Waffo 公钥,Base64 或 PEM 内容均可": "", + "填写生产环境 Waffo 公钥,Base64 或 PEM 内容均可": "", "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "ユーザー利用規約のコンテンツを設定すると、ユーザーがサインアップする際に、利用規約を読んだことへの同意チェックが求められます", "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "プライバシーポリシーのコンテンツを設定すると、ユーザーがサインアップする際に、プライバシーポリシーを読んだことへの同意チェックが求められます", "填写音频补全价格前,需要先填写音频输入价格。": "音声補完価格を入力する前に、先に音声入力価格を入力してください。", @@ -1171,6 +1217,8 @@ "多密钥管理": "複数APIキー管理", "多模型统一接入,只需将基址替换为:": "マルチモデル統一アクセス。ベースURLを以下に置換するだけでご利用いただけます:", "多种充值方式,安全便捷": "多様なチャージ方法で、安全かつ便利にチャージできます", + "大于": "", + "大于等于": "", "大模型接口网关": "LLM APIゲートウェイ", "天": "日", "天前": "日前", @@ -1187,14 +1235,14 @@ "套餐标题": "プラン名", "套餐标题不能为空": "プラン名は空にできません", "套餐的基本信息和定价": "プランの基本情報と価格", - "如:大带宽批量分析图片推荐": "例:広帯域での画像一括分析に推奨", - "如:香港线路": "例:香港回線", "如向中华人民共和国境内公众提供生成式人工智能服务,你将依法履行备案登记、安全评估、内容安全、投诉举报、生成合成内容标识、日志留存、个人信息保护等合规义务;": "If you provide generative AI services to the public in mainland China, you will fulfill legal compliance obligations.", "如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。": "アフィニティチャネルが失敗した場合、別のチャネルでリトライが成功すると、アフィニティが成功したチャネルに更新されます。", "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "New APIなどのリレープロジェクトに接続する場合は、OpenAIタイプを利用してください。設定内容を熟知している場合を除き、このタイプは利用しないでください", "如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "ユーザーリクエストにシステムプロンプトが含まれている場合、この設定内容がユーザーのシステムプロンプトの前に追加されます", "如果镜像为私有,请填写密码或Token": "If the image is private, please fill in the password or token", "如果镜像为私有,请填写用户名": "If the image is private, please fill in the username", + "如:大带宽批量分析图片推荐": "例:広帯域での画像一括分析に推奨", + "如:香港线路": "例:香港回線", "始终使用浅色主题": "常にライトテーマを使用", "始终使用深色主题": "常にダークテーマを使用", "字段映射": "フィールドマッピング", @@ -1202,6 +1250,7 @@ "字段路径": "フィールドパス", "字段透传控制": "フィールドパススルー制御", "字段速查": "フィールドクイックリファレンス", + "存在": "", "存在惩罚,鼓励讨论新话题": "存在ペナルティ、新しいトピックを促進", "存在重复的分组名称:": "重複するグループ名:", "存在重复的键名:": "キー名が重複しています:", @@ -1263,7 +1312,6 @@ "密钥": "APIキー", "密钥 JSON 必须包含 access_token": "キーJSONにはaccess_tokenを含む必要があります", "密钥 JSON 必须包含 account_id": "キーJSONにはaccount_idを含む必要があります", - "密钥(编辑模式下,保存的密钥不会显示)": "APIキー(編集モードでは、保存済みのAPIキーは表示されません)", "密钥去重": "APIキーの重複排除", "密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性": "シークレットキーは、Webhookリクエストの正当性を検証するため、Bearerトークンとしてリクエストヘッダーに追加されます。", "密钥已删除": "APIキーが削除されました", @@ -1281,6 +1329,7 @@ "密钥获取成功": "APIキーの取得に成功しました", "密钥输入方式": "APIキーの入力方式", "密钥预览": "APIキーのプレビュー", + "密钥(编辑模式下,保存的密钥不会显示)": "APIキー(編集モードでは、保存済みのAPIキーは表示されません)", "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "公式チャネルの場合、new-apiにはベースURLが組み込まれているため、サードパーティのプロキシサイトやAzureの専用のエンドポイントでない限り、入力する必要はありません。", "对免费模型启用预消耗": "Enable pre-consumption for free models", "对域名启用 IP 过滤(推荐开启)": "ドメインのIPフィルタリングを有効にする(推奨)", @@ -1309,6 +1358,8 @@ "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "保存されているすべての設定がクリアされ、デフォルト設定に復元されます。この操作は元に戻すことはできません。続行しますか?", "将清除选定时间之前的所有日志": "選択した日時以前のすべてのログをクリアします", "将追加 2 条规则到现有规则列表。": "既存のルールリストに2つのルールが追加されます。", + "小于": "", + "小于等于": "", "小时": "時間", "小时费率": "Hourly Rate", "尚未使用": "未使用", @@ -1319,10 +1370,12 @@ "展开更多": "もっと見る", "展示价格": "Display Pricing", "嵌套映射:用户分组 → 使用分组 → 倍率": "ネスト構造:ユーザーグループ → 使用グループ → レート", + "工具标识": "", + "工具调用定价": "", "左侧边栏个人设置": "サイドバーのアカウント設定", + "已为 ${count} 个渠道设置标签!": "${count}個のチャネルにタグが設定されました", "已为 {{count}} 个模型设置{{type}}_one": "{{count}}個のモデルに{{type}}が設定されました_one", "已为 {{count}} 个模型设置{{type}}_other": "{{count}}個のモデルに{{type}}_otherが設定されました_other", - "已为 ${count} 个渠道设置标签!": "${count}個のチャネルにタグが設定されました", "已从 Discovery 自动填充配置": "Discoveryから設定を自動入力しました", "已从 Discovery 获取配置,可继续手动修改所有字段。": "Discoveryから設定を取得しました。すべてのフィールドを手動で変更できます。", "已作废": "無効化済み", @@ -1334,15 +1387,16 @@ "已分配内存": "割り当て済みメモリ", "已切换为Assistant角色": "アシスタントロールに切り替えられました", "已切换为System角色": "システムロールに切り替えられました", + "已切换到新版前端,正在刷新页面": "新しいフロントエンドに切り替えました。ページを更新しています", "已切换到新版前端,正在跳转首页": "新しいフロントエンドに切り替えました。ホームに移動しています", "已切换至最优倍率视图,每个模型使用其最低倍率分组": "各モデルが最低倍率グループを利用する最適倍率ビューに切り替えられました", "已初始化": "初期化済み", "已删除": "削除済み", + "已删除 ${data} 个通道!": "${data}個のチャネルが削除されました!", "已删除 {{count}} 个令牌!": "{{count}}個のトークンが削除されました", "已删除 {{count}} 个令牌!_other": "Deleted {{count}} tokens!", "已删除 {{count}} 条失效兑换码_one": "無効な引き換えコードが{{count}}件削除されました_one", "已删除 {{count}} 条失效兑换码_other": "無効な引き換えコードが{{count}}件削除されました_other", - "已删除 ${data} 个通道!": "${data}個のチャネルが削除されました!", "已删除所有禁用渠道,共计 ${data} 个": "すべての無効なチャネル(合計${data}個)が削除されました", "已删除消息及其回复": "メッセージとその返信が削除されました", "已勾选": "選択済み", @@ -1364,8 +1418,6 @@ "已复制": "コピーされました", "已复制 ${count} 个模型": "${count}個のモデルがコピーされました", "已复制 ID 到剪贴板": "ID copied to clipboard", - "已复制:": "コピーされました:", - "已复制:{{name}}": "コピーされました:{{name}}", "已复制全部数据": "すべてのデータをコピーしました", "已复制到剪切板": "クリップボードにコピーされました", "已复制到剪贴板": "クリップボードにコピーされました", @@ -1374,6 +1426,8 @@ "已复制模型名称": "モデル名がコピーされました", "已复制版本号": "Version copied", "已复制自动生成的 API Key": "Auto-generated API Key copied", + "已复制:": "コピーされました:", + "已复制:{{name}}": "コピーされました:{{name}}", "已完成": "Completed", "已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型": "モデル {{name}} の価格設定を {{count}} 個のモデルに一括適用しました", "已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型_other": "", @@ -1415,10 +1469,10 @@ "已达到购买上限": "購入上限に達しました", "已过期": "有効期限切れ", "已运行时间": "Uptime", + "已选择 ${count} 个渠道": "${count}個のチャネルが選択されました", "已选择 {{count}} 个模型_one": "{{count}}個のモデルが選択されました_one", "已选择 {{count}} 个模型_other": "{{count}}個のモデルが選択されました_other", "已选择 {{selected}} / {{total}}": "{{selected}} / {{total}} 件選択済み", - "已选择 ${count} 个渠道": "${count}個のチャネルが選択されました", "已重置为默认配置": "デフォルト設定にリセットされました", "已销毁": "Destroyed", "币种": "通貨", @@ -1442,16 +1496,21 @@ "延长容器时长将会产生额外费用,请确认您有足够的账户余额。": "Extending container duration will incur additional charges, please ensure you have sufficient account balance.", "延长操作一旦确认无法撤销,费用将立即扣除。": "Once confirmed, the extension operation cannot be undone, and charges will be deducted immediately.", "延长时长": "Extension Duration", - "延长时长(小时)": "Extension Duration (hours)", "延长时长不能超过720小时(30天)": "Extension duration cannot exceed 720 hours (30 days)", "延长时长失败": "Failed to extend duration", "延长时长至少为1小时": "Extension duration must be at least 1 hour", + "延长时长(小时)": "Extension Duration (hours)", "建立连接时发生错误": "接続時にエラーが発生しました", "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "本番環境では MySQL または PostgreSQL データベースを使用するか、SQLite データベースファイルがホストマシンの永続ストレージにマッピングされていることを確認することを推奨します", "开": "On", "开启「默认使用 auto 分组」后,新建令牌和初始令牌都会自动设为 auto。": "「デフォルトでautoグループを使用」を有効にすると、新規トークンと初期トークンが自動的にautoに設定されます。", "开启之后会清除用户提示词中的": "有効にすると、ユーザープロンプトがクリアされます", "开启之后将上游地址替换为服务器地址": "有効にすると、アップストリームアドレスがサーバーURLに置換されます", + "开启后不限制:必须设置模型倍率": "有効化後は制限なし:モデル倍率の設定が必須", + "开启后创建令牌默认选择auto分组,初始令牌也将设为auto": "有効にするとトークン作成時にデフォルトでautoグループが選択され、初期トークンもautoに設定されます", + "开启后未登录用户无法访问模型广场": "有効にすると、ログインしていないユーザーはモデルマーケットプレイスにアクセスできなくなります", + "开启后检测到新增模型会自动加入当前渠道模型列表": "", + "开启后由后端定时任务检测该渠道上游模型变化": "", "开启后,using_group 会参与 cache key(不同分组隔离)。": "有効にすると、using_groupがキャッシュキーに含まれます(グループごとに隔離)。", "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。": "有効にすると、アフィニティチャネルが無効化された、または現在のグループ/モデルで利用できなくなった場合でも、そのアフィニティエントリを保持します。無効にすると、エントリを削除して別のチャネルを選択します。", "开启后,仅\"消费\"和\"错误\"日志将记录您的客户端IP地址": "有効にすると、「消費」と「エラー」のログにのみ、クライアントIPアドレスが記録されます", @@ -1464,11 +1523,6 @@ "开启后,规则名称会参与 cache key(不同规则隔离)。": "有効にすると、ルール名がキャッシュキーに含まれます(ルールごとに隔離)。", "开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)": "有効にすると、このチャネルでClaudeにリクエストする際に?beta=trueが強制追加されます(クライアント側で手動パラメータ渡し不要)", "开启后,违规请求将额外扣费。": "有効にすると、違反リクエストには追加料金が発生します。", - "开启后不限制:必须设置模型倍率": "有効化後は制限なし:モデル倍率の設定が必須", - "开启后创建令牌默认选择auto分组,初始令牌也将设为auto": "有効にするとトークン作成時にデフォルトでautoグループが選択され、初期トークンもautoに設定されます", - "开启后未登录用户无法访问模型广场": "有効にすると、ログインしていないユーザーはモデルマーケットプレイスにアクセスできなくなります", - "开启后检测到新增模型会自动加入当前渠道模型列表": "", - "开启后由后端定时任务检测该渠道上游模型变化": "", "开启批量操作": "一括操作を有効にする", "开始": "開始", "开始同步": "同期開始", @@ -1508,6 +1562,7 @@ "当前没有未设置定价的模型": "現在、価格未設定のモデルはありません", "当前版本": "現在のバージョン", "当前状态": "Current Status", + "当前环境共用同一商户 ID": "", "当前缓存大小": "現在のキャッシュサイズ", "当前编辑": "編集中", "当前规则不支持写入到该位置": "現在のルールはこの場所への書き込みをサポートしていません", @@ -1551,6 +1606,7 @@ "总收益": "総収益", "总计": "総計", "总额度": "総クォータ", + "恢复默认": "", "您可以个性化设置侧边栏的要显示功能": "サイドバーに表示する機能をカスタマイズできます", "您可以在上方拉取需要的模型": "You can pull the required models above", "您无权访问此页面,请联系管理员": "このページにアクセスする権限がありません。管理者にお問い合わせください", @@ -1620,22 +1676,23 @@ "拉取进度": "Pull Progress", "拒绝提示模板(可选)": "拒否プロンプトテンプレート(オプション)", "拦截原因": "ブロック理由", + "按 1 美元对应的站内价格填写": "", "按K显示单位": "K単位で表示", "按价格设置": "料金設定", "按倍率类型筛选": "倍率タイプで絞り込み", "按倍率设置": "倍率設定", "按次": "リクエストごと", "按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "リクエストごと {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "按次计费": "リクエストごとの課金", "按次:{{symbol}}{{price}}": "リクエストごと:{{symbol}}{{price}}", "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "リクエストごと:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}", - "按次计费": "リクエストごとの課金", "按照如下格式输入:AccessKey|SecretAccessKey|Region": "Enter in the format: AccessKey|SecretAccessKey|Region", "按量计费": "従量課金", "按量计费下需要先填写输入价格,才能保存其它价格项。": "従量課金では、他の価格項目を保存する前に入力価格を設定する必要があります。", "按顺序替换content中的变量占位符": "content内の変数プレースホルダーを順番に置換します", "换脸": "フェイススワップ", - "授权,需在遵守": "の条件に基づき、", "授权失败": "認可に失敗しました", + "授权,需在遵守": "の条件に基づき、", "排序": "並び順", "排队中": "待機中", "接受未设置价格模型": "料金未設定モデルを許可", @@ -1647,8 +1704,8 @@ "控制输出的随机性和创造性": "出力のランダム性と創造性を制御", "控制顶栏模块显示状态,全局生效": "トップバーモジュールの表示ステータスをグローバルに制御します", "推荐": "おすすめ", - "推荐:用户可以选择是否使用指纹等验证": "推奨:ユーザーは指紋認証などの利用を選択できます", "推荐使用(用户可选)": "(オプション)推奨", + "推荐:用户可以选择是否使用指纹等验证": "推奨:ユーザーは指紋認証などの利用を選択できます", "描述": "説明", "提交": "送信", "提交时间": "送信日時", @@ -1658,6 +1715,8 @@ "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "Prompt {{input}} tokens / 1M tokens * {{symbol}}{{price}}", "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "プロンプト {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 補完 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "プロンプト {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + キャッシュ {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + キャッシュ作成 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 補完 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "提示价格:{{symbol}}{{price}} / 1M tokens": "プロンプト料金:{{symbol}}{{price}} / 1M tokens", + "提示缓存倍率": "プロンプトキャッシュ倍率", "提示:如果切换后页面无法正常渲染,请清空浏览器缓存后重试。": "ヒント:切り替え後にページが正常に表示されない場合は、ブラウザのキャッシュを削除してからもう一度お試しください。", "提示:如需备份数据,只需复制上述目录即可": "ヒント:データをバックアップする際は、上記のディレクトリをコピーしてください", "提示:此处配置仅用于控制「模型广场」对用户的展示效果,不会影响模型的实际调用与路由。若需配置真实调用行为,请前往「渠道管理」进行设置。": "注意: ここでの設定は「モデル広場」での表示にのみ影響し、実際の呼び出しやルーティングには影響しません。実際の呼び出しを設定する場合は、「チャネル管理」で設定してください。", @@ -1665,8 +1724,6 @@ "提示:该功能为测试版,未来配置结构与功能行为可能发生变更,请勿在生产环境使用。": "注意: この機能はベータ版です。今後、設定構造や挙動が変更される可能性があります。本番環境では使用しないでください。", "提示:语言偏好会同步到您登录的所有设备,并影响API返回的错误消息语言。": "ヒント:言語設定はログインしているすべてのデバイスに同期され、APIが返すエラーメッセージの言語に影響します。", "提示:链接中的{key}将被替换为API密钥,{address}将被替换为服务器地址": "ヒント:リンク内の{key}はAPIキーに、{address}はサーバーURLに置換されます", - "提示价格:{{symbol}}{{price}} / 1M tokens": "プロンプト料金:{{symbol}}{{price}} / 1M tokens", - "提示缓存倍率": "プロンプトキャッシュ倍率", "搜索供应商": "プロバイダーで検索", "搜索关键字": "検索キーワード", "搜索失败": "Search failed", @@ -1701,6 +1758,7 @@ "支付方式名称": "決済方法名", "支付方式名称不能为空": "", "支付方式类型": "決済方法タイプ", + "支付方式设置": "", "支付渠道": "決済チャネル", "支付设置": "決済", "支付请求失败": "決済リクエストに失敗しました", @@ -1746,9 +1804,9 @@ "数据看板默认时间粒度": "ダッシュボードのデフォルト時間粒度", "数据管理和日志查看": "データ管理とログ閲覧", "文件上传": "ファイルアップロード", - "文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "ファイル検索: {{count}} / 1K * 単価 {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "文件搜索价格:{{symbol}}{{price}} / 1K 次": "ファイル検索料金:{{symbol}}{{price}} / 1K 回", "文件搜索调用 {{fileSearchCallCount}} 次": "ファイル検索呼び出し {{fileSearchCallCount}} 回", + "文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "ファイル検索: {{count}} / 1K * 単価 {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "プロンプト {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 補完 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "テキストプロンプト {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + テキスト補完 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音声プロンプト {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音声補完 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "プロンプト {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + キャッシュ {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 補完 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", @@ -1778,9 +1836,9 @@ "新建容器部署": "Create Container Deployment", "新建数量": "作成数", "新建组": "新規グループ", + "新格式模板": "新規形式テンプレート", "新格式(支持条件判断与json自定义):": "新規形式(条件判断とカスタムJSONに対応):", "新格式(规则 + 条件)": "新形式(ルール + 条件)", - "新格式模板": "新規形式テンプレート", "新版本": "新しいバージョン", "新用户使用邀请码奖励额度": "招待コードを利用した新規ユーザーへの特典クォータ", "新用户初始额度": "新規ユーザーの初期クォータ", @@ -1794,6 +1852,7 @@ "无加密": "暗号化なし", "无效的部署信息": "Invalid deployment information", "无效的重置链接,请重新发起密码重置请求": "無効なパスワードリセットリンクです。再度パスワードのリセットをリクエストしてください", + "无条件(兜底档)": "", "无法发起 Passkey 注册": "Passkeyの登録を開始できません", "无法复制到剪贴板,请手动复制": "クリップボードにコピーできません。手動でコピーしてください。", "无法添加图片": "画像を追加できません", @@ -1818,19 +1877,22 @@ "日志类型": "ログタイプ", "日志设置": "ログ設定", "日志详情": "ログ詳細", - "旧格式(JSON 对象)": "レガシー形式(JSONオブジェクト)", - "旧格式(直接覆盖):": "旧形式(直接上書き):", "旧格式必须是 JSON 对象": "レガシー形式はJSONオブジェクトである必要があります", "旧格式模板": "旧形式テンプレート", + "旧格式(JSON 对象)": "レガシー形式(JSONオブジェクト)", + "旧格式(直接覆盖):": "旧形式(直接上書き):", "旧版前端即将停止维护": "旧版フロントエンドはまもなくメンテナンス終了", "旧的备用码已失效,请保存新的备用码": "古いバックアップコードは無効になりました。新規バックアップコードを保存してください", "早上好": "おはようございます", + "时区": "", "时间": "時間", "时间信息": "Time Information", + "时间条件": "", "时间粒度": "時間粒度", "易支付": "Epay", "易支付商户ID": "Epay マーチャントID", "易支付商户密钥": "Epay APIキー", + "易支付设置": "", "是": "はい", "是否为企业账户": "エンタープライズアカウント", "是否同时重置对话消息?选择\"是\"将清空所有对话记录并恢复默认示例;选择\"否\"将保留当前对话记录。": "チャットメッセージも同時にリセットしますか?「はい」を選択すると、すべてのチャット履歴がクリアされ、デフォルトのサンプルが復元されます。「いいえ」を選択すると、現在のチャット履歴は保持されます。", @@ -1841,16 +1903,17 @@ "是否自动禁用": "自動的に無効にする", "是否要求指纹/面容等生物识别": "生体認証を要求する", "显示倍率": "表示倍率", - "显示最新20条": "最新20件を表示", "显示名称": "表示名", "显示名称字段(可选)": "表示名フィールド(オプション)", "显示完整内容": "すべてのコンテンツを表示", "显示操作项": "操作項目を表示", "显示更多": "もっと見る", + "显示最新20条": "最新20件を表示", "显示第": "表示", "显示设置": "表示設定", "显示调试": "デバッグを表示", "晚上好": "こんばんは", + "普通按量/按次直接填价格就行;如果价格要跟请求参数或请求头联动,请切到表达式/阶梯计费。": "", "普通环境变量": "Regular Environment Variables", "普通用户": "一般ユーザー", "普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "通常入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}", @@ -1906,9 +1969,12 @@ "更多": "もっと見る", "更多信息请参考": "詳細については、こちらをご参照ください", "更多参数请参考": "その他のパラメータについては、こちらをご参照ください", + "更多模板...": "", + "更好的价格,更好的稳定性,只需要将模型基址替换为:": "よりお得な料金、さらに向上した安定性。モデルのベースURLを以下に置換するだけで、すぐにご利用いただけます:", "更新": "更新", "更新 Creem 设置": "Update Creem Settings", "更新 Stripe 设置": "Stripe設定の更新", + "更新 Waffo Pancake 设置": "", "更新 Waffo 设置": "", "更新SSRF防护设置": "SSRF保護設定の更新", "更新Worker设置": "Worker設定の更新", @@ -1924,6 +1990,7 @@ "更新所有已启用通道余额": "有効なすべてのチャネルの残高を更新", "更新支付设置": "決済設定の更新", "更新时间": "更新日時", + "更新易支付设置": "", "更新服务器地址": "サーバーURLを更新", "更新模型信息": "モデル情報の更新", "更新渠道信息": "チャネル情報を更新", @@ -1933,6 +2000,20 @@ "更新配置失败": "Failed to update configuration", "更新预填组": "事前入力グループの更新", "替换": "", + "最低": "最低", + "最低充值数量": "", + "最低充值美元数量": "最低チャージUSD額", + "最低充值美元数量必须大于 0": "最低チャージUSD額は 0 より大きい必要があります", + "最后使用时间": "最終利用日時", + "最后更新": "Last Updated", + "最后请求": "最終リクエスト日時", + "最大GPU数量": "Max Number of GPUs", + "最大可用": "Max Available", + "最大条目数": "最大エントリ数", + "最终抵扣": "最終控除", + "最近一次": "最新", + "最近事件": "Recent Events", + "最高优先级": "最高優先", "月": "月", "有 Reasoning": "推論あり", "有序字符串数组": "順序付き文字列配列", @@ -1989,8 +2070,8 @@ "本地数据存储": "ローカルストレージ", "本地计费": "Local billing", "本月获得": "今月の獲得", - "本设备:手机指纹/面容,外接:USB安全密钥": "内蔵:スマートフォンの指紋/顔認証、外部:USBセキュリティキー", "本设备内置": "このデバイス", + "本设备:手机指纹/面容,外接:USB安全密钥": "内蔵:スマートフォンの指紋/顔認証、外部:USBセキュリティキー", "本项目根据": "本プロジェクトは", "机密环境变量": "Secret Environment Variables", "机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。": "Secret environment variables will be stored encrypted, suitable for storing passwords, API keys and other sensitive information.", @@ -1999,13 +2080,15 @@ "权限设置": "権限設定", "条": "件", "条 - 第": "~", - "条,共": "件、合計", + "条件": "", + "条件乘数": "", "条件取反": "条件を反転", "条件数": "条件数", "条件规则": "条件ルール", "条件项设置": "条件項目設定", "条日志已清理!": "件のログがクリアされました", "条规则": "件のルール", + "条,共": "件、合計", "来源": "ソース", "来源于 IO.NET 部署": "From IO.NET Deployment", "来源端点": "ソースエンドポイント", @@ -2039,11 +2122,15 @@ "核采样,控制词汇选择的多样性": "ニュークリアスサンプリング、語彙選択の多様性を制御", "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。": "Anthropic の仕様により、/v1/messages の入力 tokens は非キャッシュ入力のみを集計し、キャッシュ読み取り/書き込み tokens は含みません。", "根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含": "モデル名とマッチングルールに基づきモデルメタデータを検索します。優先度:完全一致 > プレフィックス > サフィックス > 部分一致", + "格式": "", "格式化": "フォーマット", "格式化 JSON": "JSON を整形", "格式正确": "有効な形式", "格式示例:": "フォーマット例:", "格式错误": "無効な形式", + "档": "", + "档位": "", + "档位名称": "", "检查更新": "更新を確認", "检测全部渠道上游更新": "", "检测到 FluentRead(流畅阅读)": "FluentReadが検出されました", @@ -2064,10 +2151,10 @@ "模型价格 {{symbol}}{{price}} / 次": "モデル価格 {{symbol}}{{price}} / リクエスト", "模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "モデル価格 {{symbol}}{{price}} / リクエスト * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}": "モデル料金 {{symbol}}{{price}}、{{ratioType}} {{ratio}}", + "模型价格未配置": "モデル価格が未設定", "模型价格:{{symbol}}{{price}}": "モデル価格:{{symbol}}{{price}}", "模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "モデル料金:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}", "模型价格:{{symbol}}{{price}} / 次": "モデル価格:{{symbol}}{{price}} / リクエスト", - "模型价格未配置": "モデル価格が未設定", "模型倍率": "モデル倍率", "模型倍率 {{modelRatio}}": "Model ratio {{modelRatio}}", "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}": "モデル倍率 {{modelRatio}}、キャッシュ倍率 {{cacheRatio}}、補完倍率 {{completionRatio}}、{{ratioType}} {{ratio}}", @@ -2092,8 +2179,8 @@ "模型名称已存在": "そのモデル名はすでに存在します", "模型固定价格": "モデル固定料金", "模型图标": "モデルアイコン", - "模型定价,需要登录访问": "モデル料金(アクセスにはログインが必要です)", "模型定价设置": "モデル料金設定", + "模型定价,需要登录访问": "モデル料金(アクセスにはログインが必要です)", "模型广场": "モデルマーケットプレイス", "模型拉取失败: {{error}}": "Failed to pull model: {{error}}", "模型排行": "モデルランキング", @@ -2103,8 +2190,8 @@ "模型更新成功!": "モデルの更新に成功しました!", "模型未加入列表,可能无法调用": "Model not in the list; requests may fail", "模型正则": "モデル正規表現", - "模型正则(每行一个)": "モデル正規表現(1行に1つ)", "模型正则不能为空": "モデル正規表現は空にできません", + "模型正则(每行一个)": "モデル正規表現(1行に1つ)", "模型消耗分布": "モデル消費分布", "模型消耗趋势": "モデル消費推移", "模型版本": "モデルバージョン", @@ -2138,8 +2225,8 @@ "正则替换": "", "正在加载可用部署位置...": "Loading available deployment locations...", "正在加载签到状态...": "チェックイン状態を読み込み中...", - "正在处理,请稍候": "", "正在处理大内容...": "大容量のコンテンツを処理中...", + "正在处理,请稍候": "", "正在批量处理,请稍候": "", "正在批量检测,请稍候": "", "正在提交": "送信中", @@ -2181,6 +2268,7 @@ "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "(オプション)カスタムベースURLでのAPIコール用。末尾に/v1, /は含めません", "每个充值单位对应的 USD 金额,默认 1.0": "", "每个分组代表一个价格档位。管理员创建分组后,可以选择哪些档位对用户开放自选。": "各グループは1つの料金プランを表します。管理者がグループを作成後、どのプランをユーザーに公開するか選択できます。", + "每个档位可设置 0~2 个条件(对 p 和 c),最后一档为兜底档无需条件。": "", "每个用户最多可创建的令牌数量,默认 1000,设置过大可能会影响性能": "各ユーザーが作成できるトークンの最大数、デフォルト1000。大きすぎるとパフォーマンスに影響する場合があります", "每周": "毎週", "每天": "毎日", @@ -2214,8 +2302,8 @@ "注册": "サインアップ", "注册 Passkey": "Passkeyの登録", "注意": "ご注意", - "注意:JSON中重复的键只会保留最后一个同名键的值": "ご注意:JSONでは、キーが重複している場合、最後の同名キーの値のみが保持されます", "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "ご注意:Chat API以外の場合、必ず正しいベースURLを入力してください。正しく入力しないと、利用できません", + "注意:JSON中重复的键只会保留最后一个同名键的值": "ご注意:JSONでは、キーが重複している場合、最後の同名キーの値のみが保持されます", "注销": "ログアウト", "注销成功!": "ログアウトしました", "活跃文件": "アクティブファイル", @@ -2270,6 +2358,10 @@ "添加启动命令": "Add Startup Command", "添加密钥环境变量": "Add Secret Environment Variable", "添加成功": "追加に成功しました", + "添加时间条件": "", + "添加更多档位": "", + "添加条件": "", + "添加条件组": "", "添加模型": "モデル追加", "添加模型区域": "モデルリージョン追加", "添加渠道": "チャネル追加", @@ -2296,8 +2388,8 @@ "渠道 ID": "チャネルID", "渠道ID,名称,密钥,API地址": "チャネルID\\名称\\キー\\ベースURL", "渠道亲和性": "チャネル親和性", - "渠道亲和性:上游缓存命中": "チャネルアフィニティ:上流キャッシュヒット", "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。": "チャネルアフィニティは、リクエストコンテキストまたはJSON Bodyから抽出されたキーに基づいて、前回成功したチャネルを優先的に再利用します。", + "渠道亲和性:上游缓存命中": "チャネルアフィニティ:上流キャッシュヒット", "渠道优先级": "チャネル優先度", "渠道信息": "チャネル情報", "渠道创建成功!": "チャネルの作成に成功しました", @@ -2320,6 +2412,7 @@ "渠道额外设置": "チャネル詳細設定", "源地址": "ベースURL", "满足任一条件(OR)": "いずれかの条件を満たす(OR)", + "满足条件时,整单价格乘以 X;如果有多条同时命中,会继续相乘。": "", "演示站点": "デモサイト", "演示站点模式": "デモサイトモード", "点击 + 按钮添加图片URL进行多模态对话": "+ ボタンをクリックして画像URLを追加し、マルチモーダル会話を行います", @@ -2357,7 +2450,13 @@ "生成音乐": "音楽生成", "生效": "有効", "用于API调用的身份验证令牌,请妥善保管": "API呼び出し用の認証トークンです。大切に保管してください。", + "用于切换当前下单和回调校验所使用的环境": "", "用于唯一标识用户的字段路径": "ユーザーを一意に識別するためのフィールドパス", + "用于校验 Stripe Webhook 签名,保存后不会回显": "", + "用于校验测试环境的 Waffo Pancake Webhook 签名": "", + "用于校验测试环境的 Waffo 回调签名": "", + "用于校验生产环境的 Waffo Pancake Webhook 签名": "", + "用于校验生产环境的 Waffo 回调签名": "", "用于配置网络代理,支持 socks5 协议": "ネットワークプロキシの設定に使用し、SOCKS5プロトコルに対応しています", "用于验证回调 new-api 的 webhook 请求的密钥,敏感信息不显示": "The key used to validate webhook requests for the callback new-api, sensitive information is not displayed.", "用以支持基于 WebAuthn 的无密码登录注册": "WebAuthnベースのパスワードレスログインとサインアップを有効にします", @@ -2372,13 +2471,13 @@ "用以防止恶意用户利用临时邮箱批量注册": "使い捨てメールアドレスを利用した、悪意のあるユーザーによる大量サインアップを防止します", "用户": "ユーザー", "用户 ID 字段(可选)": "ユーザーIDフィールド(オプション)", + "用户ID": "", "用户个人功能": "アカウント設定", "用户主页,展示系统信息": "ユーザー向けのホーム。システム情報を表示します", "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "ユーザー優先:ユーザーがリクエストでシステムプロンプトを指定した場合、ユーザーの設定が優先されます", "用户信息": "ユーザー情報", "用户信息更新成功!": "ユーザー情報の更新に成功しました", "用户信息缺失": "ユーザー情報がありません", - "用户最大令牌数量": "ユーザーあたりの最大トークン数", "用户分组": "ユーザーグループ", "用户分组和额度管理": "ユーザーグループとクォータの管理", "用户分组的联动作用": "ユーザーグループの連動効果", @@ -2388,6 +2487,7 @@ "用户协议": "ユーザー利用規約", "用户协议已更新": "ユーザー利用規約が更新されました", "用户协议更新失败": "ユーザー利用規約の更新に失敗しました", + "用户单次最少可充值的美元数量": "", "用户可选": "ユーザー選択可", "用户可选分组": "ユーザー選択可能グループ", "用户可选分组的名称和描述(只包含勾选了用户可选的分组)": "ユーザーが選択可能なグループの名前と説明(チェック済みのグループのみ)", @@ -2398,6 +2498,7 @@ "用户在充值页面看到的支付方式名称,例如:Credit Card": "", "用户控制面板,管理账户": "ユーザーコンソールでアカウントを管理します", "用户新建令牌时可选的分组,格式为 JSON 字符串,例如:{\"vip\": \"VIP 用户\", \"test\": \"测试\"},表示用户可以选择 vip 分组和 test 分组": "ユーザーが新規トークンを作成する際に利用可能なグループです。JSON文字列の形式で入力してください。例:{\"vip\": \"VIPユーザー\", \"test\": \"テスト\"} は、ユーザーがvipグループとtestグループを選択できることを示します。", + "用户最大令牌数量": "ユーザーあたりの最大トークン数", "用户每周期最多请求完成次数": "期間ごとのユーザー最大成功リクエスト数", "用户每周期最多请求次数": "期間ごとのユーザー最大リクエスト数", "用户注册时看到的网站名称,比如'我的网站'": "ユーザーがサインアップ時に表示されるウェブサイト名です。例:「マイサイト」", @@ -2419,8 +2520,10 @@ "留空则使用默认端点;支持 {path, method}": "未入力の場合、デフォルトのエンドポイントが使用されます。{path, method}に対応しています", "留空则保持原有密钥": "空欄で既存のキーを保持", "留空则自动使用 服务器地址 + /api/waffo/webhook": "", + "留空则自动使用当前站点的默认充值页地址": "", "留空则自动使用当前站点的默认回调地址": "空欄の場合は現在のサイトのデフォルトのコールバックアドレスを使用します", "留空则默认使用服务器地址,注意不能携带http://或者https://": "未入力の場合、デフォルトのサーバーURLが使用されます。ご注意:http://またはhttps://は含めないでください", + "留空时默认使用服务器地址作为回调地址,填写后将覆盖默认值": "", "登 录": "ログイン", "登录": "ログイン", "登录成功!": "ログインに成功しました", @@ -2447,7 +2550,6 @@ "硬件类型": "Hardware Type", "硬件配置": "Hardware Configuration", "确定": "確認", - "确定?": "確認", "确定删除此组?": "このグループを削除してもよろしいですか?", "确定导入": "インポート", "确定是否要修复数据库一致性?": "データベースの整合性を修復してもよろしいですか?", @@ -2491,6 +2593,7 @@ "确定要降级此用户吗?": "このユーザーを降格させてもよろしいですか?", "确定重置": "リセットの確認", "确定重置模型倍率吗?": "モデル倍率をリセットしますか?", + "确定?": "確認", "确认": "確認", "确认作废": "無効化の確認", "确认关闭提示": "閉じる確認", @@ -2498,8 +2601,8 @@ "确认切换": "切り替えを確認", "确认删除": "削除の確認", "确认删除模型": "Confirm Delete Model", - "确认删除该分组?": "このグループを削除しますか?", "确认删除该分组的所有规则?": "このグループの全ルールを削除しますか?", + "确认删除该分组?": "このグループを削除しますか?", "确认删除该规则?": "このルールを削除しますか?", "确认前,支付、兑换码、订阅计划和邀请返利功能将保持锁定。": "Payment, redemption codes, subscription plans, and invitation rewards remain locked until confirmation.", "确认取消密码登录": "パスワードログイン無効化の確認", @@ -2532,16 +2635,13 @@ "磁盘使用率超过此值时拒绝请求": "ディスク使用率がこの値を超えた場合にリクエストを拒否", "磁盘可用空间小于缓存最大总量设置": "ディスクの空き容量がキャッシュの最大合計サイズ設定より少ないです", "磁盘命中": "ディスクヒット", - "磁盘缓存最大总量 (MB)": "ディスクキャッシュ最大合計 (MB)", "磁盘缓存占用的最大空间": "ディスクキャッシュが占める最大容量", "磁盘缓存已清理": "ディスクキャッシュがクリアされました", + "磁盘缓存最大总量 (MB)": "ディスクキャッシュ最大合計 (MB)", "磁盘缓存设置(磁盘换内存)": "ディスクキャッシュ設定(ディスク/メモリ交換)", "磁盘缓存阈值 (MB)": "ディスクキャッシュ閾値 (MB)", "示例": "サンプル", "示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。": "例:{\"default\": [200, 100], \"vip\": [0, 1000]}。", - "视频": "動画", - "视频Remix": "動画リミックス", - "视频无法在当前浏览器中播放,这可能是由于:": "The video cannot be played in this browser, possibly because:", "禁用": "無効にする", "禁用 store 透传": "ストアパススルーを無効にする", "禁用2FA失败": "2要素認証の無効化に失敗しました", @@ -2563,8 +2663,8 @@ "移动请求头": "", "移除": "削除", "移除 (-:)": "削除 (-:)", - "移除 functionResponse.id 字段": "functionResponse.idフィールドを削除", "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "One APIの著作権表示を削除するには、事前の許可が必要です。プロジェクトの維持には多大な労力がかかります。もしこのプロジェクトがあなたにとって有意義でしたら、積極的なご支援をお願いいたします", + "移除 functionResponse.id 字段": "functionResponse.idフィールドを削除", "空": "空", "窗口处理": "ウィンドウ処理", "窗口等待": "ウィンドウ待機中", @@ -2591,25 +2691,28 @@ "第 {{line}} 条操作缺少目标路径": "ルール#{{line}} 操作にターゲットパスがありません", "第 {{line}} 条请求头透传格式无效": "ルール#{{line}} ヘッダーパススルー形式が無効です", "第 {{line}} 条请求头透传缺少请求头名称": "ルール#{{line}} ヘッダーパススルーにヘッダー名がありません", + "第 {{n}} 档": "", + "第 {{n}} 组": "", "第三方支付配置": "サードパーティ決済設定", "第三方账户绑定状态(只读)": "サードパーティアカウントの連携ステータス(読み取り専用)", + "等于": "", "等价金额:": "相当額:", "等待中": "待機中", "等待获取邮箱信息...": "メールアドレス情報を取得中...", "筛选": "フィルター", - "签到最大额度": "チェックイン最大クォータ", - "签到最小额度": "チェックイン最小クォータ", "签到功能允许用户每日签到获取随机额度奖励": "チェックイン機能により、ユーザーは毎日チェックインしてランダムなクォータ報酬を獲得できます", "签到失败": "チェックインに失敗しました", "签到奖励将直接添加到您的账户余额": "チェックイン報酬は直接アカウント残高に追加されます", "签到奖励的最大额度": "チェックイン報酬の最大クォータ", "签到奖励的最小额度": "チェックイン報酬の最小クォータ", "签到成功!获得": "チェックイン成功!獲得", + "签到最大额度": "チェックイン最大クォータ", + "签到最小额度": "チェックイン最小クォータ", "签到设置": "チェックイン設定", "简单来说:同一个令牌分组,不同等级的用户可以享受不同的价格。": "簡単に言えば:同じトークングループでも、異なる等級のユーザーが異なる価格を享受できます。", "简洁": "シンプル", - "简洁模式:按 type 全量清理对象,例如 redacted_thinking。": "シンプルモード:typeごとにすべてのオブジェクトをプルーニングします(例:redacted_thinking)。", "简洁模式仅返回 message;状态码和错误类型将使用系统默认值。": "シンプルモードはメッセージのみを返します。ステータスコードとエラータイプはシステムのデフォルト値を使用します。", + "简洁模式:按 type 全量清理对象,例如 redacted_thinking。": "シンプルモード:typeごとにすべてのオブジェクトをプルーニングします(例:redacted_thinking)。", "管理": "管理", "管理 Ollama 模型的拉取和删除": "Manage Ollama model pulling and deletion", "管理你的 LinuxDO OAuth App": "LinuxDO OAuth Appの管理", @@ -2619,9 +2722,11 @@ "管理员区域": "管理者エリア", "管理员暂时未设置任何关于内容": "管理者はまだ「このサービスについて」のコンテンツを設定していません", "管理员未开启 Creem 充值!": "The administrator has not enabled Creem recharge!", + "管理员未开启 Waffo Pancake 充值!": "", + "管理员未开启 Waffo 充值!": "", "管理员未开启Stripe充值!": "管理者がStripeチャージを有効にしていません", - "管理员未开启在线充值!": "管理者がオンラインチャージを有効にしていません", "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "管理者がオンラインチャージ機能を有効にしていません。管理者にお問い合わせいただくか、引き換えコードでチャージしてください。", + "管理员未开启在线充值!": "管理者がオンラインチャージを有効にしていません", "管理员未开启在线支付功能,请联系管理员配置。": "管理者がオンライン決済を有効にしていません。管理者に連絡してください。", "管理员未设置用户可选分组": "管理者がユーザー利用可能なグループを設定していません", "管理员给用户分配的分组(如 vip)不仅决定用户身份,还会影响后续两个功能:": "ユーザーに割り当てるグループ(例:vip)はユーザーの身分を決定するだけでなく、以下の2つの機能にも影響します:", @@ -2665,12 +2770,12 @@ "系统管理功能": "システム管理機能", "系统设置": "システム", "系统访问令牌": "システムアクセストークン", - "约": "約", "索引": "インデックス", "紧凑列表": "コンパクトリスト", - "纯字符串会直接覆盖整条请求头,或者点击“查看 JSON 示例”按 token 规则处理。": "", "累计签到": "累計チェックイン", "累计获得": "累計獲得", + "约": "約", + "纯字符串会直接覆盖整条请求头,或者点击“查看 JSON 示例”按 token 规则处理。": "", "线路描述": "チャネルの説明", "组列表": "グループリスト", "组名": "グループ名", @@ -2700,45 +2805,48 @@ "统计次数": "リクエスト数統計", "统计额度": "クォータ統計", "继续": "次へ", + "缓存 Tokens": "キャッシュトークン", "缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "キャッシュ {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "Cache {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (ratio: {{ratio}})", - "缓存 Tokens": "キャッシュトークン", "缓存: {{cacheRatio}}": "キャッシュ:{{cacheRatio}}", "缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "キャッシュ料金:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens(キャッシュ倍率:{{cacheRatio}})", "缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "キャッシュ料金:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens(キャッシュ倍率:{{cacheRatio}})", "缓存倍率": "キャッシュ倍率", "缓存倍率 {{cacheRatio}}": "Cache ratio {{cacheRatio}}", "缓存写": "キャッシュ書込", + "缓存创建": "", + "缓存创建 Tokens": "キャッシュ作成トークン", "缓存创建 {{price}} / 1M tokens": "キャッシュ作成 {{price}} / 1M tokens", "缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "キャッシュ作成 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "Cache creation {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (ratio: {{ratio}})", - "缓存创建 Tokens": "キャッシュ作成トークン", - "缓存创建: {{cacheCreationRatio}}": "キャッシュ作成:{{cacheCreationRatio}}", + "缓存创建-1小时": "", "缓存创建: 1h {{cacheCreationRatio1h}}": "キャッシュ作成:1h {{cacheCreationRatio1h}}", "缓存创建: 5m {{cacheCreationRatio5m}}": "キャッシュ作成:5m {{cacheCreationRatio5m}}", "缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "Cache creation: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}", - "缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "キャッシュ作成: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * キャッシュ作成倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "缓存创建: {{cacheCreationRatio}}": "キャッシュ作成:{{cacheCreationRatio}}", "缓存创建价格": "入力キャッシュ作成価格", "缓存创建价格 {{symbol}}{{price}} / 1M tokens": "キャッシュ作成価格 {{symbol}}{{price}} / 1M tokens", + "缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens": "Cache creation price total: 5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens", "缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "キャッシュ作成料金:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1Mtokens(キャッシュ作成倍率:{{cacheCreationRatio}})", "缓存创建价格:{{symbol}}{{price}} / 1M tokens": "キャッシュ作成価格:{{symbol}}{{price}} / 1M tokens", - "缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens": "Cache creation price total: 5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens", "缓存创建倍率": "キャッシュ作成倍率", - "缓存创建倍率 {{cacheCreationRatio}}": "Cache creation ratio {{cacheCreationRatio}}", "缓存创建倍率 1h {{cacheCreationRatio1h}}": "キャッシュ作成倍率 1h {{cacheCreationRatio1h}}", "缓存创建倍率 5m {{cacheCreationRatio5m}}": "キャッシュ作成倍率 5m {{cacheCreationRatio5m}}", "缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "キャッシュ作成倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}", + "缓存创建倍率 {{cacheCreationRatio}}": "Cache creation ratio {{cacheCreationRatio}}", + "缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "キャッシュ作成: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * キャッシュ作成倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "缓存条目数": "キャッシュエントリ数", "缓存目录": "キャッシュディレクトリ", "缓存目录磁盘空间": "キャッシュディレクトリのディスク容量", "缓存读": "キャッシュ読取", "缓存读 {{price}} / 1M tokens": "キャッシュ読み取り {{price}} / 1M tokens", - "缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "キャッシュ読み取り: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * キャッシュ倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "缓存读取": "", "缓存读取价格": "入力キャッシュ読み取り価格", "缓存读取价格 {{symbol}}{{price}} / 1M tokens": "キャッシュ読み取り価格 {{symbol}}{{price}} / 1M tokens", "缓存读取价格:{{symbol}}{{price}} / 1M tokens": "キャッシュ読み取り価格:{{symbol}}{{price}} / 1M tokens", "缓存读取价格:{{symbol}}{{total}} / 1M tokens": "キャッシュ読み取り価格:{{symbol}}{{total}} / 1M tokens", "缓存读取倍率": "", + "缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "キャッシュ読み取り: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * キャッシュ倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "キャッシュ入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * キャッシュ倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "编辑": "編集", "编辑 OAuth 提供商": "OAuthプロバイダーを編集", @@ -2820,10 +2928,10 @@ "若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置": "お使いのOIDCプロバイダーがディスカバリーエンドポイントに対応している場合、OIDC Well-Known URLを入力するだけで、システムが自動的にOIDC設定を取得します。", "获取 Discovery 配置": "Discovery設定を取得", "获取 Discovery 配置失败:": "Discovery設定の取得に失敗しました:", - "获取 io.net API Key": "Get io.net API Key", "获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确": "OIDC設定の取得に失敗しました。ネットワーク状況とWell-Known URLが正しいかご確認ください", "获取 OIDC 配置成功!": "OIDC設定の取得に成功しました", "获取 Ollama 版本失败": "Failed to get Ollama version", + "获取 io.net API Key": "Get io.net API Key", "获取2FA状态失败": "2FAステータスの取得に失敗しました", "获取令牌密钥失败": "", "获取充值配置失败": "チャージ設定の取得に失敗しました", @@ -2857,9 +2965,9 @@ "补全 {{completion}} tokens * 输出倍率 {{completionRatio}}": "補完 {{completion}} tokens * 出力倍率 {{completionRatio}}", "补全 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}": "Completion {{completion}} tokens / 1M tokens * {{symbol}}{{price}}", "补全价格": "補完価格", + "补全价格已锁定": "補完価格はロックされています", "补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "補完料金:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens(補完倍率:{{completionRatio}})", "补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens": "補完料金:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens", - "补全价格已锁定": "補完価格はロックされています", "补全倍率": "補完倍率", "补全倍率 {{completionRatio}}": "補完倍率 {{completionRatio}}", "补全倍率值": "補完倍率", @@ -2868,10 +2976,15 @@ "补单成功": "手動チャージに成功しました", "表单引用错误,请刷新页面重试": "フォームの参照でエラーが発生しました。ページを更新して再試行してください", "表格视图": "テーブルビュー", + "表达式/阶梯计费": "", + "表达式编辑": "", + "表达式计费": "", + "表达式错误": "", "覆盖": "上書き", "覆盖模式:将完全替换现有的所有密钥": "上書きモード:既存のすべてのAPIキーを完全に置き換えます", "覆盖模板": "オーバーライドテンプレート", "覆盖现有密钥": "既存のAPIキーを上書き", + "见上方动态计费详情": "", "规则": "ルール", "规则 JSON": "ルールJSON", "规则 JSON 格式不正确": "ルールJSONの形式が正しくありません", @@ -2881,6 +2994,9 @@ "规则导航": "ルールナビゲーション", "规则描述(可选)": "", "规则未找到,请刷新后重试": "ルールが見つかりません。更新してから再試行してください", + "视频": "動画", + "视频Remix": "動画リミックス", + "视频无法在当前浏览器中播放,这可能是由于:": "The video cannot be played in this browser, possibly because:", "角色": "ロール", "解析响应数据时发生错误": "レスポンスデータの解析時にエラーが発生しました", "解析密钥文件失败: {{msg}}": "APIキーファイルの解析に失敗しました:{{msg}}", @@ -2889,6 +3005,9 @@ "解绑 Passkey": "Passkey連携解除", "解绑后将无法使用 Passkey 登录,确定要继续吗?": "連携解除後は、Passkeyでログインできなくなります。連携を解除してもよろしいですか?", "解绑成功": "バインド解除に成功しました", + "警告": "警告", + "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "警告:キープアライブを有効にした後、データ書き込み後にチャネルエラーが発生した場合、システムは再試行できません。有効化が必須の場合は、Ping間隔を可能な限り長く設定することを推奨します", + "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "警告:2要素認証を無効にすると、認証設定とすべてのバックアップコードが永久に削除されます。この操作は元に戻すことができません", "计价币种": "Pricing Currency", "计算中": "Calculating", "计算成本": "Calculate Cost", @@ -2980,10 +3099,10 @@ "语音输入": "音声入力", "语音输出": "音声出力", "说明": "説明", + "说明信息": "説明", "说明:": "説明:", "说明:本页测试为非流式请求;若渠道仅支持流式返回,可能出现测试失败,请以实际使用为准。": "注意: このページのテストは非ストリーミングリクエストです。チャネルがストリーミング応答のみ対応の場合、テストが失敗することがあります。実際の利用結果を優先してください。", "说明:生成结果是可直接粘贴到渠道密钥里的 JSON(包含 access_token / refresh_token / account_id)。": "注:生成結果はチャネルキーに直接貼り付けられるJSON(access_token / refresh_token / account_idを含む)です。", - "说明信息": "説明", "请上传密钥文件": "APIキーファイルをアップロードしてください", "请上传密钥文件!": "APIキーファイルをアップロードしてください", "请为渠道命名": "チャネル名を入力してください", @@ -3017,6 +3136,9 @@ "请填写完整的产品信息": "Please fill in complete product information", "请填写完整的管理员账号信息": "管理者アカウント情報をすべて入力してください", "请填写密钥": "APIキーを入力してください", + "请填写当前环境对应的 Product ID": "", + "请填写当前环境对应的 Store ID": "", + "请填写当前环境对应的商户 ID": "", "请填写渠道名称和渠道密钥!": "チャネル名とAPIキーを入力してください", "请填写部署地区": "デプロイ先リージョンを入力してください", "请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "APIキーは大切に保管し、他人に漏洩しないでください。セキュリティ上の懸念がある場合は、速やかにAPIキーを再発行してください。", @@ -3028,16 +3150,20 @@ "请求体内存缓存": "リクエストボディメモリキャッシュ", "请求体磁盘缓存": "リクエストボディディスクキャッシュ", "请求体超过此大小时使用磁盘缓存": "リクエストボディがこのサイズを超えた場合にディスクキャッシュを使用", + "请求参数": "", "请求参数无效": "Invalid request parameters", "请求发生错误": "リクエストでエラーが発生しました", "请求发生错误: ": "リクエストでエラーが発生しました:", "请求后端接口失败:": "バックエンドAPIリクエストに失敗しました:", "请求失败": "リクエストに失敗しました", + "请求头": "", "请求头覆盖": "リクエストヘッダーの上書き", "请求并计费模型": "リクエスト課金モデル", "请求时长: ${time}s": "応答時間:${time}s", + "请求条件调价": "", "请求次数": "リクエスト数", "请求结束后多退少补": "リクエスト完了後、差額が精算されます", + "请求规则": "", "请求超时,请刷新页面后重新发起 GitHub 登录": "タイムアウトしました。ページをリロードして GitHub ログインをやり直してください", "请求路径": "Request path", "请求转换": "リクエスト変換", @@ -3063,18 +3189,20 @@ "请至少选择一个渠道": "チャネルを少なくとも1つ選択してください", "请输入 API Key,一行一个,格式:APIKey|Region": "Enter API Key, one per line, format: APIKey|Region", "请输入 API Key,格式:APIKey|Region": "Enter API Key, format: APIKey|Region", - "请输入 Authorization Endpoint": "Authorization Endpointを入力してください", "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "AZURE_OPENAI_ENDPOINTを入力してください(例:https://docs-test-001.openai.azure.com)", + "请输入 Authorization Endpoint": "Authorization Endpointを入力してください", "请输入 Client ID": "Client IDを入力してください", "请输入 Client Secret": "Client Secretを入力してください", - "请输入 io.net API Key": "Please enter io.net API Key", - "请输入 io.net API Key(敏感信息不显示)": "Please enter io.net API Key (sensitive information not displayed)", "请输入 JSON 格式的 OAuth 凭据,例如:\n{\n \"access_token\": \"...\",\n \"account_id\": \"...\" \n}": "JSON形式のOAuth資格情報を入力してください。例:\n{\n \"access_token\": \"...\",\n \"account_id\": \"...\" \n}", "请输入 JSON 格式的密钥内容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}": "JSON形式のAPIキーを入力してください。例:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}", "请输入 OIDC 的 Well-Known URL": "OIDCのWell-Known URLを入力してください", + "请输入 Product ID": "", "请输入 Slug": "Slugを入力してください", + "请输入 Store ID": "", "请输入 Token Endpoint": "Token Endpointを入力してください", "请输入 User Info Endpoint": "User Info Endpointを入力してください", + "请输入 io.net API Key": "Please enter io.net API Key", + "请输入 io.net API Key(敏感信息不显示)": "Please enter io.net API Key (sensitive information not displayed)", "请输入6位验证码或8位备用码": "6桁の認証コードまたは8桁のバックアップコードを入力してください", "请输入API地址": "ベースURLを入力してください", "请输入API地址!": "ベースURLを入力してください!", @@ -3084,9 +3212,9 @@ "请输入Gotify服务器地址": "GotifyサーバーURLを入力してください", "请输入Gotify服务器地址,例如: https://gotify.example.com": "GotifyサーバーURLを入力してください(例: https://gotify.example.com)", "请输入JSON数组,如 [\"model-a\",\"model-b\"]": "JSON配列を入力してください(例:[\"model-a\",\"model-b\"])", + "请输入URL链接": "URLを入力してください", "请输入Uptime Kuma地址": "Uptime Kumaアドレスを入力してください", "请输入Uptime Kuma服务地址,如:https://status.example.com": "Uptime Kumaサービスアドレスを入力してください(例: https://status.example.com)", - "请输入URL链接": "URLを入力してください", "请输入Webhook地址": "Webhook URLを入力してください", "请输入Webhook地址,例如: https://example.com/webhook": "Webhook URLを入力してください(例:https://example.com/webhook)", "请输入以下文字以确认:": "Please type the following text to confirm:", @@ -3105,6 +3233,7 @@ "请输入原密码": "現在のパスワードを入力してください", "请输入原密码!": "現在のパスワードを入力してください", "请输入名称": "名称を入力してください", + "请输入商户 ID": "", "请输入回答内容": "回答を入力してください", "请输入回答内容(支持 Markdown/HTML)": "回答を入力してください(Markdown/HTMLに対応しています)", "请输入图标名称": "アイコン名を入力してください", @@ -3112,14 +3241,14 @@ "请输入备注(仅管理员可见)": "備考を入力してください(管理者のみ閲覧可能です)", "请输入套餐标题": "プラン名を入力してください", "请输入完整的 JSON 格式密钥内容": "完全なJSON形式のAPIキーを入力してください", - "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "完全なURLを入力してください(例:https://api.openai.com/v1/chat/completions)", "请输入完整的URL链接": "完全なURLを入力してください", + "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "完全なURLを入力してください(例:https://api.openai.com/v1/chat/completions)", "请输入容器名称": "Please enter container name", "请输入密码": "パスワードを入力してください", "请输入密钥": "APIキーを入力してください", + "请输入密钥!": "APIキーを入力してください", "请输入密钥,一行一个": "APIキーを入力してください(1行に1つずつ)", "请输入密钥,一行一个,格式:AccessKey|SecretAccessKey|Region": "Enter keys one per line, format: AccessKey|SecretAccessKey|Region", - "请输入密钥!": "APIキーを入力してください", "请输入延长时长": "Please enter extension duration", "请输入总额度": "総クォータを入力してください", "请输入您的密码": "パスワードを入力してください", @@ -3173,9 +3302,9 @@ "请输入认证器验证码或备用码": "オーセンティケーターの認証コードまたはバックアップコードを入力してください", "请输入说明": "説明を入力してください", "请输入运行时长": "Please enter runtime duration", - "请输入邮箱!": "メールアドレスを入力してください", "请输入邮箱地址": "メールアドレスを入力してください", "请输入邮箱验证码!": "メール認証コードを入力してください", + "请输入邮箱!": "メールアドレスを入力してください", "请输入部署名称": "Please enter deployment name", "请输入部署名称以完成二次确认": "Enter deployment name to complete secondary confirmation", "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "デプロイ先リージョンを入力してください(例:us-central1)\nモデルマッピング形式に対応しています\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}", @@ -3227,9 +3356,6 @@ "调用趋势": "呼び出し推移", "调试信息": "デバッグ情報", "谨慎": "注意", - "警告": "警告", - "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "警告:キープアライブを有効にした後、データ書き込み後にチャネルエラーが発生した場合、システムは再試行できません。有効化が必須の場合は、Ping間隔を可能な限り長く設定することを推奨します", - "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "警告:2要素認証を無効にすると、認証設定とすべてのバックアップコードが永久に削除されます。この操作は元に戻すことができません", "豆包": "豆包", "账单": "請求情報", "账户充值": "アカウントチャージ", @@ -3261,6 +3387,7 @@ "跨分组": "グループ間", "跨分组特殊倍率": "クロスグループ特殊レート", "跨分组重试": "グループ間リトライ", + "跨夜范围": "", "路径正则": "パス正規表現", "路径正则(每行一个)": "パス正規表現(1行に1つ)", "跳转": "リダイレクト", @@ -3271,12 +3398,15 @@ "轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能": "ポーリングモードは、Redisとメモリキャッシュ機能との併用が必須です。併用しない場合、パフォーマンスが大幅に低下し、ポーリング機能も実現できません", "软错误": "", "输入": "入力", - "输入 {{price}} / 1M tokens": "入力 {{price}} / 1M tokens", "输入 $/1M tokens": "", "输入 OIDC 的 Authorization Endpoint": "OIDCのAuthorization Endpointを入力してください", "输入 OIDC 的 Client ID": "OIDCのClient IDを入力してください", "输入 OIDC 的 Token Endpoint": "OIDCのToken Endpointを入力してください", "输入 OIDC 的 Userinfo Endpoint": "OIDCのUserinfo Endpointを入力してください", + "输入 Token": "", + "输入 Token 数": "", + "输入 Token 数量,查看按当前配置的预计费用(不含分组倍率)。": "", + "输入 {{price}} / 1M tokens": "入力 {{price}} / 1M tokens", "输入IP地址后回车,如:8.8.8.8": "IPアドレスを入力してEnter(例:8.8.8.8)", "输入JSON对象": "JSONオブジェクトを入力してください", "输入价格": "入力価格", @@ -3302,6 +3432,7 @@ "输入补全价格": "補完料金を入力してください", "输入补全倍率": "補完倍率を入力してください", "输入要添加的邮箱域名": "追加するメールドメインを入力してください", + "输入计费表达式...": "", "输入认证器应用显示的6位数字验证码": "認証アプリに表示される6桁の認証コードを入力してください", "输入邮箱地址": "メールアドレスを入力", "输入金额": "金額を入力", @@ -3310,15 +3441,17 @@ "输入验证码": "認証コードを入力してください", "输入验证码完成设置": "認証コードを入力して設定を完了してください", "输出": "出力", + "输出 Token": "", + "输出 Token 数": "", "输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}": "補完 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}", - "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 補完倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", - "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 出力倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "输出价格": "補完料金", "输出价格 {{symbol}}{{price}} / 1M tokens": "補完料金 {{symbol}}{{price}} / 1M tokens", "输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "補完料金:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (補完倍率:{{completionRatio}})", "输出价格:{{symbol}}{{price}} / 1M tokens": "補完料金:{{symbol}}{{price}} / 1M tokens", "输出价格:{{symbol}}{{total}} / 1M tokens": "補完料金:{{symbol}}{{total}} / 1M tokens", "输出倍率 {{completionRatio}}": "Output ratio {{completionRatio}}", + "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 補完倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 出力倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "边栏设置": "サイドバー設定", "过期于": "有効期限", "过期时间": "有効期限", @@ -3334,12 +3467,14 @@ "返回修改": "Go back and edit", "返回登录": "ログインに戻る", "返回错误": "", + "这个公式比较复杂,下面的简化表单没法完整还原,请在表达式编辑模式下修改。": "", "这个界面默认按价格填写,保存时会自动换算回后端需要的倍率 JSON。": "この画面では価格を基準に入力し、保存時にバックエンドが必要とする倍率 JSON に自動変換されます。", "这些价格都是可选项,不填也可以。": "これらの価格はすべて任意項目で、未入力でも構いません。", "这将删除超过 10 分钟未使用的临时缓存文件": "10分以上使用されていない一時キャッシュファイルを削除します", "这是基础金额,实际扣费 = 基础金额 x 系统分组倍率。": "これは基本金額です。実際の課金 = 基本金額 × システムグループ倍率。", "这是重复键中的最后一个,其值将被使用": "重複するキーのうち、最後のキーの値が使用されます", "这里直接编辑 JSON 对象。适合简单覆盖参数的场景。": "ここでJSONオブジェクトを直接編集します。シンプルなパラメータオーバーライドシナリオに適しています。", + "这里配置 Waffo 下展示给用户的 Card、Apple Pay、Google Pay 等子支付方式。": "", "进度": "進捗", "进行中": "進行中", "进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "この操作の実行時、チャネルへのアクセスエラーが発生する可能性があります。データベースに問題がある場合のみ使用してください", @@ -3396,7 +3531,9 @@ "透传请求头": "", "递归": "再帰", "递归策略": "再帰戦略", + "逗号分隔的通配符模式。设置后,仅匹配的客户端 User-Agent 可使用此渠道。留空则允许所有客户端。": "", "通义千问": "Qwen", + "通用缓存": "", "通用设置": "一般設定", "通知": "通知", "通知、价格和隐私相关设置": "通知、料金、プライバシー関連設定", @@ -3466,7 +3603,7 @@ "配置 Turnstile": "Turnstile 設定", "配置 Waffo 充值时可用的支付方式,保存后在充值页面展示给用户。": "", "配置 WeChat Server": "WeChatサーバー設定", - "配置:": "設定:", + "配置各工具的调用价格($/1K次调用)。按次计费模型不额外收取工具费用。": "", "配置后的效果:": "設定後の効果:", "配置和消息已全部重置": "設定とメッセージがすべてリセットされました", "配置套餐的有效时长": "プランの有効期間を設定", @@ -3485,6 +3622,7 @@ "配置自定义 OAuth 提供商,支持 GitHub Enterprise、GitLab、Gitea、Nextcloud、Keycloak、ORY 等兼容 OAuth 2.0 协议的身份提供商": "カスタムOAuthプロバイダーを設定。GitHub Enterprise、GitLab、Gitea、Nextcloud、Keycloak、ORYなどのOAuth 2.0互換IDプロバイダーをサポート", "配置说明": "設定の説明", "配置邮箱域名白名单": "メールドメインのホワイトリスト設定", + "配置:": "設定:", "重启部署失败": "Failed to restart deployment", "重命名部署": "Rename Deployment", "重复提交": "二重送信", @@ -3538,6 +3676,7 @@ "镜像配置": "Image Configuration", "问题标题": "質問タイトル", "队列中": "待機中", + "阶梯计费": "", "阶梯计费(未匹配到对应阶梯)": "段階課金(一致する階層なし)", "阶梯计费(表达式解析失败)": "段階課金(式の解析に失敗)", "附加条件": "追加条件", @@ -3547,15 +3686,15 @@ "限制周期统一使用上方配置的“限制周期”值。": "制限期間は、一律で上記にて設定された「制限期間」の値を使用します。", "限流": "レート制限", "限购": "購入制限", + "随机": "ランダム", + "随机模式": "ランダムモード", + "随机种子 (留空为随机)": "ランダムシード(空欄でランダム)", "隐私政策": "プライバシーポリシー", "隐私政策已更新": "プライバシーポリシーが更新されました", "隐私政策更新失败": "プライバシーポリシーの更新に失敗しました", "隐私设置": "プライバシー設定", "隐藏操作项": "操作項目を非表示", "隐藏调试": "デバッグを非表示", - "随机": "ランダム", - "随机模式": "ランダムモード", - "随机种子 (留空为随机)": "ランダムシード(空欄でランダム)", "零一万物": "Yi", "需要安全验证": "セキュリティ認証が必要です", "需要添加的额度(支持负数)": "追加する残高(マイナス値も可)", @@ -3577,13 +3716,13 @@ "音频补全价格:{{symbol}}{{price}} / 1M tokens": "音声補完価格:{{symbol}}{{price}} / 1M tokens", "音频补全倍率": "", "音频补全倍率(仅部分模型支持该计费)": "オーディオ補完倍率(一部のモデルのみこの課金に対応)", - "音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音声入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 音声倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "音频输入价格": "音声入力価格", "音频输入价格:{{symbol}}{{price}} / 1M tokens": "音声入力価格:{{symbol}}{{price}} / 1M tokens", "音频输入倍率": "", "音频输入相关的倍率设置,键为模型名称,值为倍率": "オーディオ入力に関する倍率設定です。キー:モデル名、値:倍率。", - "音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音声出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 音声倍率 {{audioRatio}} * 音声補完倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音声入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 音声倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "音频输出补全相关的倍率设置,键为模型名称,值为倍率": "オーディオ補完に関する倍率設定です。キー:モデル名、値:倍率。", + "音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音声出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 音声倍率 {{audioRatio}} * 音声補完倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "页脚": "フッター", "页面未找到,请检查您的浏览器地址是否正确": "ページが見つかりませんでした。ブラウザのアドレスが正しいかご確認ください", "页面渲染出错,请刷新页面重试": "ページのレンダリング中にエラーが発生しました。ページを更新して再試行してください。", @@ -3601,10 +3740,11 @@ "预览更新": "更新のプレビュー", "预览模板": "テンプレートをプレビュー", "预览请求体": "リクエストボディのプレビュー", + "预警阈值必须为正数": "アラートしきい値は0より大きい必要があります", "预计结束": "Estimated End", "预计结果": "予想結果", + "预计费用": "", "预设模板": "プリセットテンプレート", - "预警阈值必须为正数": "アラートしきい値は0より大きい必要があります", "频率惩罚,减少重复词汇的出现": "頻度ペナルティ、単語の繰り返しを減少", "频率限制的周期(分钟)": "レート制限の期間(分)", "颜色": "カラー", @@ -3644,6 +3784,7 @@ "默认 Responses API 版本,为空则使用上方版本": "デフォルトのレスポンスAPIバージョン。未入力の場合、上記のバージョンが使用されます。", "默认 TTL(秒)": "デフォルトTTL(秒)", "默认为 5m 缓存创建倍率;1h 缓存创建倍率按固定乘法自动计算(当前为 1.6x)": "デフォルトは5mのキャッシュ作成倍率です。1hのキャッシュ作成倍率は固定乗数で自動計算されます(現在は1.6倍)", + "默认使用 USD 结算": "", "默认使用auto分组": "デフォルトでautoグループを使用", "默认使用系统名称": "デフォルトのシステム名称", "默认助手消息": "こんにちは!何かお手伝いできることはありますか?", @@ -3653,6 +3794,12 @@ "默认折叠侧边栏": "サイドバーをデフォルトで折りたたむ", "默认测试模型": "デフォルトテストモデル", "默认用户消息": "こんにちは", - "默认补全倍率": "デフォルト補完倍率" + "默认补全倍率": "デフォルト補完倍率", + "(共 {{total}} 个)": "", + "(共 {{total}} 个,省略 {{omit}} 个)": "", + ",": ", ", + ",当前无生效订阅,将自动使用钱包": "、有効なサブスクリプションがないため、自動的にウォレットを使用します", + ",时间:": "、時間:", + ",点击更新": "、クリックして更新してください" } } diff --git a/web/classic/src/i18n/locales/ru.json b/web/classic/src/i18n/locales/ru.json index 63660aa14a8..37307d1fa79 100644 --- a/web/classic/src/i18n/locales/ru.json +++ b/web/classic/src/i18n/locales/ru.json @@ -13,13 +13,8 @@ " 吗?": "?", " 秒": " сек", " 秒。": " сек.", - ",": ", ", - ",当前无生效订阅,将自动使用钱包": ", нет активной подписки, автоматически будет использоваться кошелек.", - ",时间:": ", время: ", - ",点击更新": ", нажмите для обновления", - "、": ", ", - "(共 {{total}} 个,省略 {{omit}} 个)": "", - "(共 {{total}} 个)": "", + "$/1M tokens": "$/1M токенов", + "$/次": "", "(筛选后显示 {{count}} 条)_few": "(Показано {{count}} элемента после фильтрации)", "(筛选后显示 {{count}} 条)_many": "(Показано {{count}} элементов после фильтрации)", "(筛选后显示 {{count}} 条)_one": "(Showing {{count}} item after filtering)", @@ -28,26 +23,6 @@ "(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(Ввод {{nonAudioInput}} токенов / 1M токенов * {{symbol}}{{price}} + аудио ввод {{audioInput}} токенов / 1M токенов * {{symbol}}{{audioPrice}}", "(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}": "(Ввод {{nonCacheInput}} токенов / 1M токенов * {{symbol}}{{price}} + кэш {{cacheInput}} токенов / 1M токенов * {{symbol}}{{cachePrice}}", "(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}": "(Ввод {{nonImageInput}} токенов + ввод изображения {{imageInput}} токенов / 1M токенов * {{symbol}}{{price}}", - "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "[Максимальное количество запросов] и [Максимальное количество выполненных запросов] имеют максимальное значение 2147483647.", - "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[Максимальное количество запросов] должно быть больше или равно 0, [Максимальное количество выполненных запросов] должно быть больше или равно 1.", - "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", - "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", - "{{count}} 项操作_few": "", - "{{count}} 项操作_many": "", - "{{count}} 项操作_one": "", - "{{count}} 项操作_other": "", - "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", - "{{name}} ID": "{{name}} ID", - "{{ratioType}} {{ratio}}": "{{ratioType}} {{ratio}}", - "{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x", - "「用户可选」决定用户创建令牌时能否自主选择该分组。未勾选的分组只能由管理员在后台分配给用户,不会出现在用户的令牌创建页面中。": "\"User Selectable\" controls whether users can choose this group when creating tokens. Unchecked groups can only be assigned by admin and won't appear in the token creation page.", - "• 视频服务商的跨域限制": "• Ограничения кросс-доменных запросов со стороны видеосервиса", - "• 防盗链保护机制": "• Защита от хотлинков", - "• 需要特定的请求头或认证": "• Требуются специальные заголовки или авторизация", - "© {{currentYear}}": "© {{currentYear}}", - "| 基于": "| Основано на", - "$/1M tokens": "$/1M токенов", - "$/次": "", "0 - 最低": "0 - Минимум", "0 表示不限": "0 означает без лимита", "0.002-1之间的小数": "Десятичное число между 0.002-1", @@ -57,10 +32,10 @@ "1h缓存创建 {{price}} / 1M tokens": "Создание кэша 1h {{price}} / 1M tokens", "1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Создание кэша 1h {{tokens}} токенов / 1M токенов * {{symbol}}{{price}}", "1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "Создание кеша 1ч {{tokens}} токенов / 1M токенов * {{symbol}}{{price}} (множитель: {{ratio}})", - "1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "Создание кэша 1h: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент создания кэша 1h {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}", "1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Цена создания кеша 1h {{symbol}}{{price}} / 1M tokens", "1h缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (1h缓存创建倍率: {{cacheCreationRatio1h}})": "Цена создания кеша за 1ч: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M токенов (множитель создания 1ч: {{cacheCreationRatio1h}})", "1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Цена создания кеша 1h: {{symbol}}{{price}} / 1M tokens", + "1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "Создание кэша 1h: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент создания кэша 1h {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}", "2 - 低": "2 - Низкий", "2. 管理员在用户管理中将用户分配到对应分组(如 vip)": "2. Admin assigns users to groups in User Management (e.g., vip)", "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "Каналы, добавленные после 10 мая 2025 года, не требуют удаления \".\" из имен моделей при развертывании", @@ -71,17 +46,16 @@ "5m缓存创建 {{price}} / 1M tokens": "Создание кэша 5m {{price}} / 1M tokens", "5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Создание кэша 5m {{tokens}} токенов / 1M токенов * {{symbol}}{{price}}", "5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "Создание кеша 5м {{tokens}} токенов / 1M токенов * {{symbol}}{{price}} (множитель: {{ratio}})", - "5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "Создание кэша 5m: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент создания кэша 5m {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}", "5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Цена создания кеша 5m {{symbol}}{{price}} / 1M tokens", "5m缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (5m缓存创建倍率: {{cacheCreationRatio5m}})": "Цена создания кеша за 5м: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M токенов (множитель создания 5м: {{cacheCreationRatio5m}})", "5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Цена создания кеша 5m: {{symbol}}{{price}} / 1M tokens", + "5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "Создание кэша 5m: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент создания кэша 5m {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}", "8 - 高": "8 - Высокий", "AGPL v3.0协议": "Лицензия AGPL v3.0", "AI 对话": "AI диалог", "AI模型测试环境": "Среда тестирования AI моделей", "AI模型配置": "Конфигурация AI моделей", "AK/SK 模式:使用 AccessKey 和 SecretAccessKey;API Key 模式:使用 API Key": "Режим AK/SK: используйте AccessKey и SecretAccessKey; режим API Key: используйте API Key", - "anthropic-beta JSON 示例": "", "API Key": "API Key", "API Key 模式下不支持批量创建": "Режим API Key не поддерживает массовое создание", "API Key 验证失败": "API Key verification failed", @@ -90,7 +64,12 @@ "API 密钥": "Ключ API", "API 密钥 (沙盒)": "", "API 密钥 (生产)": "", + "API 密钥(测试环境)": "", + "API 密钥(生产环境)": "", "API 文档": "Документация API", + "API 私钥": "", + "API 私钥(测试环境)": "", + "API 私钥(生产环境)": "", "API 配置": "Конфигурация API", "API令牌管理": "Управление токенами API", "API使用记录": "История использования API", @@ -99,13 +78,14 @@ "API地址": "Адрес API", "API渠道配置": "Конфигурация каналов API", "API端点": "Конечная точка API", - "Authorization callback URL 填": "URL обратного вызова авторизации:", "Authorization Endpoint": "Конечная точка авторизации", - "auto分组调用链路": "Цепочка вызовов автоматической группировки", + "Authorization callback URL 填": "URL обратного вызова авторизации:", "Bark推送URL": "URL для push-уведомлений Bark", "Bark推送URL必须以http://或https://开头": "URL для push-уведомлений Bark должен начинаться с http:// или https://", "Bark通知": "Уведомления Bark", "Basic Auth 头": "Заголовок Basic Auth", + "CPU 使用率超过此值时拒绝请求": "Отклонять запросы, когда использование CPU превышает это значение", + "CPU 阈值 (%)": "Порог CPU (%)", "Cached tokens": "Cached tokens", "Cached tokens 占比口径由后端返回:Claude 语义按 cached/(prompt+cached),其余按 cached/prompt。": "Доля кэшированных токенов возвращается бэкендом: семантика Claude считает cached/(prompt+cached), остальные — cached/prompt.", "Changing batch type to:": "Изменение типа пакета на:", @@ -120,20 +100,13 @@ "Client ID": "ID клиента", "Client Secret": "Секрет клиента", "Codex 渠道不支持批量创建": "Канал Codex не поддерживает пакетное создание", - "common.changeLanguage": "common.changeLanguage", "Completion tokens": "Completion tokens", "Configuration": "Конфигурация", - "context_int/context_string 从请求上下文读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "context_int/context_string читаются из контекста запроса; gjson читает из JSON body входящего запроса по gjson path.", - "CPU 使用率超过此值时拒绝请求": "Отклонять запросы, когда использование CPU превышает это значение", - "CPU 阈值 (%)": "Порог CPU (%)", "Creem API 密钥,敏感信息不显示": "API-ключ Creem, чувствительные данные не отображаются", "Creem Setting Tips": "Creem поддерживает только преднастроенные товары с фиксированной суммой. Эти товары и их цены нужно заранее создать и настроить на сайте Creem, поэтому пополнения с произвольной суммой не поддерживаются. Настройте название и цену товара в Creem, получите идентификатор товара и укажите его ниже. Затем задайте сумму пополнения и отображаемую цену в new-api.", "Creem 介绍": "О сервисе Creem", "Creem 充值": "Пополнение через Creem", "Creem 设置": "Настройки Creem", - "default 和 vip 只能由管理员在「用户管理」中分配给用户。适用于按用户等级定价、内部测试等不希望用户自主选择的场景。": "default and vip can only be assigned to users by admin in \"User Management\". Suitable for tiered pricing, internal testing, or other scenarios where user self-selection is not desired.", - "default为默认设置,可单独设置每个分类的安全等级": "default - это настройка по умолчанию, можно отдельно установить уровень безопасности для каждой категории", - "default为默认设置,可单独设置每个模型的版本": "default - это настройка по умолчанию, можно отдельно установить версию для каждой модели", "Dify渠道只适配chatflow和agent,并且agent不支持图片!": "Канал Dify адаптирован только для chatflow и agent, и agent не поддерживает изображения!", "Discord": "Discord", "Discord Client ID": "ID клиента Discord", @@ -143,10 +116,11 @@ "Discovery scopes": "Discovery scopes", "Discovery 建议 scopes:": "Рекомендуемые Discovery scopes:", "EUR (欧元)": "EUR (евро)", - "false": "false", "GC 已执行": "GC выполнен", "GC 执行失败": "Ошибка выполнения GC", "GC 次数": "Количество GC", + "GPU/容器": "GPU/Container", + "GPU数量": "Number of GPUs", "Gemini安全设置": "Настройки безопасности Gemini", "Gemini思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Адаптация мышления Gemini BudgetTokens = MaxTokens * процент BudgetTokens", "Gemini思考适配设置": "Настройки адаптации мышления Gemini", @@ -161,14 +135,10 @@ "Gotify服务器地址": "Адрес сервера Gotify", "Gotify服务器地址必须以http://或https://开头": "Адрес сервера Gotify должен начинаться с http:// или https://", "Gotify通知": "Уведомления Gotify", - "GPU/容器": "GPU/Container", - "GPU数量": "Number of GPUs", "Grok设置": "Настройки Grok", "Haiku 模型": "Модель Haiku", "Homepage URL 填": "URL домашней страницы:", "ID": "ID", - "include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护": "include_obfuscation управляет полями обфускации в потоке Responses. Отключено по умолчанию, чтобы клиенты не отключали эту защиту", - "inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息": "Поле inference_geo управляет регионом размещения данных инференса Claude. Отключено по умолчанию для предотвращения несанкционированной передачи географической информации", "IP": "IP", "IP白名单": "IP Whitelist", "IP白名单(支持CIDR表达式)": "Белый список IP (поддерживает выражения CIDR)", @@ -176,6 +146,7 @@ "IP黑名单": "Черный список IP", "JSON": "JSON", "JSON 已格式化": "JSON отформатирован", + "JSON 必须是对象": "", "JSON 文本": "Текст JSON", "JSON 无效": "Недопустимый JSON", "JSON 格式参考": "JSON Reference", @@ -208,6 +179,7 @@ "Ollama 模型管理": "Ollama Model Management", "Ollama 版本信息": "Ollama Version Info", "Opus 模型": "Модель Opus", + "POST 参数": "Параметры POST", "Passkey": "Passkey", "Passkey 已解绑": "Passkey отвязан", "Passkey 已重置": "Passkey сброшен", @@ -218,71 +190,73 @@ "Pay Method Name": "", "Pay Method Type": "", "Ping间隔(秒)": "Интервал Ping (секунды)", - "POST 参数": "Параметры POST", - "price_xxx 的商品价格 ID,新建产品后可获得": "ID цены товара price_xxx, можно получить после создания нового продукта", + "Product ID": "", "Prompt cache hit tokens": "Prompt cache hit tokens", "Prompt tokens": "Prompt tokens", - "Reasoning Effort": "Усилие рассуждения", - "Request ID": "Request ID", "RSA 私钥 (沙盒)": "", "RSA 私钥 (生产)": "", - "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "Поле safety_identifier помогает OpenAI идентифицировать пользователей приложений, которые могут нарушать политику использования. По умолчанию отключено для защиты конфиденциальности пользователей", - "Scopes(可选)": "Scopes (необязательно)", - "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "Поле service_tier используется для указания уровня сервиса, позволяет передавать параметры, которые могут привести к фактической оплате выше ожидаемой. По умолчанию отключено для избежания дополнительных расходов", - "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "Ключ Stripe sk_xxx или rk_xxx, конфиденциальная информация не отображается", + "Reasoning Effort": "Усилие рассуждения", + "Request ID": "Request ID", "SMTP 加密方式": "Шифрование SMTP", "SMTP 发送者邮箱": "Email отправителя SMTP", "SMTP 服务器地址": "Адрес сервера SMTP", "SMTP 端口": "Порт SMTP", "SMTP 访问凭证": "Учетные данные доступа SMTP", "SMTP 账户": "Учетная запись SMTP", - "Sonnet 模型": "Модель Sonnet", - "speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "Поле speed управляет режимом скорости инференса Claude. По умолчанию отключено, чтобы избежать непреднамеренного переключения в режим fast", "SSE 事件": "Событие SSE", "SSE数据流": "Поток данных SSE", "SSL/TLS": "SSL/TLS", "SSRF防护开关详细说明": "Подробное описание переключателя защиты SSRF", "SSRF防护设置": "Настройки защиты SSRF", "SSRF防护详细说明": "Подробное описание защиты SSRF", - "standard 已被移除,vip 用户看不到": "standard has been removed, vip users cannot see it", "STARTTLS": "STARTTLS", - "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "Поле store используется для авторизации OpenAI хранить данные запросов для оценки и оптимизации продукта. По умолчанию отключено, после включения может привести к неработоспособности Codex", + "Scopes(可选)": "Scopes (необязательно)", + "Sonnet 模型": "Модель Sonnet", + "Store ID": "", "Stripe 设置": "Настройки Stripe", "Stripe/Creem 商品ID(可选)": "ID продукта Stripe/Creem (необязательно)", "Stripe/Creem 需在第三方平台创建商品并填入 ID": "Товары Stripe/Creem нужно создать на сторонней платформе и указать их ID", + "TTL(秒)": "TTL (секунды)", + "TTL(秒,0 表示默认)": "TTL (секунды, 0 — по умолчанию)", "Telegram": "Telegram", "Telegram Bot Token": "Токен бота Telegram", "Telegram Bot 名称": "Имя бота Telegram", "Telegram ID": "ID Telegram", "Token Endpoint": "Конечная точка токена", - "token 会按倍率换算成“额度/次数”,请求结束后再做差额结算(补扣/返还)。": "Токены конвертируются в квоту/количество использований по коэффициенту. После завершения запроса производится расчёт разницы (дополнительное списание/возврат).", + "Token 估算器": "", "Total tokens": "Total tokens", - "true": "true", - "TTL(秒,0 表示默认)": "TTL (секунды, 0 — по умолчанию)", - "TTL(秒)": "TTL (секунды)", "Turnstile Secret Key": "Секретный ключ Turnstile", "Turnstile Site Key": "Ключ сайта Turnstile", - "Unix时间戳": "Временная метка Unix", - "Uptime Kuma地址": "Адрес Uptime Kuma", - "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Управление категориями мониторинга Uptime Kuma, можно настроить несколько категорий мониторинга для отображения статуса сервисов (максимум 20)", "URL 标识,只能包含小写字母、数字和连字符": "Идентификатор URL, допускаются только строчные буквы, цифры и дефисы", "URL链接": "URL ссылка", "USD (美元)": "USD (доллар США)", + "Unix时间戳": "Временная метка Unix", + "Uptime Kuma地址": "Адрес Uptime Kuma", + "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Управление категориями мониторинга Uptime Kuma, можно настроить несколько категорий мониторинга для отображения статуса сервисов (максимум 20)", "User Info Endpoint": "Конечная точка информации о пользователе", "User-Agent include(每行一个,可不写)": "User-Agent include (по одному в строке, необязательно)", + "User-Agent 过滤": "", "Value 正则": "Regex значения", "Vertex AI 不支持 functionResponse.id 字段,开启后将自动移除该字段": "Vertex AI не поддерживает поле functionResponse.id. При включении это поле будет автоматически удалено", - "Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)": "", "Waffo API 参数,可空(最多64位)": "", + "Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)": "", + "Waffo Pancake 设置": "", "Waffo 充值": "", "Waffo 充值的最低数量,默认 1": "", "Waffo 公钥 (沙盒)": "", "Waffo 公钥 (生产)": "", + "Waffo 公钥(测试环境)": "", + "Waffo 公钥(生产环境)": "", "Waffo 商户 ID": "", + "Waffo 当前使用 USD 结算": "", "Waffo 是一个支付聚合平台,支持多种支付方式。": "", "Waffo 设置": "", - "Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web-поиск: {{count}} / 1K * цена за единицу {{price}} * {{ratioType}} {{ratio}} = {{amount}}", + "WeChat Server 服务器地址": "Адрес сервера WeChat Server", + "WeChat Server 访问凭证": "Учетные данные доступа WeChat Server", "Web 搜索调用 {{webSearchCallCount}} 次": "Web-поиск вызван {{webSearchCallCount}} раз", + "Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web-поиск: {{count}} / 1K * цена за единицу {{price}} * {{ratioType}} {{ratio}} = {{amount}}", + "Webhook 公钥(测试环境)": "", + "Webhook 公钥(生产环境)": "", "Webhook 密钥": "Секрет вебхука", "Webhook 签名密钥": "Ключ подписи Webhook", "Webhook地址": "Адрес Webhook", @@ -290,19 +264,58 @@ "Webhook请求结构说明": "Описание структуры запроса Webhook", "Webhook通知": "Уведомления Webhook", "Web搜索价格:{{symbol}}{{price}} / 1K 次": "Цена Web-поиска: {{symbol}}{{price}} / 1K раз", - "WeChat Server 服务器地址": "Адрес сервера WeChat Server", - "WeChat Server 访问凭证": "Учетные данные доступа WeChat Server", "Well-Known URL": "Well-Known URL", "Well-Known URL 必须以 http:// 或 https:// 开头": "Well-Known URL должен начинаться с http:// или https://", - "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "Ключ подписи Webhook whsec_xxx, конфиденциальная информация не отображается", "Worker地址": "Адрес Worker", "Worker密钥": "Ключ Worker", + "X 也可以小于 1,当折扣用。想做\"只给输出加价\"或\"额外加固定费用\",请直接写完整计费公式。": "", + "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "[Максимальное количество запросов] и [Максимальное количество выполненных запросов] имеют максимальное значение 2147483647.", + "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[Максимальное количество запросов] должно быть больше или равно 0, [Максимальное количество выполненных запросов] должно быть больше или равно 1.", + "anthropic-beta JSON 示例": "", + "auto分组调用链路": "Цепочка вызовов автоматической группировки", + "common.changeLanguage": "common.changeLanguage", + "context_int/context_string 从请求上下文读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "context_int/context_string читаются из контекста запроса; gjson читает из JSON body входящего запроса по gjson path.", + "context_int/context_string 从请求上下文读取;header 从 HTTP 请求头读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "", + "default 和 vip 只能由管理员在「用户管理」中分配给用户。适用于按用户等级定价、内部测试等不希望用户自主选择的场景。": "default and vip can only be assigned to users by admin in \"User Management\". Suitable for tiered pricing, internal testing, or other scenarios where user self-selection is not desired.", + "default为默认设置,可单独设置每个分类的安全等级": "default - это настройка по умолчанию, можно отдельно установить уровень безопасности для каждой категории", + "default为默认设置,可单独设置每个模型的版本": "default - это настройка по умолчанию, можно отдельно установить версию для каждой модели", + "false": "false", + "include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护": "include_obfuscation управляет полями обфускации в потоке Responses. Отключено по умолчанию, чтобы клиенты не отключали эту защиту", + "inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息": "Поле inference_geo управляет регионом размещения данных инференса Claude. Отключено по умолчанию для предотвращения несанкционированной передачи географической информации", + "price_xxx 的商品价格 ID,新建产品后可获得": "ID цены товара price_xxx, можно получить после создания нового продукта", + "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "Поле safety_identifier помогает OpenAI идентифицировать пользователей приложений, которые могут нарушать политику использования. По умолчанию отключено для защиты конфиденциальности пользователей", + "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "Поле service_tier используется для указания уровня сервиса, позволяет передавать параметры, которые могут привести к фактической оплате выше ожидаемой. По умолчанию отключено для избежания дополнительных расходов", + "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "Ключ Stripe sk_xxx или rk_xxx, конфиденциальная информация не отображается", + "speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "Поле speed управляет режимом скорости инференса Claude. По умолчанию отключено, чтобы избежать непреднамеренного переключения в режим fast", + "standard 已被移除,vip 用户看不到": "standard has been removed, vip users cannot see it", + "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "Поле store используется для авторизации OpenAI хранить данные запросов для оценки и оптимизации продукта. По умолчанию отключено, после включения может привести к неработоспособности Codex", + "token 会按倍率换算成“额度/次数”,请求结束后再做差额结算(补扣/返还)。": "Токены конвертируются в квоту/количество использований по коэффициенту. После завершения запроса производится расчёт разницы (дополнительное списание/возврат).", + "true": "true", + "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "Ключ подписи Webhook whsec_xxx, конфиденциальная информация не отображается", + "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", + "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "{{count}} 项操作_few": "", + "{{count}} 项操作_many": "", + "{{count}} 项操作_one": "", + "{{count}} 项操作_other": "", + "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", + "{{label}} {{price}} / 1M tokens": "", + "{{name}} ID": "{{name}} ID", + "{{ratioType}} {{ratio}}": "{{ratioType}} {{ratio}}", + "{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x", + "| 基于": "| Основано на", + "© {{currentYear}}": "© {{currentYear}}", + "• 视频服务商的跨域限制": "• Ограничения кросс-доменных запросов со стороны видеосервиса", + "• 防盗链保护机制": "• Защита от хотлинков", + "• 需要特定的请求头或认证": "• Требуются специальные заголовки или авторизация", + "、": ", ", + "「用户可选」决定用户创建令牌时能否自主选择该分组。未勾选的分组只能由管理员在后台分配给用户,不会出现在用户的令牌创建页面中。": "\"User Selectable\" controls whether users can choose this group when creating tokens. Unchecked groups can only be assigned by admin and won't appear in the token creation page.", "一个月": "Один месяц", "一天": "Один день", "一小时": "Один час", "一次调用消耗多少刀,优先级大于模型倍率": "Сколько долларов потребляется за один вызов, приоритет выше чем коэффициент модели", - "一行一个,不区分大小写": "Один элемент на строку, без учета регистра", "一行一个屏蔽词,不需要符号分割": "Одно запрещенное слово на строку, не требуют разделителей", + "一行一个,不区分大小写": "Один элемент на строку, без учета регистра", "一键填充到 FluentRead": "Однократное заполнение в FluentRead", "三种操作的区别:": "Differences between the three operations:", "上一个表单块": "Предыдущий блок формы", @@ -346,10 +359,10 @@ "两次输入的密码不一致": "Введенные пароли не совпадают", "两次输入的密码不一致!": "Введенные пароли не совпадают!", "两步验证": "Двухфакторная аутентификация", - "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "Двухфакторная аутентификация (2FA) предоставляет дополнительную защиту для вашего аккаунта. После включения, при входе потребуется вводить пароль и код подтверждения из приложения аутентификатора.", "两步验证启用成功!": "Двухфакторная аутентификация успешно включена!", "两步验证已禁用": "Двухфакторная аутентификация отключена", "两步验证设置": "Настройки двухфакторной аутентификации", + "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "Двухфакторная аутентификация (2FA) предоставляет дополнительную защиту для вашего аккаунта. После включения, при входе потребуется вводить пароль и код подтверждения из приложения аутентификатора.", "个": "шт.", "个GPU": " GPUs", "个人中心": "Личный кабинет", @@ -384,8 +397,10 @@ "为了保护账户安全,请验证您的身份。": "Для защиты безопасности вашего аккаунта, пожалуйста, подтвердите вашу личность.", "为什么 default 不勾选「用户可选」?": "Why is default not set as \"User Selectable\"?", "为保证匹配准确,请确保客户端直连本站点(避免反向代理/网关改写 User-Agent)。": "Для точного сопоставления убедитесь, что клиент подключается напрямую к этому сайту (избегайте обратных прокси/шлюзов, которые перезаписывают User-Agent).", + "为模型前缀覆盖": "", "为特定用户分组配置可用分组的增减规则。「添加」为该分组新增可用分组,「移除」移除默认可用分组,「追加」直接追加分组": "Configure add/remove rules for specific user groups. \"Add\" adds a new selectable group, \"Remove\" removes a default selectable group, \"Append\" directly appends a group", "为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https": "Если пусто, используется адрес сервера по умолчанию. Несколько Origin разделяются запятыми, например https://newapi.pro,https://newapi.com. Обратите внимание, что нельзя использовать [], необходимо использовать https", + "为默认价格": "", "主模型": "Основная модель", "主页链接填": "Введите ссылку на главную страницу", "之前的所有日志": "Все предыдущие журналы", @@ -437,9 +452,9 @@ "令牌": "Токен", "令牌不存在": "", "令牌分组": "Группа токенов", - "令牌分组,默认为用户的分组": "Группа токенов, по умолчанию используется группа пользователя", "令牌分组设为 auto 时,按以下顺序依次尝试选择可用分组,排在前面的优先级更高": "When token group is set to auto, groups are selected in order of priority, with higher priority groups listed first", "令牌分组设为 auto 时,系统按优先级顺序自动选择一个可用分组。": "When token group is set to auto, the system automatically selects an available group by priority.", + "令牌分组,默认为用户的分组": "Группа токенов, по умолчанию используется группа пользователя", "令牌创建成功,请在列表页面点击复制获取令牌!": "Токен успешно создан, пожалуйста, нажмите копировать на странице списка для получения токена!", "令牌名称": "Имя токена", "令牌已重置并已复制到剪贴板": "Токен сброшен и скопирован в буфер обмена", @@ -451,10 +466,9 @@ "以及": "а также", "仪表盘设置": "Панель управления", "价格": "Цена", - "价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}": "Price: {{symbol}}{{price}} * {{ratioType}}: {{ratio}}", - "价格:${{price}} * {{ratioType}}:{{ratio}}": "Цена: ${{price}} * {{ratioType}}: {{ratio}}", "价格摘要": "Сводка цен", "价格暂时不可用,请稍后重试": "Price temporarily unavailable, please try again later", + "价格根据用量档位和请求条件动态调整": "", "价格模式": "", "价格模式(默认)": "Режим цен (по умолчанию)", "价格计算中...": "Calculating price...", @@ -464,6 +478,8 @@ "价格设置方式": "Способ настройки цен", "价格重新计算中...": "Recalculating price...", "价格预估": "Price Estimate", + "价格:${{price}} * {{ratioType}}:{{ratio}}": "Цена: ${{price}} * {{ratioType}}: {{ratio}}", + "价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}": "Price: {{symbol}}{{price}} * {{ratioType}}: {{ratio}}", "任一满足(OR)": "Любое совпадение (OR)", "任务 ID": "ID задачи", "任务ID": "ID задачи", @@ -498,7 +514,6 @@ "你还没有处理{{type}}模型({{count}}个)。是否仅提交当前已勾选内容?_many": "", "你还没有处理{{type}}模型({{count}}个)。是否仅提交当前已勾选内容?_one": "", "你还没有处理{{type}}模型({{count}}个)。是否仅提交当前已勾选内容?_other": "", - "使用 {{name}} 继续": "Продолжить с {{name}}", "使用 Discord 继续": "Продолжить через Discord", "使用 GitHub 继续": "Продолжить с GitHub", "使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}": "Используйте формат объекта JSON, формат: {\"Имя группы\": [Максимальное количество запросов, Максимальное количество выполненных запросов]}", @@ -507,6 +522,7 @@ "使用 Passkey 实现免密且更安全的登录体验": "Используйте Passkey для безпарольного и более безопасного входа", "使用 Passkey 登录": "Войти с Passkey", "使用 Passkey 验证": "Проверить с Passkey", + "使用 {{name}} 继续": "Продолжить с {{name}}", "使用 微信 继续": "Продолжить с WeChat", "使用 用户名 注册": "Зарегистрироваться с именем пользователя", "使用 邮箱或用户名 登录": "Войти с email или именем пользователя", @@ -516,52 +532,63 @@ "使用日志": "Журнал использования", "使用模式": "Режим использования", "使用统计": "Статистика использования", - "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "Отсканируйте QR-код ниже с помощью приложения аутентификатора (например, Google Authenticator, Microsoft Authenticator):", "使用认证器应用扫描二维码": "Отсканировать QR-код с помощью приложения аутентификатора", + "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "Отсканируйте QR-код ниже с помощью приложения аутентификатора (например, Google Authenticator, Microsoft Authenticator):", "使用说明": "Guide", "例如 /var/cache/new-api": "напр.: /var/cache/new-api", - "例如 €, £, Rp, ₩, ₹...": "Например €, £, Rp, ₩, ₹...", + "例如 0.5 或 2": "", + "例如 anthropic-beta": "", "例如 https://docs.newapi.pro": "Например https://docs.newapi.pro", "例如 https://example.com/api/waffo/webhook": "", "例如 https://example.com/console/topup": "", - "例如:": "например:", + "例如 service_tier": "", + "例如 €, £, Rp, ₩, ₹...": "Например €, £, Rp, ₩, ₹...", "例如: /bin/bash -c \"python app.py\"": "e.g.: /bin/bash -c \"python app.py\"", "例如: nginx:latest": "e.g.: nginx:latest", "例如: socks5://user:pass@host:port": "например: socks5://user:pass@host:port", + "例如发卡网站的购买链接": "например ссылка на покупку на сайте карт", + "例如(全渠道):": "Пример (все каналы):", + "例如(指定渠道):": "Пример (указанные каналы):", + "例如:": "например:", "例如:-c": "e.g.: -c", "例如:/bin/bash": "e.g.: /bin/bash", "例如:0001": "например: 0001", "例如:1000": "например: 1000", "例如:100000": "Например: 100000", - "例如:2,就是最低充值2$": "например: 2, это минимальное пополнение 2$", "例如:2000": "например: 2000", + "例如:2,就是最低充值2$": "например: 2, это минимальное пополнение 2$", "例如:4.99": "Например: 4.99", "例如:401, 403, 429, 500-599": "напр.: 401, 403, 429, 500-599", "例如:7,就是7元/美金": "например: 7, это 7 юаней/доллар США", "例如:Credit Card": "", + "例如:GitHub Enterprise": "напр.: GitHub Enterprise", + "例如:MER_xxx": "", + "例如:PROD_xxx": "", + "例如:STO_xxx": "", "例如:email": "напр.: email", "例如:example.com": "например: example.com", "例如:github / si:google / https://example.com/logo.png / 🐱": "напр.: github / si:google / https://example.com/logo.png / 🐱", - "例如:GitHub Enterprise": "напр.: GitHub Enterprise", "例如:github-enterprise": "напр.: github-enterprise", "例如:gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$": "", "例如:https://example.com/.well-known/openid-configuration": "напр.: https://example.com/.well-known/openid-configuration", + "例如:https://example.com/api/waffo/webhook": "", + "例如:https://example.com/console/topup": "", "例如:https://gitea.example.com": "напр.: https://gitea.example.com", "例如:https://yourdomain.com": "например: https://yourdomain.com", "例如:name、full_name": "напр.: name, full_name", "例如:nginx:latest": "e.g.: nginx:latest", "例如:preferred_username、login": "напр.: preferred_username, login", "例如:preview": "например: preview", + "例如:price_xxx": "", "例如:prod_6I8rBerHpPxyoiU9WK4kot": "Например: prod_6I8rBerHpPxyoiU9WK4kot", + "例如:sk_xxx 或 rk_xxx,留空表示保持当前不变": "", "例如:sub、id、data.user.id": "напр.: sub, id, data.user.id", + "例如:whsec_xxx,留空表示保持当前不变": "", "例如:基础套餐": "Например: базовый пакет", "例如:清理工具参数,避免上游校验错误": "", "例如:该请求不满足准入策略": "напр.: Этот запрос не соответствует политике допуска", "例如:适合轻度使用": "Например: для легкого использования", "例如:需要等级 {{required}},你当前等级 {{current}}": "напр.: Требуется уровень {{required}}, ваш текущий уровень {{current}}", - "例如(全渠道):": "Пример (все каналы):", - "例如(指定渠道):": "Пример (указанные каналы):", - "例如发卡网站的购买链接": "например ссылка на покупку на сайте карт", "供应商": "Поставщик", "供应商介绍": "Описание поставщика", "供应商信息:": "Информация о поставщике:", @@ -586,11 +613,17 @@ "保存 WeChat Server 设置": "Сохранить настройки WeChat Server", "保存分组倍率设置": "Сохранить настройки коэффициентов групп", "保存分组相关设置": "Сохранить настройки, связанные с группами", + "保存后不会回显,请填写当前环境对应的 API 私钥": "", + "保存后不会回显,请填写当前环境对应的 Stripe API 密钥": "", + "保存后不会回显,请填写测试环境对应的 API 密钥": "", + "保存后不会回显,请填写测试环境对应的 API 私钥": "", + "保存后不会回显,请填写生产环境对应的 API 密钥": "", + "保存后不会回显,请填写生产环境对应的 API 私钥": "", "保存备用码": "Сохранить резервные коды", "保存备用码以备不时之需": "Сохраните резервные коды на случай необходимости", "保存失败": "Не удалось сохранить", - "保存失败,请重试": "Не удалось сохранить, попробуйте еще раз", "保存失败:": "Не удалось сохранить:", + "保存失败,请重试": "Не удалось сохранить, попробуйте еще раз", "保存屏蔽词过滤设置": "Сохранить настройки фильтрации запрещенных слов", "保存性能设置": "Сохранить настройки производительности", "保存成功": "Успешно сохранено", @@ -608,11 +641,11 @@ "保存预览": "Предпросмотр сохранения", "保存额度设置": "Сохранить настройки лимитов", "保留": "kept", - "保留最近N个文件": "Хранить последние N файлов", - "保留最近N天": "Хранить за последние N дней", "保留原值(目标已有值时不覆盖)": "Сохранить исходное значение (не перезаписывать, если цель уже имеет значение)", "保留天数": "Дней для хранения", "保留文件数": "Файлов для хранения", + "保留最近N个文件": "Хранить последние N файлов", + "保留最近N天": "Хранить за последние N дней", "修复数据库一致性": "Исправить согласованность базы данных", "修改为": "Изменить на", "修改子渠道优先级": "Изменить приоритет дочерних каналов", @@ -634,12 +667,12 @@ "停用": "Отключить", "允许 AccountFilter 参数": "Разрешить параметр AccountFilter", "允许 HTTP 协议图片请求(适用于自部署代理)": "Разрешить запросы изображений по протоколу HTTP (применимо для самостоятельно развернутых прокси)", + "允许 Turnstile 用户校验": "Разрешить проверку пользователей Turnstile", "允许 inference_geo 透传": "Разрешить передачу inference_geo", "允许 safety_identifier 透传": "Разрешить сквозную передачу safety_identifier", "允许 service_tier 透传": "Разрешить сквозную передачу service_tier", "允许 speed 透传": "Разрешить передачу speed", "允许 stream_options.include_obfuscation 透传": "Разрешить передачу stream_options.include_obfuscation", - "允许 Turnstile 用户校验": "Разрешить проверку пользователей Turnstile", "允许不安全的 Origin(HTTP)": "Разрешить небезопасные Origin (HTTP)", "允许回调(会泄露服务器 IP 地址)": "Разрешить обратные вызовы (может раскрыть IP-адрес сервера)", "允许在 Stripe 支付中输入促销码": "Разрешить ввод промокодов при оплате через Stripe", @@ -660,13 +693,14 @@ "允许重试": "Разрешить повтор", "元": "Юань", "充值": "Пополнить", - "充值价格(x元/美金)": "Цена пополнения (x юаней/доллар США)", + "充值价格必须大于 0": "", "充值价格显示": "Отображение цены пополнения", + "充值价格(x元/美金)": "Цена пополнения (x юаней/доллар США)", "充值分组倍率": "Коэффициенты групп пополнения", "充值分组倍率不是合法的 JSON 字符串": "Коэффициенты групп пополнения не являются допустимой JSON строкой", "充值数量": "Количество пополнения", - "充值数量,最低 ": "Количество пополнения, минимум ", "充值数量不能小于": "Количество пополнения не может быть меньше", + "充值数量,最低 ": "Количество пополнения, минимум ", "充值方式设置": "Настройки способов пополнения", "充值方式设置不是合法的 JSON 字符串": "Настройки способов пополнения не являются допустимой JSON строкой", "充值确认": "Подтверждение пополнения", @@ -682,14 +716,15 @@ "兑换成功!": "Обмен успешен!", "兑换码充值": "Пополнение кодом купона", "兑换码创建成功": "Код купона успешно создан", - "兑换码创建成功,是否下载兑换码?": "Код купона успешно создан, скачать код купона?", "兑换码创建成功!": "Код купона успешно создан!", + "兑换码创建成功,是否下载兑换码?": "Код купона успешно создан, скачать код купона?", "兑换码功能已禁用,管理员需先确认合规声明。": "Redemption codes are disabled until the administrator confirms compliance terms.", "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "Код купона будет загружен в виде текстового файла, имя файла - название кода купона.", "兑换码更新成功!": "Код купона успешно обновлен!", "兑换码生成管理": "Управление генерацией кодов купонов", "兑换码管理": "Управление кодами купонов", "兑换额度": "Обменять квоту", + "兜底档": "", "全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用": "Глобальный контроль отображения области и функций боковой панели, пользователи не могут включить функции, скрытые администратором", "全局设置": "Глобальные настройки", "全选": "Выбрать все", @@ -738,8 +773,8 @@ "关闭": "Закрыть", "关闭侧边栏": "Закрыть боковую панель", "关闭公告": "Закрыть объявление", - "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "После отключения эта модель не будет автоматически перезаписана или создана при \"синхронизации с официальной\"", "关闭后将不再显示此提示(仅对当前浏览器生效)。确定要关闭吗?": "После закрытия это уведомление больше не будет показываться (только в этом браузере). Закрыть?", + "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "После отключения эта модель не будет автоматически перезаписана или создана при \"синхронизации с официальной\"", "关闭弹窗,已停止批量测试": "Окно закрыто, массовое тестирование остановлено", "关闭提示": "Закрыть уведомление", "其他": "Другое", @@ -757,20 +792,6 @@ "内置": "Встроенный", "内置 Ollama 镜像": "Built-in Ollama Image", "再次输入部署名称": "Enter Deployment Name Again", - "最低": "Минимум", - "最低充值数量": "", - "最低充值美元数量": "Минимальная сумма пополнения в долларах", - "最低充值美元数量必须大于 0": "Минимальная сумма пополнения в долларах должна быть больше 0", - "最后使用时间": "Время последнего использования", - "最后更新": "Last Updated", - "最后请求": "Последний запрос", - "最大GPU数量": "Max Number of GPUs", - "最大可用": "Max Available", - "最大条目数": "Макс. кол-во записей", - "最终抵扣": "Итоговое списание", - "最近一次": "Последний", - "最近事件": "Recent Events", - "最高优先级": "highest priority", "写": "Запись", "准入策略": "Политика допуска", "准入策略 JSON(可选)": "JSON политики допуска (необязательно)", @@ -778,6 +799,9 @@ "准备完成初始化": "Подготовка к инициализации завершена", "减少": "Уменьшить", "凭证已刷新": "Учётные данные обновлены", + "函数": "", + "分时缓存 (Claude)": "", + "分档价格表": "", "分类名称": "Название категории", "分组": "Группа", "分组JSON设置": "Group JSON Settings", @@ -862,20 +886,20 @@ "刷新缓存统计": "Обновить статистику кэша", "刷新缓存统计失败": "Не удалось обновить статистику кэша", "刷新页面": "Обновить страницу", - "前:": "До:", "前往 io.net API Keys": "Go to io.net API Keys", "前往设置": "Go to Settings", "前往设置页面": "Go to Settings Page", "前缀": "Префикс", "前置": "", - "剪贴板中未检测到连接信息": "Данные подключения не найдены в буфере обмена", - "副本数量": "Number of Replicas", + "前:": "До:", "剩余": "Remaining", "剩余备用码:": "Оставшиеся резервные коды:", "剩余时间": "Remaining Time", "剩余额度": "Оставшаяся квота", - "剩余额度/总额度": "Оставшаяся квота/Общая квота", "剩余额度$": "Оставшаяся квота$", + "剩余额度/总额度": "Оставшаяся квота/Общая квота", + "剪贴板中未检测到连接信息": "Данные подключения не найдены в буфере обмена", + "副本数量": "Number of Replicas", "功能特性": "Функциональные возможности", "加入渠道": "Join Channel", "加入预填组": "Присоединиться к группе предварительного заполнения", @@ -896,12 +920,14 @@ "加载详情中...": "Loading details...", "加载账单失败": "Не удалось загрузить счёт", "加载隐私政策内容失败...": "Не удалось загрузить содержимое политики конфиденциальности...", + "动态计费": "", "勾选后,该分组会出现在用户创建令牌时的下拉菜单中。未勾选的分组只能由管理员分配,用户自己无法选择。": "When checked, this group appears in the dropdown when users create tokens. Unchecked groups can only be assigned by admin.", "包含": "Включает", "包含来自未知或未标明供应商的AI模型,这些模型可能来自小型供应商或开源项目。": "Включает модели ИИ от неизвестных или неуказанных поставщиков, эти модели могут быть от небольших поставщиков или проектов с открытым исходным кодом.", "包括失败请求的次数,0代表不限制": "Включает количество неудачных запросов, 0 означает без ограничений", "匹配值": "Значение совпадения", "匹配值(可选)": "Значение совпадения (необязательно)", + "匹配内容": "", "匹配方式": "Метод сопоставления", "匹配类型": "Тип соответствия", "区域": "Регион", @@ -912,6 +938,7 @@ "原价": "Первоначальная цена", "原价,和普通用户一样": "original price, same as regular users", "原因:": "Причина:", + "原始额度": "", "原密码": "Старый пароль", "原生格式": "Нативный формат", "原生额度": "Исходный лимит", @@ -947,6 +974,7 @@ "变换": "Трансформация", "变更": "Изменение", "变焦": "Масштабирование", + "变量": "", "变量值": "Variable Value", "变量名": "Variable Name", "只包括请求成功的次数": "Включать только успешные запросы", @@ -969,22 +997,23 @@ "可用端点类型": "Доступные типы конечных точек", "可用邀请额度": "Доступная пригласительная квота", "可留空;留空时会尝试使用 Issuer URL + /.well-known/openid-configuration": "Можно оставить пустым; при пустом значении будет попытка использовать Issuer URL + /.well-known/openid-configuration", + "可空": "", "可视化": "Визуализация", "可视化倍率设置": "Визуальные настройки коэффициента", "可视化编辑": "Визуальное редактирование", - "可空": "", - "可选,公告的补充说明": "Необязательно, дополнительное описание объявления", - "可选,用于复现结果": "Необязательно, для воспроизводимых результатов", - "可选:基于用户信息 JSON 做组合条件准入,条件不满足时返回自定义提示": "Необязательно: Допуск на основе комбинированных условий из JSON пользовательской информации; при несоответствии условиям возвращается пользовательское сообщение", - "可选:用于自动生成端点或 Discovery URL": "Необязательно: Используется для автоматической генерации конечных точек или Discovery URL", "可选。匹配入口请求的 User-Agent;任意一行作为子串匹配(忽略大小写)即命中。": "Необязательно. Сопоставление User-Agent входящего запроса; любая строка, совпадающая как подстрока (без учёта регистра), считается совпадением.", "可选。对提取到的亲和 Key 做正则校验;不填表示不校验。": "Необязательно. Проверка извлечённого ключа аффинити по regex; пустое поле — без проверки.", "可选。对请求路径进行匹配;不填表示匹配所有路径。": "Необязательно. Сопоставление пути запроса; пустое поле — совпадение со всеми путями.", "可选值": "Дополнительные значения", + "可选,公告的补充说明": "Необязательно, дополнительное описание объявления", + "可选,用于复现结果": "Необязательно, для воспроизводимых результатов", + "可选:基于用户信息 JSON 做组合条件准入,条件不满足时返回自定义提示": "Необязательно: Допуск на основе комбинированных условий из JSON пользовательской информации; при несоответствии условиям возвращается пользовательское сообщение", + "可选:用于自动生成端点或 Discovery URL": "Необязательно: Используется для автоматической генерации конечных точек или Discovery URL", "合规声明已确认": "Compliance confirmed", "合规声明确认成功": "Compliance confirmed successfully", "合计:{{total}}": "Итого: {{total}}", "合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "Итого: текстовая часть {{textTotal}} + аудиочасть {{audioTotal}} = {{total}}", + "同时满足": "", "同时重置消息": "Одновременно сбросить сообщения", "同步": "Синхронизация", "同步到渠道": "Sync to Channel", @@ -1000,14 +1029,16 @@ "名称+密钥": "Название+ключ", "名称不能为空": "Название не может быть пустым", "名称匹配类型": "Тип соответствия названия", - "后:": "После:", "后端固定": "", "后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。": "Фиксированный backend-коэффициент: {{ratio}}. Это поле только показывает вычисленную цену.", "后端请求失败": "Запрос к бэкенду не удался", "后缀": "Суффикс", + "后:": "После:", "向右展开": "Развернуть вправо", "向左展开": "Развернуть влево", "否": "Нет", + "含时间条件": "", + "含请求条件": "", "启动": "Start", "启动参数 (Args)": "Startup Args", "启动命令": "Startup Command", @@ -1017,11 +1048,14 @@ "启动部署失败": "Failed to start deployment", "启动配置": "Startup Configuration", "启用": "Включить", + "启用 Prompt 检查": "Включить проверку Prompt", + "启用 Stripe 微信支付": "", + "启用 Stripe 支付宝": "", + "启用 Waffo": "", + "启用 Waffo Pancake": "", "启用 io.net 部署": "Enable io.net Deployment", "启用 io.net 部署开关": "Enable io.net Deployment Switch", "启用 io.net 部署时必须填写 API Key": "API Key is required when enabling io.net deployment", - "启用 Prompt 检查": "Включить проверку Prompt", - "启用 Waffo": "", "启用2FA失败": "Не удалось включить 2FA", "启用Claude思考适配(-thinking后缀)": "Включить адаптацию мышления Claude (суффикс -thinking)", "启用FunctionCall思维签名填充": "Включить автозаполнение thoughtSignature для FunctionCall", @@ -1057,6 +1091,7 @@ "启用验证": "Включить проверку", "周": "Неделя", "命中判定:usage 中存在 cached tokens(例如 cached_tokens/prompt_cache_hit_tokens)即视为命中。": "Определение попадания: наличие кэшированных токенов в usage (например, cached_tokens/prompt_cache_hit_tokens) считается попаданием.", + "命中档位": "", "命中率": "Процент попаданий", "命中该亲和规则后,会把此模板合并到渠道参数覆盖中(同名键由模板覆盖)。": "При срабатывании этого правила аффинити шаблон объединяется с переопределениями параметров канала (одноимённые ключи переопределяются шаблоном).", "和": "и", @@ -1083,15 +1118,15 @@ "图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ": "Используйте библиотеку @lobehub/icons, например: OpenAI, Claude.Color, поддерживаются цепочечные параметры: OpenAI.Avatar.type={'platform'}, OpenRouter.Avatar.shape={'square'}, для просмотра всех доступных иконок, пожалуйста, ", "图标文件不能超过 100KB,请压缩后重新上传": "", "图混合": "Смешивание изображений", + "图片/音频价格(可选)": "", "图片倍率 {{imageRatio}}": "Коэффициент изображения {{imageRatio}}", "图片功能在自定义请求体模式下不可用": "Функция изображений недоступна в режиме пользовательского запроса", "图片地址": "URL изображения", "图片已添加": "Изображение добавлено", - "图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Генерация изображения: 1 вызов * цена за единицу {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "图片生成调用:{{symbol}}{{price}} / 1次": "Вызов генерации изображения: {{symbol}}{{price}} / 1 раз", + "图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Генерация изображения: 1 вызов * цена за единицу {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "图片输入 {{price}} / 1M tokens": "Ввод изображения {{price}} / 1M tokens", "图片输入: {{imageRatio}}": "Ввод изображения: {{imageRatio}}", - "图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Ввод изображения: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент изображения {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "图片输入价格": "Цена входного изображения", "图片输入价格 {{symbol}}{{price}} / 1M tokens": "Цена входного изображения {{symbol}}{{price}} / 1M tokens", "图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})": "Цена ввода изображения: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M токенов (коэффициент изображения: {{imageRatio}})", @@ -1100,8 +1135,10 @@ "图片输入倍率": "", "图片输入倍率(仅部分模型支持该计费)": "Коэффициент ввода изображения (только некоторые модели поддерживают эту тарификацию)", "图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费": "Настройки коэффициента, связанные с вводом изображения, ключ - название модели, значение - коэффициент, только некоторые модели поддерживают эту тарификацию", + "图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Ввод изображения: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент изображения {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "图生文": "Изображение в текст", "图生视频": "Изображение в видео", + "在 Stripe 后台创建价格后获得": "", "在Gotify服务器创建应用后获得的令牌,用于发送通知": "Токен, полученный после создания приложения на сервере Gotify, используется для отправки уведомлений", "在Gotify服务器的应用管理中创建新应用": "Создать новое приложение в управлении приложениями на сервере Gotify", "在找兑换码?": "Ищете код купона?", @@ -1130,12 +1167,12 @@ "填充新模板": "Заполнить новый шаблон", "填充旧模板": "Заполнить старый шаблон", "填充模板": "Заполнить шаблон", + "填充模板(全渠道)": "Заполнить шаблон (все каналы)", + "填充模板(指定渠道)": "Заполнить шаблон (выбранные каналы)", "填充模板:等级+激活": "Заполнить шаблон: Уровень + Активация", "填充模板:等级提示": "Заполнить шаблон: Промпт уровня", "填充模板:组织或角色": "Заполнить шаблон: Организация или роль", "填充模板:组织提示": "Заполнить шаблон: Промпт организации", - "填充模板(全渠道)": "Заполнить шаблон (все каналы)", - "填充模板(指定渠道)": "Заполнить шаблон (выбранные каналы)", "填入": "Заполнить", "填入 CC Switch": "Заполнить CC Switch", "填入所有模型": "Заполнить все модели", @@ -1149,7 +1186,16 @@ "填写 Issuer URL 后自动生成:": "Автогенерация после заполнения Issuer URL:", "填写Gotify服务器的完整URL地址": "Введите полный URL-адрес сервера Gotify", "填写后会自动拼接预设端点": "Предустановленные конечные точки будут автоматически добавлены после заполнения", + "填写后覆盖当前测试环境 API 密钥,留空表示保持当前不变": "", + "填写后覆盖当前测试环境 Webhook 公钥,留空表示保持当前不变": "", + "填写后覆盖当前测试环境私钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境 API 密钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境 Webhook 公钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境私钥,留空表示保持当前不变": "", + "填写后覆盖当前私钥,留空表示保持当前不变": "", "填写带https的域名,逗号分隔": "Введите домены с https, разделённые запятыми", + "填写测试环境 Waffo 公钥,Base64 或 PEM 内容均可": "", + "填写生产环境 Waffo 公钥,Base64 或 PEM 内容均可": "", "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "После заполнения содержимого пользовательского соглашения, пользователям потребуется отметить, что они прочитали пользовательское соглашение при регистрации", "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "После заполнения содержимого политики конфиденциальности, пользователям потребуется отметить, что они прочитали политику конфиденциальности при регистрации", "填写音频补全价格前,需要先填写音频输入价格。": "Перед указанием цены аудио-завершения сначала задайте цену аудио-ввода.", @@ -1192,6 +1238,8 @@ "多密钥管理": "Управление множественными ключами", "多模型统一接入,只需将基址替换为:": "Единый доступ к множеству моделей, просто замените базовый адрес на:", "多种充值方式,安全便捷": "Множество способов пополнения, безопасно и удобно", + "大于": "", + "大于等于": "", "大模型接口网关": "Шлюз API LLM", "天": "день", "天前": "дней назад", @@ -1208,14 +1256,14 @@ "套餐标题": "Название плана", "套餐标题不能为空": "Название тарифа не может быть пустым", "套餐的基本信息和定价": "Основная информация и цена плана", - "如:大带宽批量分析图片推荐": "Например: рекомендуется для пакетного анализа изображений с большой пропускной способностью", - "如:香港线路": "Например: Гонконгская линия", "如向中华人民共和国境内公众提供生成式人工智能服务,你将依法履行备案登记、安全评估、内容安全、投诉举报、生成合成内容标识、日志留存、个人信息保护等合规义务;": "If you provide generative AI services to the public in mainland China, you will fulfill legal compliance obligations.", "如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。": "Если канал аффинити не сработал, после успешного повтора на другом канале аффинити будет обновлена на успешный канал.", "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "Если вы интегрируетесь с восходящими проектами пересылки, такими как One API или New API, используйте тип OpenAI, не используйте этот тип, если вы не знаете, что делаете.", "如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "Если запрос пользователя содержит системный промпт, используйте эту настройку для добавления перед системным промптом пользователя", "如果镜像为私有,请填写密码或Token": "If the image is private, please fill in the password or token", "如果镜像为私有,请填写用户名": "If the image is private, please fill in the username", + "如:大带宽批量分析图片推荐": "Например: рекомендуется для пакетного анализа изображений с большой пропускной способностью", + "如:香港线路": "Например: Гонконгская линия", "始终使用浅色主题": "Всегда использовать светлую тему", "始终使用深色主题": "Всегда использовать темную тему", "字段映射": "Сопоставление полей", @@ -1223,6 +1271,7 @@ "字段路径": "Путь поля", "字段透传控制": "Управление прозрачной передачей полей", "字段速查": "Быстрый поиск полей", + "存在": "", "存在惩罚,鼓励讨论新话题": "Штраф за присутствие, поощряет новые темы", "存在重复的分组名称:": "Duplicate group names: ", "存在重复的键名:": "Обнаружены повторяющиеся имена ключей:", @@ -1284,7 +1333,6 @@ "密钥": "Ключ", "密钥 JSON 必须包含 access_token": "JSON ключа должен содержать access_token", "密钥 JSON 必须包含 account_id": "JSON ключа должен содержать account_id", - "密钥(编辑模式下,保存的密钥不会显示)": "Токен (в режиме редактирования сохраненные токены не отображаются)", "密钥去重": "Удаление дубликатов ключей", "密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性": "Ключ будет добавлен в заголовок запроса методом Bearer для проверки легитимности webhook-запросов", "密钥已删除": "Ключ удален", @@ -1302,6 +1350,7 @@ "密钥获取成功": "Ключ успешно получен", "密钥输入方式": "Способ ввода ключа", "密钥预览": "Предпросмотр ключа", + "密钥(编辑模式下,保存的密钥不会显示)": "Токен (в режиме редактирования сохраненные токены не отображаются)", "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "Для официальных каналов new-api уже имеет встроенные адреса, если это не сторонние прокси-сайты или специальные адреса доступа Azure, заполнять не нужно", "对免费模型启用预消耗": "Включить предварительное списание для бесплатных моделей", "对域名启用 IP 过滤(推荐开启)": "Включить IP-фильтрацию для доменов (рекомендуется)", @@ -1333,6 +1382,8 @@ "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "Будут очищены все сохраненные конфигурации и восстановлены настройки по умолчанию, эта операция необратима. Продолжить?", "将清除选定时间之前的所有日志": "Будут очищены все логи до выбранного времени", "将追加 2 条规则到现有规则列表。": "2 правила будут добавлены к существующему списку правил.", + "小于": "", + "小于等于": "", "小时": "час", "小时费率": "Hourly Rate", "尚未使用": "Еще не использовано", @@ -1343,12 +1394,14 @@ "展开更多": "Развернуть больше", "展示价格": "Отображаемая цена", "嵌套映射:用户分组 → 使用分组 → 倍率": "Nested mapping: user group → using group → ratio", + "工具标识": "", + "工具调用定价": "", "左侧边栏个人设置": "Персональные настройки левой боковой панели", + "已为 ${count} 个渠道设置标签!": "Установлены метки для ${count} каналов!", "已为 {{count}} 个模型设置{{type}}_few": "Установлено {{type}} для {{count}} моделей", "已为 {{count}} 个模型设置{{type}}_many": "Установлено {{type}} для {{count}} моделей", "已为 {{count}} 个模型设置{{type}}_one": "Установлено {{type}} для {{count}} модели", "已为 {{count}} 个模型设置{{type}}_other": "Установлено {{type}} для {{count}} моделей", - "已为 ${count} 个渠道设置标签!": "Установлены метки для ${count} каналов!", "已从 Discovery 自动填充配置": "Конфигурация автозаполнена из Discovery", "已从 Discovery 获取配置,可继续手动修改所有字段。": "Конфигурация получена из Discovery. Все поля можно продолжить редактировать вручную.", "已作废": "Аннулировано", @@ -1360,10 +1413,12 @@ "已分配内存": "Выделенная память", "已切换为Assistant角色": "Переключено на роль Assistant", "已切换为System角色": "Переключено на роль System", + "已切换到新版前端,正在刷新页面": "Переключено на новый интерфейс, страница обновляется", "已切换到新版前端,正在跳转首页": "Переключено на новый интерфейс, переход на главную", "已切换至最优倍率视图,每个模型使用其最低倍率分组": "Переключено на оптимальный вид множителей, каждая модель использует свою группу с минимальным множителем", "已初始化": "Инициализировано", "已删除": "Удалено", + "已删除 ${data} 个通道!": "Удалено ${data} каналов!", "已删除 {{count}} 个令牌!": "Удалено {{count}} токенов!", "已删除 {{count}} 个令牌!_few": "Удалено {{count}} токена!", "已删除 {{count}} 个令牌!_many": "Удалено {{count}} токенов!", @@ -1373,7 +1428,6 @@ "已删除 {{count}} 条失效兑换码_many": "Удалено {{count}} недействительных кодов купонов", "已删除 {{count}} 条失效兑换码_one": "Удален {{count}} недействительный код купона", "已删除 {{count}} 条失效兑换码_other": "Удалено {{count}} недействительных кодов купонов", - "已删除 ${data} 个通道!": "Удалено ${data} каналов!", "已删除所有禁用渠道,共计 ${data} 个": "Удалены все отключенные каналы, всего ${data}", "已删除消息及其回复": "Сообщение и его ответы удалены", "已勾选": "Выбрано", @@ -1398,8 +1452,6 @@ "已复制": "Скопировано", "已复制 ${count} 个模型": "Скопировано ${count} моделей", "已复制 ID 到剪贴板": "ID copied to clipboard", - "已复制:": "Скопировано: ", - "已复制:{{name}}": "Скопировано: {{name}}", "已复制全部数据": "Все данные скопированы", "已复制到剪切板": "Скопировано в буфер обмена", "已复制到剪贴板": "Скопировано в буфер обмена", @@ -1408,6 +1460,8 @@ "已复制模型名称": "Название модели скопировано", "已复制版本号": "Version copied", "已复制自动生成的 API Key": "Auto-generated API Key copied", + "已复制:": "Скопировано: ", + "已复制:{{name}}": "Скопировано: {{name}}", "已完成": "Completed", "已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型": "Ценовая конфигурация модели {{name}} массово применена к {{count}} моделям", "已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型_few": "", @@ -1460,12 +1514,12 @@ "已达到购买上限": "Достигнут лимит покупок", "已过期": "Просрочено", "已运行时间": "Uptime", + "已选择 ${count} 个渠道": "Выбрано ${count} каналов", "已选择 {{count}} 个模型_few": "Выбрано {{count}} модели", "已选择 {{count}} 个模型_many": "Выбрано {{count}} моделей", "已选择 {{count}} 个模型_one": "Выбрана {{count}} модель", "已选择 {{count}} 个模型_other": "Выбрано {{count}} моделей", "已选择 {{selected}} / {{total}}": "Выбрано {{selected}} / {{total}}", - "已选择 ${count} 个渠道": "Выбрано ${count} каналов", "已重置为默认配置": "Сброшено на конфигурацию по умолчанию", "已销毁": "Destroyed", "币种": "Валюта", @@ -1489,16 +1543,21 @@ "延长容器时长将会产生额外费用,请确认您有足够的账户余额。": "Extending container duration will incur additional charges, please ensure you have sufficient account balance.", "延长操作一旦确认无法撤销,费用将立即扣除。": "Once confirmed, the extension operation cannot be undone, and charges will be deducted immediately.", "延长时长": "Extension Duration", - "延长时长(小时)": "Extension Duration (hours)", "延长时长不能超过720小时(30天)": "Extension duration cannot exceed 720 hours (30 days)", "延长时长失败": "Failed to extend duration", "延长时长至少为1小时": "Extension duration must be at least 1 hour", + "延长时长(小时)": "Extension Duration (hours)", "建立连接时发生错误": "Ошибка при установке соединения", "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "Рекомендуется использовать базы данных MySQL или PostgreSQL в производственной среде, или убедиться, что файл базы данных SQLite сопоставлен с постоянным хранилищем хоста.", "开": "Вкл", "开启「默认使用 auto 分组」后,新建令牌和初始令牌都会自动设为 auto。": "After enabling \"Default to auto group\", new tokens and initial tokens will be set to auto.", "开启之后会清除用户提示词中的": "После включения будет очищено в промптах пользователя:", "开启之后将上游地址替换为服务器地址": "После включения адреса восходящих каналов будут заменены на адрес сервера", + "开启后不限制:必须设置模型倍率": "После включения без ограничений: необходимо установить множители моделей", + "开启后创建令牌默认选择auto分组,初始令牌也将设为auto": "When enabled, new tokens default to auto group, initial tokens are also set to auto", + "开启后未登录用户无法访问模型广场": "После включения незарегистрированные пользователи не смогут получить доступ к площади моделей", + "开启后检测到新增模型会自动加入当前渠道模型列表": "", + "开启后由后端定时任务检测该渠道上游模型变化": "", "开启后,using_group 会参与 cache key(不同分组隔离)。": "При включении using_group будет частью ключа кэша (изоляция по группам).", "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。": "Если включено, запись аффинити сохраняется, даже когда канал аффинити отключён или больше не подходит для текущей группы/модели. Если выключено, запись будет удалена и выбран другой канал.", "开启后,仅\"消费\"和\"错误\"日志将记录您的客户端IP地址": "После включения, только логи \"потребление\" и \"ошибки\" будут записывать IP-адрес вашего клиента", @@ -1511,11 +1570,6 @@ "开启后,规则名称会参与 cache key(不同规则隔离)。": "При включении имя правила будет частью ключа кэша (изоляция по правилам).", "开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)": "При включении запросы к Claude через этот канал будут принудительно дополнены ?beta=true (клиенту не нужно передавать этот параметр вручную)", "开启后,违规请求将额外扣费。": "При включении за нарушающие запросы будет взиматься дополнительная плата.", - "开启后不限制:必须设置模型倍率": "После включения без ограничений: необходимо установить множители моделей", - "开启后创建令牌默认选择auto分组,初始令牌也将设为auto": "When enabled, new tokens default to auto group, initial tokens are also set to auto", - "开启后未登录用户无法访问模型广场": "После включения незарегистрированные пользователи не смогут получить доступ к площади моделей", - "开启后检测到新增模型会自动加入当前渠道模型列表": "", - "开启后由后端定时任务检测该渠道上游模型变化": "", "开启批量操作": "Включить пакетные операции", "开始": "Начало", "开始同步": "Начать синхронизацию", @@ -1555,6 +1609,7 @@ "当前没有未设置定价的模型": "Сейчас нет моделей без цены", "当前版本": "Текущая версия", "当前状态": "Current Status", + "当前环境共用同一商户 ID": "", "当前缓存大小": "Текущий размер кэша", "当前编辑": "Текущее редактирование", "当前规则不支持写入到该位置": "Текущее правило не поддерживает запись в это расположение", @@ -1598,6 +1653,7 @@ "总收益": "Общий доход", "总计": "Итого", "总额度": "Общая квота", + "恢复默认": "", "您可以个性化设置侧边栏的要显示功能": "Вы можете персонализировать отображаемые функции боковой панели", "您可以在上方拉取需要的模型": "You can pull the required models above", "您无权访问此页面,请联系管理员": "У вас нет прав доступа к этой странице, свяжитесь с администратором", @@ -1667,22 +1723,23 @@ "拉取进度": "Pull Progress", "拒绝提示模板(可选)": "Шаблон промпта отказа (необязательно)", "拦截原因": "Причина блокировки", + "按 1 美元对应的站内价格填写": "", "按K显示单位": "Отображать единицы в K", "按价格设置": "Настроить по цене", "按倍率类型筛选": "Фильтровать по типу коэффициента", "按倍率设置": "Настроить по множителю", "按次": "За запрос", "按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "За запрос {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "按次计费": "Оплата за запрос", "按次:{{symbol}}{{price}}": "За запрос: {{symbol}}{{price}}", "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "За запрос: {{symbol}}{{price}} * {{ratioType}}: {{ratio}} = {{symbol}}{{total}}", - "按次计费": "Оплата за запрос", "按照如下格式输入:AccessKey|SecretAccessKey|Region": "Введите в формате: AccessKey|SecretAccessKey|Region", "按量计费": "Оплата по объему", "按量计费下需要先填写输入价格,才能保存其它价格项。": "При тарификации по объему сначала нужно указать входную цену, чтобы сохранить остальные ценовые поля.", "按顺序替换content中的变量占位符": "Последовательно заменять переменные-заполнители в content", "换脸": "Замена лица", - "授权,需在遵守": "Авторизация, необходимо соблюдать", "授权失败": "Авторизация не удалась", + "授权,需在遵守": "Авторизация, необходимо соблюдать", "排序": "Порядок", "排队中": "В очереди", "接受未设置价格模型": "Принимать модели без установленной цены", @@ -1694,8 +1751,8 @@ "控制输出的随机性和创造性": "Управляет случайностью и креативностью вывода", "控制顶栏模块显示状态,全局生效": "Управление состоянием отображения модулей верхней панели, действует глобально", "推荐": "Рекомендуется", - "推荐:用户可以选择是否使用指纹等验证": "Рекомендуется: пользователи могут выбирать, использовать ли проверку по отпечатку пальца и другие методы", "推荐使用(用户可选)": "Рекомендуется использовать (по выбору пользователя)", + "推荐:用户可以选择是否使用指纹等验证": "Рекомендуется: пользователи могут выбирать, использовать ли проверку по отпечатку пальца и другие методы", "描述": "Описание", "提交": "Отправить", "提交时间": "Время отправки", @@ -1705,6 +1762,8 @@ "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "Промпт {{input}} токенов / 1M токенов * {{symbol}}{{price}}", "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Ввод {{input}} токенов / 1M токенов * {{symbol}}{{price}} + Вывод {{completion}} токенов / 1M токенов * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Ввод {{nonCacheInput}} токенов / 1M токенов * {{symbol}}{{price}} + Кэш {{cacheInput}} токенов / 1M токенов * {{symbol}}{{cachePrice}} + Создание кэша {{cacheCreationInput}} токенов / 1M токенов * {{symbol}}{{cacheCreationPrice}} + Вывод {{completion}} токенов / 1M токенов * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "提示价格:{{symbol}}{{price}} / 1M tokens": "Цена промпта: {{symbol}}{{price}} / 1M токенов", + "提示缓存倍率": "Коэффициент кэша промптов", "提示:如果切换后页面无法正常渲染,请清空浏览器缓存后重试。": "Совет: если после переключения страница отображается некорректно, очистите кеш браузера и повторите попытку.", "提示:如需备份数据,只需复制上述目录即可": "Промпт: для резервного копирования данных просто скопируйте указанный выше каталог", "提示:此处配置仅用于控制「模型广场」对用户的展示效果,不会影响模型的实际调用与路由。若需配置真实调用行为,请前往「渠道管理」进行设置。": "Примечание: эта настройка влияет только на отображение моделей в «Маркетплейсе моделей» и не влияет на фактический вызов или маршрутизацию. Чтобы настроить реальное поведение вызовов, перейдите в «Управление каналами».", @@ -1712,8 +1771,6 @@ "提示:该功能为测试版,未来配置结构与功能行为可能发生变更,请勿在生产环境使用。": "Примечание: это бета-функция. Структура конфигурации и поведение могут измениться в будущем. Не используйте в продакшене.", "提示:语言偏好会同步到您登录的所有设备,并影响API返回的错误消息语言。": "Подсказка: Языковые настройки синхронизируются на всех ваших устройствах и влияют на язык сообщений об ошибках API.", "提示:链接中的{key}将被替换为API密钥,{address}将被替换为服务器地址": "Промпт: {key} в ссылке будет заменен на API-ключ, {address} будет заменен на адрес сервера", - "提示价格:{{symbol}}{{price}} / 1M tokens": "Цена промпта: {{symbol}}{{price}} / 1M токенов", - "提示缓存倍率": "Коэффициент кэша промптов", "搜索供应商": "Поиск поставщиков", "搜索关键字": "Поиск по ключевым словам", "搜索失败": "Search failed", @@ -1748,6 +1805,7 @@ "支付方式名称": "Название метода оплаты", "支付方式名称不能为空": "", "支付方式类型": "Тип метода оплаты", + "支付方式设置": "", "支付渠道": "Платежные каналы", "支付设置": "Оплата", "支付请求失败": "Запрос на оплату не удался", @@ -1793,9 +1851,9 @@ "数据看板默认时间粒度": "Временная гранулярность панели данных по умолчанию", "数据管理和日志查看": "Управление данными и просмотр журналов", "文件上传": "Загрузка файла", - "文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Поиск файлов: {{count}} / 1K * цена за единицу {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "文件搜索价格:{{symbol}}{{price}} / 1K 次": "Цена поиска файлов: {{symbol}}{{price}} / 1K запросов", "文件搜索调用 {{fileSearchCallCount}} 次": "Поиск файлов вызван {{fileSearchCallCount}} раз", + "文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Поиск файлов: {{count}} / 1K * цена за единицу {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "Текстовый ввод {{input}} токенов / 1M токенов * {{symbol}}{{price}} + Текстовый вывод {{completion}} токенов / 1M токенов * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Текстовый промпт {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + Текстовое дополнение {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + Аудио промпт {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + Аудио дополнение {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "Текстовый ввод {{nonCacheInput}} токенов / 1M токенов * {{symbol}}{{price}} + Кэш {{cacheInput}} токенов / 1M токенов * {{symbol}}{{cachePrice}} + Текстовый вывод {{completion}} токенов / 1M токенов * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", @@ -1825,9 +1883,9 @@ "新建容器部署": "Create Container Deployment", "新建数量": "Количество для создания", "新建组": "Создать группу", + "新格式模板": "Шаблон нового формата", "新格式(支持条件判断与json自定义):": "Новый формат (поддерживает условные суждения и пользовательскую настройку json):", "新格式(规则 + 条件)": "Новый формат (Правила + Условия)", - "新格式模板": "Шаблон нового формата", "新版本": "Новая версия", "新用户使用邀请码奖励额度": "Квота вознаграждения для новых пользователей, использующих приглашение", "新用户初始额度": "Начальная квота для новых пользователей", @@ -1841,6 +1899,7 @@ "无加密": "Без шифрования", "无效的部署信息": "Invalid deployment information", "无效的重置链接,请重新发起密码重置请求": "Недействительная ссылка для сброса, пожалуйста, отправьте запрос на сброс пароля повторно", + "无条件(兜底档)": "", "无法发起 Passkey 注册": "Не удалось инициировать регистрацию Passkey", "无法复制到剪贴板,请手动复制": "Не удалось скопировать в буфер обмена, пожалуйста, скопируйте вручную", "无法添加图片": "Невозможно добавить изображение", @@ -1865,19 +1924,22 @@ "日志类型": "Тип журнала", "日志设置": "Настройки журнала", "日志详情": "Детали журнала", - "旧格式(JSON 对象)": "Устаревший формат (JSON-объект)", - "旧格式(直接覆盖):": "Старый формат (прямая перезапись):", "旧格式必须是 JSON 对象": "Устаревший формат должен быть JSON-объектом", "旧格式模板": "Шаблон старого формата", + "旧格式(JSON 对象)": "Устаревший формат (JSON-объект)", + "旧格式(直接覆盖):": "Старый формат (прямая перезапись):", "旧版前端即将停止维护": "Обслуживание классического интерфейса скоро прекратится", "旧的备用码已失效,请保存新的备用码": "Старые резервные коды больше не действительны, пожалуйста, сохраните новые резервные коды", "早上好": "Доброе утро", + "时区": "", "时间": "Время", "时间信息": "Time Information", + "时间条件": "", "时间粒度": "Временная гранулярность", "易支付": "Epay", "易支付商户ID": "ID торговца EasyPay", "易支付商户密钥": "Ключ торговца EasyPay", + "易支付设置": "", "是": "Да", "是否为企业账户": "Является ли корпоративным аккаунтом", "是否同时重置对话消息?选择\"是\"将清空所有对话记录并恢复默认示例;选择\"否\"将保留当前对话记录。": "Одновременно сбросить сообщения диалога? Выбор \"Да\" очистит все записи диалогов и восстановит примеры по умолчанию; выбор \"Нет\" сохранит текущие записи диалогов.", @@ -1888,16 +1950,17 @@ "是否自动禁用": "Автоматически отключать", "是否要求指纹/面容等生物识别": "Требовать биометрическую аутентификацию (отпечаток пальца/лицо и т.д.)", "显示倍率": "Отображать коэффициент", - "显示最新20条": "Отображать последние 20 записей", "显示名称": "Отображаемое имя", "显示名称字段(可选)": "Поле отображаемого имени (необязательно)", "显示完整内容": "Отображать полное содержимое", "显示操作项": "Отображать элементы операций", "显示更多": "Отображать больше", + "显示最新20条": "Отображать последние 20 записей", "显示第": "Отображать", "显示设置": "Настройки отображения", "显示调试": "Отображать отладку", "晚上好": "Добрый вечер", + "普通按量/按次直接填价格就行;如果价格要跟请求参数或请求头联动,请切到表达式/阶梯计费。": "", "普通环境变量": "Regular Environment Variables", "普通用户": "Обычный пользователь", "普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Обычный ввод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}", @@ -1953,9 +2016,12 @@ "更多": "Больше", "更多信息请参考": "Для получения дополнительной информации см.", "更多参数请参考": "Для получения дополнительных параметров см.", + "更多模板...": "", + "更好的价格,更好的稳定性,只需要将模型基址替换为:": "Лучшая цена, лучшая стабильность, просто замените базовый адрес модели на:", "更新": "Обновить", "更新 Creem 设置": "Обновить настройки Creem", "更新 Stripe 设置": "Обновить настройки Stripe", + "更新 Waffo Pancake 设置": "", "更新 Waffo 设置": "", "更新SSRF防护设置": "Обновить настройки защиты SSRF", "更新Worker设置": "Обновить настройки Worker", @@ -1971,6 +2037,7 @@ "更新所有已启用通道余额": "Обновить баланс всех включенных каналов", "更新支付设置": "Обновить настройки оплаты", "更新时间": "Время обновления", + "更新易支付设置": "", "更新服务器地址": "Обновить адрес сервера", "更新模型信息": "Обновить информацию о модели", "更新渠道信息": "Обновить информацию о канале", @@ -1980,6 +2047,20 @@ "更新配置失败": "Failed to update configuration", "更新预填组": "Обновить предварительно заполненную группу", "替换": "", + "最低": "Минимум", + "最低充值数量": "", + "最低充值美元数量": "Минимальная сумма пополнения в долларах", + "最低充值美元数量必须大于 0": "Минимальная сумма пополнения в долларах должна быть больше 0", + "最后使用时间": "Время последнего использования", + "最后更新": "Last Updated", + "最后请求": "Последний запрос", + "最大GPU数量": "Max Number of GPUs", + "最大可用": "Max Available", + "最大条目数": "Макс. кол-во записей", + "最终抵扣": "Итоговое списание", + "最近一次": "Последний", + "最近事件": "Recent Events", + "最高优先级": "highest priority", "月": "мес.", "有 Reasoning": "Есть рассуждение", "有序字符串数组": "Ordered string array", @@ -2036,8 +2117,8 @@ "本地数据存储": "Локальное хранение данных", "本地计费": "Локальная тарификация", "本月获得": "В этом месяце", - "本设备:手机指纹/面容,外接:USB安全密钥": "Это устройство: отпечаток пальца/лицо телефона, внешнее: USB-ключ безопасности", "本设备内置": "Встроенное в это устройство", + "本设备:手机指纹/面容,外接:USB安全密钥": "Это устройство: отпечаток пальца/лицо телефона, внешнее: USB-ключ безопасности", "本项目根据": "Этот проект основан на", "机密环境变量": "Secret Environment Variables", "机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。": "Secret environment variables will be stored encrypted, suitable for storing passwords, API keys and other sensitive information.", @@ -2046,13 +2127,15 @@ "权限设置": "Настройки прав доступа", "条": "запись", "条 - 第": "запись -", - "条,共": "записей, всего", + "条件": "", + "条件乘数": "", "条件取反": "Инвертировать условие", "条件数": "Условия", "条件规则": "Правила условий", "条件项设置": "Настройки элементов условий", "条日志已清理!": "записей журнала очищено!", "条规则": "rules", + "条,共": "записей, всего", "来源": "Источник", "来源于 IO.NET 部署": "From IO.NET Deployment", "来源端点": "Конечная точка источника", @@ -2086,11 +2169,15 @@ "核采样,控制词汇选择的多样性": "Ядерная выборка, управляет разнообразием выбора слов", "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。": "Согласно соглашению Anthropic, входные токены /v1/messages учитывают только некэшированный ввод и не включают токены чтения/записи кэша.", "根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含": "Поиск метаданных модели по имени и правилам соответствия, приоритет: точный > префикс > суффикс > содержит", + "格式": "", "格式化": "Форматировать", "格式化 JSON": "Форматировать JSON", "格式正确": "Действительный формат", "格式示例:": "Пример формата: ", "格式错误": "Недействительный формат", + "档": "", + "档位": "", + "档位名称": "", "检查更新": "Проверить обновления", "检测全部渠道上游更新": "", "检测到 FluentRead(流畅阅读)": "Обнаружен FluentRead (плавное чтение)", @@ -2111,10 +2198,10 @@ "模型价格 {{symbol}}{{price}} / 次": "Цена модели {{symbol}}{{price}} / запрос", "模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Цена модели {{symbol}}{{price}} / запрос * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}": "Цена модели {{symbol}}{{price}}, {{ratioType}} {{ratio}}", + "模型价格未配置": "Цена модели не настроена", "模型价格:{{symbol}}{{price}}": "Цена модели: {{symbol}}{{price}}", "模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "Цена модели: {{symbol}}{{price}} * {{ratioType}}: {{ratio}} = {{symbol}}{{total}}", "模型价格:{{symbol}}{{price}} / 次": "Цена модели: {{symbol}}{{price}} / запрос", - "模型价格未配置": "Цена модели не настроена", "模型倍率": "Коэффициент модели", "模型倍率 {{modelRatio}}": "Коэффициент модели {{modelRatio}}", "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}": "Коэффициент модели {{modelRatio}}, коэффициент кэша {{cacheRatio}}, коэффициент вывода {{completionRatio}}, {{ratioType}} {{ratio}}", @@ -2139,8 +2226,8 @@ "模型名称已存在": "Название модели уже существует", "模型固定价格": "Фиксированная цена модели", "模型图标": "Иконка модели", - "模型定价,需要登录访问": "Ценообразование моделей, требуется вход для доступа", "模型定价设置": "Model Pricing", + "模型定价,需要登录访问": "Ценообразование моделей, требуется вход для доступа", "模型广场": "Площадка моделей", "模型拉取失败: {{error}}": "Failed to pull model: {{error}}", "模型排行": "Рейтинг моделей", @@ -2150,8 +2237,8 @@ "模型更新成功!": "Модель успешно обновлена!", "模型未加入列表,可能无法调用": "Модель не добавлена в список, вызовы могут не работать", "模型正则": "Regex модели", - "模型正则(每行一个)": "Regex модели (по одному в строке)", "模型正则不能为空": "Regex модели не может быть пустым", + "模型正则(每行一个)": "Regex модели (по одному в строке)", "模型消耗分布": "Распределение потребления моделей", "模型消耗趋势": "Тенденции потребления моделей", "模型版本": "Версия модели", @@ -2185,8 +2272,8 @@ "正则替换": "", "正在加载可用部署位置...": "Loading available deployment locations...", "正在加载签到状态...": "Загрузка статуса регистрации...", - "正在处理,请稍候": "", "正在处理大内容...": "Обработка большого содержимого...", + "正在处理,请稍候": "", "正在批量处理,请稍候": "", "正在批量检测,请稍候": "", "正在提交": "Отправка...", @@ -2228,6 +2315,7 @@ "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "Этот параметр необязательный, используется для выполнения API вызовов через пользовательский адрес API, в конце не должно быть /v1 и /", "每个充值单位对应的 USD 金额,默认 1.0": "", "每个分组代表一个价格档位。管理员创建分组后,可以选择哪些档位对用户开放自选。": "Each group represents a pricing tier. After creating groups, admins can choose which tiers are open for user self-selection.", + "每个档位可设置 0~2 个条件(对 p 和 c),最后一档为兜底档无需条件。": "", "每个用户最多可创建的令牌数量,默认 1000,设置过大可能会影响性能": "Максимальное количество токенов, которое может создать каждый пользователь, по умолчанию 1000. Слишком большое значение может повлиять на производительность", "每周": "Еженедельно", "每天": "Ежедневно", @@ -2261,8 +2349,8 @@ "注册": "Регистрация", "注册 Passkey": "Регистрация Passkey", "注意": "Внимание", - "注意:JSON中重复的键只会保留最后一个同名键的值": "Внимание: в JSON повторяющиеся ключи сохранят только значение последнего ключа с тем же именем", "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Внимание: это не Chat API, обязательно укажите правильный адрес API, иначе это может привести к невозможности использования", + "注意:JSON中重复的键只会保留最后一个同名键的值": "Внимание: в JSON повторяющиеся ключи сохранят только значение последнего ключа с тем же именем", "注销": "Выйти", "注销成功!": "Выход выполнен успешно!", "活跃文件": "Активные файлы", @@ -2317,6 +2405,10 @@ "添加启动命令": "Add Startup Command", "添加密钥环境变量": "Add Secret Environment Variable", "添加成功": "Добавлено успешно", + "添加时间条件": "", + "添加更多档位": "", + "添加条件": "", + "添加条件组": "", "添加模型": "Добавить модель", "添加模型区域": "Добавить область модели", "添加渠道": "Добавить канал", @@ -2343,8 +2435,8 @@ "渠道 ID": "ID канала", "渠道ID,名称,密钥,API地址": "ID Канала, имя, Токен, адрес API", "渠道亲和性": "Аффинитет канала", - "渠道亲和性:上游缓存命中": "Аффинити канала: попадание в кэш вышестоящего", "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。": "Аффинити канала повторно использует последний успешный канал на основе ключей, извлечённых из контекста запроса или JSON body.", + "渠道亲和性:上游缓存命中": "Аффинити канала: попадание в кэш вышестоящего", "渠道优先级": "Приоритет канала", "渠道信息": "Информация о канале", "渠道创建成功!": "Канал создан успешно!", @@ -2367,6 +2459,7 @@ "渠道额外设置": "Дополнительные настройки канала", "源地址": "Исходный адрес", "满足任一条件(OR)": "Совпадение любого условия (OR)", + "满足条件时,整单价格乘以 X;如果有多条同时命中,会继续相乘。": "", "演示站点": "Демонстрационный сайт", "演示站点模式": "Режим демонстрационного сайта", "点击 + 按钮添加图片URL进行多模态对话": "Нажмите + для добавления URL изображений для мультимодального диалога", @@ -2404,7 +2497,13 @@ "生成音乐": "Сгенерировать музыку", "生效": "Активно", "用于API调用的身份验证令牌,请妥善保管": "Токен аутентификации для API вызовов, пожалуйста, храните его надёжно", + "用于切换当前下单和回调校验所使用的环境": "", "用于唯一标识用户的字段路径": "Путь поля для уникальной идентификации пользователей", + "用于校验 Stripe Webhook 签名,保存后不会回显": "", + "用于校验测试环境的 Waffo Pancake Webhook 签名": "", + "用于校验测试环境的 Waffo 回调签名": "", + "用于校验生产环境的 Waffo Pancake Webhook 签名": "", + "用于校验生产环境的 Waffo 回调签名": "", "用于配置网络代理,支持 socks5 协议": "Используется для настройки сетевого прокси, поддерживает протокол socks5", "用于验证回调 new-api 的 webhook 请求的密钥,敏感信息不显示": "Ключ для проверки обратных запросов new-api по webhook, чувствительные данные не показываются.", "用以支持基于 WebAuthn 的无密码登录注册": "Используется для поддержки входа и регистрации без пароля на основе WebAuthn", @@ -2419,13 +2518,13 @@ "用以防止恶意用户利用临时邮箱批量注册": "Используется для предотвращения массовой регистрации злоумышленниками с использованием временных почтовых ящиков", "用户": "Пользователь", "用户 ID 字段(可选)": "Поле ID пользователя (необязательно)", + "用户ID": "", "用户个人功能": "Персональные функции пользователя", "用户主页,展示系统信息": "Главная страница пользователя, отображение системной информации", "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "Приоритет пользователя: если пользователь указал системное приглашение в запросе, будут использоваться настройки пользователя", "用户信息": "Информация о пользователе", "用户信息更新成功!": "Информация о пользователе обновлена успешно!", "用户信息缺失": "Информация о пользователе отсутствует", - "用户最大令牌数量": "Максимальное количество токенов на пользователя", "用户分组": "Группы пользователей", "用户分组和额度管理": "Управление группами пользователей и лимитами", "用户分组的联动作用": "How user group affects other features", @@ -2435,6 +2534,7 @@ "用户协议": "Пользовательское соглашение", "用户协议已更新": "Пользовательское соглашение обновлено", "用户协议更新失败": "Не удалось обновить пользовательское соглашение", + "用户单次最少可充值的美元数量": "", "用户可选": "User Selectable", "用户可选分组": "Доступные для выбора группы пользователей", "用户可选分组的名称和描述(只包含勾选了用户可选的分组)": "Names and descriptions of user-selectable groups (only includes checked groups)", @@ -2445,6 +2545,7 @@ "用户在充值页面看到的支付方式名称,例如:Credit Card": "", "用户控制面板,管理账户": "Панель управления пользователя, управление аккаунтом", "用户新建令牌时可选的分组,格式为 JSON 字符串,例如:{\"vip\": \"VIP 用户\", \"test\": \"测试\"},表示用户可以选择 vip 分组和 test 分组": "Группы, доступные для выбора при создании токена пользователем, формат JSON строки, например: {\"vip\": \"VIP пользователь\", \"test\": \"тест\"}, означает, что пользователь может выбрать группу vip и группу test", + "用户最大令牌数量": "Максимальное количество токенов на пользователя", "用户每周期最多请求完成次数": "Максимальное количество выполненных запросов пользователя за период", "用户每周期最多请求次数": "Максимальное количество запросов пользователя за период", "用户注册时看到的网站名称,比如'我的网站'": "Название сайта, которое видят пользователи при регистрации, например 'Мой сайт'", @@ -2466,8 +2567,10 @@ "留空则使用默认端点;支持 {path, method}": "Если оставить пустым, будет использоваться конечная точка по умолчанию; поддерживает {path, method}", "留空则保持原有密钥": "Оставьте пустым для сохранения существующего ключа", "留空则自动使用 服务器地址 + /api/waffo/webhook": "", + "留空则自动使用当前站点的默认充值页地址": "", "留空则自动使用当前站点的默认回调地址": "Оставьте пустым, чтобы использовать адрес обратного вызова сайта по умолчанию", "留空则默认使用服务器地址,注意不能携带http://或者https://": "Если оставить пустым, по умолчанию будет использоваться адрес сервера, обратите внимание, что нельзя указывать http:// или https://", + "留空时默认使用服务器地址作为回调地址,填写后将覆盖默认值": "", "登 录": "ВОЙТИ", "登录": "Войти", "登录成功!": "Вход выполнен успешно!", @@ -2494,7 +2597,6 @@ "硬件类型": "Hardware Type", "硬件配置": "Hardware Configuration", "确定": "Подтвердить", - "确定?": "Подтвердить?", "确定删除此组?": "Удалить эту группу?", "确定导入": "Подтвердить импорт", "确定是否要修复数据库一致性?": "Подтвердить, нужно ли восстановить согласованность базы данных?", @@ -2542,6 +2644,7 @@ "确定要降级此用户吗?": "Подтвердить понижение этого пользователя?", "确定重置": "Подтвердить сброс", "确定重置模型倍率吗?": "Подтвердить сброс коэффициента модели?", + "确定?": "Подтвердить?", "确认": "Подтверждение", "确认作废": "Подтвердить аннулирование", "确认关闭提示": "Подтвердить закрытие", @@ -2549,8 +2652,8 @@ "确认切换": "Подтвердить переключение", "确认删除": "Подтвердить удаление", "确认删除模型": "Confirm Delete Model", - "确认删除该分组?": "Confirm delete this group?", "确认删除该分组的所有规则?": "Delete all rules for this group?", + "确认删除该分组?": "Confirm delete this group?", "确认删除该规则?": "Confirm delete this rule?", "确认前,支付、兑换码、订阅计划和邀请返利功能将保持锁定。": "Payment, redemption codes, subscription plans, and invitation rewards remain locked until confirmation.", "确认取消密码登录": "Подтвердить отмену входа по паролю", @@ -2583,16 +2686,13 @@ "磁盘使用率超过此值时拒绝请求": "Отклонять запросы, когда использование диска превышает это значение", "磁盘可用空间小于缓存最大总量设置": "Доступное дисковое пространство меньше настройки максимального размера кэша", "磁盘命中": "Попадания на диск", - "磁盘缓存最大总量 (MB)": "Максимальный объём дискового кэша (МБ)", "磁盘缓存占用的最大空间": "Максимальное пространство, занимаемое дисковым кэшем", "磁盘缓存已清理": "Дисковый кэш очищен", + "磁盘缓存最大总量 (MB)": "Максимальный объём дискового кэша (МБ)", "磁盘缓存设置(磁盘换内存)": "Настройки дискового кэша (обмен диска/памяти)", "磁盘缓存阈值 (MB)": "Порог дискового кэша (МБ)", "示例": "Пример", "示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。": "Пример: {\"default\": [200, 100], \"vip\": [0, 1000]}.", - "视频": "Видео", - "视频Remix": "Видео ремикс", - "视频无法在当前浏览器中播放,这可能是由于:": "Видео нельзя воспроизвести в этом браузере, возможные причины:", "禁用": "Отключить", "禁用 store 透传": "Отключить сквозную передачу store", "禁用2FA失败": "Ошибка отключения 2FA", @@ -2614,8 +2714,8 @@ "移动请求头": "", "移除": "Remove", "移除 (-:)": "Remove (-:)", - "移除 functionResponse.id 字段": "Удалить поле functionResponse.id", "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "Удаление авторских знаков One API требует предварительного разрешения, поддержка проекта требует больших усилий, если этот проект важен для вас, пожалуйста, поддержите его", + "移除 functionResponse.id 字段": "Удалить поле functionResponse.id", "空": "Пусто", "窗口处理": "Обработка окна", "窗口等待": "Ожидание окна", @@ -2642,25 +2742,28 @@ "第 {{line}} 条操作缺少目标路径": "Правило #{{line}}: операция без целевого пути", "第 {{line}} 条请求头透传格式无效": "Правило #{{line}}: недопустимый формат передачи заголовка", "第 {{line}} 条请求头透传缺少请求头名称": "Правило #{{line}}: передача заголовка без имени заголовка", + "第 {{n}} 档": "", + "第 {{n}} 组": "", "第三方支付配置": "Настройки сторонних платежей", "第三方账户绑定状态(只读)": "Статус привязки сторонних аккаунтов (только для чтения)", + "等于": "", "等价金额:": "Эквивалентная сумма:", "等待中": "Ожидание", "等待获取邮箱信息...": "Ожидание получения информации об email...", "筛选": "Фильтр", - "签到最大额度": "Максимальная квота регистрации", - "签到最小额度": "Минимальная квота регистрации", "签到功能允许用户每日签到获取随机额度奖励": "Функция регистрации позволяет пользователям регистрироваться ежедневно для получения случайных наград в виде квоты", "签到失败": "Регистрация не удалась", "签到奖励将直接添加到您的账户余额": "Награды за регистрацию будут напрямую добавлены на баланс вашего счета", "签到奖励的最大额度": "Максимальная квота для наград за регистрацию", "签到奖励的最小额度": "Минимальная квота для наград за регистрацию", "签到成功!获得": "Регистрация успешна! Получено", + "签到最大额度": "Максимальная квота регистрации", + "签到最小额度": "Минимальная квота регистрации", "签到设置": "Настройки регистрации", "简单来说:同一个令牌分组,不同等级的用户可以享受不同的价格。": "In short: same token group, different user tiers can enjoy different prices.", "简洁": "Простой", - "简洁模式:按 type 全量清理对象,例如 redacted_thinking。": "Простой режим: очистка всех объектов по типу, напр. redacted_thinking.", "简洁模式仅返回 message;状态码和错误类型将使用系统默认值。": "Простой режим возвращает только сообщение; код состояния и тип ошибки будут использовать системные значения по умолчанию.", + "简洁模式:按 type 全量清理对象,例如 redacted_thinking。": "Простой режим: очистка всех объектов по типу, напр. redacted_thinking.", "管理": "Управление", "管理 Ollama 模型的拉取和删除": "Manage Ollama model pulling and deletion", "管理你的 LinuxDO OAuth App": "Управление вашим LinuxDO OAuth App", @@ -2670,9 +2773,11 @@ "管理员区域": "Область администратора", "管理员暂时未设置任何关于内容": "Администратор пока не установил никакой информации о проекте", "管理员未开启 Creem 充值!": "Администратор не включил пополнение через Creem!", + "管理员未开启 Waffo Pancake 充值!": "", + "管理员未开启 Waffo 充值!": "", "管理员未开启Stripe充值!": "Администратор не включил пополнение через Stripe!", - "管理员未开启在线充值!": "Администратор не включил онлайн пополнение!", "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "Администратор не включил функцию онлайн пополнения, свяжитесь с администратором для включения или используйте коды купонов для пополнения.", + "管理员未开启在线充值!": "Администратор не включил онлайн пополнение!", "管理员未开启在线支付功能,请联系管理员配置。": "Онлайн-оплата не включена администратором. Пожалуйста, свяжитесь с администратором.", "管理员未设置用户可选分组": "Администратор не установил доступные для выбора группы пользователей", "管理员给用户分配的分组(如 vip)不仅决定用户身份,还会影响后续两个功能:": "The group assigned to a user (e.g., vip) not only determines their identity, but also affects the following two features:", @@ -2716,12 +2821,12 @@ "系统管理功能": "Функции системного управления", "系统设置": "Система", "系统访问令牌": "Токен доступа к системе", - "约": "Приблизительно", "索引": "Индекс", "紧凑列表": "Компактный список", - "纯字符串会直接覆盖整条请求头,或者点击“查看 JSON 示例”按 token 规则处理。": "", "累计签到": "Всего регистраций", "累计获得": "Всего получено", + "约": "Приблизительно", + "纯字符串会直接覆盖整条请求头,或者点击“查看 JSON 示例”按 token 规则处理。": "", "线路描述": "Описание маршрута", "组列表": "Список групп", "组名": "Имя группы", @@ -2751,45 +2856,48 @@ "统计次数": "Статистика количества", "统计额度": "Статистика лимитов", "继续": "Продолжить", + "缓存 Tokens": "Кэширование токенов", "缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Кэш {{tokens}} токенов / 1M токенов * {{symbol}}{{price}}", "缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "Кэш {{tokens}} токенов / 1M токенов * {{symbol}}{{price}} (множитель: {{ratio}})", - "缓存 Tokens": "Кэширование токенов", "缓存: {{cacheRatio}}": "Кэш: {{cacheRatio}}", "缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Цена кэша: {{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M токенов (коэффициент кэширования: {{cacheRatio}})", "缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Цена кэша: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M токенов (коэффициент кэширования: {{cacheRatio}})", "缓存倍率": "Коэффициент кэширования", "缓存倍率 {{cacheRatio}}": "Коэффициент кэша {{cacheRatio}}", "缓存写": "Запись в кэш", + "缓存创建": "", + "缓存创建 Tokens": "Создание кэша токенов", "缓存创建 {{price}} / 1M tokens": "Создание кэша {{price}} / 1M tokens", "缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Создание кэша {{tokens}} токенов / 1M токенов * {{symbol}}{{price}}", "缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "Создание кэша {{tokens}} токенов / 1M токенов * {{symbol}}{{price}} (множитель: {{ratio}})", - "缓存创建 Tokens": "Создание кэша токенов", - "缓存创建: {{cacheCreationRatio}}": "Создание кэша: {{cacheCreationRatio}}", + "缓存创建-1小时": "", "缓存创建: 1h {{cacheCreationRatio1h}}": "Создание кэша: 1h {{cacheCreationRatio1h}}", "缓存创建: 5m {{cacheCreationRatio5m}}": "Создание кэша: 5m {{cacheCreationRatio5m}}", "缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "Создание кэша: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}", - "缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Создание кэша: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент создания кэша {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "缓存创建: {{cacheCreationRatio}}": "Создание кэша: {{cacheCreationRatio}}", "缓存创建价格": "Цена создания входного кеша", "缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Цена создания кеша {{symbol}}{{price}} / 1M tokens", + "缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens": "Итого цена создания кэша: 5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M токенов", "缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "Цена создания кэша: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M токенов (коэффициент создания кэша: {{cacheCreationRatio}})", "缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Цена создания кеша: {{symbol}}{{price}} / 1M tokens", - "缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens": "Итого цена создания кэша: 5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M токенов", "缓存创建倍率": "Коэффициент создания кэша", - "缓存创建倍率 {{cacheCreationRatio}}": "Коэффициент создания кэша {{cacheCreationRatio}}", "缓存创建倍率 1h {{cacheCreationRatio1h}}": "Множитель создания кэша 1h {{cacheCreationRatio1h}}", "缓存创建倍率 5m {{cacheCreationRatio5m}}": "Множитель создания кэша 5m {{cacheCreationRatio5m}}", "缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "Коэффициент создания кэша 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}", + "缓存创建倍率 {{cacheCreationRatio}}": "Коэффициент создания кэша {{cacheCreationRatio}}", + "缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Создание кэша: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент создания кэша {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "缓存条目数": "Количество записей кэша", "缓存目录": "Директория кэша", "缓存目录磁盘空间": "Дисковое пространство директории кэша", "缓存读": "Чтение кэша", "缓存读 {{price}} / 1M tokens": "Чтение кеша {{price}} / 1M tokens", - "缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Чтение кэша: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент кэша {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "缓存读取": "", "缓存读取价格": "Цена чтения входного кеша", "缓存读取价格 {{symbol}}{{price}} / 1M tokens": "Цена чтения кеша {{symbol}}{{price}} / 1M tokens", "缓存读取价格:{{symbol}}{{price}} / 1M tokens": "Цена чтения кеша: {{symbol}}{{price}} / 1M tokens", "缓存读取价格:{{symbol}}{{total}} / 1M tokens": "Цена чтения кеша: {{symbol}}{{total}} / 1M tokens", "缓存读取倍率": "", + "缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Чтение кэша: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент кэша {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Кэшированный ввод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент кэша {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "编辑": "Редактировать", "编辑 OAuth 提供商": "Редактировать OAuth-провайдера", @@ -2871,10 +2979,10 @@ "若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置": "Если ваш OIDC Provider поддерживает Discovery Endpoint, вы можете указать только OIDC Well-Known URL, и система автоматически получит OIDC конфигурацию", "获取 Discovery 配置": "Получить конфигурацию Discovery", "获取 Discovery 配置失败:": "Не удалось получить конфигурацию Discovery: ", - "获取 io.net API Key": "Get io.net API Key", "获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确": "Не удалось получить OIDC конфигурацию, проверьте состояние сети и правильность Well-Known URL", "获取 OIDC 配置成功!": "OIDC конфигурация успешно получена!", "获取 Ollama 版本失败": "Failed to get Ollama version", + "获取 io.net API Key": "Get io.net API Key", "获取2FA状态失败": "Не удалось получить статус 2FA", "获取令牌密钥失败": "", "获取充值配置失败": "Не удалось получить конфигурацию пополнения", @@ -2908,9 +3016,9 @@ "补全 {{completion}} tokens * 输出倍率 {{completionRatio}}": "Дополнение {{completion}} токенов * коэффициент вывода {{completionRatio}}", "补全 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}": "Дополнение {{completion}} токенов / 1M токенов * {{symbol}}{{price}}", "补全价格": "Цена завершения", + "补全价格已锁定": "Цена завершения заблокирована", "补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "Цена вывода: {{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M токенов (коэффициент вывода: {{completionRatio}})", "补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens": "Цена вывода: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M токенов", - "补全价格已锁定": "Цена завершения заблокирована", "补全倍率": "Коэффициент вывода", "补全倍率 {{completionRatio}}": "Коэффициент вывода {{completionRatio}}", "补全倍率值": "Значение коэффициента вывода", @@ -2919,10 +3027,15 @@ "补单成功": "Заказ успешно дополнен", "表单引用错误,请刷新页面重试": "Ошибка ссылки формы, обновите страницу и попробуйте снова", "表格视图": "Табличное представление", + "表达式/阶梯计费": "", + "表达式编辑": "", + "表达式计费": "", + "表达式错误": "", "覆盖": "Заменить", "覆盖模式:将完全替换现有的所有密钥": "Режим перезаписи: полностью заменит все существующие ключи", "覆盖模板": "Шаблон переопределения", "覆盖现有密钥": "Перезаписать существующие ключи", + "见上方动态计费详情": "", "规则": "Правило", "规则 JSON": "JSON правила", "规则 JSON 格式不正确": "Некорректный формат JSON правила", @@ -2932,6 +3045,9 @@ "规则导航": "Навигация по правилам", "规则描述(可选)": "", "规则未找到,请刷新后重试": "Правило не найдено, обновите страницу и попробуйте снова", + "视频": "Видео", + "视频Remix": "Видео ремикс", + "视频无法在当前浏览器中播放,这可能是由于:": "Видео нельзя воспроизвести в этом браузере, возможные причины:", "角色": "Роль", "解析响应数据时发生错误": "Произошла ошибка при разборе данных ответа", "解析密钥文件失败: {{msg}}": "Не удалось разобрать файл ключа: {{msg}}", @@ -2940,6 +3056,9 @@ "解绑 Passkey": "Отвязать Passkey", "解绑后将无法使用 Passkey 登录,确定要继续吗?": "После отвязки невозможно будет использовать Passkey для входа, продолжить?", "解绑成功": "Успешно отвязано", + "警告": "Предупреждение", + "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "Предупреждение: после включения поддержания активности, если канал выдаёт ошибку после записи данных поддержания активности, система не может повторить попытку, если необходимо включить, рекомендуется установить максимально возможный интервал Ping", + "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "Предупреждение: отключение двухфакторной аутентификации навсегда удалит ваши настройки проверки и все резервные коды, эта операция необратима!", "计价币种": "Pricing Currency", "计算中": "Calculating", "计算成本": "Calculate Cost", @@ -3031,10 +3150,10 @@ "语音输入": "Голосовой ввод", "语音输出": "Голосовой вывод", "说明": "Описание", + "说明信息": "Информация об описании", "说明:": "Описание:", "说明:本页测试为非流式请求;若渠道仅支持流式返回,可能出现测试失败,请以实际使用为准。": "Примечание: тесты на этой странице используют нестриминговые запросы. Если канал поддерживает только стриминговые ответы, тест может завершиться неудачей. Ориентируйтесь на реальное использование.", "说明:生成结果是可直接粘贴到渠道密钥里的 JSON(包含 access_token / refresh_token / account_id)。": "Примечание: Результат — это JSON, который можно вставить непосредственно в ключ канала (содержит access_token / refresh_token / account_id).", - "说明信息": "Информация об описании", "请上传密钥文件": "Пожалуйста, загрузите файл ключа", "请上传密钥文件!": "Пожалуйста, загрузите файл ключа!", "请为渠道命名": "Пожалуйста, назовите канал", @@ -3068,6 +3187,9 @@ "请填写完整的产品信息": "Пожалуйста, заполните всю информацию о продукте", "请填写完整的管理员账号信息": "Пожалуйста, заполните полную информацию об учётной записи администратора", "请填写密钥": "Пожалуйста, заполните ключ", + "请填写当前环境对应的 Product ID": "", + "请填写当前环境对应的 Store ID": "", + "请填写当前环境对应的商户 ID": "", "请填写渠道名称和渠道密钥!": "Пожалуйста, заполните имя канала и ключ канала!", "请填写部署地区": "Пожалуйста, заполните регион развертывания", "请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "Пожалуйста, храните информацию о ключе в безопасности, не разглашайте её другим. При наличии сомнений в безопасности, своевременно замените ключ.", @@ -3079,16 +3201,20 @@ "请求体内存缓存": "Кэш тела запроса в памяти", "请求体磁盘缓存": "Дисковый кэш тела запроса", "请求体超过此大小时使用磁盘缓存": "Использовать дисковый кэш при превышении тела запроса этого размера", + "请求参数": "", "请求参数无效": "Invalid request parameters", "请求发生错误": "Произошла ошибка запроса", "请求发生错误: ": "Произошла ошибка запроса: ", "请求后端接口失败:": "Не удалось запросить внутренний интерфейс:", "请求失败": "Запрос не удался", + "请求头": "", "请求头覆盖": "Переопределение заголовков запроса", "请求并计费模型": "Запрос и выставление счёта модели", "请求时长: ${time}s": "Время запроса: ${time}s", + "请求条件调价": "", "请求次数": "Количество запросов", "请求结束后多退少补": "После вывода запроса возврат излишков и доплата недостатка", + "请求规则": "", "请求超时,请刷新页面后重新发起 GitHub 登录": "Время ожидания истекло, обновите страницу и снова запустите вход через GitHub", "请求路径": "Путь запроса", "请求转换": "Преобразование запроса", @@ -3114,18 +3240,20 @@ "请至少选择一个渠道": "Пожалуйста, выберите хотя бы один канал", "请输入 API Key,一行一个,格式:APIKey|Region": "Введите API Key, по одному в строке, формат: APIKey|Region", "请输入 API Key,格式:APIKey|Region": "Введите API Key в формате: APIKey|Region", - "请输入 Authorization Endpoint": "Введите Authorization Endpoint", "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "Пожалуйста, введите AZURE_OPENAI_ENDPOINT, например: https://docs-test-001.openai.azure.com", + "请输入 Authorization Endpoint": "Введите Authorization Endpoint", "请输入 Client ID": "Введите Client ID", "请输入 Client Secret": "Введите Client Secret", - "请输入 io.net API Key": "Please enter io.net API Key", - "请输入 io.net API Key(敏感信息不显示)": "Please enter io.net API Key (sensitive information not displayed)", "请输入 JSON 格式的 OAuth 凭据,例如:\n{\n \"access_token\": \"...\",\n \"account_id\": \"...\" \n}": "Введите учётные данные OAuth в формате JSON, напр.:\n{\n \"access_token\": \"...\",\n \"account_id\": \"...\" \n}", "请输入 JSON 格式的密钥内容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}": "Пожалуйста, введите содержимое ключа в формате JSON, например:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}", "请输入 OIDC 的 Well-Known URL": "Пожалуйста, введите Well-Known URL OIDC", + "请输入 Product ID": "", "请输入 Slug": "Введите Slug", + "请输入 Store ID": "", "请输入 Token Endpoint": "Введите Token Endpoint", "请输入 User Info Endpoint": "Введите User Info Endpoint", + "请输入 io.net API Key": "Please enter io.net API Key", + "请输入 io.net API Key(敏感信息不显示)": "Please enter io.net API Key (sensitive information not displayed)", "请输入6位验证码或8位备用码": "Пожалуйста, введите 6-значный код подтверждения или 8-значный резервный код", "请输入API地址": "Пожалуйста, введите адрес API", "请输入API地址!": "Пожалуйста, введите адрес API!", @@ -3135,9 +3263,9 @@ "请输入Gotify服务器地址": "Пожалуйста, введите адрес сервера Gotify", "请输入Gotify服务器地址,例如: https://gotify.example.com": "Пожалуйста, введите адрес сервера Gotify, например: https://gotify.example.com", "请输入JSON数组,如 [\"model-a\",\"model-b\"]": "Введите JSON-массив, например [\"model-a\",\"model-b\"]", + "请输入URL链接": "Пожалуйста, введите URL-ссылку", "请输入Uptime Kuma地址": "Пожалуйста, введите адрес Uptime Kuma", "请输入Uptime Kuma服务地址,如:https://status.example.com": "Пожалуйста, введите адрес службы Uptime Kuma, например: https://status.example.com", - "请输入URL链接": "Пожалуйста, введите URL-ссылку", "请输入Webhook地址": "Пожалуйста, введите адрес Webhook", "请输入Webhook地址,例如: https://example.com/webhook": "Пожалуйста, введите адрес Webhook, например: https://example.com/webhook", "请输入以下文字以确认:": "Please type the following text to confirm:", @@ -3156,6 +3284,7 @@ "请输入原密码": "Пожалуйста, введите старый пароль", "请输入原密码!": "Пожалуйста, введите старый пароль!", "请输入名称": "Пожалуйста, введите имя", + "请输入商户 ID": "", "请输入回答内容": "Пожалуйста, введите содержание ответа", "请输入回答内容(支持 Markdown/HTML)": "Пожалуйста, введите содержание ответа (поддерживается Markdown/HTML)", "请输入图标名称": "Пожалуйста, введите имя иконки", @@ -3163,14 +3292,14 @@ "请输入备注(仅管理员可见)": "Пожалуйста, введите примечание (видимо только администратору)", "请输入套餐标题": "Введите название плана", "请输入完整的 JSON 格式密钥内容": "Пожалуйста, введите полное содержимое ключа в формате JSON", - "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Пожалуйста, введите полный URL, например: https://api.openai.com/v1/chat/completions", "请输入完整的URL链接": "Пожалуйста, введите полную URL-ссылку", + "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Пожалуйста, введите полный URL, например: https://api.openai.com/v1/chat/completions", "请输入容器名称": "Please enter container name", "请输入密码": "Пожалуйста, введите пароль", "请输入密钥": "Пожалуйста, введите ключ", + "请输入密钥!": "Пожалуйста, введите ключ!", "请输入密钥,一行一个": "Пожалуйста, введите ключи, по одному в строке", "请输入密钥,一行一个,格式:AccessKey|SecretAccessKey|Region": "Введите ключи по одному в строке в формате: AccessKey|SecretAccessKey|Region", - "请输入密钥!": "Пожалуйста, введите ключ!", "请输入延长时长": "Please enter extension duration", "请输入总额度": "Введите общий лимит", "请输入您的密码": "Пожалуйста, введите ваш пароль", @@ -3224,9 +3353,9 @@ "请输入认证器验证码或备用码": "Пожалуйста, введите код подтверждения аутентификатора или резервный код", "请输入说明": "Пожалуйста, введите описание", "请输入运行时长": "Please enter runtime duration", - "请输入邮箱!": "Пожалуйста, введите адрес электронной почты!", "请输入邮箱地址": "Пожалуйста, введите адрес электронной почты", "请输入邮箱验证码!": "Пожалуйста, введите код подтверждения электронной почты!", + "请输入邮箱!": "Пожалуйста, введите адрес электронной почты!", "请输入部署名称": "Please enter deployment name", "请输入部署名称以完成二次确认": "Enter deployment name to complete secondary confirmation", "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Пожалуйста, введите регион развертывания, например: us-central1\nПоддерживается формат сопоставления моделей\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}", @@ -3278,9 +3407,6 @@ "调用趋势": "Тенденция вызовов", "调试信息": "Отладочная информация", "谨慎": "Осторожно", - "警告": "Предупреждение", - "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "Предупреждение: после включения поддержания активности, если канал выдаёт ошибку после записи данных поддержания активности, система не может повторить попытку, если необходимо включить, рекомендуется установить максимально возможный интервал Ping", - "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "Предупреждение: отключение двухфакторной аутентификации навсегда удалит ваши настройки проверки и все резервные коды, эта операция необратима!", "豆包": "Doubao", "账单": "Счёт", "账户充值": "Пополнение счёта", @@ -3312,6 +3438,7 @@ "跨分组": "Межгрупповой", "跨分组特殊倍率": "Cross-Group Special Ratios", "跨分组重试": "Повторная попытка между группами", + "跨夜范围": "", "路径正则": "Regex пути", "路径正则(每行一个)": "Regex пути (по одному в строке)", "跳转": "Перейти", @@ -3322,12 +3449,15 @@ "轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能": "Режим опроса должен использоваться вместе с функциями Redis и кэширования памяти, иначе производительность значительно снизится, и функция опроса не будет реализована", "软错误": "", "输入": "Ввод", - "输入 {{price}} / 1M tokens": "Вход {{price}} / 1M tokens", "输入 $/1M tokens": "", "输入 OIDC 的 Authorization Endpoint": "Введите Authorization Endpoint OIDC", "输入 OIDC 的 Client ID": "Введите Client ID OIDC", "输入 OIDC 的 Token Endpoint": "Введите Token Endpoint OIDC", "输入 OIDC 的 Userinfo Endpoint": "Введите Userinfo Endpoint OIDC", + "输入 Token": "", + "输入 Token 数": "", + "输入 Token 数量,查看按当前配置的预计费用(不含分组倍率)。": "", + "输入 {{price}} / 1M tokens": "Вход {{price}} / 1M tokens", "输入IP地址后回车,如:8.8.8.8": "Введите IP-адрес и нажмите Enter, например: 8.8.8.8", "输入JSON对象": "Введите JSON-объект", "输入价格": "Цена ввода", @@ -3353,6 +3483,7 @@ "输入补全价格": "Введите цену вывода", "输入补全倍率": "Введите коэффициент вывода", "输入要添加的邮箱域名": "Введите доменное имя электронной почты для добавления", + "输入计费表达式...": "", "输入认证器应用显示的6位数字验证码": "Введите 6-значный код подтверждения, отображаемый в приложении аутентификатора", "输入邮箱地址": "Введите адрес электронной почты", "输入金额": "Введите сумму", @@ -3361,15 +3492,17 @@ "输入验证码": "Введите код подтверждения", "输入验证码完成设置": "Введите код подтверждения для вывода настройки", "输出": "Вывод", + "输出 Token": "", + "输出 Token 数": "", "输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}": "Вывод {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}", - "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Вывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент завершения {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", - "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Вывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент вывода {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "输出价格": "Цена вывода", "输出价格 {{symbol}}{{price}} / 1M tokens": "Цена вывода {{symbol}}{{price}} / 1M tokens", "输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "Цена вывода: {{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M токенов (коэффициент вывода: {{completionRatio}})", "输出价格:{{symbol}}{{price}} / 1M tokens": "Цена вывода: {{symbol}}{{price}} / 1M tokens", "输出价格:{{symbol}}{{total}} / 1M tokens": "Цена вывода: {{symbol}}{{total}} / 1M tokens", "输出倍率 {{completionRatio}}": "Коэффициент вывода {{completionRatio}}", + "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Вывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент завершения {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Вывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент вывода {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "边栏设置": "Настройки боковой панели", "过期于": "Истекает", "过期时间": "Время истечения", @@ -3385,12 +3518,14 @@ "返回修改": "Вернуться и исправить", "返回登录": "Вернуться к входу", "返回错误": "", + "这个公式比较复杂,下面的简化表单没法完整还原,请在表达式编辑模式下修改。": "", "这个界面默认按价格填写,保存时会自动换算回后端需要的倍率 JSON。": "В этом интерфейсе значения по умолчанию задаются через цены, а при сохранении они автоматически преобразуются в JSON коэффициентов, требуемый backend.", "这些价格都是可选项,不填也可以。": "Все эти цены необязательны и могут быть оставлены пустыми.", "这将删除超过 10 分钟未使用的临时缓存文件": "Это удалит временные файлы кэша, которые не использовались более 10 минут", "这是基础金额,实际扣费 = 基础金额 x 系统分组倍率。": "Это базовая сумма. Фактическое удержание = базовая сумма × коэффициент системной группы.", "这是重复键中的最后一个,其值将被使用": "Это последний ключ в повторяющихся, его значение будет использовано", "这里直接编辑 JSON 对象。适合简单覆盖参数的场景。": "Редактируйте JSON-объект непосредственно здесь. Подходит для простых сценариев переопределения параметров.", + "这里配置 Waffo 下展示给用户的 Card、Apple Pay、Google Pay 等子支付方式。": "", "进度": "Прогресс", "进行中": "В процессе", "进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "При выполнении этой операции могут возникнуть ошибки доступа к каналам, используйте только при проблемах с базой данных", @@ -3447,7 +3582,9 @@ "透传请求头": "", "递归": "Рекурсия", "递归策略": "Стратегия рекурсии", + "逗号分隔的通配符模式。设置后,仅匹配的客户端 User-Agent 可使用此渠道。留空则允许所有客户端。": "", "通义千问": "Tongyi Qianwen", + "通用缓存": "", "通用设置": "Общие настройки", "通知": "Уведомления", "通知、价格和隐私相关设置": "Настройки уведомлений, цен и конфиденциальности", @@ -3517,7 +3654,7 @@ "配置 Turnstile": "Настроить Turnstile", "配置 Waffo 充值时可用的支付方式,保存后在充值页面展示给用户。": "", "配置 WeChat Server": "Настроить WeChat Server", - "配置:": "Конфиг:", + "配置各工具的调用价格($/1K次调用)。按次计费模型不额外收取工具费用。": "", "配置后的效果:": "After configuration:", "配置和消息已全部重置": "Конфигурация и сообщения полностью сброшены", "配置套餐的有效时长": "Настроить срок действия плана", @@ -3536,6 +3673,7 @@ "配置自定义 OAuth 提供商,支持 GitHub Enterprise、GitLab、Gitea、Nextcloud、Keycloak、ORY 等兼容 OAuth 2.0 协议的身份提供商": "Настройте пользовательских OAuth-провайдеров, поддерживаются GitHub Enterprise, GitLab, Gitea, Nextcloud, Keycloak, ORY и другие провайдеры идентификации, совместимые с OAuth 2.0", "配置说明": "Описание конфигурации", "配置邮箱域名白名单": "Настроить белый список доменов электронной почты", + "配置:": "Конфиг:", "重启部署失败": "Failed to restart deployment", "重命名部署": "Rename Deployment", "重复提交": "Повторная отправка", @@ -3589,6 +3727,7 @@ "镜像配置": "Image Configuration", "问题标题": "Заголовок проблемы", "队列中": "В очереди", + "阶梯计费": "", "阶梯计费(未匹配到对应阶梯)": "Многоуровневая тарификация (подходящий уровень не найден)", "阶梯计费(表达式解析失败)": "Многоуровневая тарификация (ошибка разбора выражения)", "附加条件": "Дополнительные условия", @@ -3598,15 +3737,15 @@ "限制周期统一使用上方配置的“限制周期”值。": "Период ограничения равномерно использует значение 'Период ограничения', настроенное выше.", "限流": "Ограничение скорости", "限购": "Лимит", + "随机": "Случайный", + "随机模式": "Случайный режим", + "随机种子 (留空为随机)": "Случайное зерно (оставьте пустым для случайного)", "隐私政策": "Политика конфиденциальности", "隐私政策已更新": "Политика конфиденциальности обновлена", "隐私政策更新失败": "Не удалось обновить политику конфиденциальности", "隐私设置": "Настройки конфиденциальности", "隐藏操作项": "Скрыть элементы операций", "隐藏调试": "Скрыть отладку", - "随机": "Случайный", - "随机模式": "Случайный режим", - "随机种子 (留空为随机)": "Случайное зерно (оставьте пустым для случайного)", "零一万物": "01.AI", "需要安全验证": "Требуется проверка безопасности", "需要添加的额度(支持负数)": "Квота для добавления (поддерживаются отрицательные значения)", @@ -3628,13 +3767,13 @@ "音频补全价格:{{symbol}}{{price}} / 1M tokens": "Цена завершения аудио: {{symbol}}{{price}} / 1M tokens", "音频补全倍率": "", "音频补全倍率(仅部分模型支持该计费)": "Коэффициент аудиовывода (только некоторые модели поддерживают эту тарификацию)", - "音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Аудиоввод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * аудио-коэффициент {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "音频输入价格": "Цена входного аудио", "音频输入价格:{{symbol}}{{price}} / 1M tokens": "Цена входного аудио: {{symbol}}{{price}} / 1M tokens", "音频输入倍率": "", "音频输入相关的倍率设置,键为模型名称,值为倍率": "Настройки коэффициентов, связанные с аудиовводом, ключ - имя модели, значение - коэффициент", - "音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Аудиовывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * аудио-коэффициент {{audioRatio}} * коэффициент аудиозавершения {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Аудиоввод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * аудио-коэффициент {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "音频输出补全相关的倍率设置,键为模型名称,值为倍率": "Настройки коэффициентов, связанные с аудиовыводом и завершением, ключ - имя модели, значение - коэффициент", + "音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Аудиовывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * аудио-коэффициент {{audioRatio}} * коэффициент аудиозавершения {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "页脚": "Подвал", "页面未找到,请检查您的浏览器地址是否正确": "Страница не найдена, пожалуйста, проверьте правильность адреса в браузере", "页面渲染出错,请刷新页面重试": "Произошла ошибка при отрисовке страницы. Пожалуйста, обновите страницу и попробуйте снова.", @@ -3652,10 +3791,11 @@ "预览更新": "Обновление предварительного просмотра", "预览模板": "Предпросмотр шаблона", "预览请求体": "Предварительный просмотр тела запроса", + "预警阈值必须为正数": "Порог предупреждения должен быть положительным числом", "预计结束": "Estimated End", "预计结果": "Ожидаемый результат", + "预计费用": "", "预设模板": "Предустановленный шаблон", - "预警阈值必须为正数": "Порог предупреждения должен быть положительным числом", "频率惩罚,减少重复词汇的出现": "Штраф за частоту, уменьшает повторение слов", "频率限制的周期(分钟)": "Период ограничения частоты (минуты)", "颜色": "Цвет", @@ -3695,6 +3835,7 @@ "默认 Responses API 版本,为空则使用上方版本": "Версия Responses API по умолчанию, если пусто, используется версия выше", "默认 TTL(秒)": "TTL по умолчанию (секунды)", "默认为 5m 缓存创建倍率;1h 缓存创建倍率按固定乘法自动计算(当前为 1.6x)": "По умолчанию используется коэффициент создания кэша 5m; коэффициент создания кэша 1h автоматически вычисляется фиксированным умножением (сейчас 1.6x)", + "默认使用 USD 结算": "", "默认使用auto分组": "Default to auto group", "默认使用系统名称": "Использовать системное имя по умолчанию", "默认助手消息": "Здравствуйте! Чем я могу вам помочь?", @@ -3704,6 +3845,12 @@ "默认折叠侧边栏": "Сворачивать боковую панель по умолчанию", "默认测试模型": "Модель для тестирования по умолчанию", "默认用户消息": "Здравствуйте", - "默认补全倍率": "Коэффициент завершения по умолчанию" + "默认补全倍率": "Коэффициент завершения по умолчанию", + "(共 {{total}} 个)": "", + "(共 {{total}} 个,省略 {{omit}} 个)": "", + ",": ", ", + ",当前无生效订阅,将自动使用钱包": ", нет активной подписки, автоматически будет использоваться кошелек.", + ",时间:": ", время: ", + ",点击更新": ", нажмите для обновления" } } diff --git a/web/classic/src/i18n/locales/vi.json b/web/classic/src/i18n/locales/vi.json index 6a8f09ea685..1c0a87091df 100644 --- a/web/classic/src/i18n/locales/vi.json +++ b/web/classic/src/i18n/locales/vi.json @@ -9,35 +9,13 @@ " 吗?": " không?", " 秒": " giây", " 秒。": " giây.", - ",": ", ", - ",当前无生效订阅,将自动使用钱包": ", hiện không có gói đăng ký hiệu lực, sẽ tự động dùng ví.", - ",时间:": ", thời gian:", - ",点击更新": ", nhấn để cập nhật", - "、": ", ", - "(共 {{total}} 个,省略 {{omit}} 个)": "", - "(共 {{total}} 个)": "", + "$/1M tokens": "$/1M tokens", + "$/次": "", "(筛选后显示 {{count}} 条)_other": "(Showing {{count}} items after filtering)", "(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(Đầu vào {{input}} tokens / 1M tokens * {{symbol}}{{price}}", "(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(Đầu vào {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + Đầu vào âm thanh {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}", "(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}": "(Đầu vào {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Bộ nhớ đệm {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}", "(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}": "(Đầu vào {{nonImageInput}} tokens + Đầu vào hình ảnh {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}", - "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "Giá trị tối đa của [Số lần yêu cầu tối đa] và [Số lần hoàn thành yêu cầu tối đa] là 2147483647.", - "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[Số lần yêu cầu tối đa] phải lớn hơn hoặc bằng 0, [Số lần hoàn thành yêu cầu tối đa] phải lớn hơn hoặc bằng 1.", - "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", - "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", - "{{count}} 项操作_other": "", - "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", - "{{name}} ID": "{{name}} ID", - "{{ratioType}} {{ratio}}": "{{ratioType}} {{ratio}}", - "{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x", - "「用户可选」决定用户创建令牌时能否自主选择该分组。未勾选的分组只能由管理员在后台分配给用户,不会出现在用户的令牌创建页面中。": "\"User Selectable\" controls whether users can choose this group when creating tokens. Unchecked groups can only be assigned by admin and won't appear in the token creation page.", - "• 视频服务商的跨域限制": "• Cross-origin limitations from the video provider", - "• 防盗链保护机制": "• Hotlink protection mechanisms", - "• 需要特定的请求头或认证": "• Specific headers or authentication are required", - "© {{currentYear}}": "© {{currentYear}}", - "| 基于": " | Dựa trên ", - "$/1M tokens": "$/1M tokens", - "$/次": "", "0 - 最低": "0 - Thấp nhất", "0 表示不限": "0 nghĩa là không giới hạn", "0.002-1之间的小数": "Số thập phân giữa 0.002-1", @@ -47,10 +25,10 @@ "1h缓存创建 {{price}} / 1M tokens": "Tạo bộ nhớ đệm 1h {{price}} / 1M tokens", "1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Tạo bộ nhớ đệm 1h {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "1h cache creation {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (ratio: {{ratio}})", - "1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo bộ nhớ đệm 1h: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số tạo bộ nhớ đệm 1h {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}", "1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm 1h {{symbol}}{{price}} / 1M tokens", "1h缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (1h缓存创建倍率: {{cacheCreationRatio1h}})": "1h cache creation price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (1h cache creation ratio: {{cacheCreationRatio1h}})", "1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm 1h: {{symbol}}{{price}} / 1M tokens", + "1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo bộ nhớ đệm 1h: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số tạo bộ nhớ đệm 1h {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}", "2 - 低": "2 - Thấp", "2. 管理员在用户管理中将用户分配到对应分组(如 vip)": "2. Admin assigns users to groups in User Management (e.g., vip)", "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "Các kênh được thêm sau ngày 10 tháng 5 năm 2025 không cần xóa dấu chấm trong tên mô hình khi triển khai", @@ -61,17 +39,16 @@ "5m缓存创建 {{price}} / 1M tokens": "Tạo bộ nhớ đệm 5m {{price}} / 1M tokens", "5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Tạo bộ nhớ đệm 5m {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "5m cache creation {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (ratio: {{ratio}})", - "5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo bộ nhớ đệm 5m: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số tạo bộ nhớ đệm 5m {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}", "5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm 5m {{symbol}}{{price}} / 1M tokens", "5m缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (5m缓存创建倍率: {{cacheCreationRatio5m}})": "5m cache creation price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (5m cache creation ratio: {{cacheCreationRatio5m}})", "5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm 5m: {{symbol}}{{price}} / 1M tokens", + "5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo bộ nhớ đệm 5m: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số tạo bộ nhớ đệm 5m {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}", "8 - 高": "8 - Cao", "AGPL v3.0协议": "Giấy phép AGPL v3.0", "AI 对话": "Trò chuyện AI", "AI模型测试环境": "Môi trường thử nghiệm mô hình AI", "AI模型配置": "Cấu hình mô hình AI", "AK/SK 模式:使用 AccessKey 和 SecretAccessKey;API Key 模式:使用 API Key": "AK/SK mode uses AccessKey and SecretAccessKey; API Key mode uses an API Key", - "anthropic-beta JSON 示例": "", "API Key": "API Key", "API Key 模式下不支持批量创建": "Không hỗ trợ tạo hàng loạt trong chế độ API Key", "API Key 验证失败": "API Key verification failed", @@ -80,7 +57,12 @@ "API 密钥": "Khóa API", "API 密钥 (沙盒)": "", "API 密钥 (生产)": "", + "API 密钥(测试环境)": "", + "API 密钥(生产环境)": "", "API 文档": "Tài liệu API", + "API 私钥": "", + "API 私钥(测试环境)": "", + "API 私钥(生产环境)": "", "API 配置": "Cấu hình API", "API令牌管理": "Quản lý mã thông báo API", "API使用记录": "Hồ sơ sử dụng API", @@ -89,13 +71,14 @@ "API地址": "Base URL", "API渠道配置": "Cấu hình kênh API", "API端点": "Điểm cuối API", - "Authorization callback URL 填": "Điền URL gọi lại ủy quyền", "Authorization Endpoint": "Điểm cuối ủy quyền", - "auto分组调用链路": "chuỗi gọi nhóm tự động", + "Authorization callback URL 填": "Điền URL gọi lại ủy quyền", "Bark推送URL": "URL đẩy Bark", "Bark推送URL必须以http://或https://开头": "URL đẩy Bark phải bắt đầu bằng http:// hoặc https://", "Bark通知": "Thông báo Bark", "Basic Auth 头": "Header Basic Auth", + "CPU 使用率超过此值时拒绝请求": "Từ chối yêu cầu khi sử dụng CPU vượt quá giá trị này", + "CPU 阈值 (%)": "Ngưỡng CPU (%)", "Cached tokens": "Cached tokens", "Cached tokens 占比口径由后端返回:Claude 语义按 cached/(prompt+cached),其余按 cached/prompt。": "Tỷ lệ cached tokens được trả về từ backend: ngữ nghĩa Claude tính theo cached/(prompt+cached), còn lại tính theo cached/prompt.", "Changing batch type to:": "Đang thay đổi loại hàng loạt thành:", @@ -110,20 +93,13 @@ "Client ID": "Client ID", "Client Secret": "Client Secret", "Codex 渠道不支持批量创建": "Kênh Codex không hỗ trợ tạo hàng loạt", - "common.changeLanguage": "Thay đổi ngôn ngữ", "Completion tokens": "Completion tokens", "Configuration": "Cấu hình", - "context_int/context_string 从请求上下文读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "context_int/context_string đọc từ context yêu cầu; gjson đọc từ JSON body yêu cầu đầu vào theo gjson path.", - "CPU 使用率超过此值时拒绝请求": "Từ chối yêu cầu khi sử dụng CPU vượt quá giá trị này", - "CPU 阈值 (%)": "Ngưỡng CPU (%)", "Creem API 密钥,敏感信息不显示": "Khóa API Creem, thông tin nhạy cảm không được hiển thị", "Creem Setting Tips": "Creem chỉ hỗ trợ các sản phẩm có số tiền cố định được thiết lập sẵn. Các sản phẩm này và giá của chúng cần được tạo và cấu hình trước trên trang web Creem, vì vậy việc nạp tiền số tiền động tùy chỉnh không được hỗ trợ. Cấu hình tên sản phẩm và giá trên Creem, lấy ID sản phẩm, sau đó điền vào sản phẩm bên dưới. Đặt số tiền nạp và giá hiển thị cho sản phẩm này trong API mới.", "Creem 介绍": "Creem là đối tác thanh toán mà bạn luôn xứng đáng có được, chúng tôi phấn đấu cho sự đơn giản và thẳng thắn trên các API của mình.", "Creem 充值": "Nạp tiền Creem", "Creem 设置": "Cài đặt Creem", - "default 和 vip 只能由管理员在「用户管理」中分配给用户。适用于按用户等级定价、内部测试等不希望用户自主选择的场景。": "default and vip can only be assigned to users by admin in \"User Management\". Suitable for tiered pricing, internal testing, or other scenarios where user self-selection is not desired.", - "default为默认设置,可单独设置每个分类的安全等级": "\"default\" là cài đặt mặc định, và mỗi danh mục có thể được đặt riêng", - "default为默认设置,可单独设置每个模型的版本": "\"default\" là cài đặt mặc định, và mỗi mô hình có thể được đặt riêng", "Dify渠道只适配chatflow和agent,并且agent不支持图片!": "Kênh Dify chỉ hỗ trợ chatflow và agent, và agent không hỗ trợ hình ảnh!", "Discord": "Discord", "Discord Client ID": "Discord Client ID", @@ -133,10 +109,11 @@ "Discovery scopes": "Discovery scopes", "Discovery 建议 scopes:": "Discovery scopes được đề xuất:", "EUR (欧元)": "EUR (Euro)", - "false": "sai", "GC 已执行": "GC đã thực thi", "GC 执行失败": "Thực thi GC thất bại", "GC 次数": "Số lần GC", + "GPU/容器": "GPU/Container", + "GPU数量": "Number of GPUs", "Gemini安全设置": "Cài đặt an toàn Gemini", "Gemini思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Thích ứng tư duy Gemini BudgetTokens = MaxTokens * Tỷ lệ phần trăm BudgetTokens", "Gemini思考适配设置": "Cài đặt thích ứng tư duy Gemini", @@ -151,14 +128,10 @@ "Gotify服务器地址": "Địa chỉ máy chủ Gotify", "Gotify服务器地址必须以http://或https://开头": "Địa chỉ máy chủ Gotify phải bắt đầu bằng http:// hoặc https://", "Gotify通知": "Thông báo Gotify", - "GPU/容器": "GPU/Container", - "GPU数量": "Number of GPUs", "Grok设置": "Cài đặt Grok", "Haiku 模型": "Model Haiku", "Homepage URL 填": "Điền URL trang chủ", "ID": "ID", - "include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护": "include_obfuscation kiểm soát trường làm mờ trong luồng Responses. Mặc định tắt để tránh client vô hiệu hóa bảo vệ bảo mật này", - "inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息": "Trường inference_geo kiểm soát vùng lưu trữ dữ liệu suy luận của Claude. Mặc định tắt để ngăn truyền thông tin địa lý trái phép", "IP": "IP", "IP白名单": "IP Whitelist", "IP白名单(支持CIDR表达式)": "Danh sách trắng IP (hỗ trợ biểu thức CIDR)", @@ -166,6 +139,7 @@ "IP黑名单": "Danh sách đen IP", "JSON": "JSON", "JSON 已格式化": "JSON đã được định dạng", + "JSON 必须是对象": "", "JSON 文本": "Văn bản JSON", "JSON 无效": "JSON không hợp lệ", "JSON 格式参考": "JSON Reference", @@ -198,6 +172,7 @@ "Ollama 模型管理": "Ollama Model Management", "Ollama 版本信息": "Ollama Version Info", "Opus 模型": "Model Opus", + "POST 参数": "Tham số POST", "Passkey": "Passkey", "Passkey 已解绑": "Đã xóa Passkey", "Passkey 已重置": "Passkey đã được đặt lại", @@ -208,72 +183,74 @@ "Pay Method Name": "", "Pay Method Type": "", "Ping间隔(秒)": "Khoảng thời gian Ping (giây)", - "POST 参数": "Tham số POST", - "price_xxx 的商品价格 ID,新建产品后可获得": "ID giá sản phẩm cho price_xxx, có sẵn sau khi tạo sản phẩm mới", + "Product ID": "", "Prompt cache hit tokens": "Prompt cache hit tokens", "Prompt tokens": "Prompt tokens", + "RSA 私钥 (沙盒)": "", + "RSA 私钥 (生产)": "", "Reasoning Effort": "Nỗ lực suy luận", "Recharge Quota": "Hạn ngạch nạp tiền", "Request ID": "Request ID", - "RSA 私钥 (沙盒)": "", - "RSA 私钥 (生产)": "", - "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "Trường safety_identifier giúp OpenAI xác định người dùng ứng dụng có thể vi phạm chính sách sử dụng. Tắt theo mặc định để bảo vệ quyền riêng tư của người dùng", - "Scopes(可选)": "Scopes (tùy chọn)", - "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "Trường service_tier được sử dụng để chỉ định cấp độ dịch vụ. Cho phép truyền qua có thể dẫn đến việc tính phí thực tế cao hơn dự kiến. Tắt theo mặc định để tránh phí bổ sung", - "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "Khóa Stripe cho sk_xxx hoặc rk_xxx, thông tin nhạy cảm không được hiển thị", "SMTP 加密方式": "Mã hóa SMTP", "SMTP 发送者邮箱": "Email người gửi SMTP", "SMTP 服务器地址": "Địa chỉ máy chủ SMTP", "SMTP 端口": "Cổng SMTP", "SMTP 访问凭证": "Thông tin xác thực truy cập SMTP", "SMTP 账户": "Tài khoản SMTP", - "Sonnet 模型": "Model Sonnet", - "speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "Trường speed kiểm soát chế độ tốc độ suy luận Claude. Mặc định tắt để tránh vô tình chuyển sang chế độ fast", "SSE 事件": "Sự kiện SSE", "SSE数据流": "Luồng dữ liệu SSE", "SSL/TLS": "SSL/TLS", "SSRF防护开关详细说明": "Công tắc chính kiểm soát xem bảo vệ SSRF có được bật hay không. Khi tắt, tất cả các kiểm tra SSRF sẽ bị bỏ qua, cho phép truy cập vào bất kỳ URL nào. ⚠️ Chỉ tắt tính năng này trong môi trường hoàn toàn tin cậy.", "SSRF防护设置": "Cài đặt bảo vệ SSRF", "SSRF防护详细说明": "Bảo vệ SSRF ngăn chặn người dùng độc hại sử dụng máy chủ của bạn để truy cập tài nguyên mạng nội bộ. Cấu hình danh sách trắng cho các tên miền/IP đáng tin cậy và hạn chế các cổng được phép. Áp dụng cho tải xuống tệp, webhook và thông báo.", - "standard 已被移除,vip 用户看不到": "standard has been removed, vip users cannot see it", "STARTTLS": "STARTTLS", - "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "Trường store ủy quyền cho OpenAI lưu trữ dữ liệu yêu cầu để đánh giá và tối ưu hóa sản phẩm. Tắt theo mặc định. Bật có thể khiến Codex hoạt động không chính xác", + "Scopes(可选)": "Scopes (tùy chọn)", + "Sonnet 模型": "Model Sonnet", + "Store ID": "", "Stripe 设置": "Cài đặt Stripe", "Stripe/Creem 商品ID(可选)": "ID sản phẩm Stripe/Creem (tùy chọn)", "Stripe/Creem 需在第三方平台创建商品并填入 ID": "Sản phẩm Stripe/Creem phải được tạo trên nền tảng bên thứ ba và điền ID", + "TTL(秒)": "TTL (giây)", + "TTL(秒,0 表示默认)": "TTL (giây, 0 là mặc định)", "Telegram": "Telegram", "Telegram Bot Token": "Telegram Bot Token", "Telegram Bot 名称": "Tên Telegram Bot", "Telegram ID": "Telegram ID", "Token Endpoint": "Token Endpoint", - "token 会按倍率换算成“额度/次数”,请求结束后再做差额结算(补扣/返还)。": "Token được quy đổi thành hạn mức/số lần theo tỷ lệ. Sau khi yêu cầu hoàn tất, chênh lệch sẽ được quyết toán (trừ thêm/hoàn trả).", + "Token 估算器": "", "Total tokens": "Total tokens", - "true": "đúng", - "TTL(秒,0 表示默认)": "TTL (giây, 0 là mặc định)", - "TTL(秒)": "TTL (giây)", "Turnstile Secret Key": "Turnstile Secret Key", "Turnstile Site Key": "Turnstile Site Key", - "Unix时间戳": "Dấu thời gian Unix", - "Uptime Kuma地址": "Địa chỉ Uptime Kuma", - "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Quản lý danh mục giám sát Uptime Kuma, bạn có thể cấu hình nhiều danh mục giám sát để hiển thị trạng thái dịch vụ (tối đa 20)", "URL 标识,只能包含小写字母、数字和连字符": "Định danh URL, chỉ cho phép chữ thường, số và dấu gạch ngang", "URL链接": "Liên kết URL", "USD (美元)": "USD (Đô la Mỹ)", + "Unix时间戳": "Dấu thời gian Unix", + "Uptime Kuma地址": "Địa chỉ Uptime Kuma", + "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Quản lý danh mục giám sát Uptime Kuma, bạn có thể cấu hình nhiều danh mục giám sát để hiển thị trạng thái dịch vụ (tối đa 20)", "User Info Endpoint": "User Info Endpoint", "User-Agent include(每行一个,可不写)": "User-Agent include (mỗi dòng một mục, tùy chọn)", + "User-Agent 过滤": "", "Value 正则": "Regex giá trị", "Vertex AI 不支持 functionResponse.id 字段,开启后将自动移除该字段": "Vertex AI không hỗ trợ trường functionResponse.id. Khi bật, trường này sẽ tự động bị xóa", - "Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)": "", "Waffo API 参数,可空(最多64位)": "", + "Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)": "", + "Waffo Pancake 设置": "", "Waffo 充值": "", "Waffo 充值的最低数量,默认 1": "", "Waffo 公钥 (沙盒)": "", "Waffo 公钥 (生产)": "", + "Waffo 公钥(测试环境)": "", + "Waffo 公钥(生产环境)": "", "Waffo 商户 ID": "", + "Waffo 当前使用 USD 结算": "", "Waffo 是一个支付聚合平台,支持多种支付方式。": "", "Waffo 设置": "", - "Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Tìm kiếm Web: {{count}} / 1K * đơn giá {{price}} * {{ratioType}} {{ratio}} = {{amount}}", + "WeChat Server 服务器地址": "Địa chỉ máy chủ WeChat Server", + "WeChat Server 访问凭证": "Thông tin xác thực truy cập WeChat Server", "Web 搜索调用 {{webSearchCallCount}} 次": "Đã gọi tìm kiếm Web {{webSearchCallCount}} lần", + "Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Tìm kiếm Web: {{count}} / 1K * đơn giá {{price}} * {{ratioType}} {{ratio}} = {{amount}}", + "Webhook 公钥(测试环境)": "", + "Webhook 公钥(生产环境)": "", "Webhook 密钥": "Khóa Webhook", "Webhook 签名密钥": "Khóa chữ ký Webhook", "Webhook地址": "URL Webhook", @@ -281,19 +258,55 @@ "Webhook请求结构说明": "Mô tả cấu trúc yêu cầu Webhook", "Webhook通知": "Thông báo Webhook", "Web搜索价格:{{symbol}}{{price}} / 1K 次": "Giá tìm kiếm Web: {{symbol}}{{price}} / 1K yêu cầu", - "WeChat Server 服务器地址": "Địa chỉ máy chủ WeChat Server", - "WeChat Server 访问凭证": "Thông tin xác thực truy cập WeChat Server", "Well-Known URL": "Well-Known URL", "Well-Known URL 必须以 http:// 或 https:// 开头": "Well-Known URL phải bắt đầu bằng http:// hoặc https://", - "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "Khóa chữ ký Webhook cho whsec_xxx, thông tin nhạy cảm không được hiển thị", "Worker地址": "Địa chỉ Worker", "Worker密钥": "Khóa Worker", + "X 也可以小于 1,当折扣用。想做\"只给输出加价\"或\"额外加固定费用\",请直接写完整计费公式。": "", + "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "Giá trị tối đa của [Số lần yêu cầu tối đa] và [Số lần hoàn thành yêu cầu tối đa] là 2147483647.", + "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[Số lần yêu cầu tối đa] phải lớn hơn hoặc bằng 0, [Số lần hoàn thành yêu cầu tối đa] phải lớn hơn hoặc bằng 1.", + "anthropic-beta JSON 示例": "", + "auto分组调用链路": "chuỗi gọi nhóm tự động", + "common.changeLanguage": "Thay đổi ngôn ngữ", + "context_int/context_string 从请求上下文读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "context_int/context_string đọc từ context yêu cầu; gjson đọc từ JSON body yêu cầu đầu vào theo gjson path.", + "context_int/context_string 从请求上下文读取;header 从 HTTP 请求头读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "", + "default 和 vip 只能由管理员在「用户管理」中分配给用户。适用于按用户等级定价、内部测试等不希望用户自主选择的场景。": "default and vip can only be assigned to users by admin in \"User Management\". Suitable for tiered pricing, internal testing, or other scenarios where user self-selection is not desired.", + "default为默认设置,可单独设置每个分类的安全等级": "\"default\" là cài đặt mặc định, và mỗi danh mục có thể được đặt riêng", + "default为默认设置,可单独设置每个模型的版本": "\"default\" là cài đặt mặc định, và mỗi mô hình có thể được đặt riêng", + "false": "sai", + "include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护": "include_obfuscation kiểm soát trường làm mờ trong luồng Responses. Mặc định tắt để tránh client vô hiệu hóa bảo vệ bảo mật này", + "inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息": "Trường inference_geo kiểm soát vùng lưu trữ dữ liệu suy luận của Claude. Mặc định tắt để ngăn truyền thông tin địa lý trái phép", + "price_xxx 的商品价格 ID,新建产品后可获得": "ID giá sản phẩm cho price_xxx, có sẵn sau khi tạo sản phẩm mới", + "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "Trường safety_identifier giúp OpenAI xác định người dùng ứng dụng có thể vi phạm chính sách sử dụng. Tắt theo mặc định để bảo vệ quyền riêng tư của người dùng", + "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "Trường service_tier được sử dụng để chỉ định cấp độ dịch vụ. Cho phép truyền qua có thể dẫn đến việc tính phí thực tế cao hơn dự kiến. Tắt theo mặc định để tránh phí bổ sung", + "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "Khóa Stripe cho sk_xxx hoặc rk_xxx, thông tin nhạy cảm không được hiển thị", + "speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "Trường speed kiểm soát chế độ tốc độ suy luận Claude. Mặc định tắt để tránh vô tình chuyển sang chế độ fast", + "standard 已被移除,vip 用户看不到": "standard has been removed, vip users cannot see it", + "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "Trường store ủy quyền cho OpenAI lưu trữ dữ liệu yêu cầu để đánh giá và tối ưu hóa sản phẩm. Tắt theo mặc định. Bật có thể khiến Codex hoạt động không chính xác", + "token 会按倍率换算成“额度/次数”,请求结束后再做差额结算(补扣/返还)。": "Token được quy đổi thành hạn mức/số lần theo tỷ lệ. Sau khi yêu cầu hoàn tất, chênh lệch sẽ được quyết toán (trừ thêm/hoàn trả).", + "true": "đúng", + "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "Khóa chữ ký Webhook cho whsec_xxx, thông tin nhạy cảm không được hiển thị", + "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", + "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "{{count}} 项操作_other": "", + "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", + "{{label}} {{price}} / 1M tokens": "", + "{{name}} ID": "{{name}} ID", + "{{ratioType}} {{ratio}}": "{{ratioType}} {{ratio}}", + "{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x", + "| 基于": " | Dựa trên ", + "© {{currentYear}}": "© {{currentYear}}", + "• 视频服务商的跨域限制": "• Cross-origin limitations from the video provider", + "• 防盗链保护机制": "• Hotlink protection mechanisms", + "• 需要特定的请求头或认证": "• Specific headers or authentication are required", + "、": ", ", + "「用户可选」决定用户创建令牌时能否自主选择该分组。未勾选的分组只能由管理员在后台分配给用户,不会出现在用户的令牌创建页面中。": "\"User Selectable\" controls whether users can choose this group when creating tokens. Unchecked groups can only be assigned by admin and won't appear in the token creation page.", "一个月": "Một tháng", "一天": "Một ngày", "一小时": "Một giờ", "一次调用消耗多少刀,优先级大于模型倍率": "Một cuộc gọi tốn bao nhiêu USD, ưu tiên hơn tỷ lệ mô hình", - "一行一个,不区分大小写": "Mỗi dòng một cái, không phân biệt chữ hoa chữ thường", "一行一个屏蔽词,不需要符号分割": "Mỗi dòng một từ bị chặn, không cần ký hiệu phân cách", + "一行一个,不区分大小写": "Mỗi dòng một cái, không phân biệt chữ hoa chữ thường", "一键填充到 FluentRead": "Điền vào FluentRead bằng một cú nhấp chuột", "三种操作的区别:": "Differences between the three operations:", "上一个表单块": "Khối biểu mẫu trước", @@ -337,10 +350,10 @@ "两次输入的密码不一致": "Hai mật khẩu đã nhập không khớp", "两次输入的密码不一致!": "Mật khẩu nhập hai lần không nhất quán!", "两步验证": "Xác thực hai yếu tố", - "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "Xác thực hai yếu tố (2FA) cung cấp bảo vệ bảo mật bổ sung cho tài khoản của bạn. Sau khi bật, bạn cần nhập mật khẩu và mã xác minh được tạo bởi ứng dụng xác thực khi đăng nhập.", "两步验证启用成功!": "Đã bật xác thực hai yếu tố thành công!", "两步验证已禁用": "Xác thực hai yếu tố đã bị vô hiệu hóa", "两步验证设置": "Cài đặt xác thực hai yếu tố", + "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "Xác thực hai yếu tố (2FA) cung cấp bảo vệ bảo mật bổ sung cho tài khoản của bạn. Sau khi bật, bạn cần nhập mật khẩu và mã xác minh được tạo bởi ứng dụng xác thực khi đăng nhập.", "个": "cái", "个GPU": " GPUs", "个人中心": "Trung tâm cá nhân", @@ -375,8 +388,10 @@ "为了保护账户安全,请验证您的身份。": "Để bảo vệ an toàn tài khoản, vui lòng xác minh danh tính của bạn.", "为什么 default 不勾选「用户可选」?": "Why is default not set as \"User Selectable\"?", "为保证匹配准确,请确保客户端直连本站点(避免反向代理/网关改写 User-Agent)。": "Để đảm bảo khớp chính xác, hãy đảm bảo client kết nối trực tiếp đến trang web này (tránh reverse proxy/gateway ghi đè User-Agent).", + "为模型前缀覆盖": "", "为特定用户分组配置可用分组的增减规则。「添加」为该分组新增可用分组,「移除」移除默认可用分组,「追加」直接追加分组": "Configure add/remove rules for specific user groups. \"Add\" adds a new selectable group, \"Remove\" removes a default selectable group, \"Append\" directly appends a group", "为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https": "Nếu để trống, mặc định sử dụng địa chỉ máy chủ. Nhiều Origin được phân tách bằng dấu phẩy, ví dụ: https://newapi.pro,https://newapi.com. Lưu ý: không được chứa [], phải sử dụng https", + "为默认价格": "", "主模型": "Model chính", "主页链接填": "Nhập liên kết trang chủ", "之前的所有日志": "Tất cả nhật ký trước đó", @@ -428,9 +443,9 @@ "令牌": "Mã thông báo", "令牌不存在": "", "令牌分组": "Nhóm mã thông báo", - "令牌分组,默认为用户的分组": "Nhóm mã thông báo, mặc định là nhóm của bạn", "令牌分组设为 auto 时,按以下顺序依次尝试选择可用分组,排在前面的优先级更高": "When token group is set to auto, groups are selected in order of priority, with higher priority groups listed first", "令牌分组设为 auto 时,系统按优先级顺序自动选择一个可用分组。": "When token group is set to auto, the system automatically selects an available group by priority.", + "令牌分组,默认为用户的分组": "Nhóm mã thông báo, mặc định là nhóm của bạn", "令牌创建成功,请在列表页面点击复制获取令牌!": "Tạo mã thông báo thành công, vui lòng nhấp vào sao chép trên trang danh sách để lấy mã thông báo!", "令牌名称": "Tên mã thông báo", "令牌已重置并已复制到剪贴板": "Mã thông báo đã được đặt lại và sao chép vào khay nhớ tạm", @@ -442,10 +457,9 @@ "以及": "và", "仪表盘设置": "Bảng điều khiển", "价格": "Giá cả", - "价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}": "Price: {{symbol}}{{price}} * {{ratioType}}: {{ratio}}", - "价格:${{price}} * {{ratioType}}:{{ratio}}": "Giá: ${{price}} * {{ratioType}}: {{ratio}}", "价格摘要": "Tóm tắt giá", "价格暂时不可用,请稍后重试": "Price temporarily unavailable, please try again later", + "价格根据用量档位和请求条件动态调整": "", "价格模式": "", "价格模式(默认)": "Chế độ giá (mặc định)", "价格计算中...": "Calculating price...", @@ -455,6 +469,8 @@ "价格设置方式": "Phương thức cấu hình giá", "价格重新计算中...": "Recalculating price...", "价格预估": "Price Estimate", + "价格:${{price}} * {{ratioType}}:{{ratio}}": "Giá: ${{price}} * {{ratioType}}: {{ratio}}", + "价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}": "Price: {{symbol}}{{price}} * {{ratioType}}: {{ratio}}", "任一满足(OR)": "Bất kỳ khớp (OR)", "任务 ID": "ID tác vụ", "任务ID": "ID tác vụ", @@ -486,7 +502,6 @@ "你理解并自行承担部署、运营和收费行为产生的法律责任。": "You understand and independently bear legal responsibility arising from deployment, operation, and charging behavior.", "你理解本合规提醒仅用于风险提示,不构成法律意见、合规审查结论或对你使用本系统行为合法性的保证;你应根据实际业务场景自行咨询专业法律或合规顾问。": "You understand this compliance reminder is only for risk notice and does not constitute legal advice, a compliance review conclusion, or a guarantee of the legality of your use of this system; you should consult professional legal or compliance advisors based on your actual business scenario.", "你还没有处理{{type}}模型({{count}}个)。是否仅提交当前已勾选内容?_other": "", - "使用 {{name}} 继续": "Tiếp tục với {{name}}", "使用 Discord 继续": "Continue with Discord", "使用 GitHub 继续": "Tiếp tục với GitHub", "使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}": "Sử dụng định dạng đối tượng JSON, định dạng: {\"group_name\": [max_requests, max_completions]}", @@ -495,6 +510,7 @@ "使用 Passkey 实现免密且更安全的登录体验": "Sử dụng Passkey để trải nghiệm đăng nhập không cần mật khẩu và an toàn hơn", "使用 Passkey 登录": "Đăng nhập bằng Passkey", "使用 Passkey 验证": "Xác minh bằng Passkey", + "使用 {{name}} 继续": "Tiếp tục với {{name}}", "使用 微信 继续": "Tiếp tục với WeChat", "使用 用户名 注册": "Đăng ký bằng Tên người dùng", "使用 邮箱或用户名 登录": "Đăng nhập bằng Email hoặc Tên người dùng", @@ -504,52 +520,63 @@ "使用日志": "Nhật ký sử dụng", "使用模式": "Chế độ sử dụng", "使用统计": "Thống kê sử dụng", - "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "Sử dụng ứng dụng xác thực (như Google Authenticator, Microsoft Authenticator) để quét mã QR bên dưới:", "使用认证器应用扫描二维码": "Quét mã QR bằng ứng dụng xác thực", + "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "Sử dụng ứng dụng xác thực (như Google Authenticator, Microsoft Authenticator) để quét mã QR bên dưới:", "使用说明": "Guide", "例如 /var/cache/new-api": "VD: /var/cache/new-api", - "例如 €, £, Rp, ₩, ₹...": "Ví dụ, €, £, Rp, ₩, ₹...", + "例如 0.5 或 2": "", + "例如 anthropic-beta": "", "例如 https://docs.newapi.pro": "Ví dụ, https://docs.newapi.pro", "例如 https://example.com/api/waffo/webhook": "", "例如 https://example.com/console/topup": "", - "例如:": "Ví dụ:", + "例如 service_tier": "", + "例如 €, £, Rp, ₩, ₹...": "Ví dụ, €, £, Rp, ₩, ₹...", "例如: /bin/bash -c \"python app.py\"": "e.g.: /bin/bash -c \"python app.py\"", "例如: nginx:latest": "e.g.: nginx:latest", "例如: socks5://user:pass@host:port": "ví dụ: socks5://user:pass@host:port", + "例如发卡网站的购买链接": "Ví dụ, liên kết mua hàng từ trang web phát hành thẻ", + "例如(全渠道):": "Ví dụ (tất cả kênh):", + "例如(指定渠道):": "Ví dụ (kênh chỉ định):", + "例如:": "Ví dụ:", "例如:-c": "e.g.: -c", "例如:/bin/bash": "e.g.: /bin/bash", "例如:0001": "ví dụ: 0001", "例如:1000": "ví dụ: 1000", "例如:100000": "Ví dụ: 100000", - "例如:2,就是最低充值2$": "ví dụ: 2, nghĩa là nạp tối thiểu $2", "例如:2000": "ví dụ: 2000", + "例如:2,就是最低充值2$": "ví dụ: 2, nghĩa là nạp tối thiểu $2", "例如:4.99": "Ví dụ: 4.99", "例如:401, 403, 429, 500-599": "VD: 401, 403, 429, 500-599", "例如:7,就是7元/美金": "ví dụ: 7, nghĩa là 7 tệ mỗi USD", "例如:Credit Card": "", + "例如:GitHub Enterprise": "VD: GitHub Enterprise", + "例如:MER_xxx": "", + "例如:PROD_xxx": "", + "例如:STO_xxx": "", "例如:email": "VD: email", "例如:example.com": "ví dụ: example.com", "例如:github / si:google / https://example.com/logo.png / 🐱": "VD: github / si:google / https://example.com/logo.png / 🐱", - "例如:GitHub Enterprise": "VD: GitHub Enterprise", "例如:github-enterprise": "VD: github-enterprise", "例如:gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$": "", "例如:https://example.com/.well-known/openid-configuration": "VD: https://example.com/.well-known/openid-configuration", + "例如:https://example.com/api/waffo/webhook": "", + "例如:https://example.com/console/topup": "", "例如:https://gitea.example.com": "VD: https://gitea.example.com", "例如:https://yourdomain.com": "ví dụ: https://yourdomain.com", "例如:name、full_name": "VD: name, full_name", "例如:nginx:latest": "e.g.: nginx:latest", "例如:preferred_username、login": "VD: preferred_username, login", "例如:preview": "ví dụ: preview", + "例如:price_xxx": "", "例如:prod_6I8rBerHpPxyoiU9WK4kot": "Ví dụ: prod_6I8rBerHpPxyoiU9WK4kot", + "例如:sk_xxx 或 rk_xxx,留空表示保持当前不变": "", "例如:sub、id、data.user.id": "VD: sub, id, data.user.id", + "例如:whsec_xxx,留空表示保持当前不变": "", "例如:基础套餐": "Ví dụ: Gói cơ bản", "例如:清理工具参数,避免上游校验错误": "", "例如:该请求不满足准入策略": "VD: Yêu cầu này không đáp ứng chính sách tiếp nhận", "例如:适合轻度使用": "Ví dụ: Phù hợp dùng nhẹ", "例如:需要等级 {{required}},你当前等级 {{current}}": "VD: Yêu cầu cấp {{required}}, cấp hiện tại của bạn là {{current}}", - "例如(全渠道):": "Ví dụ (tất cả kênh):", - "例如(指定渠道):": "Ví dụ (kênh chỉ định):", - "例如发卡网站的购买链接": "Ví dụ, liên kết mua hàng từ trang web phát hành thẻ", "供应商": "Nhà cung cấp", "供应商介绍": "Giới thiệu nhà cung cấp", "供应商信息:": "Thông tin nhà cung cấp:", @@ -574,11 +601,17 @@ "保存 WeChat Server 设置": "Lưu cài đặt WeChat Server", "保存分组倍率设置": "Lưu cài đặt tỷ lệ nhóm", "保存分组相关设置": "Lưu cài đặt liên quan đến nhóm", + "保存后不会回显,请填写当前环境对应的 API 私钥": "", + "保存后不会回显,请填写当前环境对应的 Stripe API 密钥": "", + "保存后不会回显,请填写测试环境对应的 API 密钥": "", + "保存后不会回显,请填写测试环境对应的 API 私钥": "", + "保存后不会回显,请填写生产环境对应的 API 密钥": "", + "保存后不会回显,请填写生产环境对应的 API 私钥": "", "保存备用码": "Lưu mã dự phòng", "保存备用码以备不时之需": "Lưu mã dự phòng cho trường hợp khẩn cấp", "保存失败": "Lưu thất bại", - "保存失败,请重试": "Lưu thất bại, vui lòng thử lại", "保存失败:": "Lưu thất bại:", + "保存失败,请重试": "Lưu thất bại, vui lòng thử lại", "保存屏蔽词过滤设置": "Lưu cài đặt lọc từ bị chặn", "保存性能设置": "Lưu cài đặt hiệu suất", "保存成功": "Lưu thành công", @@ -596,11 +629,11 @@ "保存预览": "Xem trước khi lưu", "保存额度设置": "Lưu cài đặt hạn ngạch", "保留": "kept", - "保留最近N个文件": "Giữ lại N tệp gần nhất", - "保留最近N天": "Giữ lại N ngày gần nhất", "保留原值(目标已有值时不覆盖)": "Giữ giá trị gốc (không ghi đè nếu mục tiêu đã có giá trị)", "保留天数": "Số ngày giữ lại", "保留文件数": "Số tệp giữ lại", + "保留最近N个文件": "Giữ lại N tệp gần nhất", + "保留最近N天": "Giữ lại N ngày gần nhất", "修复数据库一致性": "Sửa chữa tính nhất quán của cơ sở dữ liệu", "修改为": "Sửa đổi thành", "修改子渠道优先级": "Sửa đổi ưu tiên kênh phụ", @@ -622,12 +655,12 @@ "停用": "Vô hiệu hóa", "允许 AccountFilter 参数": "Cho phép tham số AccountFilter", "允许 HTTP 协议图片请求(适用于自部署代理)": "Cho phép yêu cầu hình ảnh giao thức HTTP (đối với proxy tự triển khai)", + "允许 Turnstile 用户校验": "Cho phép xác minh người dùng Turnstile", "允许 inference_geo 透传": "Cho phép truyền inference_geo", "允许 safety_identifier 透传": "Cho phép safety_identifier truyền qua", "允许 service_tier 透传": "Cho phép service_tier truyền qua", "允许 speed 透传": "Cho phép truyền speed", "允许 stream_options.include_obfuscation 透传": "Cho phép truyền stream_options.include_obfuscation", - "允许 Turnstile 用户校验": "Cho phép xác minh người dùng Turnstile", "允许不安全的 Origin(HTTP)": "Cho phép Origin không an toàn (HTTP)", "允许回调(会泄露服务器 IP 地址)": "Cho phép gọi lại (sẽ làm lộ địa chỉ IP máy chủ)", "允许在 Stripe 支付中输入促销码": "Cho phép nhập mã khuyến mãi khi thanh toán Stripe", @@ -648,13 +681,14 @@ "允许重试": "Cho phép thử lại", "元": "CNY", "充值": "Nạp tiền", - "充值价格(x元/美金)": "Giá nạp (x tệ/đô la)", + "充值价格必须大于 0": "", "充值价格显示": "Giá nạp", + "充值价格(x元/美金)": "Giá nạp (x tệ/đô la)", "充值分组倍率": "Tỷ lệ nhóm nạp tiền", "充值分组倍率不是合法的 JSON 字符串": "Tỷ lệ nhóm nạp tiền không phải là chuỗi JSON hợp lệ", "充值数量": "Số lượng nạp", - "充值数量,最低 ": "Số lượng nạp, tối thiểu", "充值数量不能小于": "Số tiền nạp không được nhỏ hơn", + "充值数量,最低 ": "Số lượng nạp, tối thiểu", "充值方式设置": "Cài đặt phương thức nạp tiền", "充值方式设置不是合法的 JSON 字符串": "Cài đặt phương thức nạp tiền không phải là chuỗi JSON hợp lệ", "充值确认": "Xác nhận nạp tiền", @@ -670,14 +704,15 @@ "兑换成功!": "Đổi thành công!", "兑换码充值": "Nạp tiền bằng mã đổi thưởng", "兑换码创建成功": "Đã tạo mã đổi thưởng", - "兑换码创建成功,是否下载兑换码?": "Tạo mã đổi thưởng thành công. Bạn có muốn tải xuống không?", "兑换码创建成功!": "Tạo mã đổi thưởng thành công!", + "兑换码创建成功,是否下载兑换码?": "Tạo mã đổi thưởng thành công. Bạn có muốn tải xuống không?", "兑换码功能已禁用,管理员需先确认合规声明。": "Redemption codes are disabled until the administrator confirms compliance terms.", "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "Mã đổi thưởng sẽ được tải xuống dưới dạng tệp văn bản, với tên tệp là tên mã đổi thưởng.", "兑换码更新成功!": "Cập nhật mã đổi thưởng thành công!", "兑换码生成管理": "Quản lý tạo mã đổi thưởng", "兑换码管理": "Quản lý mã đổi thưởng", "兑换额度": "Đổi", + "兜底档": "", "全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用": "Kiểm soát toàn cầu các khu vực và chức năng thanh bên, người dùng không thể bật các chức năng bị quản trị viên ẩn", "全局设置": "Cài đặt toàn cầu", "全选": "Chọn tất cả", @@ -718,8 +753,8 @@ "关闭": "Đóng", "关闭侧边栏": "Đóng thanh bên", "关闭公告": "Đóng thông báo", - "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "Sau khi đóng, mô hình này sẽ không tự động bị ghi đè hoặc tạo bởi \"Đồng bộ chính thức\"", "关闭后将不再显示此提示(仅对当前浏览器生效)。确定要关闭吗?": "Sau khi đóng, thông báo này sẽ không còn hiển thị nữa (chỉ với trình duyệt này). Bạn có chắc muốn đóng không?", + "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "Sau khi đóng, mô hình này sẽ không tự động bị ghi đè hoặc tạo bởi \"Đồng bộ chính thức\"", "关闭弹窗,已停止批量测试": "Đã đóng hộp thoại, đã dừng kiểm tra hàng loạt", "关闭提示": "Đóng thông báo", "其他": "Khác", @@ -737,20 +772,6 @@ "内置": "Tích hợp sẵn", "内置 Ollama 镜像": "Built-in Ollama Image", "再次输入部署名称": "Enter Deployment Name Again", - "最低": "thấp nhất", - "最低充值数量": "", - "最低充值美元数量": "Số tiền nạp đô la tối thiểu", - "最低充值美元数量必须大于 0": "Số tiền nạp đô la tối thiểu phải lớn hơn 0", - "最后使用时间": "Thời gian sử dụng cuối cùng", - "最后更新": "Last Updated", - "最后请求": "Yêu cầu cuối cùng", - "最大GPU数量": "Max Number of GPUs", - "最大可用": "Max Available", - "最大条目数": "Số mục tối đa", - "最终抵扣": "Khấu trừ cuối cùng", - "最近一次": "Lần gần nhất", - "最近事件": "Recent Events", - "最高优先级": "highest priority", "写": "Ghi", "准入策略": "Chính sách tiếp nhận", "准入策略 JSON(可选)": "JSON chính sách tiếp nhận (tùy chọn)", @@ -758,6 +779,9 @@ "准备完成初始化": "Sẵn sàng hoàn tất khởi tạo", "减少": "Giảm", "凭证已刷新": "Thông tin xác thực đã được làm mới", + "函数": "", + "分时缓存 (Claude)": "", + "分档价格表": "", "分类名称": "Tên danh mục", "分组": "Nhóm", "分组JSON设置": "Group JSON Settings", @@ -842,20 +866,20 @@ "刷新缓存统计": "Làm mới thống kê bộ nhớ đệm", "刷新缓存统计失败": "Làm mới thống kê bộ nhớ đệm thất bại", "刷新页面": "Tải lại trang", - "前:": "Trước:", "前往 io.net API Keys": "Go to io.net API Keys", "前往设置": "Go to Settings", "前往设置页面": "Go to Settings Page", "前缀": "Tiền tố", "前置": "", - "剪贴板中未检测到连接信息": "Không tìm thấy thông tin kết nối trong bộ nhớ tạm", - "副本数量": "Number of Replicas", + "前:": "Trước:", "剩余": "Remaining", "剩余备用码:": "Mã dự phòng còn lại: ", "剩余时间": "Remaining Time", "剩余额度": "Hạn ngạch còn lại", - "剩余额度/总额度": "Còn lại/Tổng cộng", "剩余额度$": "Hạn ngạch còn lại $", + "剩余额度/总额度": "Còn lại/Tổng cộng", + "剪贴板中未检测到连接信息": "Không tìm thấy thông tin kết nối trong bộ nhớ tạm", + "副本数量": "Number of Replicas", "功能特性": "Tính năng", "加入渠道": "Join Channel", "加入预填组": "Tham gia nhóm điền sẵn", @@ -876,12 +900,14 @@ "加载详情中...": "Loading details...", "加载账单失败": "Tải hóa đơn thất bại", "加载隐私政策内容失败...": "Tải nội dung chính sách bảo mật thất bại...", + "动态计费": "", "勾选后,该分组会出现在用户创建令牌时的下拉菜单中。未勾选的分组只能由管理员分配,用户自己无法选择。": "When checked, this group appears in the dropdown when users create tokens. Unchecked groups can only be assigned by admin.", "包含": "Chứa", "包含来自未知或未标明供应商的AI模型,这些模型可能来自小型供应商或开源项目。": "Bao gồm các mô hình AI từ các nhà cung cấp không xác định hoặc không được đánh dấu, có thể đến từ các nhà cung cấp nhỏ hoặc các dự án mã nguồn mở.", "包括失败请求的次数,0代表不限制": "Bao gồm số lần yêu cầu thất bại, 0 nghĩa là không giới hạn", "匹配值": "Giá trị khớp", "匹配值(可选)": "Giá trị khớp (tùy chọn)", + "匹配内容": "", "匹配方式": "Phương thức khớp", "匹配类型": "Loại khớp", "区域": "Khu vực", @@ -892,6 +918,7 @@ "原价": "Giá gốc", "原价,和普通用户一样": "original price, same as regular users", "原因:": "Lý do: ", + "原始额度": "", "原密码": "Mật khẩu cũ", "原生格式": "Định dạng gốc", "原生额度": "Hạn mức gốc", @@ -927,6 +954,7 @@ "变换": "Biến đổi", "变更": "Thay đổi", "变焦": "thu phóng", + "变量": "", "变量值": "Variable Value", "变量名": "Variable Name", "只包括请求成功的次数": "Chỉ bao gồm số lần yêu cầu thành công", @@ -949,22 +977,23 @@ "可用端点类型": "Loại điểm cuối được hỗ trợ", "可用邀请额度": "Hạn ngạch mời khả dụng", "可留空;留空时会尝试使用 Issuer URL + /.well-known/openid-configuration": "Có thể để trống; khi trống sẽ thử sử dụng Issuer URL + /.well-known/openid-configuration", + "可空": "", "可视化": "Trực quan hóa", "可视化倍率设置": "Cài đặt tỷ lệ mô hình trực quan", "可视化编辑": "Chỉnh sửa trực quan", - "可空": "", - "可选,公告的补充说明": "Tùy chọn, thông tin bổ sung cho thông báo", - "可选,用于复现结果": "Tùy chọn, để tái tạo kết quả", - "可选:基于用户信息 JSON 做组合条件准入,条件不满足时返回自定义提示": "Tùy chọn: Tiếp nhận dựa trên điều kiện kết hợp từ JSON thông tin người dùng; trả về thông báo tùy chỉnh khi điều kiện không được đáp ứng", - "可选:用于自动生成端点或 Discovery URL": "Tùy chọn: Dùng để tự động tạo endpoint hoặc Discovery URL", "可选。匹配入口请求的 User-Agent;任意一行作为子串匹配(忽略大小写)即命中。": "Tùy chọn. Khớp User-Agent của yêu cầu đầu vào; bất kỳ dòng nào khớp dưới dạng chuỗi con (không phân biệt hoa thường) được tính là trúng.", "可选。对提取到的亲和 Key 做正则校验;不填表示不校验。": "Tùy chọn. Xác thực key ưu ái đã trích xuất bằng regex; để trống để bỏ qua xác thực.", "可选。对请求路径进行匹配;不填表示匹配所有路径。": "Tùy chọn. Khớp đường dẫn yêu cầu; để trống để khớp tất cả đường dẫn.", "可选值": "Giá trị tùy chọn", + "可选,公告的补充说明": "Tùy chọn, thông tin bổ sung cho thông báo", + "可选,用于复现结果": "Tùy chọn, để tái tạo kết quả", + "可选:基于用户信息 JSON 做组合条件准入,条件不满足时返回自定义提示": "Tùy chọn: Tiếp nhận dựa trên điều kiện kết hợp từ JSON thông tin người dùng; trả về thông báo tùy chỉnh khi điều kiện không được đáp ứng", + "可选:用于自动生成端点或 Discovery URL": "Tùy chọn: Dùng để tự động tạo endpoint hoặc Discovery URL", "合规声明已确认": "Compliance confirmed", "合规声明确认成功": "Compliance confirmed successfully", "合计:{{total}}": "Tổng cộng: {{total}}", "合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "Tổng cộng: phần văn bản {{textTotal}} + phần âm thanh {{audioTotal}} = {{total}}", + "同时满足": "", "同时重置消息": "Đặt lại tin nhắn đồng thời", "同步": "Đồng bộ", "同步到渠道": "Sync to Channel", @@ -980,14 +1009,16 @@ "名称+密钥": "Tên + Khóa", "名称不能为空": "Tên không được để trống", "名称匹配类型": "Loại khớp tên", - "后:": "Sau:", "后端固定": "", "后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。": "Tỷ lệ cố định từ backend: {{ratio}}. Trường này chỉ hiển thị giá sau khi quy đổi.", "后端请求失败": "Yêu cầu phụ trợ thất bại", "后缀": "Hậu tố", + "后:": "Sau:", "向右展开": "Mở rộng sang phải", "向左展开": "Mở rộng sang trái", "否": "Không", + "含时间条件": "", + "含请求条件": "", "启动": "Start", "启动参数 (Args)": "Startup Args", "启动命令": "Startup Command", @@ -997,11 +1028,14 @@ "启动部署失败": "Failed to start deployment", "启动配置": "Startup Configuration", "启用": "Bật", + "启用 Prompt 检查": "Bật kiểm tra Prompt", + "启用 Stripe 微信支付": "", + "启用 Stripe 支付宝": "", + "启用 Waffo": "", + "启用 Waffo Pancake": "", "启用 io.net 部署": "Enable io.net Deployment", "启用 io.net 部署开关": "Enable io.net Deployment Switch", "启用 io.net 部署时必须填写 API Key": "API Key is required when enabling io.net deployment", - "启用 Prompt 检查": "Bật kiểm tra Prompt", - "启用 Waffo": "", "启用2FA失败": "Bật xác thực hai yếu tố thất bại", "启用Claude思考适配(-thinking后缀)": "Bật thích ứng tư duy Claude (hậu tố -thinking)", "启用FunctionCall思维签名填充": "Bật điền chữ ký tư duy FunctionCall", @@ -1037,6 +1071,7 @@ "启用验证": "Bật xác thực", "周": "tuần", "命中判定:usage 中存在 cached tokens(例如 cached_tokens/prompt_cache_hit_tokens)即视为命中。": "Xác định trúng: Sự tồn tại của cached tokens trong usage (VD: cached_tokens/prompt_cache_hit_tokens) được coi là trúng.", + "命中档位": "", "命中率": "Tỷ lệ trúng", "命中该亲和规则后,会把此模板合并到渠道参数覆盖中(同名键由模板覆盖)。": "Khi quy tắc ưu ái này trúng, mẫu sẽ được hợp nhất vào ghi đè tham số kênh (key cùng tên bị mẫu ghi đè).", "和": "và", @@ -1063,15 +1098,15 @@ "图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ": "Biểu tượng sử dụng thư viện @lobehub/icons, như: OpenAI, Claude.Color, hỗ trợ tham số chuỗi: OpenAI.Avatar.type={'platform'}, OpenRouter.Avatar.shape={'square'}, truy vấn tất cả biểu tượng khả dụng vui lòng ", "图标文件不能超过 100KB,请压缩后重新上传": "", "图混合": "Pha trộn", + "图片/音频价格(可选)": "", "图片倍率 {{imageRatio}}": "Hệ số hình ảnh {{imageRatio}}", "图片功能在自定义请求体模式下不可用": "Chức năng hình ảnh không khả dụng trong chế độ yêu cầu tùy chỉnh", "图片地址": "URL hình ảnh", "图片已添加": "Hình ảnh đã được thêm", - "图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo ảnh: 1 lần gọi * đơn giá {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "图片生成调用:{{symbol}}{{price}} / 1次": "Gọi tạo hình ảnh: {{symbol}}{{price}} / 1 lần", + "图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo ảnh: 1 lần gọi * đơn giá {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "图片输入 {{price}} / 1M tokens": "Đầu vào hình ảnh {{price}} / 1M tokens", "图片输入: {{imageRatio}}": "Đầu vào hình ảnh: {{imageRatio}}", - "图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào hình ảnh: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số hình ảnh {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "图片输入价格": "Giá đầu vào hình ảnh", "图片输入价格 {{symbol}}{{price}} / 1M tokens": "Giá đầu vào hình ảnh {{symbol}}{{price}} / 1M tokens", "图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})": "Giá đầu vào hình ảnh: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (Tỷ lệ hình ảnh: {{imageRatio}})", @@ -1080,8 +1115,10 @@ "图片输入倍率": "", "图片输入倍率(仅部分模型支持该计费)": "Tỷ lệ đầu vào hình ảnh (chỉ được hỗ trợ bởi một số mô hình để tính phí)", "图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费": "Cài đặt tỷ lệ liên quan đến đầu vào hình ảnh, khóa là tên mô hình, giá trị là tỷ lệ, chỉ được hỗ trợ bởi một số mô hình để tính phí", + "图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào hình ảnh: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số hình ảnh {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "图生文": "Mô tả", "图生视频": "Hình ảnh sang Video", + "在 Stripe 后台创建价格后获得": "", "在Gotify服务器创建应用后获得的令牌,用于发送通知": "Mã thông báo nhận được sau khi tạo ứng dụng trên máy chủ Gotify, được sử dụng để gửi thông báo", "在Gotify服务器的应用管理中创建新应用": "Tạo ứng dụng mới trong quản lý ứng dụng của máy chủ Gotify", "在找兑换码?": "Đang tìm mã đổi thưởng? ", @@ -1110,12 +1147,12 @@ "填充新模板": "Điền mẫu mới", "填充旧模板": "Điền mẫu cũ", "填充模板": "Điền mẫu", + "填充模板(全渠道)": "Điền mẫu (tất cả kênh)", + "填充模板(指定渠道)": "Điền mẫu (kênh được chọn)", "填充模板:等级+激活": "Điền mẫu: Cấp + Kích hoạt", "填充模板:等级提示": "Điền mẫu: Prompt cấp", "填充模板:组织或角色": "Điền mẫu: Tổ chức hoặc vai trò", "填充模板:组织提示": "Điền mẫu: Prompt tổ chức", - "填充模板(全渠道)": "Điền mẫu (tất cả kênh)", - "填充模板(指定渠道)": "Điền mẫu (kênh được chọn)", "填入": "Điền", "填入 CC Switch": "Điền CC Switch", "填入所有模型": "Điền tất cả mô hình", @@ -1129,7 +1166,16 @@ "填写 Issuer URL 后自动生成:": "Tự động tạo sau khi điền Issuer URL:", "填写Gotify服务器的完整URL地址": "Điền địa chỉ URL đầy đủ của máy chủ Gotify", "填写后会自动拼接预设端点": "Endpoint đặt trước sẽ được tự động thêm sau khi điền", + "填写后覆盖当前测试环境 API 密钥,留空表示保持当前不变": "", + "填写后覆盖当前测试环境 Webhook 公钥,留空表示保持当前不变": "", + "填写后覆盖当前测试环境私钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境 API 密钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境 Webhook 公钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境私钥,留空表示保持当前不变": "", + "填写后覆盖当前私钥,留空表示保持当前不变": "", "填写带https的域名,逗号分隔": "Điền tên miền có https, phân tách bằng dấu phẩy", + "填写测试环境 Waffo 公钥,Base64 或 PEM 内容均可": "", + "填写生产环境 Waffo 公钥,Base64 或 PEM 内容均可": "", "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "Sau khi điền nội dung thỏa thuận người dùng, người dùng sẽ được yêu cầu tích vào đã đọc thỏa thuận người dùng khi đăng ký", "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "Sau khi điền nội dung chính sách bảo mật, người dùng sẽ được yêu cầu tích vào đã đọc chính sách bảo mật khi đăng ký", "填写音频补全价格前,需要先填写音频输入价格。": "Trước khi nhập giá hoàn thành âm thanh, hãy nhập giá đầu vào âm thanh trước.", @@ -1172,6 +1218,8 @@ "多密钥管理": "Quản lý đa khóa", "多模型统一接入,只需将基址替换为:": "Truy cập thống nhất đa mô hình, chỉ cần thay thế URL cơ sở bằng: ", "多种充值方式,安全便捷": "Nhiều phương thức nạp tiền, an toàn và tiện lợi", + "大于": "", + "大于等于": "", "大模型接口网关": "Cổng API LLM", "天": "ngày", "天前": "ngày trước", @@ -1188,14 +1236,14 @@ "套餐标题": "Tiêu đề gói", "套餐标题不能为空": "Tên gói không được để trống", "套餐的基本信息和定价": "Thông tin cơ bản và giá của gói", - "如:大带宽批量分析图片推荐": "ví dụ: Phân tích hàng loạt băng thông lớn đề xuất hình ảnh", - "如:香港线路": "ví dụ: Tuyến Hồng Kông", "如向中华人民共和国境内公众提供生成式人工智能服务,你将依法履行备案登记、安全评估、内容安全、投诉举报、生成合成内容标识、日志留存、个人信息保护等合规义务;": "If you provide generative AI services to the public in mainland China, you will fulfill legal compliance obligations.", "如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。": "Nếu kênh ưu ái thất bại, sau khi thử lại thành công trên kênh khác, ưu ái sẽ được cập nhật sang kênh thành công.", "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "Nếu bạn đang kết nối với các dự án chuyển tiếp One API hoặc New API thượng nguồn, vui lòng sử dụng loại OpenAI. Đừng sử dụng loại này trừ khi bạn biết mình đang làm gì.", "如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "Nếu yêu cầu của người dùng chứa từ nhắc hệ thống, cài đặt này sẽ được nối vào trước từ nhắc hệ thống của người dùng", "如果镜像为私有,请填写密码或Token": "If the image is private, please fill in the password or token", "如果镜像为私有,请填写用户名": "If the image is private, please fill in the username", + "如:大带宽批量分析图片推荐": "ví dụ: Phân tích hàng loạt băng thông lớn đề xuất hình ảnh", + "如:香港线路": "ví dụ: Tuyến Hồng Kông", "始终使用浅色主题": "Luôn sử dụng chủ đề sáng", "始终使用深色主题": "Luôn sử dụng chủ đề tối", "字段映射": "Ánh xạ trường", @@ -1203,6 +1251,7 @@ "字段路径": "Đường dẫn trường", "字段透传控制": "Kiểm soát truyền qua trường", "字段速查": "Tra cứu nhanh trường", + "存在": "", "存在惩罚,鼓励讨论新话题": "Phạt sự hiện diện, khuyến khích chủ đề mới", "存在重复的分组名称:": "Duplicate group names: ", "存在重复的键名:": "Tồn tại tên khóa trùng lặp:", @@ -1264,7 +1313,6 @@ "密钥": "Khóa", "密钥 JSON 必须包含 access_token": "JSON khóa phải bao gồm access_token", "密钥 JSON 必须包含 account_id": "JSON khóa phải bao gồm account_id", - "密钥(编辑模式下,保存的密钥不会显示)": "Khóa (trong chế độ chỉnh sửa, khóa đã lưu sẽ không hiển thị)", "密钥去重": "Loại bỏ khóa trùng lặp", "密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性": "Khóa sẽ được thêm vào tiêu đề yêu cầu dưới dạng Bearer để xác minh tính hợp pháp của yêu cầu webhook", "密钥已删除": "Khóa đã bị xóa", @@ -1282,6 +1330,7 @@ "密钥获取成功": "Lấy khóa thành công", "密钥输入方式": "Phương thức nhập khóa", "密钥预览": "Xem trước khóa", + "密钥(编辑模式下,保存的密钥不会显示)": "Khóa (trong chế độ chỉnh sửa, khóa đã lưu sẽ không hiển thị)", "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "Đối với các kênh chính thức, new-api đã tích hợp sẵn địa chỉ. Trừ khi đó là trang web proxy của bên thứ ba hoặc địa chỉ truy cập đặc biệt của Azure, không cần điền vào", "对免费模型启用预消耗": "Enable pre-consumption for free models", "对域名启用 IP 过滤(推荐开启)": "Bật lọc IP cho tên miền (khuyến nghị)", @@ -1310,6 +1359,8 @@ "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "Thao tác này sẽ xóa tất cả các cấu hình đã lưu và khôi phục cài đặt mặc định, thao tác này không thể hoàn tác. Tiếp tục?", "将清除选定时间之前的所有日志": "Thao tác này sẽ xóa tất cả nhật ký trước thời gian đã chọn", "将追加 2 条规则到现有规则列表。": "2 quy tắc sẽ được thêm vào danh sách quy tắc hiện có.", + "小于": "", + "小于等于": "", "小时": "Giờ", "小时费率": "Hourly Rate", "尚未使用": "Chưa sử dụng", @@ -1320,10 +1371,12 @@ "展开更多": "Mở rộng thêm", "展示价格": "Giá hiển thị", "嵌套映射:用户分组 → 使用分组 → 倍率": "Nested mapping: user group → using group → ratio", + "工具标识": "", + "工具调用定价": "", "左侧边栏个人设置": "Cài đặt cá nhân ở thanh bên trái", + "已为 ${count} 个渠道设置标签!": "Đã đặt thẻ cho ${count} kênh!", "已为 {{count}} 个模型设置{{type}}_one": "Đã đặt {{type}} cho {{count}} mô hình", "已为 {{count}} 个模型设置{{type}}_other": "Đã đặt {{type}} cho {{count}} mô hình", - "已为 ${count} 个渠道设置标签!": "Đã đặt thẻ cho ${count} kênh!", "已从 Discovery 自动填充配置": "Cấu hình đã được tự động điền từ Discovery", "已从 Discovery 获取配置,可继续手动修改所有字段。": "Đã nhận cấu hình từ Discovery. Bạn có thể tiếp tục chỉnh sửa thủ công tất cả các trường.", "已作废": "Đã vô hiệu", @@ -1335,15 +1388,16 @@ "已分配内存": "Bộ nhớ đã phân bổ", "已切换为Assistant角色": "Đã chuyển sang vai trò Assistant", "已切换为System角色": "Đã chuyển sang vai trò System", + "已切换到新版前端,正在刷新页面": "Đã chuyển sang frontend mới, đang làm mới trang", "已切换到新版前端,正在跳转首页": "Đã chuyển sang frontend mới, đang chuyển đến trang chủ", "已切换至最优倍率视图,每个模型使用其最低倍率分组": "Đã chuyển sang chế độ xem tỷ lệ tối ưu, mỗi mô hình sử dụng nhóm tỷ lệ thấp nhất của nó", "已初始化": "Đã khởi tạo", "已删除": "Đã xóa", + "已删除 ${data} 个通道!": "Đã xóa ${data} kênh!", "已删除 {{count}} 个令牌!": "Đã xóa {{count}} mã thông báo!", "已删除 {{count}} 个令牌!_other": "Deleted {{count}} tokens!", "已删除 {{count}} 条失效兑换码_one": "Đã xóa {{count}} mã đổi thưởng hết hạn", "已删除 {{count}} 条失效兑换码_other": "Đã xóa {{count}} mã đổi thưởng hết hạn", - "已删除 ${data} 个通道!": "Đã xóa ${data} kênh!", "已删除所有禁用渠道,共计 ${data} 个": "Đã xóa tất cả các kênh bị vô hiệu hóa, tổng cộng ${data}", "已删除消息及其回复": "Đã xóa tin nhắn và các câu trả lời của nó", "已勾选": "Đã chọn", @@ -1365,8 +1419,6 @@ "已复制": "Đã sao chép", "已复制 ${count} 个模型": "Đã sao chép ${count} mô hình", "已复制 ID 到剪贴板": "ID copied to clipboard", - "已复制:": "Đã sao chép:", - "已复制:{{name}}": "Đã sao chép: {{name}}", "已复制全部数据": "Tất cả dữ liệu đã được sao chép", "已复制到剪切板": "Đã sao chép vào khay nhớ tạm", "已复制到剪贴板": "Đã sao chép vào khay nhớ tạm", @@ -1375,6 +1427,8 @@ "已复制模型名称": "Đã sao chép tên mô hình", "已复制版本号": "Version copied", "已复制自动生成的 API Key": "Auto-generated API Key copied", + "已复制:": "Đã sao chép:", + "已复制:{{name}}": "Đã sao chép: {{name}}", "已完成": "Completed", "已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型": "Đã áp dụng hàng loạt cấu hình giá của mô hình {{name}} cho {{count}} mô hình", "已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型_other": "", @@ -1416,10 +1470,10 @@ "已达到购买上限": "Đã đạt giới hạn mua", "已过期": "Đã hết hạn", "已运行时间": "Uptime", + "已选择 ${count} 个渠道": "Đã chọn ${count} kênh", "已选择 {{count}} 个模型_one": "Đã chọn {{count}} mô hình", "已选择 {{count}} 个模型_other": "Đã chọn {{count}} mô hình", "已选择 {{selected}} / {{total}}": "Đã chọn {{selected}} / {{total}}", - "已选择 ${count} 个渠道": "Đã chọn ${count} kênh", "已重置为默认配置": "Đã đặt lại về cấu hình mặc định", "已销毁": "Destroyed", "币种": "Tiền tệ", @@ -1443,16 +1497,21 @@ "延长容器时长将会产生额外费用,请确认您有足够的账户余额。": "Extending container duration will incur additional charges, please ensure you have sufficient account balance.", "延长操作一旦确认无法撤销,费用将立即扣除。": "Once confirmed, the extension operation cannot be undone, and charges will be deducted immediately.", "延长时长": "Extension Duration", - "延长时长(小时)": "Extension Duration (hours)", "延长时长不能超过720小时(30天)": "Extension duration cannot exceed 720 hours (30 days)", "延长时长失败": "Failed to extend duration", "延长时长至少为1小时": "Extension duration must be at least 1 hour", + "延长时长(小时)": "Extension Duration (hours)", "建立连接时发生错误": "Đã xảy ra lỗi khi thiết lập kết nối", "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "Khuyên dùng cơ sở dữ liệu MySQL hoặc PostgreSQL trong môi trường sản xuất, hoặc đảm bảo tệp cơ sở dữ liệu SQLite được ánh xạ tới bộ nhớ bền vững của máy chủ.", "开": "mở", "开启「默认使用 auto 分组」后,新建令牌和初始令牌都会自动设为 auto。": "After enabling \"Default to auto group\", new tokens and initial tokens will be set to auto.", "开启之后会清除用户提示词中的": "Sau khi bật, từ nhắc của người dùng sẽ bị xóa", "开启之后将上游地址替换为服务器地址": "Sau khi bật, địa chỉ thượng nguồn sẽ được thay thế bằng địa chỉ máy chủ", + "开启后不限制:必须设置模型倍率": "Sau khi bật, không giới hạn: phải đặt tỷ lệ mô hình", + "开启后创建令牌默认选择auto分组,初始令牌也将设为auto": "When enabled, new tokens default to auto group, initial tokens are also set to auto", + "开启后未登录用户无法访问模型广场": "Khi bật, người dùng chưa xác thực không thể truy cập thị trường mô hình", + "开启后检测到新增模型会自动加入当前渠道模型列表": "", + "开启后由后端定时任务检测该渠道上游模型变化": "", "开启后,using_group 会参与 cache key(不同分组隔离)。": "Khi bật, using_group sẽ tham gia vào cache key (cách ly theo nhóm).", "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。": "Khi bật, giữ mục ưu ái ngay cả khi kênh ưu ái bị tắt hoặc không còn dùng được cho nhóm/mô hình hiện tại. Khi tắt, mục đó sẽ bị xóa và kênh khác sẽ được chọn.", "开启后,仅\"消费\"和\"错误\"日志将记录您的客户端IP地址": "Sau khi bật, chỉ nhật ký \"tiêu thụ\" và \"lỗi\" sẽ ghi lại địa chỉ IP máy khách của bạn", @@ -1465,11 +1524,6 @@ "开启后,规则名称会参与 cache key(不同规则隔离)。": "Khi bật, tên quy tắc sẽ tham gia vào cache key (cách ly theo quy tắc).", "开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)": "Khi bật, yêu cầu đến Claude qua kênh này sẽ tự động thêm ?beta=true (client không cần truyền thủ công)", "开启后,违规请求将额外扣费。": "Khi bật, các yêu cầu vi phạm sẽ bị tính phí bổ sung.", - "开启后不限制:必须设置模型倍率": "Sau khi bật, không giới hạn: phải đặt tỷ lệ mô hình", - "开启后创建令牌默认选择auto分组,初始令牌也将设为auto": "When enabled, new tokens default to auto group, initial tokens are also set to auto", - "开启后未登录用户无法访问模型广场": "Khi bật, người dùng chưa xác thực không thể truy cập thị trường mô hình", - "开启后检测到新增模型会自动加入当前渠道模型列表": "", - "开启后由后端定时任务检测该渠道上游模型变化": "", "开启批量操作": "Bật chọn hàng loạt", "开始": "Bắt đầu", "开始同步": "Bắt đầu đồng bộ", @@ -1509,6 +1563,7 @@ "当前没有未设置定价的模型": "Hiện không có mô hình nào chưa thiết lập giá", "当前版本": "Phiên bản hiện tại", "当前状态": "Current Status", + "当前环境共用同一商户 ID": "", "当前缓存大小": "Kích thước bộ nhớ đệm hiện tại", "当前编辑": "Đang chỉnh sửa", "当前规则不支持写入到该位置": "Quy tắc hiện tại không hỗ trợ ghi vào vị trí này", @@ -1552,6 +1607,7 @@ "总收益": "tổng doanh thu", "总计": "Tổng cộng", "总额度": "Tổng hạn ngạch", + "恢复默认": "", "您可以个性化设置侧边栏的要显示功能": "Bạn có thể tùy chỉnh các chức năng thanh bên để hiển thị", "您可以在上方拉取需要的模型": "You can pull the required models above", "您无权访问此页面,请联系管理员": "Bạn không có quyền truy cập trang này. Vui lòng liên hệ với quản trị viên.", @@ -1621,22 +1677,23 @@ "拉取进度": "Pull Progress", "拒绝提示模板(可选)": "Mẫu prompt từ chối (tùy chọn)", "拦截原因": "Lý do chặn", + "按 1 美元对应的站内价格填写": "", "按K显示单位": "Hiển thị theo K", "按价格设置": "Đặt theo giá", "按倍率类型筛选": "Lọc theo loại tỷ lệ", "按倍率设置": "Đặt theo tỷ lệ", "按次": "Theo lượt gọi", "按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Theo lượt gọi {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "按次计费": "Tính phí theo lượt gọi", "按次:{{symbol}}{{price}}": "Theo lượt gọi: {{symbol}}{{price}}", "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "Theo lượt gọi: {{symbol}}{{price}} * {{ratioType}}: {{ratio}} = {{symbol}}{{total}}", - "按次计费": "Tính phí theo lượt gọi", "按照如下格式输入:AccessKey|SecretAccessKey|Region": "Enter in the format: AccessKey|SecretAccessKey|Region", "按量计费": "Trả tiền theo mức sử dụng", "按量计费下需要先填写输入价格,才能保存其它价格项。": "Ở chế độ tính phí theo lượng, cần điền giá đầu vào trước thì mới lưu được các mục giá khác.", "按顺序替换content中的变量占位符": "Thay thế các trình giữ chỗ biến trong nội dung theo thứ tự", "换脸": "Hoán đổi khuôn mặt", - "授权,需在遵守": " và phải được sử dụng tuân thủ ", "授权失败": "Ủy quyền thất bại", + "授权,需在遵守": " và phải được sử dụng tuân thủ ", "排序": "Thứ tự", "排队中": "Đang xếp hàng", "接受未设置价格模型": "Chấp nhận các mô hình không có cài đặt giá", @@ -1648,8 +1705,8 @@ "控制输出的随机性和创造性": "Kiểm soát tính ngẫu nhiên và sáng tạo của đầu ra", "控制顶栏模块显示状态,全局生效": "Kiểm soát trạng thái hiển thị mô-đun tiêu đề, hiệu ứng toàn cầu", "推荐": "Đề xuất", - "推荐:用户可以选择是否使用指纹等验证": "Khuyên dùng: Người dùng có thể chọn sử dụng xác minh vân tay hay không", "推荐使用(用户可选)": "Khuyên dùng (người dùng tùy chọn)", + "推荐:用户可以选择是否使用指纹等验证": "Khuyên dùng: Người dùng có thể chọn sử dụng xác minh vân tay hay không", "描述": "Mô tả", "提交": "Gửi", "提交时间": "Thời gian gửi", @@ -1659,6 +1716,8 @@ "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "Prompt {{input}} tokens / 1M tokens * {{symbol}}{{price}}", "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Gợi ý {{input}} tokens / 1M tokens * {{symbol}}{{price}} + Hoàn thành {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Gợi ý {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Bộ nhớ đệm {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + Tạo bộ nhớ đệm {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + Hoàn thành {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "提示价格:{{symbol}}{{price}} / 1M tokens": "Giá gợi ý: {{symbol}}{{price}} / 1M tokens", + "提示缓存倍率": "Tỷ lệ bộ nhớ đệm gợi ý", "提示:如果切换后页面无法正常渲染,请清空浏览器缓存后重试。": "Gợi ý: Nếu trang không hiển thị đúng sau khi chuyển đổi, hãy xóa bộ nhớ đệm của trình duyệt rồi thử lại.", "提示:如需备份数据,只需复制上述目录即可": "Mẹo: Để sao lưu dữ liệu, chỉ cần sao chép thư mục trên", "提示:此处配置仅用于控制「模型广场」对用户的展示效果,不会影响模型的实际调用与路由。若需配置真实调用行为,请前往「渠道管理」进行设置。": "Lưu ý: Cấu hình tại đây chỉ ảnh hưởng đến cách hiển thị trong \"Chợ mô hình\" và không ảnh hưởng đến việc gọi hoặc định tuyến thực tế. Nếu cần cấu hình hành vi gọi thực tế, vui lòng thiết lập trong \"Quản lý kênh\".", @@ -1666,8 +1725,6 @@ "提示:该功能为测试版,未来配置结构与功能行为可能发生变更,请勿在生产环境使用。": "Lưu ý: Đây là tính năng beta. Cấu trúc cấu hình và hành vi có thể thay đổi trong tương lai. Không dùng trong môi trường production.", "提示:语言偏好会同步到您登录的所有设备,并影响API返回的错误消息语言。": "Gợi ý: Tùy chọn ngôn ngữ sẽ được đồng bộ trên tất cả thiết bị đã đăng nhập và ảnh hưởng đến ngôn ngữ thông báo lỗi API trả về.", "提示:链接中的{key}将被替换为API密钥,{address}将被替换为服务器地址": "Mẹo: {key} trong liên kết sẽ được thay thế bằng khóa API, {address} sẽ được thay thế bằng địa chỉ máy chủ", - "提示价格:{{symbol}}{{price}} / 1M tokens": "Giá gợi ý: {{symbol}}{{price}} / 1M tokens", - "提示缓存倍率": "Tỷ lệ bộ nhớ đệm gợi ý", "搜索供应商": "Tìm kiếm nhà cung cấp", "搜索关键字": "Từ khóa tìm kiếm", "搜索失败": "Search failed", @@ -1702,6 +1759,7 @@ "支付方式名称": "Tên phương thức thanh toán", "支付方式名称不能为空": "", "支付方式类型": "Loại phương thức thanh toán", + "支付方式设置": "", "支付渠道": "Kênh thanh toán", "支付设置": "Thanh toán", "支付请求失败": "Yêu cầu thanh toán thất bại", @@ -1747,9 +1805,9 @@ "数据看板默认时间粒度": "Độ chi tiết thời gian mặc định của bảng dữ liệu", "数据管理和日志查看": "Quản lý dữ liệu và xem nhật ký", "文件上传": "Tải lên tệp", - "文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Tìm kiếm tệp: {{count}} / 1K * đơn giá {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "文件搜索价格:{{symbol}}{{price}} / 1K 次": "Giá tìm kiếm tệp: {{symbol}}{{price}} / 1K lần", "文件搜索调用 {{fileSearchCallCount}} 次": "Đã gọi tìm kiếm tệp {{fileSearchCallCount}} lần", + "文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Tìm kiếm tệp: {{count}} / 1K * đơn giá {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "Gợi ý văn bản {{input}} tokens / 1M tokens * {{symbol}}{{price}} + Hoàn thành văn bản {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Prompt văn bản {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + Hoàn thành văn bản {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + Prompt âm thanh {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + Hoàn thành âm thanh {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "Gợi ý văn bản {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Bộ nhớ đệm {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + Hoàn thành văn bản {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", @@ -1779,9 +1837,9 @@ "新建容器部署": "Create Container Deployment", "新建数量": "Số lượng mới", "新建组": "Nhóm mới", + "新格式模板": "Mẫu định dạng mới", "新格式(支持条件判断与json自定义):": "Định dạng mới (hỗ trợ phán đoán điều kiện và tùy chỉnh JSON):", "新格式(规则 + 条件)": "Định dạng mới (Quy tắc + Điều kiện)", - "新格式模板": "Mẫu định dạng mới", "新版本": "Phiên bản mới", "新用户使用邀请码奖励额度": "Hạn ngạch thưởng mã mời người dùng mới", "新用户初始额度": "Hạn ngạch ban đầu cho người dùng mới", @@ -1795,6 +1853,7 @@ "无加密": "Không mã hóa", "无效的部署信息": "Invalid deployment information", "无效的重置链接,请重新发起密码重置请求": "Liên kết đặt lại không hợp lệ, vui lòng bắt đầu yêu cầu đặt lại mật khẩu mới", + "无条件(兜底档)": "", "无法发起 Passkey 注册": "Không thể bắt đầu đăng ký Passkey", "无法复制到剪贴板,请手动复制": "Không thể sao chép vào khay nhớ tạm, vui lòng sao chép thủ công", "无法添加图片": "Không thể thêm hình ảnh", @@ -1819,19 +1878,22 @@ "日志类型": "Loại nhật ký", "日志设置": "Cài đặt nhật ký", "日志详情": "Chi tiết nhật ký", - "旧格式(JSON 对象)": "Định dạng cũ (Đối tượng JSON)", - "旧格式(直接覆盖):": "Định dạng cũ (ghi đè trực tiếp):", "旧格式必须是 JSON 对象": "Định dạng cũ phải là đối tượng JSON", "旧格式模板": "Mẫu định dạng cũ", + "旧格式(JSON 对象)": "Định dạng cũ (Đối tượng JSON)", + "旧格式(直接覆盖):": "Định dạng cũ (ghi đè trực tiếp):", "旧版前端即将停止维护": "Frontend cũ sắp ngừng bảo trì", "旧的备用码已失效,请保存新的备用码": "Mã dự phòng cũ đã bị vô hiệu hóa, vui lòng lưu mã dự phòng mới", "早上好": "Chào buổi sáng", + "时区": "", "时间": "Thời gian", "时间信息": "Time Information", + "时间条件": "", "时间粒度": "Độ chi tiết thời gian", "易支付": "Epay", "易支付商户ID": "ID người bán Epay", "易支付商户密钥": "Khóa người bán Epay", + "易支付设置": "", "是": "Có", "是否为企业账户": "Đây có phải là tài khoản doanh nghiệp không?", "是否同时重置对话消息?选择\"是\"将清空所有对话记录并恢复默认示例;选择\"否\"将保留当前对话记录。": "Đặt lại tin nhắn trò chuyện cùng lúc? Chọn \"Có\" sẽ xóa tất cả hồ sơ trò chuyện và khôi phục các ví dụ mặc định; chọn \"Không\" sẽ giữ lại hồ sơ trò chuyện hiện tại.", @@ -1842,16 +1904,17 @@ "是否自动禁用": "Có tự động vô hiệu hóa không", "是否要求指纹/面容等生物识别": "Có yêu cầu nhận dạng vân tay/khuôn mặt không", "显示倍率": "Hiển thị tỷ lệ", - "显示最新20条": "Hiển thị 20 mới nhất", "显示名称": "Tên hiển thị", "显示名称字段(可选)": "Trường tên hiển thị (tùy chọn)", "显示完整内容": "Hiển thị nội dung đầy đủ", "显示操作项": "Hiển thị hành động", "显示更多": "Hiển thị thêm", + "显示最新20条": "Hiển thị 20 mới nhất", "显示第": "Đang hiển thị", "显示设置": "Cài đặt hiển thị", "显示调试": "Hiển thị gỡ lỗi", "晚上好": "Chào buổi tối", + "普通按量/按次直接填价格就行;如果价格要跟请求参数或请求头联动,请切到表达式/阶梯计费。": "", "普通环境变量": "Regular Environment Variables", "普通用户": "Người dùng thường", "普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào thường: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}", @@ -1907,9 +1970,12 @@ "更多": "Mở rộng thêm", "更多信息请参考": "Để biết thêm thông tin, vui lòng tham khảo", "更多参数请参考": "Để biết thêm tham số, vui lòng tham khảo", + "更多模板...": "", + "更好的价格,更好的稳定性,只需要将模型基址替换为:": "Giá tốt hơn, ổn định hơn, không cần đăng ký, chỉ cần thay thế URL CƠ SỞ mô hình bằng: ", "更新": "Cập nhật", "更新 Creem 设置": "Cập nhật cài đặt Creem", "更新 Stripe 设置": "Cập nhật cài đặt Stripe", + "更新 Waffo Pancake 设置": "", "更新 Waffo 设置": "", "更新SSRF防护设置": "Cập nhật cài đặt bảo vệ SSRF", "更新Worker设置": "Cập nhật cài đặt Worker", @@ -1925,6 +1991,7 @@ "更新所有已启用通道余额": "Cập nhật số dư cho tất cả các kênh đã bật", "更新支付设置": "Cập nhật cài đặt thanh toán", "更新时间": "Thời gian cập nhật", + "更新易支付设置": "", "更新服务器地址": "Cập nhật địa chỉ máy chủ", "更新模型信息": "Cập nhật thông tin mô hình", "更新渠道信息": "Cập nhật thông tin kênh", @@ -1934,6 +2001,20 @@ "更新配置失败": "Failed to update configuration", "更新预填组": "Cập nhật nhóm điền sẵn", "替换": "", + "最低": "thấp nhất", + "最低充值数量": "", + "最低充值美元数量": "Số tiền nạp đô la tối thiểu", + "最低充值美元数量必须大于 0": "Số tiền nạp đô la tối thiểu phải lớn hơn 0", + "最后使用时间": "Thời gian sử dụng cuối cùng", + "最后更新": "Last Updated", + "最后请求": "Yêu cầu cuối cùng", + "最大GPU数量": "Max Number of GPUs", + "最大可用": "Max Available", + "最大条目数": "Số mục tối đa", + "最终抵扣": "Khấu trừ cuối cùng", + "最近一次": "Lần gần nhất", + "最近事件": "Recent Events", + "最高优先级": "highest priority", "月": "tháng", "有 Reasoning": "Có lập luận", "有序字符串数组": "Ordered string array", @@ -1990,8 +2071,8 @@ "本地数据存储": "Lưu trữ dữ liệu cục bộ", "本地计费": "Local billing", "本月获得": "Tháng này", - "本设备:手机指纹/面容,外接:USB安全密钥": "Tích hợp: vân tay/khuôn mặt điện thoại, Bên ngoài: khóa bảo mật USB", "本设备内置": "Thiết bị tích hợp", + "本设备:手机指纹/面容,外接:USB安全密钥": "Tích hợp: vân tay/khuôn mặt điện thoại, Bên ngoài: khóa bảo mật USB", "本项目根据": "Dự án này được cấp phép theo ", "机密环境变量": "Secret Environment Variables", "机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。": "Secret environment variables will be stored encrypted, suitable for storing passwords, API keys and other sensitive information.", @@ -2000,13 +2081,15 @@ "权限设置": "Cài đặt quyền", "条": "mục", "条 - 第": "đến", - "条,共": "của", + "条件": "", + "条件乘数": "", "条件取反": "Đảo ngược điều kiện", "条件数": "Số điều kiện", "条件规则": "Quy tắc điều kiện", "条件项设置": "Cài đặt mục điều kiện", "条日志已清理!": "nhật ký đã được xóa!", "条规则": "rules", + "条,共": "của", "来源": "Nguồn", "来源于 IO.NET 部署": "From IO.NET Deployment", "来源端点": "Endpoint nguồn", @@ -2040,11 +2123,15 @@ "核采样,控制词汇选择的多样性": "Lấy mẫu hạt nhân, kiểm soát sự đa dạng của lựa chọn từ vựng", "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。": "Theo quy ước của Anthropic, input tokens của /v1/messages chỉ tính phần đầu vào không dùng cache và không bao gồm tokens đọc/ghi cache.", "根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含": "Tìm siêu dữ liệu mô hình dựa trên tên mô hình và quy tắc khớp, ưu tiên: chính xác > tiền tố > hậu tố > chứa", + "格式": "", "格式化": "Định dạng", "格式化 JSON": "Định dạng JSON", "格式正确": "Định dạng hợp lệ", "格式示例:": "Ví dụ định dạng:", "格式错误": "Định dạng không hợp lệ", + "档": "", + "档位": "", + "档位名称": "", "检查更新": "Kiểm tra cập nhật", "检测全部渠道上游更新": "", "检测到 FluentRead(流畅阅读)": "Đã phát hiện FluentRead (đọc trôi chảy)", @@ -2065,10 +2152,10 @@ "模型价格 {{symbol}}{{price}} / 次": "Giá mô hình {{symbol}}{{price}} / lượt gọi", "模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Giá mô hình {{symbol}}{{price}} / lượt gọi * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}": "Giá mô hình {{symbol}}{{price}}, {{ratioType}} {{ratio}}", + "模型价格未配置": "Giá mô hình chưa được cấu hình", "模型价格:{{symbol}}{{price}}": "Giá mô hình: {{symbol}}{{price}}", "模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "Giá mô hình: {{symbol}}{{price}} * {{ratioType}}: {{ratio}} = {{symbol}}{{total}}", "模型价格:{{symbol}}{{price}} / 次": "Giá mô hình: {{symbol}}{{price}} / lượt gọi", - "模型价格未配置": "Giá mô hình chưa được cấu hình", "模型倍率": "Tỷ lệ mô hình", "模型倍率 {{modelRatio}}": "Model ratio {{modelRatio}}", "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}": "Tỷ lệ mô hình {{modelRatio}}, tỷ lệ bộ nhớ đệm {{cacheRatio}}, tỷ lệ hoàn thành {{completionRatio}}, {{ratioType}} {{ratio}}", @@ -2083,10 +2170,10 @@ "模型倍率设置": "Cài đặt tỷ lệ mô hình", "模型关键字": "Từ khóa mô hình", "模型列表": "Danh sách mô hình", - "模型列表,使用逗号分隔,例如:gpt-3.5-turbo,gpt-4": "Danh sách mô hình, phân tách bằng dấu phẩy, ví dụ: gpt-3.5-turbo,gpt-4", "模型列表已复制到剪贴板": "Danh sách mô hình đã được sao chép vào khay nhớ tạm", "模型列表已更新": "Danh sách mô hình đã được cập nhật", "模型列表已追加更新": "Model list has been updated", + "模型列表,使用逗号分隔,例如:gpt-3.5-turbo,gpt-4": "Danh sách mô hình, phân tách bằng dấu phẩy, ví dụ: gpt-3.5-turbo,gpt-4", "模型创建成功!": "Tạo mô hình thành công!", "模型删除失败": "Failed to delete model", "模型删除失败: {{error}}": "Failed to delete model: {{error}}", @@ -2100,8 +2187,8 @@ "模型名称正则": "Regex tên mô hình", "模型固定价格": "Giá cố định mô hình", "模型图标": "Biểu tượng mô hình", - "模型定价,需要登录访问": "Định giá mô hình, yêu cầu đăng nhập để truy cập", "模型定价设置": "Model Pricing", + "模型定价,需要登录访问": "Định giá mô hình, yêu cầu đăng nhập để truy cập", "模型广场": "Thị trường mô hình", "模型库": "Thư viện mô hình", "模型拉取失败: {{error}}": "Failed to pull model: {{error}}", @@ -2116,8 +2203,8 @@ "模型未加入列表,可能无法调用": "Model not in the list; requests may fail", "模型权限": "Quyền mô hình", "模型正则": "Regex model", - "模型正则(每行一个)": "Regex model (mỗi dòng một mục)", "模型正则不能为空": "Regex model không được để trống", + "模型正则(每行一个)": "Regex model (mỗi dòng một mục)", "模型消耗分布": "Phân phối tiêu thụ mô hình", "模型消耗趋势": "Xu hướng tiêu thụ mô hình", "模型版本": "Phiên bản mô hình", @@ -2143,8 +2230,8 @@ "模型部署设置": "Triển khai mô hình", "模型配置": "Cấu hình mô hình", "模型重定向": "Chuyển hướng mô hình", - "模型重定向,JSON格式,例如:{\"gpt-3.5-turbo\": \"gpt-3.5-turbo-0613\"}": "Chuyển hướng mô hình, định dạng JSON, ví dụ: {\"gpt-3.5-turbo\": \"gpt-3.5-turbo-0613\"}", "模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "The following models from the redirect have not been added to the “Models” list and requests will fail due to no available model:", + "模型重定向,JSON格式,例如:{\"gpt-3.5-turbo\": \"gpt-3.5-turbo-0613\"}": "Chuyển hướng mô hình, định dạng JSON, ví dụ: {\"gpt-3.5-turbo\": \"gpt-3.5-turbo-0613\"}", "模型限制": "Giới hạn mô hình", "模型限制列表": "Danh sách giới hạn mô hình", "模式": "Chế độ", @@ -2164,8 +2251,8 @@ "正在加载可用部署位置...": "Loading available deployment locations...", "正在加载签到状态...": "Đang tải trạng thái đăng nhập...", "正在处理": "Đang xử lý", - "正在处理,请稍候": "", "正在处理大内容...": "Đang xử lý nội dung lớn...", + "正在处理,请稍候": "", "正在导出...": "Đang xuất...", "正在批量处理,请稍候": "", "正在批量检测,请稍候": "", @@ -2214,6 +2301,7 @@ "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "Tùy chọn cho các cuộc gọi API thông qua địa chỉ API tùy chỉnh, không thêm /v1 và / ở cuối", "每个充值单位对应的 USD 金额,默认 1.0": "", "每个分组代表一个价格档位。管理员创建分组后,可以选择哪些档位对用户开放自选。": "Each group represents a pricing tier. After creating groups, admins can choose which tiers are open for user self-selection.", + "每个档位可设置 0~2 个条件(对 p 和 c),最后一档为兜底档无需条件。": "", "每个用户最多可创建的令牌数量,默认 1000,设置过大可能会影响性能": "Số lượng token tối đa mỗi người dùng có thể tạo, mặc định 1000. Đặt quá lớn có thể ảnh hưởng hiệu suất", "每周": "Hàng tuần", "每天": "Hàng ngày", @@ -2261,12 +2349,12 @@ "注册用户": "Người dùng đã đăng ký", "注册设置": "Cài đặt đăng ký", "注意": "Lưu ý", + "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Lưu ý: Đối với API không phải Chat, vui lòng đảm bảo nhập đúng địa chỉ API, nếu không có thể dẫn đến không sử dụng được", "注意:": "Lưu ý: ", "注意:JSON中重复的键只会保留最后一个同名键的值": "Lưu ý: Trong JSON, các khóa trùng lặp sẽ chỉ giữ lại giá trị của khóa cuối cùng có cùng tên", "注意:修改密码后,所有已登录的设备将被强制登出。": "Lưu ý: Sau khi thay đổi mật khẩu, tất cả các thiết bị đã đăng nhập sẽ bị buộc đăng xuất.", "注意:所有配置更改在保存后立即生效。": "Lưu ý: Tất cả các thay đổi cấu hình có hiệu lực ngay sau khi lưu.", "注意:请确保您的邮箱地址正确,否则您将无法找回密码。": "Lưu ý: Vui lòng đảm bảo địa chỉ email của bạn là chính xác, nếu không bạn sẽ không thể khôi phục mật khẩu của mình.", - "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Lưu ý: Đối với API không phải Chat, vui lòng đảm bảo nhập đúng địa chỉ API, nếu không có thể dẫn đến không sử dụng được", "注销": "Đăng xuất", "注销成功!": "Đăng xuất thành công!", "活跃文件": "Tệp đang hoạt động", @@ -2360,6 +2448,10 @@ "添加新渠道": "Thêm kênh mới", "添加新用户": "Thêm người dùng mới", "添加新通道": "Thêm kênh mới", + "添加时间条件": "", + "添加更多档位": "", + "添加条件": "", + "添加条件组": "", "添加标签": "Thêm thẻ", "添加模型": "Thêm mô hình", "添加模型倍率": "Thêm tỷ lệ mô hình", @@ -2402,8 +2494,8 @@ "渠道ID": "ID kênh", "渠道ID,名称,密钥,API地址": "ID kênh, tên, khóa, Base URL", "渠道亲和性": "Độ ưu tiên kênh", - "渠道亲和性:上游缓存命中": "Ưu ái kênh: Trúng bộ nhớ đệm upstream", "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。": "Ưu ái kênh tái sử dụng kênh thành công lần cuối dựa trên key được trích xuất từ context yêu cầu hoặc JSON body.", + "渠道亲和性:上游缓存命中": "Ưu ái kênh: Trúng bộ nhớ đệm upstream", "渠道优先级": "Ưu tiên kênh", "渠道信息": "Thông tin kênh", "渠道列表": "Danh sách kênh", @@ -2444,6 +2536,7 @@ "源码": "Mã nguồn", "满": "Đầy", "满足任一条件(OR)": "Đáp ứng bất kỳ điều kiện (OR)", + "满足条件时,整单价格乘以 X;如果有多条同时命中,会继续相乘。": "", "演示站点": "Trang web demo", "演示站点模式": "Chế độ trang web demo", "激活": "Kích hoạt", @@ -2513,7 +2606,13 @@ "用于 DALL-E 3 图片生成,请前往": "Đối với việc tạo hình ảnh DALL-E 3, vui lòng truy cập", "用于 UI 显示": "Dùng cho hiển thị UI", "用于API调用的身份验证令牌,请妥善保管": "Mã thông báo xác thực cho các cuộc gọi API, vui lòng giữ an toàn", + "用于切换当前下单和回调校验所使用的环境": "", "用于唯一标识用户的字段路径": "Đường dẫn trường để nhận dạng duy nhất người dùng", + "用于校验 Stripe Webhook 签名,保存后不会回显": "", + "用于校验测试环境的 Waffo Pancake Webhook 签名": "", + "用于校验测试环境的 Waffo 回调签名": "", + "用于校验生产环境的 Waffo Pancake Webhook 签名": "", + "用于校验生产环境的 Waffo 回调签名": "", "用于配置网络代理,支持 socks5 协议": "Được sử dụng để cấu hình proxy mạng, hỗ trợ giao thức socks5", "用于非 OpenAI 格式的 Gemini/Vertex 渠道": "Dành cho các kênh Gemini/Vertex không phải định dạng OpenAI", "用于验证回调 new-api 的 webhook 请求的密钥,敏感信息不显示": "Khóa được sử dụng để xác minh các yêu cầu webhook gọi lại new-api, thông tin nhạy cảm không được hiển thị.", @@ -2540,7 +2639,6 @@ "用户信息": "Thông tin người dùng", "用户信息更新成功!": "Cập nhật thông tin người dùng thành công!", "用户信息缺失": "Thiếu thông tin người dùng", - "用户最大令牌数量": "Số token tối đa mỗi người dùng", "用户分组": "Nhóm người dùng", "用户分组和额度管理": "Quản lý nhóm người dùng và hạn ngạch", "用户分组的联动作用": "How user group affects other features", @@ -2553,6 +2651,7 @@ "用户协议": "Thỏa thuận người dùng", "用户协议已更新": "Thỏa thuận người dùng đã được cập nhật", "用户协议更新失败": "Cập nhật thỏa thuận người dùng thất bại", + "用户单次最少可充值的美元数量": "", "用户可选": "User Selectable", "用户可选分组": "Nhóm người dùng có thể chọn", "用户可选分组的名称和描述(只包含勾选了用户可选的分组)": "Names and descriptions of user-selectable groups (only includes checked groups)", @@ -2570,6 +2669,7 @@ "用户指南": "Hướng dẫn người dùng", "用户控制面板,管理账户": "Bảng điều khiển người dùng để quản lý tài khoản", "用户新建令牌时可选的分组,格式为 JSON 字符串,例如:{\"vip\": \"VIP 用户\", \"test\": \"测试\"},表示用户可以选择 vip 分组和 test 分组": "Nhóm người dùng có thể chọn khi tạo mã thông báo, ở định dạng chuỗi JSON, ví dụ: {\"vip\": \"Người dùng VIP\", \"test\": \"Kiểm tra\"}, cho biết người dùng có thể chọn nhóm vip và nhóm test", + "用户最大令牌数量": "Số token tối đa mỗi người dùng", "用户权限": "Quyền người dùng", "用户每周期最多请求完成次数": "Số lần yêu cầu thành công tối đa của người dùng mỗi chu kỳ", "用户每周期最多请求次数": "Số lần yêu cầu tối đa của người dùng mỗi chu kỳ", @@ -2611,9 +2711,11 @@ "留空则保持原有密钥": "Để trống để giữ khóa hiện tại", "留空则禁用": "Để trống để vô hiệu hóa", "留空则自动使用 服务器地址 + /api/waffo/webhook": "", + "留空则自动使用当前站点的默认充值页地址": "", "留空则自动使用当前站点的默认回调地址": "Để trống để dùng địa chỉ gọi lại mặc định của trang hiện tại", "留空则自动生成": "Để trống để tự động tạo", "留空则默认使用服务器地址,注意不能携带http://或者https://": "Nếu để trống, địa chỉ máy chủ sẽ được sử dụng theo mặc định. Lưu ý rằng không được bao gồm http:// hoặc https://", + "留空时默认使用服务器地址作为回调地址,填写后将覆盖默认值": "", "登 录": "Đăng nhập", "登录": "Đăng nhập", "登录/注册": "Đăng nhập/Đăng ký", @@ -2663,7 +2765,6 @@ "硬件类型": "Hardware Type", "硬件配置": "Hardware Configuration", "确定": "Xác nhận", - "确定?": "Chắc chắn?", "确定删除": "Xác nhận xóa", "确定删除此令牌?": "Bạn có chắc chắn muốn xóa mã thông báo này không?", "确定删除此兑换码?": "Bạn có chắc chắn muốn xóa mã đổi thưởng này không?", @@ -2718,6 +2819,7 @@ "确定要降级此用户吗?": "Bạn có chắc chắn muốn hạ cấp người dùng này không?", "确定重置": "Xác nhận đặt lại", "确定重置模型倍率吗?": "Xác nhận đặt lại tỷ lệ mô hình?", + "确定?": "Chắc chắn?", "确认": "Xác nhận", "确认作废": "Xác nhận vô hiệu", "确认修改": "Xác nhận sửa đổi", @@ -2726,8 +2828,8 @@ "确认切换": "Xác nhận chuyển đổi", "确认删除": "Xác nhận xóa", "确认删除模型": "Confirm Delete Model", - "确认删除该分组?": "Confirm delete this group?", "确认删除该分组的所有规则?": "Delete all rules for this group?", + "确认删除该分组?": "Confirm delete this group?", "确认删除该规则?": "Confirm delete this rule?", "确认前,支付、兑换码、订阅计划和邀请返利功能将保持锁定。": "Payment, redemption codes, subscription plans, and invitation rewards remain locked until confirmation.", "确认取消密码登录": "Xác nhận hủy đăng nhập mật khẩu", @@ -2765,18 +2867,15 @@ "磁盘使用率超过此值时拒绝请求": "Từ chối yêu cầu khi sử dụng đĩa vượt quá giá trị này", "磁盘可用空间小于缓存最大总量设置": "Dung lượng đĩa trống nhỏ hơn cài đặt tổng dung lượng đệm tối đa", "磁盘命中": "Lượt trúng đĩa", - "磁盘缓存最大总量 (MB)": "Tổng dung lượng tối đa bộ nhớ đệm đĩa (MB)", "磁盘缓存占用的最大空间": "Dung lượng tối đa chiếm bởi bộ nhớ đệm đĩa", "磁盘缓存已清理": "Bộ nhớ đệm đĩa đã được dọn", + "磁盘缓存最大总量 (MB)": "Tổng dung lượng tối đa bộ nhớ đệm đĩa (MB)", "磁盘缓存设置(磁盘换内存)": "Cài đặt bộ nhớ đệm đĩa (đổi đĩa/bộ nhớ)", "磁盘缓存阈值 (MB)": "Ngưỡng bộ nhớ đệm đĩa (MB)", "示例": "Ví dụ", "示例:": "Ví dụ: ", "示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。": "Ví dụ: {\"default\": [200, 100], \"vip\": [0, 1000]}.", "社群": "Cộng đồng", - "视频": "Video", - "视频Remix": "Remix video", - "视频无法在当前浏览器中播放,这可能是由于:": "The video cannot be played in this browser, possibly because:", "禁用": "Vô hiệu hóa", "禁用 Passkey": "Vô hiệu hóa Passkey", "禁用 store 透传": "Vô hiệu hóa truyền qua store", @@ -2810,8 +2909,8 @@ "移动请求头": "", "移除": "Remove", "移除 (-:)": "Remove (-:)", - "移除 functionResponse.id 字段": "Xóa trường functionResponse.id", "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "Việc xóa dấu bản quyền One API trước tiên phải được ủy quyền. Việc bảo trì dự án đòi hỏi rất nhiều nỗ lực. Nếu dự án này có ý nghĩa với bạn, vui lòng chủ động ủng hộ dự án này.", + "移除 functionResponse.id 字段": "Xóa trường functionResponse.id", "空": "Trống", "窗口处理": "xử lý cửa sổ", "窗口等待": "chờ cửa sổ", @@ -2846,29 +2945,32 @@ "第 {{line}} 条操作缺少目标路径": "Quy tắc #{{line}} thao tác thiếu đường dẫn đích", "第 {{line}} 条请求头透传格式无效": "Quy tắc #{{line}} định dạng truyền header không hợp lệ", "第 {{line}} 条请求头透传缺少请求头名称": "Quy tắc #{{line}} truyền header thiếu tên header", + "第 {{n}} 档": "", + "第 {{n}} 组": "", "第三方支付配置": "Cấu hình thanh toán bên thứ ba", "第三方登录": "Đăng nhập bên thứ ba", "第三方账户绑定状态(只读)": "Trạng thái liên kết tài khoản bên thứ ba (chỉ đọc)", + "等于": "", "等价金额:": "Số tiền tương đương: ", "等待中": "Đang chờ", "等待获取邮箱信息...": "Đang chờ lấy thông tin email...", "策略": "Chiến lược", "筛选": "Lọc", - "签到最大额度": "Hạn mức đăng nhập tối đa", - "签到最小额度": "Hạn mức đăng nhập tối thiểu", "签到功能允许用户每日签到获取随机额度奖励": "Tính năng đăng nhập cho phép người dùng đăng nhập hàng ngày để nhận phần thưởng hạn mức ngẫu nhiên", "签到失败": "Đăng nhập thất bại", "签到奖励将直接添加到您的账户余额": "Phần thưởng đăng nhập sẽ được thêm trực tiếp vào số dư tài khoản của bạn", "签到奖励的最大额度": "Hạn mức tối đa cho phần thưởng đăng nhập", "签到奖励的最小额度": "Hạn mức tối thiểu cho phần thưởng đăng nhập", "签到成功!获得": "Đăng nhập thành công! Đã nhận", + "签到最大额度": "Hạn mức đăng nhập tối đa", + "签到最小额度": "Hạn mức đăng nhập tối thiểu", "签到设置": "Cài đặt đăng nhập", "简介": "Giới thiệu", "简单来说:同一个令牌分组,不同等级的用户可以享受不同的价格。": "In short: same token group, different user tiers can enjoy different prices.", "简单模式": "Chế độ đơn giản", "简洁": "Đơn giản", - "简洁模式:按 type 全量清理对象,例如 redacted_thinking。": "Chế độ đơn giản: Dọn tất cả đối tượng theo type, VD: redacted_thinking.", "简洁模式仅返回 message;状态码和错误类型将使用系统默认值。": "Chế độ đơn giản chỉ trả về message; mã trạng thái và loại lỗi sẽ sử dụng giá trị mặc định hệ thống.", + "简洁模式:按 type 全量清理对象,例如 redacted_thinking。": "Chế độ đơn giản: Dọn tất cả đối tượng theo type, VD: redacted_thinking.", "管理": "Quản lý", "管理 Ollama 模型的拉取和删除": "Manage Ollama model pulling and deletion", "管理你的 LinuxDO OAuth App": "Quản lý LinuxDO OAuth App của bạn", @@ -2880,9 +2982,11 @@ "管理员区域": "Khu vực quản trị viên", "管理员暂时未设置任何关于内容": "Quản trị viên chưa đặt bất kỳ nội dung Giới thiệu tùy chỉnh nào", "管理员未开启 Creem 充值!": "The administrator has not enabled Creem recharge!", + "管理员未开启 Waffo Pancake 充值!": "", + "管理员未开启 Waffo 充值!": "", "管理员未开启Stripe充值!": "Quản trị viên chưa bật nạp tiền Stripe!", - "管理员未开启在线充值!": "Quản trị viên chưa bật nạp tiền trực tuyến!", "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "Quản trị viên chưa bật chức năng nạp tiền trực tuyến, vui lòng liên hệ quản trị viên để bật hoặc nạp tiền bằng mã đổi thưởng.", + "管理员未开启在线充值!": "Quản trị viên chưa bật nạp tiền trực tuyến!", "管理员未开启在线支付功能,请联系管理员配置。": "Quản trị viên chưa bật thanh toán trực tuyến, vui lòng liên hệ quản trị viên.", "管理员未设置用户可选分组": "Quản trị viên chưa đặt nhóm người dùng có thể chọn", "管理员给用户分配的分组(如 vip)不仅决定用户身份,还会影响后续两个功能:": "The group assigned to a user (e.g., vip) not only determines their identity, but also affects the following two features:", @@ -2945,15 +3049,15 @@ "系统通知": "Thông báo hệ thống", "系统错误": "Lỗi hệ thống", "系统错误,请联系管理员": "Lỗi hệ thống, vui lòng liên hệ quản trị viên", - "约": "Khoảng", - "级别": "Cấp độ", "索引": "Chỉ mục", "紧凑列表": "Danh sách thu gọn", - "纯字符串会直接覆盖整条请求头,或者点击“查看 JSON 示例”按 token 规则处理。": "", - "纯文本": "Văn bản thuần túy", "累计消耗": "Tiêu thụ tích lũy", "累计签到": "Tổng số lần đăng nhập", "累计获得": "Tổng đã nhận", + "约": "Khoảng", + "级别": "Cấp độ", + "纯字符串会直接覆盖整条请求头,或者点击“查看 JSON 示例”按 token 规则处理。": "", + "纯文本": "Văn bản thuần túy", "线路": "Tuyến", "线路描述": "Mô tả tuyến", "组": "Nhóm", @@ -3002,45 +3106,48 @@ "统计次数": "Thống kê số lần", "统计额度": "Thống kê hạn ngạch", "继续": "Tiếp tục", + "缓存 Tokens": "Tokens bộ nhớ đệm", "缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Bộ nhớ đệm {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "Cache {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (ratio: {{ratio}})", - "缓存 Tokens": "Tokens bộ nhớ đệm", "缓存: {{cacheRatio}}": "Bộ nhớ đệm: {{cacheRatio}}", "缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Giá bộ nhớ đệm: {{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (Tỷ lệ bộ nhớ đệm: {{cacheRatio}})", "缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Giá bộ nhớ đệm: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (Tỷ lệ bộ nhớ đệm: {{cacheRatio}})", "缓存倍率": "Tỷ lệ bộ nhớ đệm", "缓存倍率 {{cacheRatio}}": "Cache ratio {{cacheRatio}}", "缓存写": "Ghi bộ nhớ đệm", + "缓存创建": "", + "缓存创建 Tokens": "Tokens tạo bộ nhớ đệm", "缓存创建 {{price}} / 1M tokens": "Tạo bộ nhớ đệm {{price}} / 1M tokens", "缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Tạo bộ nhớ đệm {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "Cache creation {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (ratio: {{ratio}})", - "缓存创建 Tokens": "Tokens tạo bộ nhớ đệm", - "缓存创建: {{cacheCreationRatio}}": "Tạo bộ nhớ đệm: {{cacheCreationRatio}}", + "缓存创建-1小时": "", "缓存创建: 1h {{cacheCreationRatio1h}}": "Tạo bộ nhớ đệm: 1h {{cacheCreationRatio1h}}", "缓存创建: 5m {{cacheCreationRatio5m}}": "Tạo bộ nhớ đệm: 5m {{cacheCreationRatio5m}}", "缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "Cache creation: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}", - "缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo bộ nhớ đệm: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số tạo bộ nhớ đệm {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "缓存创建: {{cacheCreationRatio}}": "Tạo bộ nhớ đệm: {{cacheCreationRatio}}", "缓存创建价格": "Giá tạo bộ nhớ đệm đầu vào", "缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm {{symbol}}{{price}} / 1M tokens", + "缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens": "Cache creation price total: 5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens", "缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "Giá tạo bộ nhớ đệm: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (Tỷ lệ tạo bộ nhớ đệm: {{cacheCreationRatio}})", "缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm: {{symbol}}{{price}} / 1M tokens", - "缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens": "Cache creation price total: 5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens", "缓存创建倍率": "Tỷ lệ tạo bộ nhớ đệm", - "缓存创建倍率 {{cacheCreationRatio}}": "Cache creation ratio {{cacheCreationRatio}}", "缓存创建倍率 1h {{cacheCreationRatio1h}}": "Tỷ lệ tạo bộ nhớ đệm 1h {{cacheCreationRatio1h}}", "缓存创建倍率 5m {{cacheCreationRatio5m}}": "Tỷ lệ tạo bộ nhớ đệm 5m {{cacheCreationRatio5m}}", "缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "Hệ số tạo bộ nhớ đệm 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}", + "缓存创建倍率 {{cacheCreationRatio}}": "Cache creation ratio {{cacheCreationRatio}}", + "缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo bộ nhớ đệm: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số tạo bộ nhớ đệm {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "缓存条目数": "Số mục bộ nhớ đệm", "缓存目录": "Thư mục bộ nhớ đệm", "缓存目录磁盘空间": "Dung lượng đĩa thư mục bộ nhớ đệm", "缓存读": "Đọc bộ nhớ đệm", "缓存读 {{price}} / 1M tokens": "Đọc bộ nhớ đệm {{price}} / 1M tokens", - "缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đọc bộ nhớ đệm: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số bộ nhớ đệm {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "缓存读取": "", "缓存读取价格": "Giá đọc bộ nhớ đệm đầu vào", "缓存读取价格 {{symbol}}{{price}} / 1M tokens": "Giá đọc bộ nhớ đệm {{symbol}}{{price}} / 1M tokens", "缓存读取价格:{{symbol}}{{price}} / 1M tokens": "Giá đọc bộ nhớ đệm: {{symbol}}{{price}} / 1M tokens", "缓存读取价格:{{symbol}}{{total}} / 1M tokens": "Giá đọc bộ nhớ đệm: {{symbol}}{{total}} / 1M tokens", "缓存读取倍率": "", + "缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đọc bộ nhớ đệm: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số bộ nhớ đệm {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào bộ nhớ đệm: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số bộ nhớ đệm {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "编辑": "Chỉnh sửa", "编辑 OAuth 提供商": "Chỉnh sửa nhà cung cấp OAuth", @@ -3157,10 +3264,10 @@ "获取": "Lấy", "获取 Discovery 配置": "Lấy cấu hình Discovery", "获取 Discovery 配置失败:": "Lấy cấu hình Discovery thất bại: ", - "获取 io.net API Key": "Get io.net API Key", "获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确": "Lấy cấu hình OIDC thất bại, vui lòng kiểm tra trạng thái mạng và Well-Known URL có chính xác không", "获取 OIDC 配置成功!": "Lấy cấu hình OIDC thành công!", "获取 Ollama 版本失败": "Failed to get Ollama version", + "获取 io.net API Key": "Get io.net API Key", "获取2FA状态失败": "Lấy trạng thái 2FA thất bại", "获取中...": "Đang lấy...", "获取代码": "Lấy mã", @@ -3202,9 +3309,9 @@ "补全 {{completion}} tokens * 输出倍率 {{completionRatio}}": "Hoàn thành {{completion}} tokens * Tỷ lệ đầu ra {{completionRatio}}", "补全 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}": "Completion {{completion}} tokens / 1M tokens * {{symbol}}{{price}}", "补全价格": "Giá hoàn thành", + "补全价格已锁定": "Giá hoàn thành đã bị khóa", "补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "Giá hoàn thành: {{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (Tỷ lệ hoàn thành: {{completionRatio}})", "补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens": "Giá hoàn thành: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens", - "补全价格已锁定": "Giá hoàn thành đã bị khóa", "补全倍率": "Tỷ lệ hoàn thành", "补全倍率 {{completionRatio}}": "Tỷ lệ hoàn thành {{completionRatio}}", "补全倍率值": "Giá trị tỷ lệ hoàn thành", @@ -3213,6 +3320,10 @@ "补单成功": "Bổ sung đơn hàng thành công", "表单引用错误,请刷新页面重试": "Lỗi tham chiếu biểu mẫu, vui lòng làm mới trang và thử lại", "表格视图": "Chế độ xem bảng", + "表达式/阶梯计费": "", + "表达式编辑": "", + "表达式计费": "", + "表达式错误": "", "被封禁": "Bị cấm", "被禁用": "Bị vô hiệu hóa", "覆盖": "Ghi đè", @@ -3221,6 +3332,7 @@ "覆盖模式:将完全替换现有的所有密钥": "Chế độ ghi đè: sẽ thay thế hoàn toàn tất cả các khóa hiện có", "覆盖模板": "Mẫu ghi đè", "覆盖现有密钥": "Ghi đè khóa hiện có", + "见上方动态计费详情": "", "规则": "Quy tắc", "规则 JSON": "JSON quy tắc", "规则 JSON 格式不正确": "Định dạng JSON quy tắc không chính xác", @@ -3230,6 +3342,9 @@ "规则导航": "Điều hướng quy tắc", "规则描述(可选)": "", "规则未找到,请刷新后重试": "Không tìm thấy quy tắc, vui lòng làm mới và thử lại", + "视频": "Video", + "视频Remix": "Remix video", + "视频无法在当前浏览器中播放,这可能是由于:": "The video cannot be played in this browser, possibly because:", "角色": "Vai trò", "解析响应数据时发生错误": "Đã xảy ra lỗi khi phân tích dữ liệu phản hồi", "解析密钥文件失败: {{msg}}": "Phân tích tệp khóa thất bại: {{msg}}", @@ -3241,6 +3356,9 @@ "触发": "Kích hoạt", "触发关键词": "Từ khóa kích hoạt", "触发词": "Từ kích hoạt", + "警告": "Cảnh báo", + "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "Cảnh báo: Sau khi bật giữ kết nối, nếu kênh bị lỗi sau khi dữ liệu giữ kết nối đã được ghi, hệ thống không thể thử lại. Nếu bắt buộc phải bật, nên đặt khoảng thời gian Ping càng lớn càng tốt", + "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "Cảnh báo: Vô hiệu hóa xác thực hai yếu tố sẽ xóa vĩnh viễn cài đặt xác minh và tất cả mã dự phòng của bạn, thao tác này không thể hoàn tác!", "计价币种": "Pricing Currency", "计算中": "Calculating", "计算成本": "Calculate Cost", @@ -3345,10 +3463,10 @@ "语音输入": "Đầu vào giọng nói", "语音输出": "Đầu ra giọng nói", "说明": "Mô tả", + "说明信息": "Thông tin mô tả", "说明:": "Mô tả: ", "说明:本页测试为非流式请求;若渠道仅支持流式返回,可能出现测试失败,请以实际使用为准。": "Lưu ý: Bài kiểm tra trên trang này sử dụng yêu cầu không streaming. Nếu kênh chỉ hỗ trợ phản hồi streaming, bài kiểm tra có thể thất bại. Vui lòng dựa vào sử dụng thực tế.", "说明:生成结果是可直接粘贴到渠道密钥里的 JSON(包含 access_token / refresh_token / account_id)。": "Lưu ý: Kết quả tạo ra là JSON có thể dán trực tiếp vào khóa kênh (bao gồm access_token / refresh_token / account_id).", - "说明信息": "Thông tin mô tả", "请上传": "Vui lòng tải lên", "请上传图片": "Vui lòng tải lên hình ảnh", "请上传密钥文件": "Vui lòng tải lên tệp khóa", @@ -3392,6 +3510,9 @@ "请填写完整的产品信息": "Vui lòng điền đầy đủ thông tin sản phẩm", "请填写完整的管理员账号信息": "Vui lòng điền đầy đủ thông tin tài khoản quản trị viên", "请填写密钥": "Vui lòng điền khóa", + "请填写当前环境对应的 Product ID": "", + "请填写当前环境对应的 Store ID": "", + "请填写当前环境对应的商户 ID": "", "请填写正确的邮箱地址": "Vui lòng điền địa chỉ email chính xác", "请填写渠道名称和渠道密钥!": "Vui lòng điền tên kênh và khóa kênh!", "请填写邮箱地址": "Vui lòng điền địa chỉ email", @@ -3430,10 +3551,12 @@ "请求成功!": "Yêu cầu thành công!", "请求时长: ${time}s": "Thời gian yêu cầu: ${time}s", "请求时间": "Thời gian yêu cầu", + "请求条件调价": "", "请求模式": "Chế độ yêu cầu", "请求次数": "Số lần yêu cầu", "请求状态": "Trạng thái yêu cầu", "请求结束后多退少补": "Hoàn trả hoặc bổ sung sau khi yêu cầu kết thúc", + "请求规则": "", "请求详情": "Chi tiết yêu cầu", "请求超时": "Yêu cầu hết thời gian", "请求超时,请刷新页面后重新发起 GitHub 登录": "Hết thời gian chờ, vui lòng làm mới trang và đăng nhập GitHub lại", @@ -3469,20 +3592,22 @@ "请输入": "Vui lòng nhập", "请输入 API Key,一行一个,格式:APIKey|Region": "Enter API Key, one per line, format: APIKey|Region", "请输入 API Key,格式:APIKey|Region": "Enter API Key, format: APIKey|Region", - "请输入 Authorization Endpoint": "Vui lòng nhập Authorization Endpoint", "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "Vui lòng nhập AZURE_OPENAI_ENDPOINT, ví dụ: https://docs-test-001.openai.azure.com", + "请输入 Authorization Endpoint": "Vui lòng nhập Authorization Endpoint", "请输入 Client ID": "Vui lòng nhập Client ID", "请输入 Client Secret": "Vui lòng nhập Client Secret", "请输入 ID": "Vui lòng nhập ID", - "请输入 io.net API Key": "Please enter io.net API Key", - "请输入 io.net API Key(敏感信息不显示)": "Please enter io.net API Key (sensitive information not displayed)", "请输入 JSON 格式的 OAuth 凭据,例如:\n{\n \"access_token\": \"...\",\n \"account_id\": \"...\" \n}": "Vui lòng nhập thông tin OAuth dạng JSON, VD:\n{\n \"access_token\": \"...\",\n \"account_id\": \"...\" \n}", "请输入 JSON 格式的密钥内容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}": "Vui lòng nhập nội dung khóa ở định dạng JSON, ví dụ:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}", "请输入 OIDC 的 Well-Known URL": "Vui lòng nhập Well-Known URL của OIDC", + "请输入 Product ID": "", "请输入 Slug": "Vui lòng nhập Slug", + "请输入 Store ID": "", "请输入 Token Endpoint": "Vui lòng nhập Token Endpoint", "请输入 URL": "Vui lòng nhập URL", "请输入 User Info Endpoint": "Vui lòng nhập User Info Endpoint", + "请输入 io.net API Key": "Please enter io.net API Key", + "请输入 io.net API Key(敏感信息不显示)": "Please enter io.net API Key (sensitive information not displayed)", "请输入6位验证码或8位备用码": "Vui lòng nhập mã xác minh 6 chữ số hoặc mã dự phòng 8 chữ số", "请输入API地址": "Vui lòng nhập địa chỉ API", "请输入API地址!": "Vui lòng nhập địa chỉ API!", @@ -3492,9 +3617,9 @@ "请输入Gotify服务器地址": "Vui lòng nhập địa chỉ máy chủ Gotify", "请输入Gotify服务器地址,例如: https://gotify.example.com": "Vui lòng nhập địa chỉ máy chủ Gotify, ví dụ: https://gotify.example.com", "请输入JSON数组,如 [\"model-a\",\"model-b\"]": "Vui lòng nhập mảng JSON, ví dụ [\"model-a\",\"model-b\"]", + "请输入URL链接": "Vui lòng nhập liên kết URL", "请输入Uptime Kuma地址": "Vui lòng nhập địa chỉ Uptime Kuma", "请输入Uptime Kuma服务地址,如:https://status.example.com": "Vui lòng nhập địa chỉ dịch vụ Uptime Kuma, như: https://status.example.com", - "请输入URL链接": "Vui lòng nhập liên kết URL", "请输入Webhook地址": "Vui lòng nhập địa chỉ Webhook", "请输入Webhook地址,例如: https://example.com/webhook": "Vui lòng nhập địa chỉ Webhook, ví dụ: https://example.com/webhook", "请输入个人简介": "Vui lòng nhập tiểu sử", @@ -3519,6 +3644,7 @@ "请输入原密码": "Vui lòng nhập mật khẩu cũ", "请输入原密码!": "Vui lòng nhập mật khẩu cũ!", "请输入名称": "Vui lòng nhập tên", + "请输入商户 ID": "", "请输入回答内容": "Vui lòng nhập nội dung câu trả lời", "请输入回答内容(支持 Markdown/HTML)": "Vui lòng nhập nội dung câu trả lời (hỗ trợ Markdown/HTML)", "请输入图标名称": "Vui lòng nhập tên biểu tượng", @@ -3528,14 +3654,14 @@ "请输入备注(仅管理员可见)": "Vui lòng nhập ghi chú (chỉ quản trị viên mới thấy)", "请输入套餐标题": "Vui lòng nhập tiêu đề gói", "请输入完整的 JSON 格式密钥内容": "Vui lòng nhập nội dung khóa định dạng JSON đầy đủ", - "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Vui lòng nhập URL đầy đủ, ví dụ: https://api.openai.com/v1/chat/completions", "请输入完整的URL链接": "Vui lòng nhập liên kết URL đầy đủ", + "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Vui lòng nhập URL đầy đủ, ví dụ: https://api.openai.com/v1/chat/completions", "请输入容器名称": "Please enter container name", "请输入密码": "Vui lòng nhập mật khẩu", "请输入密钥": "Vui lòng nhập khóa", + "请输入密钥!": "Vui lòng nhập khóa!", "请输入密钥,一行一个": "Vui lòng nhập khóa, mỗi dòng một cái", "请输入密钥,一行一个,格式:AccessKey|SecretAccessKey|Region": "Enter keys one per line, format: AccessKey|SecretAccessKey|Region", - "请输入密钥!": "Vui lòng nhập khóa!", "请输入延长时长": "Please enter extension duration", "请输入总额度": "Vui lòng nhập tổng hạn mức", "请输入您的密码": "Vui lòng nhập mật khẩu của bạn", @@ -3605,9 +3731,9 @@ "请输入转账金额": "Vui lòng nhập số tiền chuyển", "请输入运行时长": "Please enter runtime duration", "请输入邮箱": "Vui lòng nhập email", - "请输入邮箱!": "Vui lòng nhập email của bạn!", "请输入邮箱地址": "Vui lòng nhập địa chỉ email", "请输入邮箱验证码!": "Vui lòng nhập mã xác minh email!", + "请输入邮箱!": "Vui lòng nhập email của bạn!", "请输入部署名称": "Please enter deployment name", "请输入部署名称以完成二次确认": "Enter deployment name to complete secondary confirmation", "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Vui lòng nhập khu vực triển khai, ví dụ: us-central1\nHỗ trợ sử dụng định dạng ánh xạ mô hình\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}", @@ -3677,9 +3803,6 @@ "调用趋势": "Xu hướng cuộc gọi", "调试信息": "Thông tin gỡ lỗi", "谨慎": "Thận trọng", - "警告": "Cảnh báo", - "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "Cảnh báo: Sau khi bật giữ kết nối, nếu kênh bị lỗi sau khi dữ liệu giữ kết nối đã được ghi, hệ thống không thể thử lại. Nếu bắt buộc phải bật, nên đặt khoảng thời gian Ping càng lớn càng tốt", - "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "Cảnh báo: Vô hiệu hóa xác thực hai yếu tố sẽ xóa vĩnh viễn cài đặt xác minh và tất cả mã dự phòng của bạn, thao tác này không thể hoàn tác!", "豆包": "Doubao", "财务": "Tài chính", "账单": "Hóa đơn", @@ -3729,6 +3852,7 @@ "跨分组": "Giữa các nhóm", "跨分组特殊倍率": "Cross-Group Special Ratios", "跨分组重试": "Thử lại giữa các nhóm", + "跨夜范围": "", "路径正则": "Regex đường dẫn", "路径正则(每行一个)": "Regex đường dẫn (mỗi dòng một mục)", "跳转": "Nhảy", @@ -3744,12 +3868,15 @@ "软件版本": "Phiên bản phần mềm", "软错误": "", "输入": "Đầu vào", - "输入 {{price}} / 1M tokens": "Đầu vào {{price}} / 1M tokens", "输入 $/1M tokens": "", "输入 OIDC 的 Authorization Endpoint": "Nhập Authorization Endpoint của OIDC", "输入 OIDC 的 Client ID": "Nhập Client ID của OIDC", "输入 OIDC 的 Token Endpoint": "Nhập Token Endpoint của OIDC", "输入 OIDC 的 Userinfo Endpoint": "Nhập Userinfo Endpoint của OIDC", + "输入 Token": "", + "输入 Token 数": "", + "输入 Token 数量,查看按当前配置的预计费用(不含分组倍率)。": "", + "输入 {{price}} / 1M tokens": "Đầu vào {{price}} / 1M tokens", "输入IP地址后回车,如:8.8.8.8": "Nhập địa chỉ IP và nhấn Enter, ví dụ: 8.8.8.8", "输入JSON对象": "Nhập đối tượng JSON", "输入价格": "Giá đầu vào", @@ -3777,6 +3904,7 @@ "输入补全价格": "Nhập giá hoàn thành", "输入补全倍率": "Nhập tỷ lệ hoàn thành", "输入要添加的邮箱域名": "Nhập tên miền email cần thêm", + "输入计费表达式...": "", "输入认证器应用显示的6位数字验证码": "Nhập mã xác minh 6 chữ số hiển thị trên ứng dụng xác thực", "输入邮箱地址": "Nhập địa chỉ email", "输入金额": "Nhập số tiền", @@ -3785,15 +3913,17 @@ "输入验证码": "Nhập mã xác minh", "输入验证码完成设置": "Nhập mã xác minh để hoàn tất cài đặt", "输出": "Đầu ra", + "输出 Token": "", + "输出 Token 数": "", "输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}": "Đầu ra {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}", - "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số hoàn thành {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", - "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số đầu ra {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "输出价格": "Giá đầu ra", "输出价格 {{symbol}}{{price}} / 1M tokens": "Giá đầu ra {{symbol}}{{price}} / 1M tokens", "输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "Giá đầu ra: {{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (Tỷ lệ hoàn thành: {{completionRatio}})", "输出价格:{{symbol}}{{price}} / 1M tokens": "Giá đầu ra: {{symbol}}{{price}} / 1M tokens", "输出价格:{{symbol}}{{total}} / 1M tokens": "Giá đầu ra: {{symbol}}{{total}} / 1M tokens", "输出倍率 {{completionRatio}}": "Output ratio {{completionRatio}}", + "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số hoàn thành {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số đầu ra {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "边栏设置": "Cài đặt thanh bên", "过期于": "Hết hạn vào", "过期时间": "Thời gian hết hạn", @@ -3815,12 +3945,14 @@ "返回登录": "Quay lại đăng nhập", "返回错误": "", "返回首页": "Quay lại trang chủ", + "这个公式比较复杂,下面的简化表单没法完整还原,请在表达式编辑模式下修改。": "", "这个界面默认按价格填写,保存时会自动换算回后端需要的倍率 JSON。": "Giao diện này mặc định nhập theo giá, khi lưu sẽ tự động quy đổi lại thành JSON tỷ lệ mà backend yêu cầu.", "这些价格都是可选项,不填也可以。": "Tất cả các mức giá này đều là tùy chọn và có thể để trống.", "这将删除超过 10 分钟未使用的临时缓存文件": "Điều này sẽ xóa các tệp cache tạm thời không được sử dụng trong hơn 10 phút", "这是基础金额,实际扣费 = 基础金额 x 系统分组倍率。": "Đây là số tiền cơ bản. Số tiền trừ thực tế = số tiền cơ bản × tỷ lệ nhóm hệ thống.", "这是重复键中的最后一个,其值将被使用": "Đây là khóa cuối cùng trong số các khóa trùng lặp và giá trị của nó sẽ được sử dụng", "这里直接编辑 JSON 对象。适合简单覆盖参数的场景。": "Chỉnh sửa đối tượng JSON trực tiếp tại đây. Phù hợp cho các trường hợp ghi đè tham số đơn giản.", + "这里配置 Waffo 下展示给用户的 Card、Apple Pay、Google Pay 等子支付方式。": "", "进入": "Nhập", "进度": "Tiến độ", "进行中": "Đang tiến hành", @@ -3894,8 +4026,10 @@ "透传请求头": "", "递归": "Đệ quy", "递归策略": "Chiến lược đệ quy", + "逗号分隔的通配符模式。设置后,仅匹配的客户端 User-Agent 可使用此渠道。留空则允许所有客户端。": "", "通义千问": "Qwen", "通用": "Chung", + "通用缓存": "", "通用设置": "Cài đặt chung", "通知": "Thông báo", "通知、价格和隐私相关设置": "Cài đặt liên quan đến thông báo, giá cả và quyền riêng tư", @@ -3991,9 +4125,9 @@ "配置 Turnstile": "Cấu hình Turnstile", "配置 Waffo 充值时可用的支付方式,保存后在充值页面展示给用户。": "", "配置 WeChat Server": "Cấu hình WeChat Server", - "配置:": "Cấu hình:", "配置信息": "Thông tin cấu hình", "配置列表": "Danh sách cấu hình", + "配置各工具的调用价格($/1K次调用)。按次计费模型不额外收取工具费用。": "", "配置名称": "Tên cấu hình", "配置后的效果:": "After configuration:", "配置和消息已全部重置": "Cấu hình và tin nhắn đã được đặt lại hoàn toàn", @@ -4019,6 +4153,7 @@ "配置详情": "Chi tiết cấu hình", "配置说明": "Hướng dẫn cấu hình", "配置邮箱域名白名单": "Cấu hình danh sách trắng tên miền email", + "配置:": "Cấu hình:", "重发": "Gửi lại", "重发验证码": "Gửi lại mã xác minh", "重启部署失败": "Failed to restart deployment", @@ -4103,6 +4238,7 @@ "阅读": "Đọc", "阅读更多": "Đọc thêm", "队列中": "Trong hàng đợi", + "阶梯计费": "", "阶梯计费(未匹配到对应阶梯)": "Thanh toán theo bậc (không tìm thấy bậc phù hợp)", "阶梯计费(表达式解析失败)": "Thanh toán theo bậc (không phân tích được biểu thức)", "附加条件": "Điều kiện bổ sung", @@ -4112,15 +4248,15 @@ "限制周期统一使用上方配置的“限制周期”值。": "Chu kỳ giới hạn sử dụng thống nhất giá trị \"Chu kỳ giới hạn\" được cấu hình ở trên.", "限流": "Giới hạn tốc độ", "限购": "Giới hạn mua", + "随机": "Ngẫu nhiên", + "随机模式": "Chế độ ngẫu nhiên", + "随机种子 (留空为随机)": "Hạt giống ngẫu nhiên (để trống cho ngẫu nhiên)", "隐私政策": "Chính sách bảo mật", "隐私政策已更新": "Chính sách bảo mật đã được cập nhật", "隐私政策更新失败": "Cập nhật chính sách bảo mật thất bại", "隐私设置": "Cài đặt quyền riêng tư", "隐藏操作项": "Ẩn hành động", "隐藏调试": "Ẩn gỡ lỗi", - "随机": "Ngẫu nhiên", - "随机模式": "Chế độ ngẫu nhiên", - "随机种子 (留空为随机)": "Hạt giống ngẫu nhiên (để trống cho ngẫu nhiên)", "零一万物": "01.AI", "需要安全验证": "Yêu cầu xác minh bảo mật", "需要添加的额度(支持负数)": "Hạn ngạch cần thêm (hỗ trợ số âm)", @@ -4142,13 +4278,13 @@ "音频补全价格:{{symbol}}{{price}} / 1M tokens": "Giá hoàn thành âm thanh: {{symbol}}{{price}} / 1M tokens", "音频补全倍率": "", "音频补全倍率(仅部分模型支持该计费)": "Tỷ lệ hoàn thành âm thanh (chỉ được hỗ trợ bởi một số mô hình để tính phí)", - "音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào âm thanh: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số âm thanh {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "音频输入价格": "Giá đầu vào âm thanh", "音频输入价格:{{symbol}}{{price}} / 1M tokens": "Giá đầu vào âm thanh: {{symbol}}{{price}} / 1M tokens", "音频输入倍率": "", "音频输入相关的倍率设置,键为模型名称,值为倍率": "Cài đặt tỷ lệ liên quan đến đầu vào âm thanh, khóa là tên mô hình, giá trị là tỷ lệ", - "音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra âm thanh: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số âm thanh {{audioRatio}} * hệ số hoàn thành âm thanh {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào âm thanh: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số âm thanh {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "音频输出补全相关的倍率设置,键为模型名称,值为倍率": "Cài đặt tỷ lệ liên quan đến hoàn thành đầu ra âm thanh, khóa là tên mô hình, giá trị là tỷ lệ", + "音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra âm thanh: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số âm thanh {{audioRatio}} * hệ số hoàn thành âm thanh {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "页脚": "Chân trang", "页面未找到,请检查您的浏览器地址是否正确": "Không tìm thấy trang, vui lòng kiểm tra xem địa chỉ trình duyệt của bạn có chính xác không", "页面渲染出错,请刷新页面重试": "Đã xảy ra lỗi khi hiển thị trang. Vui lòng tải lại trang và thử lại.", @@ -4166,10 +4302,11 @@ "预览更新": "Xem trước cập nhật", "预览模板": "Xem trước mẫu", "预览请求体": "Xem trước thân yêu cầu", + "预警阈值必须为正数": "Ngưỡng cảnh báo phải là số dương", "预计结束": "Estimated End", "预计结果": "Kết quả dự kiến", + "预计费用": "", "预设模板": "Mẫu đặt trước", - "预警阈值必须为正数": "Ngưỡng cảnh báo phải là số dương", "频率惩罚,减少重复词汇的出现": "Phạt tần suất, giảm sự lặp lại của từ", "频率限制的周期(分钟)": "Chu kỳ giới hạn tần suất (phút)", "颜色": "Màu sắc", @@ -4209,6 +4346,7 @@ "默认 Responses API 版本,为空则使用上方版本": "Phiên bản Responses API mặc định, nếu để trống sẽ sử dụng phiên bản ở trên", "默认 TTL(秒)": "TTL mặc định (giây)", "默认为 5m 缓存创建倍率;1h 缓存创建倍率按固定乘法自动计算(当前为 1.6x)": "Mặc định dùng tỷ lệ tạo bộ nhớ đệm 5m; tỷ lệ tạo bộ nhớ đệm 1h được tự động tính bằng phép nhân cố định (hiện là 1.6x)", + "默认使用 USD 结算": "", "默认使用auto分组": "Default to auto group", "默认使用系统名称": "Mặc định sử dụng tên hệ thống", "默认助手消息": "Xin chào! Tôi có thể giúp gì cho bạn?", @@ -4218,6 +4356,12 @@ "默认折叠侧边栏": "Mặc định thu gọn thanh bên", "默认测试模型": "Mô hình kiểm tra mặc định", "默认用户消息": "Xin chào", - "默认补全倍率": "Tỷ lệ hoàn thành mặc định" + "默认补全倍率": "Tỷ lệ hoàn thành mặc định", + "(共 {{total}} 个)": "", + "(共 {{total}} 个,省略 {{omit}} 个)": "", + ",": ", ", + ",当前无生效订阅,将自动使用钱包": ", hiện không có gói đăng ký hiệu lực, sẽ tự động dùng ví.", + ",时间:": ", thời gian:", + ",点击更新": ", nhấn để cập nhật" } } diff --git a/web/classic/src/i18n/locales/zh-TW.json b/web/classic/src/i18n/locales/zh-TW.json index d6f20609c98..6cbe173df2b 100644 --- a/web/classic/src/i18n/locales/zh-TW.json +++ b/web/classic/src/i18n/locales/zh-TW.json @@ -7,36 +7,14 @@ " 吗?": " 嗎?", " 秒": " 秒", " 秒。": "", - ",": ",", - ",当前无生效订阅,将自动使用钱包": ",當前無生效訂閱,將自動使用錢包", - ",时间:": ",時間:", - ",点击更新": ",點擊更新", - "、": "、", - "(共 {{total}} 个,省略 {{omit}} 个)": "", - "(共 {{total}} 个)": "", + "$/1M tokens": "$/1M tokens", + "$/次": "", "(筛选后显示 {{count}} 条)_other": "(篩選後顯示 {{count}} 條)", "(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(輸入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}", "(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(輸入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音訊輸入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}", "(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}": "(輸入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 快取 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}", "(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}": "(輸入 {{nonImageInput}} tokens + 圖片輸入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}", - "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "[最多請求次數]和[最多請求完成次數]的最大值為2147483647。", - "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[最多請求次數]必須大於等於0,[最多請求完成次數]必須大於等於1。", - "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", - "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", - "{{count}} 项操作_other": "", - "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", - "{{name}} ID": "{{name}} ID", - "{{ratioType}} {{ratio}}": "{{ratioType}} {{ratio}}", - "{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x", - "「用户可选」决定用户创建令牌时能否自主选择该分组。未勾选的分组只能由管理员在后台分配给用户,不会出现在用户的令牌创建页面中。": "「使用者可選」決定使用者建立令牌時能否自主選擇該分組。未勾選的分組只能由管理員在後台分配給使用者,不會出現在使用者的令牌建立頁面中。", "/api/option/": "/api/option/", - "• 视频服务商的跨域限制": "• 影片服務商的跨域限制", - "• 防盗链保护机制": "• 防盜鏈保護機制", - "• 需要特定的请求头或认证": "• 需要特定的請求頭或認證", - "© {{currentYear}}": "© {{currentYear}}", - "| 基于": "| 基於", - "$/1M tokens": "$/1M tokens", - "$/次": "", "0 - 最低": "0 - 最低", "0 表示不限": "0 表示不限", "0.002-1之间的小数": "0.002-1之間的小數", @@ -46,10 +24,10 @@ "1h缓存创建 {{price}} / 1M tokens": "1h快取建立 {{price}} / 1M tokens", "1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "1h快取建立 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "1h快取建立 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})", - "1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h快取建立倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}", "1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "1h快取建立價格 {{symbol}}{{price}} / 1M tokens", "1h缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (1h缓存创建倍率: {{cacheCreationRatio1h}})": "1h快取建立價格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (1h快取建立倍率: {{cacheCreationRatio1h}})", "1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "1h快取建立價格:{{symbol}}{{price}} / 1M tokens", + "1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h快取建立倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}", "2 - 低": "2 - 低", "2. 管理员在用户管理中将用户分配到对应分组(如 vip)": "2. 管理員在使用者管理中將使用者分配到對應分組(如 vip)", "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "2025年5月10日後添加的管道,不需要再在部署的時候移除模型名稱中的\".\"", @@ -60,17 +38,16 @@ "5m缓存创建 {{price}} / 1M tokens": "5m快取建立 {{price}} / 1M tokens", "5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "5m快取建立 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "5m快取建立 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})", - "5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "5m快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m快取建立倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}", "5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "5m快取建立價格 {{symbol}}{{price}} / 1M tokens", "5m缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (5m缓存创建倍率: {{cacheCreationRatio5m}})": "5m快取建立價格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (5m快取建立倍率: {{cacheCreationRatio5m}})", "5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "5m快取建立價格:{{symbol}}{{price}} / 1M tokens", + "5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "5m快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m快取建立倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}", "8 - 高": "8 - 高", "AGPL v3.0协议": "AGPL v3.0協議", "AI 对话": "AI 對話", "AI模型测试环境": "AI模型測試環境", "AI模型配置": "AI模型設定", "AK/SK 模式:使用 AccessKey 和 SecretAccessKey;API Key 模式:使用 API Key": "AK/SK 模式:使用 AccessKey 和 SecretAccessKey;API Key 模式:使用 API Key", - "anthropic-beta JSON 示例": "", "API Key": "API Key", "API Key 模式下不支持批量创建": "API Key 模式下不支援批量建立", "API Key 验证失败": "API Key 驗證失敗", @@ -79,7 +56,12 @@ "API 密钥": "API 密鑰", "API 密钥 (沙盒)": "", "API 密钥 (生产)": "", + "API 密钥(测试环境)": "", + "API 密钥(生产环境)": "", "API 文档": "API 文件", + "API 私钥": "", + "API 私钥(测试环境)": "", + "API 私钥(生产环境)": "", "API 配置": "API 設定", "API令牌管理": "API令牌管理", "API使用记录": "API使用記錄", @@ -88,13 +70,14 @@ "API地址": "API位址", "API渠道配置": "API管道設定", "API端点": "API端點", - "Authorization callback URL 填": "Authorization callback URL 填", "Authorization Endpoint": "Authorization Endpoint", - "auto分组调用链路": "auto分組調用鏈路", + "Authorization callback URL 填": "Authorization callback URL 填", "Bark推送URL": "Bark推送URL", "Bark推送URL必须以http://或https://开头": "Bark推送URL必須以http://或https://開頭", "Bark通知": "Bark通知", "Basic Auth 头": "Basic Auth 頭", + "CPU 使用率超过此值时拒绝请求": "CPU 使用率超過此值時拒絕請求", + "CPU 阈值 (%)": "CPU 閾值 (%)", "Cached tokens": "", "Cached tokens 占比口径由后端返回:Claude 语义按 cached/(prompt+cached),其余按 cached/prompt。": "", "Changing batch type to:": "Changing batch type to:", @@ -109,20 +92,13 @@ "Client ID": "Client ID", "Client Secret": "Client Secret", "Codex 渠道不支持批量创建": "", - "common.changeLanguage": "common.changeLanguage", "Completion tokens": "", "Configuration": "", - "context_int/context_string 从请求上下文读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "", - "CPU 使用率超过此值时拒绝请求": "CPU 使用率超過此值時拒絕請求", - "CPU 阈值 (%)": "CPU 閾值 (%)", "Creem API 密钥,敏感信息不显示": "Creem API 密鑰,敏感資訊不顯示", "Creem Setting Tips": "Creem 只支援預設的固定金額產品,這產品以及價格需要提前在Creem網站內建立設定,所以不支援自訂動態金額儲值。在Creem端設定產品的名字以及價格,獲取Product Id 後填到下面的產品,在new-api為該產品設定儲值額度,以及展示價格。", "Creem 介绍": "Creem 是一個簡單的支付處理平臺,支援固定金額產品銷售,以及訂閱銷售。", "Creem 充值": "Creem 儲值", "Creem 设置": "Creem 設定", - "default 和 vip 只能由管理员在「用户管理」中分配给用户。适用于按用户等级定价、内部测试等不希望用户自主选择的场景。": "default 和 vip 只能由管理員在「使用者管理」中分配給使用者。適用於按使用者等級定價、內部測試等不希望使用者自主選擇的場景。", - "default为默认设置,可单独设置每个分类的安全等级": "default為預設設定,可單獨設定每個分類的安全等級", - "default为默认设置,可单独设置每个模型的版本": "default為預設設定,可單獨設定每個模型的版本", "Dify渠道只适配chatflow和agent,并且agent不支持图片!": "Dify管道只相容chatflow和agent,並且agent不支援圖片!", "Discord": "Discord", "Discord Client ID": "Discord Client ID", @@ -132,10 +108,11 @@ "Discovery scopes": "", "Discovery 建议 scopes:": "", "EUR (欧元)": "EUR (歐元)", - "false": "false", "GC 已执行": "GC 已執行", "GC 执行失败": "GC 執行失敗", "GC 次数": "GC 次數", + "GPU/容器": "GPU/容器", + "GPU数量": "GPU數量", "Gemini安全设置": "Gemini安全設定", "Gemini思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Gemini思考相容 BudgetTokens = MaxTokens * BudgetTokens 百分比", "Gemini思考适配设置": "Gemini思考相容設定", @@ -150,13 +127,9 @@ "Gotify服务器地址": "Gotify伺服器位址", "Gotify服务器地址必须以http://或https://开头": "Gotify伺服器位址必須以http://或https://開頭", "Gotify通知": "Gotify通知", - "GPU/容器": "GPU/容器", - "GPU数量": "GPU數量", "Grok设置": "Grok設定", "Homepage URL 填": "Homepage URL 填", "ID": "ID", - "include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护": "", - "inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息": "inference_geo 字段用於控制 Claude 資料駐留推理區域。預設關閉以避免未經授權透傳地域資訊", "IP": "IP", "IP白名单": "IP白名單", "IP白名单(支持CIDR表达式)": "IP白名單(支援CIDR表達式)", @@ -164,6 +137,7 @@ "IP黑名单": "IP黑名單", "JSON": "JSON", "JSON 已格式化": "", + "JSON 必须是对象": "", "JSON 文本": "", "JSON 无效": "", "JSON 格式参考": "JSON 格式參考", @@ -197,6 +171,7 @@ "OIDC ID": "OIDC ID", "Ollama 模型管理": "Ollama 模型管理", "Ollama 版本信息": "Ollama 版本資訊", + "POST 参数": "", "Passkey": "Passkey", "Passkey 已解绑": "Passkey 已解綁", "Passkey 已重置": "Passkey 已重置", @@ -207,70 +182,72 @@ "Pay Method Name": "", "Pay Method Type": "", "Ping间隔(秒)": "Ping間隔(秒)", - "POST 参数": "", - "price_xxx 的商品价格 ID,新建产品后可获得": "price_xxx 的商品價格 ID,新建產品後可獲得", + "Product ID": "", "Prompt cache hit tokens": "", "Prompt tokens": "", - "Reasoning Effort": "Reasoning Effort", - "Request ID": "", "RSA 私钥 (沙盒)": "", "RSA 私钥 (生产)": "", - "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "safety_identifier 字段用於幫助 OpenAI 識別可能違反使用政策的應用程式使用者。預設關閉以保護使用者隱私", - "Scopes(可选)": "", - "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "service_tier 字段用於指定服務層級,允許透傳可能導致實際計費高於預期。預設關閉以避免額外費用", - "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "sk_xxx 或 rk_xxx 的 Stripe 密鑰,敏感資訊不顯示", + "Reasoning Effort": "Reasoning Effort", + "Request ID": "", "SMTP 加密方式": "SMTP 加密方式", "SMTP 发送者邮箱": "SMTP 發送者信箱", "SMTP 服务器地址": "SMTP 伺服器位址", "SMTP 端口": "SMTP 端口", "SMTP 访问凭证": "SMTP 訪問憑證", "SMTP 账户": "SMTP 帳號", - "speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "speed 字段用於控制 Claude 推理速度模式。預設關閉以避免意外切換到 fast 模式", "SSE 事件": "SSE 事件", "SSE数据流": "SSE數據流", "SSL/TLS": "SSL/TLS", "SSRF防护开关详细说明": "總開關控制是否啟用SSRF防護功能。關閉後將跳過所有SSRF檢查,允許訪問任意URL。⚠️ 僅在完全信任環境中關閉此功能。", "SSRF防护设置": "SSRF防護設定", "SSRF防护详细说明": "SSRF防護可防止惡意使用者利用您的伺服器訪問內網資源。您可以設定受信任域名/IP的白名單,並限制允許的端口。適用於檔案下載、Webhook回調和通知功能。", - "standard 已被移除,vip 用户看不到": "standard 已被移除,vip 使用者看不到", "STARTTLS": "STARTTLS", - "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "store 字段用於授權 OpenAI 存儲請求數據以評估和優化產品。預設關閉,開啟後可能導致 Codex 無法正常使用", + "Scopes(可选)": "", + "Store ID": "", "Stripe 设置": "Stripe 設定", "Stripe/Creem 商品ID(可选)": "Stripe/Creem 商品ID(可選)", "Stripe/Creem 需在第三方平台创建商品并填入 ID": "Stripe/Creem 需在第三方平臺建立商品並填入 ID", + "TTL(秒)": "", + "TTL(秒,0 表示默认)": "", "Telegram": "Telegram", "Telegram Bot Token": "Telegram Bot Token", "Telegram Bot 名称": "Telegram Bot 名稱", "Telegram ID": "Telegram ID", "Token Endpoint": "Token Endpoint", - "token 会按倍率换算成“额度/次数”,请求结束后再做差额结算(补扣/返还)。": "", + "Token 估算器": "", "Total tokens": "", - "true": "true", - "TTL(秒,0 表示默认)": "", - "TTL(秒)": "", "Turnstile Secret Key": "Turnstile Secret Key", "Turnstile Site Key": "Turnstile Site Key", - "Unix时间戳": "Unix時間戳", - "Uptime Kuma地址": "Uptime Kuma位址", - "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Uptime Kuma監控分類管理,可以設定多個監控分類用於服務狀態展示(最多20個)", "URL 标识,只能包含小写字母、数字和连字符": "", "URL链接": "URL連結", "USD (美元)": "USD (美元)", + "Unix时间戳": "Unix時間戳", + "Uptime Kuma地址": "Uptime Kuma位址", + "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Uptime Kuma監控分類管理,可以設定多個監控分類用於服務狀態展示(最多20個)", "User Info Endpoint": "User Info Endpoint", "User-Agent include(每行一个,可不写)": "", + "User-Agent 过滤": "", "Value 正则": "", "Vertex AI 不支持 functionResponse.id 字段,开启后将自动移除该字段": "Vertex AI 不支援 functionResponse.id 字段,開啟後將自動移除該字段", - "Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)": "", "Waffo API 参数,可空(最多64位)": "", + "Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)": "", + "Waffo Pancake 设置": "", "Waffo 充值": "", "Waffo 充值的最低数量,默认 1": "", "Waffo 公钥 (沙盒)": "", "Waffo 公钥 (生产)": "", + "Waffo 公钥(测试环境)": "", + "Waffo 公钥(生产环境)": "", "Waffo 商户 ID": "", + "Waffo 当前使用 USD 结算": "", "Waffo 是一个支付聚合平台,支持多种支付方式。": "", "Waffo 设置": "", - "Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web 搜尋:{{count}} / 1K * 單價 {{price}} * {{ratioType}} {{ratio}} = {{amount}}", + "WeChat Server 服务器地址": "WeChat Server 伺服器位址", + "WeChat Server 访问凭证": "WeChat Server 訪問憑證", "Web 搜索调用 {{webSearchCallCount}} 次": "Web 搜尋呼叫 {{webSearchCallCount}} 次", + "Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web 搜尋:{{count}} / 1K * 單價 {{price}} * {{ratioType}} {{ratio}} = {{amount}}", + "Webhook 公钥(测试环境)": "", + "Webhook 公钥(生产环境)": "", "Webhook 密钥": "Webhook 密鑰", "Webhook 签名密钥": "Webhook 簽名密鑰", "Webhook地址": "Webhook位址", @@ -278,19 +255,55 @@ "Webhook请求结构说明": "Webhook請求結構說明", "Webhook通知": "Webhook通知", "Web搜索价格:{{symbol}}{{price}} / 1K 次": "Web搜尋價格:{{symbol}}{{price}} / 1K 次", - "WeChat Server 服务器地址": "WeChat Server 伺服器位址", - "WeChat Server 访问凭证": "WeChat Server 訪問憑證", "Well-Known URL": "Well-Known URL", "Well-Known URL 必须以 http:// 或 https:// 开头": "Well-Known URL 必須以 http:// 或 https:// 開頭", - "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "whsec_xxx 的 Webhook 簽名密鑰,敏感資訊不顯示", "Worker地址": "Worker位址", "Worker密钥": "Worker密鑰", + "X 也可以小于 1,当折扣用。想做\"只给输出加价\"或\"额外加固定费用\",请直接写完整计费公式。": "", + "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "[最多請求次數]和[最多請求完成次數]的最大值為2147483647。", + "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[最多請求次數]必須大於等於0,[最多請求完成次數]必須大於等於1。", + "anthropic-beta JSON 示例": "", + "auto分组调用链路": "auto分組調用鏈路", + "common.changeLanguage": "common.changeLanguage", + "context_int/context_string 从请求上下文读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "", + "context_int/context_string 从请求上下文读取;header 从 HTTP 请求头读取;gjson 从入口请求的 JSON body 按 gjson path 读取。": "", + "default 和 vip 只能由管理员在「用户管理」中分配给用户。适用于按用户等级定价、内部测试等不希望用户自主选择的场景。": "default 和 vip 只能由管理員在「使用者管理」中分配給使用者。適用於按使用者等級定價、內部測試等不希望使用者自主選擇的場景。", + "default为默认设置,可单独设置每个分类的安全等级": "default為預設設定,可單獨設定每個分類的安全等級", + "default为默认设置,可单独设置每个模型的版本": "default為預設設定,可單獨設定每個模型的版本", + "false": "false", + "include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护": "", + "inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息": "inference_geo 字段用於控制 Claude 資料駐留推理區域。預設關閉以避免未經授權透傳地域資訊", + "price_xxx 的商品价格 ID,新建产品后可获得": "price_xxx 的商品價格 ID,新建產品後可獲得", + "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "safety_identifier 字段用於幫助 OpenAI 識別可能違反使用政策的應用程式使用者。預設關閉以保護使用者隱私", + "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "service_tier 字段用於指定服務層級,允許透傳可能導致實際計費高於預期。預設關閉以避免額外費用", + "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "sk_xxx 或 rk_xxx 的 Stripe 密鑰,敏感資訊不顯示", + "speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "speed 字段用於控制 Claude 推理速度模式。預設關閉以避免意外切換到 fast 模式", + "standard 已被移除,vip 用户看不到": "standard 已被移除,vip 使用者看不到", + "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "store 字段用於授權 OpenAI 存儲請求數據以評估和優化產品。預設關閉,開啟後可能導致 Codex 無法正常使用", + "token 会按倍率换算成“额度/次数”,请求结束后再做差额结算(补扣/返还)。": "", + "true": "true", + "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "whsec_xxx 的 Webhook 簽名密鑰,敏感資訊不顯示", + "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", + "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "{{count}} 项操作_other": "", + "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", + "{{label}} {{price}} / 1M tokens": "", + "{{name}} ID": "{{name}} ID", + "{{ratioType}} {{ratio}}": "{{ratioType}} {{ratio}}", + "{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x", + "| 基于": "| 基於", + "© {{currentYear}}": "© {{currentYear}}", + "• 视频服务商的跨域限制": "• 影片服務商的跨域限制", + "• 防盗链保护机制": "• 防盜鏈保護機制", + "• 需要特定的请求头或认证": "• 需要特定的請求頭或認證", + "、": "、", + "「用户可选」决定用户创建令牌时能否自主选择该分组。未勾选的分组只能由管理员在后台分配给用户,不会出现在用户的令牌创建页面中。": "「使用者可選」決定使用者建立令牌時能否自主選擇該分組。未勾選的分組只能由管理員在後台分配給使用者,不會出現在使用者的令牌建立頁面中。", "一个月": "一個月", "一天": "一天", "一小时": "一小時", "一次调用消耗多少刀,优先级大于模型倍率": "一次調用消耗多少刀,優先級大於模型倍率", - "一行一个,不区分大小写": "一行一個,不區分大小寫", "一行一个屏蔽词,不需要符号分割": "一行一個屏蔽詞,不需要符號分割", + "一行一个,不区分大小写": "一行一個,不區分大小寫", "一键填充到 FluentRead": "一鍵填充到 FluentRead", "三种操作的区别:": "三種操作的區別:", "上一个表单块": "上一個表單塊", @@ -334,10 +347,10 @@ "两次输入的密码不一致": "兩次輸入的密碼不一致", "两次输入的密码不一致!": "兩次輸入的密碼不一致!", "两步验证": "兩步驗證", - "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "兩步驗證(2FA)為您的帳號提供額外的安全保護。啟用後,登錄時需要輸入密碼和驗證器應用生成的驗證碼。", "两步验证启用成功!": "兩步驗證啟用成功!", "两步验证已禁用": "兩步驗證已禁用", "两步验证设置": "兩步驗證設定", + "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "兩步驗證(2FA)為您的帳號提供額外的安全保護。啟用後,登錄時需要輸入密碼和驗證器應用生成的驗證碼。", "个": "個", "个GPU": "個GPU", "个人中心": "個人中心", @@ -372,8 +385,10 @@ "为了保护账户安全,请验证您的身份。": "為了保護帳號安全,請驗證您的身份。", "为什么 default 不勾选「用户可选」?": "為什麼 default 不勾選「使用者可選」?", "为保证匹配准确,请确保客户端直连本站点(避免反向代理/网关改写 User-Agent)。": "", + "为模型前缀覆盖": "", "为特定用户分组配置可用分组的增减规则。「添加」为该分组新增可用分组,「移除」移除默认可用分组,「追加」直接追加分组": "為特定使用者分組配置可用分組的增減規則。「新增」為該分組新增可用分組,「移除」移除預設可用分組,「追加」直接追加分組", "为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https": "為空則預設使用伺服器位址,多個 Origin 用逗號分隔,例如 https://newapi.pro,https://newapi.com ,注意不能攜帶[],需使用https", + "为默认价格": "", "主页链接填": "首頁連結填", "之前的所有日志": "之前的所有日誌", "二步验证已重置": "二步驗證已重置", @@ -423,9 +438,9 @@ "令牌": "令牌", "令牌不存在": "", "令牌分组": "令牌分組", - "令牌分组,默认为用户的分组": "令牌分組,預設為使用者的分組", "令牌分组设为 auto 时,按以下顺序依次尝试选择可用分组,排在前面的优先级更高": "令牌分組設為 auto 時,按以下順序依次嘗試選擇可用分組,排在前面的優先級更高", "令牌分组设为 auto 时,系统按优先级顺序自动选择一个可用分组。": "令牌分組設為 auto 時,系統按優先級順序自動選擇一個可用分組。", + "令牌分组,默认为用户的分组": "令牌分組,預設為使用者的分組", "令牌创建成功,请在列表页面点击复制获取令牌!": "令牌建立成功,請在列表頁面點擊複製獲取令牌!", "令牌名称": "令牌名稱", "令牌已重置并已复制到剪贴板": "令牌已重置並已複製到剪貼板", @@ -438,10 +453,9 @@ "以及": "以及", "仪表盘设置": "儀表盤設定", "价格": "價格", - "价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}": "價格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}", - "价格:${{price}} * {{ratioType}}:{{ratio}}": "價格:${{price}} * {{ratioType}}:{{ratio}}", "价格摘要": "價格摘要", "价格暂时不可用,请稍后重试": "價格暫時不可用,請稍後重試", + "价格根据用量档位和请求条件动态调整": "", "价格模式": "", "价格模式(默认)": "價格模式(預設)", "价格计算中...": "價格計算中...", @@ -451,6 +465,8 @@ "价格设置方式": "價格設定方式", "价格重新计算中...": "價格重新計算中...", "价格预估": "價格預估", + "价格:${{price}} * {{ratioType}}:{{ratio}}": "價格:${{price}} * {{ratioType}}:{{ratio}}", + "价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}": "價格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}", "任一满足(OR)": "", "任务 ID": "任務 ID", "任务ID": "任務ID", @@ -482,7 +498,6 @@ "你理解并自行承担部署、运营和收费行为产生的法律责任。": "你理解并自行承担部署、运营和收费行为产生的法律责任。", "你理解本合规提醒仅用于风险提示,不构成法律意见、合规审查结论或对你使用本系统行为合法性的保证;你应根据实际业务场景自行咨询专业法律或合规顾问。": "你理解本合規提醒僅用於風險提示,不構成法律意見、合規審查結論或對你使用本系統行為合法性的保證;你應根據實際業務場景自行諮詢專業法律或合規顧問。", "你还没有处理{{type}}模型({{count}}个)。是否仅提交当前已勾选内容?_other": "", - "使用 {{name}} 继续": "使用 {{name}} 繼續", "使用 Discord 继续": "使用 Discord 繼續", "使用 GitHub 继续": "使用 GitHub 繼續", "使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}": "使用 JSON 對象格式,格式為:{\"組名\": [最多請求次數, 最多請求完成次數]}", @@ -491,6 +506,7 @@ "使用 Passkey 实现免密且更安全的登录体验": "使用 Passkey 實現免密且更安全的登錄體驗", "使用 Passkey 登录": "使用 Passkey 登錄", "使用 Passkey 验证": "使用 Passkey 驗證", + "使用 {{name}} 继续": "使用 {{name}} 繼續", "使用 微信 继续": "使用 微信 繼續", "使用 用户名 注册": "使用 使用者名 註冊", "使用 邮箱或用户名 登录": "使用 信箱或使用者名 登錄", @@ -500,53 +516,64 @@ "使用日志": "使用日誌", "使用模式": "使用模式", "使用统计": "使用統計", - "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "使用認證器應用(如 Google Authenticator、Microsoft Authenticator)掃描下方QR Code:", "使用认证器应用扫描二维码": "使用認證器應用掃描QR Code", + "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "使用認證器應用(如 Google Authenticator、Microsoft Authenticator)掃描下方QR Code:", "使用说明": "使用說明", "例如": "例如", "例如 /var/cache/new-api": "例如 /var/cache/new-api", - "例如 €, £, Rp, ₩, ₹...": "例如 €, £, Rp, ₩, ₹...", + "例如 0.5 或 2": "", + "例如 anthropic-beta": "", "例如 https://docs.newapi.pro": "例如 https://docs.newapi.pro", "例如 https://example.com/api/waffo/webhook": "", "例如 https://example.com/console/topup": "", - "例如:": "例如:", + "例如 service_tier": "", + "例如 €, £, Rp, ₩, ₹...": "例如 €, £, Rp, ₩, ₹...", "例如: /bin/bash -c \"python app.py\"": "例如: /bin/bash -c \"python app.py\"", "例如: nginx:latest": "例如: nginx:latest", "例如: socks5://user:pass@host:port": "例如: socks5://user:pass@host:port", + "例如发卡网站的购买链接": "例如髮卡網站的購買連結", + "例如(全渠道):": "", + "例如(指定渠道):": "", + "例如:": "例如:", "例如:-c": "例如:-c", "例如:/bin/bash": "例如:/bin/bash", "例如:0001": "例如:0001", "例如:1000": "例如:1000", "例如:100000": "例如:100000", - "例如:2,就是最低充值2$": "例如:2,就是最低儲值2$", "例如:2000": "例如:2000", + "例如:2,就是最低充值2$": "例如:2,就是最低儲值2$", "例如:4.99": "例如:4.99", "例如:401, 403, 429, 500-599": "例如:401,403,429,500-599", "例如:7,就是7元/美金": "例如:7,就是7元/美金", "例如:Credit Card": "", + "例如:GitHub Enterprise": "", + "例如:MER_xxx": "", + "例如:PROD_xxx": "", + "例如:STO_xxx": "", "例如:email": "", "例如:example.com": "例如:example.com", "例如:github / si:google / https://example.com/logo.png / 🐱": "", - "例如:GitHub Enterprise": "", "例如:github-enterprise": "", "例如:gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$": "", "例如:https://example.com/.well-known/openid-configuration": "", + "例如:https://example.com/api/waffo/webhook": "", + "例如:https://example.com/console/topup": "", "例如:https://gitea.example.com": "", "例如:https://yourdomain.com": "例如:https://yourdomain.com", "例如:name、full_name": "", "例如:nginx:latest": "例如:nginx:latest", "例如:preferred_username、login": "", "例如:preview": "例如:preview", + "例如:price_xxx": "", "例如:prod_6I8rBerHpPxyoiU9WK4kot": "例如:prod_6I8rBerHpPxyoiU9WK4kot", + "例如:sk_xxx 或 rk_xxx,留空表示保持当前不变": "", "例如:sub、id、data.user.id": "", + "例如:whsec_xxx,留空表示保持当前不变": "", "例如:基础套餐": "例如:基礎訂閱", "例如:清理工具参数,避免上游校验错误": "", "例如:该请求不满足准入策略": "", "例如:适合轻度使用": "例如:適合輕度使用", "例如:需要等级 {{required}},你当前等级 {{current}}": "", - "例如(全渠道):": "", - "例如(指定渠道):": "", - "例如发卡网站的购买链接": "例如髮卡網站的購買連結", "供应商": "供應商", "供应商介绍": "供應商介紹", "供应商信息:": "供應商資訊:", @@ -571,11 +598,17 @@ "保存 WeChat Server 设置": "儲存 WeChat Server 設定", "保存分组倍率设置": "儲存分組倍率設定", "保存分组相关设置": "保存分組相關設定", + "保存后不会回显,请填写当前环境对应的 API 私钥": "", + "保存后不会回显,请填写当前环境对应的 Stripe API 密钥": "", + "保存后不会回显,请填写测试环境对应的 API 密钥": "", + "保存后不会回显,请填写测试环境对应的 API 私钥": "", + "保存后不会回显,请填写生产环境对应的 API 密钥": "", + "保存后不会回显,请填写生产环境对应的 API 私钥": "", "保存备用码": "儲存備用碼", "保存备用码以备不时之需": "儲存備用碼以備不時之需", "保存失败": "儲存失敗", - "保存失败,请重试": "儲存失敗,請重試", "保存失败:": "儲存失敗:", + "保存失败,请重试": "儲存失敗,請重試", "保存屏蔽词过滤设置": "儲存屏蔽詞過濾設定", "保存性能设置": "儲存性能設定", "保存成功": "儲存成功", @@ -593,11 +626,11 @@ "保存预览": "", "保存额度设置": "儲存額度設定", "保留": "保留", - "保留最近N个文件": "保留最近N個檔案", - "保留最近N天": "保留最近N天", "保留原值(目标已有值时不覆盖)": "", "保留天数": "保留天數", "保留文件数": "保留檔案數", + "保留最近N个文件": "保留最近N個檔案", + "保留最近N天": "保留最近N天", "修复数据库一致性": "修復資料庫一致性", "修改为": "修改為", "修改子渠道优先级": "修改子管道優先級", @@ -619,12 +652,12 @@ "停用": "停用", "允许 AccountFilter 参数": "允許 AccountFilter 參數", "允许 HTTP 协议图片请求(适用于自部署代理)": "允許 HTTP 協議圖片請求(適用於自部署代理)", + "允许 Turnstile 用户校验": "允許 Turnstile 使用者校驗", "允许 inference_geo 透传": "允許 inference_geo 透傳", "允许 safety_identifier 透传": "允許 safety_identifier 透傳", "允许 service_tier 透传": "允許 service_tier 透傳", "允许 speed 透传": "允許 speed 透傳", "允许 stream_options.include_obfuscation 透传": "", - "允许 Turnstile 用户校验": "允許 Turnstile 使用者校驗", "允许不安全的 Origin(HTTP)": "允許不安全的 Origin(HTTP)", "允许回调(会泄露服务器 IP 地址)": "允許回調(會洩露伺服器 IP 位址)", "允许在 Stripe 支付中输入促销码": "允許在 Stripe 支付中輸入促銷碼", @@ -645,13 +678,14 @@ "允许重试": "", "元": "元", "充值": "儲值", - "充值价格(x元/美金)": "儲值價格(x元/美金)", + "充值价格必须大于 0": "", "充值价格显示": "儲值價格顯示", + "充值价格(x元/美金)": "儲值價格(x元/美金)", "充值分组倍率": "儲值分組倍率", "充值分组倍率不是合法的 JSON 字符串": "儲值分組倍率不是合法的 JSON 字符串", "充值数量": "儲值數量", - "充值数量,最低 ": "儲值數量,最低 ", "充值数量不能小于": "儲值數量不能小於", + "充值数量,最低 ": "儲值數量,最低 ", "充值方式设置": "儲值方式設定", "充值方式设置不是合法的 JSON 字符串": "儲值方式設定不是合法的 JSON 字符串", "充值确认": "儲值確認", @@ -667,14 +701,15 @@ "兑换成功!": "兌換成功!", "兑换码充值": "兌換碼儲值", "兑换码创建成功": "兌換碼建立成功", - "兑换码创建成功,是否下载兑换码?": "兌換碼建立成功,是否下載兌換碼?", "兑换码创建成功!": "兌換碼建立成功!", + "兑换码创建成功,是否下载兑换码?": "兌換碼建立成功,是否下載兌換碼?", "兑换码功能已禁用,管理员需先确认合规声明。": "兑换码功能已禁用,管理员需先确认合规声明。", "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "兌換碼將以文本檔案的形式下載,檔案名為兌換碼的名稱。", "兑换码更新成功!": "兌換碼更新成功!", "兑换码生成管理": "兌換碼生成管理", "兑换码管理": "兌換碼管理", "兑换额度": "兌換額度", + "兜底档": "", "全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用": "全域控制側邊欄區域和功能顯示,管理員隱藏的功能使用者無法啟用", "全局设置": "全域設定", "全选": "全選", @@ -714,8 +749,8 @@ "关闭": "關閉", "关闭侧边栏": "關閉側邊欄", "关闭公告": "關閉公告", - "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "關閉後,此模型將不會被「同步官方」自動覆蓋或建立", "关闭后将不再显示此提示(仅对当前浏览器生效)。确定要关闭吗?": "關閉後將不再顯示此提示(僅對當前瀏覽器生效)。確定要關閉嗎?", + "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "關閉後,此模型將不會被「同步官方」自動覆蓋或建立", "关闭弹窗,已停止批量测试": "關閉彈窗,已停止批量測試", "关闭提示": "關閉提示", "其他": "其他", @@ -733,20 +768,6 @@ "内置": "", "内置 Ollama 镜像": "內置 Ollama 鏡像", "再次输入部署名称": "再次輸入部署名稱", - "最低": "最低", - "最低充值数量": "", - "最低充值美元数量": "最低儲值美元數量", - "最低充值美元数量必须大于 0": "最低儲值美元數量必須大於 0", - "最后使用时间": "最後使用時間", - "最后更新": "最後更新", - "最后请求": "最後請求", - "最大GPU数量": "最大GPU數量", - "最大可用": "最大可用", - "最大条目数": "", - "最终抵扣": "", - "最近一次": "", - "最近事件": "最近事件", - "最高优先级": "最高優先級", "写": "寫", "准入策略": "", "准入策略 JSON(可选)": "", @@ -754,6 +775,9 @@ "准备完成初始化": "準備完成初始化", "减少": "減少", "凭证已刷新": "", + "函数": "", + "分时缓存 (Claude)": "", + "分档价格表": "", "分类名称": "分類名稱", "分组": "分組", "分组JSON设置": "分組 JSON 設定", @@ -838,20 +862,20 @@ "刷新缓存统计": "", "刷新缓存统计失败": "", "刷新页面": "重新整理頁面", - "前:": "前:", "前往 io.net API Keys": "前往 io.net API Keys", "前往设置": "前往設定", "前往设置页面": "前往設定頁面", "前缀": "前綴", "前置": "", - "剪贴板中未检测到连接信息": "剪貼簿中未偵測到連線資訊", - "副本数量": "副本數量", + "前:": "前:", "剩余": "剩餘", "剩余备用码:": "剩餘備用碼:", "剩余时间": "剩餘時間", "剩余额度": "剩餘額度", - "剩余额度/总额度": "剩餘額度/總額度", "剩余额度$": "剩餘額度$", + "剩余额度/总额度": "剩餘額度/總額度", + "剪贴板中未检测到连接信息": "剪貼簿中未偵測到連線資訊", + "副本数量": "副本數量", "功能特性": "功能特性", "加入渠道": "加入管道", "加入预填组": "加入預填組", @@ -872,12 +896,14 @@ "加载详情中...": "載入詳情中...", "加载账单失败": "載入帳單失敗", "加载隐私政策内容失败...": "載入隱私政策內容失敗...", + "动态计费": "", "勾选后,该分组会出现在用户创建令牌时的下拉菜单中。未勾选的分组只能由管理员分配,用户自己无法选择。": "勾選後,該分組會出現在使用者建立令牌時的下拉選單中。未勾選的分組只能由管理員分配,使用者自己無法選擇。", "包含": "包含", "包含来自未知或未标明供应商的AI模型,这些模型可能来自小型供应商或开源项目。": "包含來自未知或未標明供應商的AI模型,這些模型可能來自小型供應商或開源項目。", "包括失败请求的次数,0代表不限制": "包括失敗請求的次數,0代表不限制", "匹配值": "", "匹配值(可选)": "", + "匹配内容": "", "匹配方式": "", "匹配类型": "匹配類型", "区域": "區域", @@ -888,6 +914,7 @@ "原价": "原價", "原价,和普通用户一样": "原價,和普通使用者一樣", "原因:": "原因:", + "原始额度": "", "原密码": "原密碼", "原生格式": "原生格式", "原生额度": "原生額度", @@ -924,6 +951,7 @@ "变换": "變換", "变更": "變更", "变焦": "變焦", + "变量": "", "变量值": "變數值", "变量名": "變數名", "只包括请求成功的次数": "只包括請求成功的次數", @@ -946,22 +974,23 @@ "可用端点类型": "可用端點類型", "可用邀请额度": "可用邀請額度", "可留空;留空时会尝试使用 Issuer URL + /.well-known/openid-configuration": "", + "可空": "", "可视化": "視覺化", "可视化倍率设置": "視覺化倍率設定", "可视化编辑": "視覺化編輯", - "可空": "", - "可选,公告的补充说明": "可選,公告的補充說明", - "可选,用于复现结果": "可選,用於復現結果", - "可选:基于用户信息 JSON 做组合条件准入,条件不满足时返回自定义提示": "", - "可选:用于自动生成端点或 Discovery URL": "", "可选。匹配入口请求的 User-Agent;任意一行作为子串匹配(忽略大小写)即命中。": "", "可选。对提取到的亲和 Key 做正则校验;不填表示不校验。": "", "可选。对请求路径进行匹配;不填表示匹配所有路径。": "", "可选值": "可選值", + "可选,公告的补充说明": "可選,公告的補充說明", + "可选,用于复现结果": "可選,用於復現結果", + "可选:基于用户信息 JSON 做组合条件准入,条件不满足时返回自定义提示": "", + "可选:用于自动生成端点或 Discovery URL": "", "合规声明已确认": "合规声明已确认", "合规声明确认成功": "合规声明确认成功", "合计:{{total}}": "合計:{{total}}", "合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "合計:文字部分 {{textTotal}} + 音訊部分 {{audioTotal}} = {{total}}", + "同时满足": "", "同时重置消息": "同時重置消息", "同步": "同步", "同步到渠道": "同步到管道", @@ -977,14 +1006,16 @@ "名称+密钥": "名稱+密鑰", "名称不能为空": "名稱不能為空", "名称匹配类型": "名稱匹配類型", - "后:": "後:", "后端固定": "", "后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。": "後端固定倍率:{{ratio}}。此欄位僅展示換算後的價格。", "后端请求失败": "後端請求失敗", "后缀": "後綴", + "后:": "後:", "向右展开": "向右展開", "向左展开": "向左展開", "否": "否", + "含时间条件": "", + "含请求条件": "", "启动": "啟動", "启动参数 (Args)": "啟動參數 (Args)", "启动命令": "啟動命令", @@ -994,11 +1025,14 @@ "启动部署失败": "啟動部署失敗", "启动配置": "啟動設定", "启用": "啟用", + "启用 Prompt 检查": "啟用 Prompt 檢查", + "启用 Stripe 微信支付": "", + "启用 Stripe 支付宝": "", + "启用 Waffo": "", + "启用 Waffo Pancake": "", "启用 io.net 部署": "啟用 io.net 部署", "启用 io.net 部署开关": "啟用 io.net 部署開關", "启用 io.net 部署时必须填写 API Key": "啟用 io.net 部署時必須填寫 API Key", - "启用 Prompt 检查": "啟用 Prompt 檢查", - "启用 Waffo": "", "启用2FA失败": "啟用2FA失敗", "启用Claude思考适配(-thinking后缀)": "啟用Claude思考相容(-thinking後綴)", "启用FunctionCall思维签名填充": "啟用FunctionCall思維簽名填充", @@ -1034,6 +1068,7 @@ "启用验证": "啟用驗證", "周": "周", "命中判定:usage 中存在 cached tokens(例如 cached_tokens/prompt_cache_hit_tokens)即视为命中。": "", + "命中档位": "", "命中率": "", "命中该亲和规则后,会把此模板合并到渠道参数覆盖中(同名键由模板覆盖)。": "", "和": "和", @@ -1060,15 +1095,15 @@ "图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ": "圖示使用@lobehub/icons庫,如:OpenAI、Claude.Color,支援鏈式參數:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查詢所有可用圖示請 ", "图标文件不能超过 100KB,请压缩后重新上传": "", "图混合": "圖混合", + "图片/音频价格(可选)": "", "图片倍率 {{imageRatio}}": "圖片倍率 {{imageRatio}}", "图片功能在自定义请求体模式下不可用": "圖片功能在自訂請求體模式下不可用", "图片地址": "圖片位址", "图片已添加": "圖片已添加", - "图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "圖片生成:1 次 * 單價 {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "图片生成调用:{{symbol}}{{price}} / 1次": "圖片生成調用:{{symbol}}{{price}} / 1次", + "图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "圖片生成:1 次 * 單價 {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "图片输入 {{price}} / 1M tokens": "圖片輸入 {{price}} / 1M tokens", "图片输入: {{imageRatio}}": "圖片輸入: {{imageRatio}}", - "图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "圖片輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 圖片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "图片输入价格": "圖片輸入價格", "图片输入价格 {{symbol}}{{price}} / 1M tokens": "圖片輸入價格 {{symbol}}{{price}} / 1M tokens", "图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})": "圖片輸入價格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (圖片倍率: {{imageRatio}})", @@ -1077,8 +1112,10 @@ "图片输入倍率": "", "图片输入倍率(仅部分模型支持该计费)": "圖片輸入倍率(僅部分模型支援該計費)", "图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费": "圖片輸入相關的倍率設定,鍵為模型名稱,值為倍率,僅部分模型支援該計費", + "图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "圖片輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 圖片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "图生文": "圖生文", "图生视频": "圖生影片", + "在 Stripe 后台创建价格后获得": "", "在Gotify服务器创建应用后获得的令牌,用于发送通知": "在Gotify伺服器建立應用後獲得的令牌,用於發送通知", "在Gotify服务器的应用管理中创建新应用": "在Gotify伺服器的應用管理中建立新應用", "在找兑换码?": "在找兌換碼?", @@ -1107,12 +1144,12 @@ "填充新模板": "", "填充旧模板": "", "填充模板": "", + "填充模板(全渠道)": "填充模板(全管道)", + "填充模板(指定渠道)": "填充模板(指定管道)", "填充模板:等级+激活": "", "填充模板:等级提示": "", "填充模板:组织或角色": "", "填充模板:组织提示": "", - "填充模板(全渠道)": "填充模板(全管道)", - "填充模板(指定渠道)": "填充模板(指定管道)", "填入": "填入", "填入 CC Switch": "", "填入所有模型": "填入所有模型", @@ -1126,8 +1163,17 @@ "填写 Issuer URL 后自动生成:": "", "填写Gotify服务器的完整URL地址": "填寫Gotify伺服器的完整URL位址", "填写后会自动拼接预设端点": "", + "填写后覆盖当前测试环境 API 密钥,留空表示保持当前不变": "", + "填写后覆盖当前测试环境 Webhook 公钥,留空表示保持当前不变": "", + "填写后覆盖当前测试环境私钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境 API 密钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境 Webhook 公钥,留空表示保持当前不变": "", + "填写后覆盖当前生产环境私钥,留空表示保持当前不变": "", + "填写后覆盖当前私钥,留空表示保持当前不变": "", "填写带https的域名,逗号分隔": "填寫帶https的域名,逗號分隔", "填写服务器地址后自动生成:": "填寫伺服器位址後自動生成:", + "填写测试环境 Waffo 公钥,Base64 或 PEM 内容均可": "", + "填写生产环境 Waffo 公钥,Base64 或 PEM 内容均可": "", "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "填寫使用者協議內容後,使用者註冊時將被要求勾選已閱讀使用者協議", "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "填寫隱私政策內容後,使用者註冊時將被要求勾選已閱讀隱私政策", "填写音频补全价格前,需要先填写音频输入价格。": "填寫音訊補全價格前,需要先填寫音訊輸入價格。", @@ -1170,6 +1216,8 @@ "多密钥管理": "多密鑰管理", "多模型统一接入,只需将基址替换为:": "多模型統一接入,只需將基址替換為:", "多种充值方式,安全便捷": "多種儲值方式,安全便捷", + "大于": "", + "大于等于": "", "大模型接口网关": "大模型接口網關", "天": "天", "天前": "天前", @@ -1186,14 +1234,14 @@ "套餐标题": "訂閱標題", "套餐标题不能为空": "", "套餐的基本信息和定价": "訂閱的基本資訊和定價", - "如:大带宽批量分析图片推荐": "如:大頻寬批量分析圖片推薦", - "如:香港线路": "如:香港線路", "如向中华人民共和国境内公众提供生成式人工智能服务,你将依法履行备案登记、安全评估、内容安全、投诉举报、生成合成内容标识、日志留存、个人信息保护等合规义务;": "如向中华人民共和国境内公众提供生成式人工智能服务,你将依法履行备案登记、安全评估、内容安全、投诉举报、生成合成内容标识、日志留存、个人信息保护等合规义务;", "如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。": "", "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "如果你對接的是上游One API或者New API等轉發項目,請使用OpenAI類型,不要使用此類型,除非你知道你在做什麼。", "如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "如果使用者請求中包含系統提示詞,則使用此設定拼接到使用者的系統提示詞前面", "如果镜像为私有,请填写密码或Token": "如果鏡像為私有,請填寫密碼或Token", "如果镜像为私有,请填写用户名": "如果鏡像為私有,請填寫使用者名", + "如:大带宽批量分析图片推荐": "如:大頻寬批量分析圖片推薦", + "如:香港线路": "如:香港線路", "始终使用浅色主题": "始終使用淺色主題", "始终使用深色主题": "始終使用深色主題", "字段映射": "", @@ -1201,6 +1249,7 @@ "字段路径": "", "字段透传控制": "字段透傳控制", "字段速查": "", + "存在": "", "存在惩罚,鼓励讨论新话题": "存在懲罰,鼓勵討論新話題", "存在重复的分组名称:": "存在重複的分組名稱:", "存在重复的键名:": "存在重複的鍵名:", @@ -1262,7 +1311,6 @@ "密钥": "密鑰", "密钥 JSON 必须包含 access_token": "", "密钥 JSON 必须包含 account_id": "", - "密钥(编辑模式下,保存的密钥不会显示)": "密鑰(編輯模式下,儲存的密鑰不會顯示)", "密钥去重": "密鑰去重", "密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性": "密鑰將以Bearer方式添加到請求頭中,用於驗證webhook請求的合法性", "密钥已删除": "密鑰已刪除", @@ -1280,6 +1328,7 @@ "密钥获取成功": "密鑰獲取成功", "密钥输入方式": "密鑰輸入方式", "密钥预览": "密鑰預覽", + "密钥(编辑模式下,保存的密钥不会显示)": "密鑰(編輯模式下,儲存的密鑰不會顯示)", "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "對於官方管道,new-api已經內置位址,除非是第三方代理站點或者Azure的特殊接入位址,否則不需要填寫", "对免费模型启用预消耗": "對免費模型啟用預消耗", "对域名启用 IP 过滤(推荐开启)": "對域名啟用 IP 過濾(建議開啟)", @@ -1308,6 +1357,8 @@ "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "將清除所有儲存的設定並恢復預設設定,此操作不可撤銷。是否繼續?", "将清除选定时间之前的所有日志": "將清除選定時間之前的所有日誌", "将追加 2 条规则到现有规则列表。": "", + "小于": "", + "小于等于": "", "小时": "小時", "小时费率": "小時費率", "尚未使用": "尚未使用", @@ -1318,9 +1369,11 @@ "展开更多": "展開更多", "展示价格": "展示價格", "嵌套映射:用户分组 → 使用分组 → 倍率": "巢狀對映:使用者分組 → 使用分組 → 倍率", + "工具标识": "", + "工具调用定价": "", "左侧边栏个人设置": "左側邊欄個人設定", - "已为 {{count}} 个模型设置{{type}}_other": "已為 {{count}} 個模型設定{{type}}", "已为 ${count} 个渠道设置标签!": "已為 ${count} 個管道設定標籤!", + "已为 {{count}} 个模型设置{{type}}_other": "已為 {{count}} 個模型設定{{type}}", "已从 Discovery 自动填充配置": "", "已从 Discovery 获取配置,可继续手动修改所有字段。": "", "已作废": "已作廢", @@ -1332,14 +1385,15 @@ "已分配内存": "已分配記憶體", "已切换为Assistant角色": "已切換為Assistant角色", "已切换为System角色": "已切換為System角色", + "已切换到新版前端,正在刷新页面": "已切換到新版前端,正在重新整理頁面", "已切换到新版前端,正在跳转首页": "已切換到新版前端,正在跳轉首頁", "已切换至最优倍率视图,每个模型使用其最低倍率分组": "已切換至最優倍率視圖,每個模型使用其最低倍率分組", "已初始化": "已初始化", "已删除": "", + "已删除 ${data} 个通道!": "已刪除 ${data} 個通道!", "已删除 {{count}} 个令牌!": "已刪除 {{count}} 個令牌!", "已删除 {{count}} 个令牌!_other": "已刪除 {{count}} 個令牌!", "已删除 {{count}} 条失效兑换码_other": "已刪除 {{count}} 條失效兌換碼", - "已删除 ${data} 个通道!": "已刪除 ${data} 個通道!", "已删除所有禁用渠道,共计 ${data} 个": "已刪除所有禁用管道,共計 ${data} 個", "已删除消息及其回复": "已刪除消息及其回覆", "已勾选": "已勾選", @@ -1361,8 +1415,6 @@ "已复制": "已複製", "已复制 ${count} 个模型": "已複製 ${count} 個模型", "已复制 ID 到剪贴板": "已複製 ID 到剪貼板", - "已复制:": "已複製:", - "已复制:{{name}}": "已複製:{{name}}", "已复制全部数据": "已複製全部數據", "已复制到剪切板": "已複製到剪切板", "已复制到剪贴板": "已複製到剪貼板", @@ -1371,6 +1423,8 @@ "已复制模型名称": "已複製模型名稱", "已复制版本号": "已複製版本號", "已复制自动生成的 API Key": "已複製自動生成的 API Key", + "已复制:": "已複製:", + "已复制:{{name}}": "已複製:{{name}}", "已完成": "已完成", "已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型": "已將模型 {{name}} 的價格配置批量套用到 {{count}} 個模型", "已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型_other": "", @@ -1411,9 +1465,9 @@ "已达到购买上限": "已達到購買上限", "已过期": "已過期", "已运行时间": "已運行時間", + "已选择 ${count} 个渠道": "已選擇 ${count} 個管道", "已选择 {{count}} 个模型_other": "已選擇 {{count}} 個模型", "已选择 {{selected}} / {{total}}": "已選擇 {{selected}} / {{total}}", - "已选择 ${count} 个渠道": "已選擇 ${count} 個管道", "已重置为默认配置": "已重置為預設設定", "已销毁": "已銷燬", "币种": "幣種", @@ -1437,16 +1491,21 @@ "延长容器时长将会产生额外费用,请确认您有足够的账户余额。": "延長容器時長將會產生額外費用,請確認您有足夠的帳號餘額。", "延长操作一旦确认无法撤销,费用将立即扣除。": "延長操作一旦確認無法撤銷,費用將立即扣除。", "延长时长": "延長時長", - "延长时长(小时)": "延長時長(小時)", "延长时长不能超过720小时(30天)": "延長時長不能超過720小時(30天)", "延长时长失败": "延長時長失敗", "延长时长至少为1小时": "延長時長至少為1小時", + "延长时长(小时)": "延長時長(小時)", "建立连接时发生错误": "建立連接時發生錯誤", "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "建議在生產環境中使用 MySQL 或 PostgreSQL 資料庫,或確保 SQLite 資料庫檔案已映射到宿主機的持久化存儲。", "开": "開", "开启「默认使用 auto 分组」后,新建令牌和初始令牌都会自动设为 auto。": "開啟「預設使用 auto 分組」後,新建令牌和初始令牌都會自動設為 auto。", "开启之后会清除用户提示词中的": "開啟之後會清除使用者提示詞中的", "开启之后将上游地址替换为服务器地址": "開啟之後將上游位址替換為伺服器位址", + "开启后不限制:必须设置模型倍率": "開啟後不限制:必須設定模型倍率", + "开启后创建令牌默认选择auto分组,初始令牌也将设为auto": "開啟後建立令牌預設選擇 auto 分組,初始令牌也將設為 auto", + "开启后未登录用户无法访问模型广场": "開啟後未登錄使用者無法訪問模型廣場", + "开启后检测到新增模型会自动加入当前渠道模型列表": "", + "开启后由后端定时任务检测该渠道上游模型变化": "", "开启后,using_group 会参与 cache key(不同分组隔离)。": "", "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。": "開啟後,親和到的渠道被停用,或不再適用於目前分組/模型時,仍保留這條親和;關閉時會刪除並重新選擇渠道。", "开启后,仅\"消费\"和\"错误\"日志将记录您的客户端IP地址": "開啟後,僅\"消費\"和\"錯誤\"日誌將記錄您的客戶端IP位址", @@ -1459,11 +1518,6 @@ "开启后,规则名称会参与 cache key(不同规则隔离)。": "", "开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)": "", "开启后,违规请求将额外扣费。": "開啟後,違規請求將額外扣費。", - "开启后不限制:必须设置模型倍率": "開啟後不限制:必須設定模型倍率", - "开启后创建令牌默认选择auto分组,初始令牌也将设为auto": "開啟後建立令牌預設選擇 auto 分組,初始令牌也將設為 auto", - "开启后未登录用户无法访问模型广场": "開啟後未登錄使用者無法訪問模型廣場", - "开启后检测到新增模型会自动加入当前渠道模型列表": "", - "开启后由后端定时任务检测该渠道上游模型变化": "", "开启批量操作": "開啟批量操作", "开始": "開始", "开始同步": "開始同步", @@ -1503,6 +1557,7 @@ "当前没有未设置定价的模型": "目前沒有未設定定價的模型", "当前版本": "當前版本", "当前状态": "當前狀態", + "当前环境共用同一商户 ID": "", "当前缓存大小": "當前快取大小", "当前编辑": "目前編輯", "当前规则不支持写入到该位置": "", @@ -1524,8 +1579,8 @@ "微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "微信掃碼關注公眾號,輸入「驗證碼」獲取驗證碼(三分鐘內有效)", "微信扫码登录": "微信掃碼登錄", "微信账户绑定成功!": "微信帳號綁定成功!", - "必填:请输入服务器地址以自动生成完整端点 URL": "必填:請輸入伺服器位址以自動生成完整端點 URL", "必填。对请求的 model 名称进行匹配,任意一条匹配即命中该规则。": "", + "必填:请输入服务器地址以自动生成完整端点 URL": "必填:請輸入伺服器位址以自動生成完整端點 URL", "必须全部满足(AND)": "", "必须是有效的 JSON 字符串数组,例如:[\"g1\",\"g2\"]": "必須是有效的 JSON 字符串陣列,例如:[\"g1\",\"g2\"]", "忘记密码?": "忘記密碼?", @@ -1547,6 +1602,7 @@ "总收益": "總收益", "总计": "總計", "总额度": "總額度", + "恢复默认": "", "您可以个性化设置侧边栏的要显示功能": "您可以個性化設定側邊欄的要顯示功能", "您可以在上方拉取需要的模型": "您可以在上方拉取需要的模型", "您无权访问此页面,请联系管理员": "您無權訪問此頁面,請聯繫管理員", @@ -1617,24 +1673,25 @@ "拉取进度": "拉取進度", "拒绝提示模板(可选)": "", "拦截原因": "", + "按 1 美元对应的站内价格填写": "", "按K显示单位": "按K顯示單位", "按价格设置": "按價格設定", "按倍率类型筛选": "按倍率類型篩選", "按倍率设置": "按倍率設定", "按次": "按次", "按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "按次计费": "按次計費", "按次:{{symbol}}{{price}}": "按次:{{symbol}}{{price}}", "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}", - "按次计费": "按次計費", "按照如下格式输入:AccessKey|SecretAccessKey|Region": "按照如下格式輸入:AccessKey|SecretAccessKey|Region", "按量计费": "按量計費", "按量计费下需要先填写输入价格,才能保存其它价格项。": "按量計費下需要先填寫輸入價格,才能儲存其它價格項。", "按顺序替换content中的变量占位符": "按順序替換content中的變數佔位符", "换脸": "換臉", - "授权,需在遵守": "授權,需在遵守", "授权失败": "授權失敗", "授权端点": "授權端點", "授权范围 (Scopes)": "授權範圍 (Scopes)", + "授权,需在遵守": "授權,需在遵守", "排序": "排序", "排队中": "排隊中", "接受未设置价格模型": "接受未設定價格模型", @@ -1646,8 +1703,8 @@ "控制输出的随机性和创造性": "控制輸出的隨機性和創造性", "控制顶栏模块显示状态,全局生效": "控制頂欄模組顯示狀態,全域生效", "推荐": "推薦", - "推荐:用户可以选择是否使用指纹等验证": "推薦:使用者可以選擇是否使用指紋等驗證", "推荐使用(用户可选)": "推薦使用(使用者可選)", + "推荐:用户可以选择是否使用指纹等验证": "推薦:使用者可以選擇是否使用指紋等驗證", "描述": "描述", "提交": "提交", "提交时间": "提交時間", @@ -1658,6 +1715,8 @@ "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}", "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 補全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 快取 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 快取建立 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 補全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "提示价格:{{symbol}}{{price}} / 1M tokens": "提示價格:{{symbol}}{{price}} / 1M tokens", + "提示缓存倍率": "提示快取倍率", "提示:如果切换后页面无法正常渲染,请清空浏览器缓存后重试。": "提示:如果切換後頁面無法正常渲染,請清除瀏覽器快取後重試。", "提示:如需备份数据,只需复制上述目录即可": "提示:如需備份數據,只需複製上述目錄即可", "提示:此处配置仅用于控制「模型广场」对用户的展示效果,不会影响模型的实际调用与路由。若需配置真实调用行为,请前往「渠道管理」进行设置。": "提示:此處設定僅用於控制「模型廣場」對使用者的展示效果,不會影響模型的實際調用與路由。若需設定真實調用行為,請前往「管道管理」進行設定。", @@ -1665,8 +1724,6 @@ "提示:该功能为测试版,未来配置结构与功能行为可能发生变更,请勿在生产环境使用。": "提示:該功能為測試版,未來設定結構與功能行為可能發生變更,請勿在生產環境使用。", "提示:语言偏好会同步到您登录的所有设备,并影响API返回的错误消息语言。": "提示:語言偏好會同步到您登錄的所有設備,並影響API返回的錯誤消息語言。", "提示:链接中的{key}将被替换为API密钥,{address}将被替换为服务器地址": "提示:連結中的{key}將被替換為API密鑰,{address}將被替換為伺服器位址", - "提示价格:{{symbol}}{{price}} / 1M tokens": "提示價格:{{symbol}}{{price}} / 1M tokens", - "提示缓存倍率": "提示快取倍率", "搜索供应商": "搜尋供應商", "搜索关键字": "搜尋關鍵字", "搜索失败": "搜尋失敗", @@ -1701,6 +1758,7 @@ "支付方式名称": "付款方式名稱", "支付方式名称不能为空": "", "支付方式类型": "付款方式類型", + "支付方式设置": "", "支付渠道": "支付管道", "支付设置": "支付設定", "支付请求失败": "支付請求失敗", @@ -1750,9 +1808,9 @@ "数据看板默认时间粒度": "數據看板預設時間粒度", "数据管理和日志查看": "數據管理和日誌查看", "文件上传": "檔案上傳", - "文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "檔案搜尋:{{count}} / 1K * 單價 {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "文件搜索价格:{{symbol}}{{price}} / 1K 次": "檔案搜尋價格:{{symbol}}{{price}} / 1K 次", "文件搜索调用 {{fileSearchCallCount}} 次": "檔案搜尋呼叫 {{fileSearchCallCount}} 次", + "文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "檔案搜尋:{{count}} / 1K * 單價 {{price}} * {{ratioType}} {{ratio}} = {{amount}}", "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字補全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字補全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音訊提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音訊補全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 快取 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字補全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", @@ -1782,9 +1840,9 @@ "新建容器部署": "新建容器部署", "新建数量": "新建數量", "新建组": "新建組", + "新格式模板": "新格式模板", "新格式(支持条件判断与json自定义):": "新格式(支援條件判斷與json自訂):", "新格式(规则 + 条件)": "", - "新格式模板": "新格式模板", "新版本": "新版本", "新用户使用邀请码奖励额度": "新使用者使用邀請碼獎勵額度", "新用户初始额度": "新使用者初始額度", @@ -1798,6 +1856,7 @@ "无加密": "無加密", "无效的部署信息": "無效的部署資訊", "无效的重置链接,请重新发起密码重置请求": "無效的重置連結,請重新發起密碼重置請求", + "无条件(兜底档)": "", "无法发起 Passkey 注册": "無法發起 Passkey 註冊", "无法复制到剪贴板,请手动复制": "無法複製到剪貼板,請手動複製", "无法添加图片": "無法添加圖片", @@ -1822,19 +1881,22 @@ "日志类型": "日誌類型", "日志设置": "日誌設定", "日志详情": "日誌詳情", - "旧格式(JSON 对象)": "", - "旧格式(直接覆盖):": "舊格式(直接覆蓋):", "旧格式必须是 JSON 对象": "", "旧格式模板": "舊格式模板", + "旧格式(JSON 对象)": "", + "旧格式(直接覆盖):": "舊格式(直接覆蓋):", "旧版前端即将停止维护": "舊版前端即將停止維護", "旧的备用码已失效,请保存新的备用码": "舊的備用碼已失效,請儲存新的備用碼", "早上好": "早安", + "时区": "", "时间": "時間", "时间信息": "時間資訊", + "时间条件": "", "时间粒度": "時間粒度", "易支付": "", "易支付商户ID": "易支付商戶ID", "易支付商户密钥": "易支付商戶密鑰", + "易支付设置": "", "是": "是", "是否为企业账户": "是否為企業帳號", "是否同时重置对话消息?选择\"是\"将清空所有对话记录并恢复默认示例;选择\"否\"将保留当前对话记录。": "是否同時重置對話消息?選擇\"是\"將清空所有對話記錄並恢復預設示例;選擇\"否\"將保留當前對話記錄。", @@ -1845,17 +1907,18 @@ "是否自动禁用": "是否自動禁用", "是否要求指纹/面容等生物识别": "是否要求指紋/面容等生物識別", "显示倍率": "顯示倍率", - "显示最新20条": "顯示最新20條", "显示名称": "顯示名稱", "显示名称字段": "顯示名稱字段", "显示名称字段(可选)": "", "显示完整内容": "顯示完整內容", "显示操作项": "顯示操作項", "显示更多": "顯示更多", + "显示最新20条": "顯示最新20條", "显示第": "顯示第", "显示设置": "顯示設定", "显示调试": "顯示除錯", "晚上好": "晚安", + "普通按量/按次直接填价格就行;如果价格要跟请求参数或请求头联动,请切到表达式/阶梯计费。": "", "普通环境变量": "普通環境變數", "普通用户": "普通使用者", "普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "普通輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}", @@ -1911,9 +1974,12 @@ "更多": "更多", "更多信息请参考": "更多資訊請參考", "更多参数请参考": "更多參數請參考", + "更多模板...": "", + "更好的价格,更好的稳定性,只需要将模型基址替换为:": "更好的價格,更好的穩定性,只需要將模型基址替換為:", "更新": "更新", "更新 Creem 设置": "更新 Creem 設定", "更新 Stripe 设置": "更新 Stripe 設定", + "更新 Waffo Pancake 设置": "", "更新 Waffo 设置": "", "更新SSRF防护设置": "更新SSRF防護設定", "更新Worker设置": "更新Worker設定", @@ -1929,6 +1995,7 @@ "更新所有已启用通道余额": "更新所有已啟用通道餘額", "更新支付设置": "更新支付設定", "更新时间": "更新時間", + "更新易支付设置": "", "更新服务器地址": "更新伺服器位址", "更新模型信息": "更新模型資訊", "更新渠道信息": "更新管道資訊", @@ -1938,6 +2005,20 @@ "更新配置失败": "更新設定失敗", "更新预填组": "更新預填組", "替换": "", + "最低": "最低", + "最低充值数量": "", + "最低充值美元数量": "最低儲值美元數量", + "最低充值美元数量必须大于 0": "最低儲值美元數量必須大於 0", + "最后使用时间": "最後使用時間", + "最后更新": "最後更新", + "最后请求": "最後請求", + "最大GPU数量": "最大GPU數量", + "最大可用": "最大可用", + "最大条目数": "", + "最终抵扣": "", + "最近一次": "", + "最近事件": "最近事件", + "最高优先级": "最高優先級", "月": "", "有 Reasoning": "有 Reasoning", "有序字符串数组": "有序字串陣列", @@ -1994,8 +2075,8 @@ "本地数据存储": "本地數據存儲", "本地计费": "本地計費", "本月获得": "本月獲得", - "本设备:手机指纹/面容,外接:USB安全密钥": "本設備:手機指紋/面容,外接:USB安全密鑰", "本设备内置": "本設備內置", + "本设备:手机指纹/面容,外接:USB安全密钥": "本設備:手機指紋/面容,外接:USB安全密鑰", "本项目根据": "本項目根據", "机密环境变量": "機密環境變數", "机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。": "機密環境變數將被加密存儲,適用於存儲密碼、API密鑰等敏感資訊。", @@ -2004,13 +2085,15 @@ "权限设置": "權限設定", "条": "條", "条 - 第": "條 - 第", - "条,共": "條,共", + "条件": "", + "条件乘数": "", "条件取反": "", "条件数": "", "条件规则": "", "条件项设置": "", "条日志已清理!": "條日誌已清理!", "条规则": "條規則", + "条,共": "條,共", "来源": "來源", "来源于 IO.NET 部署": "來源於 IO.NET 部署", "来源端点": "", @@ -2045,11 +2128,15 @@ "核采样,控制词汇选择的多样性": "核採樣,控制詞彙選擇的多樣性", "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。": "根據 Anthropic 協定,/v1/messages 的輸入 tokens 僅統計非快取輸入,不包含快取讀取與快取寫入 tokens。", "根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含": "根據模型名稱和匹配規則查找模型元數據,優先級:精確 > 前綴 > 後綴 > 包含", + "格式": "", "格式化": "格式化", "格式化 JSON": "格式化 JSON", "格式正确": "格式正確", "格式示例:": "格式示例:", "格式错误": "格式錯誤", + "档": "", + "档位": "", + "档位名称": "", "检查更新": "檢查更新", "检测全部渠道上游更新": "", "检测到 FluentRead(流畅阅读)": "檢測到 FluentRead(流暢閱讀)", @@ -2071,10 +2158,10 @@ "模型价格 {{symbol}}{{price}} / 次": "模型價格 {{symbol}}{{price}} / 次", "模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "模型價格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", "模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}": "模型價格 {{symbol}}{{price}},{{ratioType}} {{ratio}}", + "模型价格未配置": "模型價格未配置", "模型价格:{{symbol}}{{price}}": "模型價格:{{symbol}}{{price}}", "模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "模型價格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}", "模型价格:{{symbol}}{{price}} / 次": "模型價格:{{symbol}}{{price}} / 次", - "模型价格未配置": "模型價格未配置", "模型倍率": "模型倍率", "模型倍率 {{modelRatio}}": "模型倍率 {{modelRatio}}", "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}": "模型倍率 {{modelRatio}},快取倍率 {{cacheRatio}},輸出倍率 {{completionRatio}},{{ratioType}} {{ratio}}", @@ -2099,8 +2186,8 @@ "模型名称已存在": "模型名稱已存在", "模型固定价格": "模型固定價格", "模型图标": "模型圖示", - "模型定价,需要登录访问": "模型定價,需要登錄訪問", "模型定价设置": "模型定價設定", + "模型定价,需要登录访问": "模型定價,需要登錄訪問", "模型广场": "模型廣場", "模型拉取失败: {{error}}": "模型拉取失敗: {{error}}", "模型排行": "模型排行", @@ -2110,8 +2197,8 @@ "模型更新成功!": "模型更新成功!", "模型未加入列表,可能无法调用": "模型未加入列表,可能無法調用", "模型正则": "", - "模型正则(每行一个)": "", "模型正则不能为空": "", + "模型正则(每行一个)": "", "模型消耗分布": "模型消耗分佈", "模型消耗趋势": "模型消耗趨勢", "模型版本": "模型版本", @@ -2145,8 +2232,8 @@ "正则替换": "", "正在加载可用部署位置...": "正在載入可用部署位置...", "正在加载签到状态...": "正在載入簽到狀態...", - "正在处理,请稍候": "", "正在处理大内容...": "正在處理大內容...", + "正在处理,请稍候": "", "正在批量处理,请稍候": "", "正在批量检测,请稍候": "", "正在提交": "正在提交", @@ -2188,6 +2275,7 @@ "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "此項可選,用於透過自訂API位址來進行 API 調用,末尾不要帶/v1和/", "每个充值单位对应的 USD 金额,默认 1.0": "", "每个分组代表一个价格档位。管理员创建分组后,可以选择哪些档位对用户开放自选。": "每個分組代表一個價格檔位。管理員建立分組後,可以選擇哪些檔位對使用者開放自選。", + "每个档位可设置 0~2 个条件(对 p 和 c),最后一档为兜底档无需条件。": "", "每个用户最多可创建的令牌数量,默认 1000,设置过大可能会影响性能": "", "每周": "", "每天": "", @@ -2221,8 +2309,8 @@ "注册": "註冊", "注册 Passkey": "註冊 Passkey", "注意": "注意", - "注意:JSON中重复的键只会保留最后一个同名键的值": "注意:JSON中重複的鍵只會保留最後一個同名鍵的值", "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "注意非Chat API,請務必填寫正確的API位址,否則可能導致無法使用", + "注意:JSON中重复的键只会保留最后一个同名键的值": "注意:JSON中重複的鍵只會保留最後一個同名鍵的值", "注销": "註銷", "注销成功!": "註銷成功!", "活跃文件": "活躍檔案", @@ -2278,6 +2366,10 @@ "添加密钥环境变量": "添加密鑰環境變數", "添加成功": "添加成功", "添加提供商": "添加提供商", + "添加时间条件": "", + "添加更多档位": "", + "添加条件": "", + "添加条件组": "", "添加模型": "添加模型", "添加模型区域": "添加模型區域", "添加渠道": "添加管道", @@ -2304,8 +2396,8 @@ "渠道 ID": "管道 ID", "渠道ID,名称,密钥,API地址": "管道ID,名稱,密鑰,API位址", "渠道亲和性": "渠道親和性", - "渠道亲和性:上游缓存命中": "", "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。": "", + "渠道亲和性:上游缓存命中": "", "渠道优先级": "管道優先級", "渠道信息": "管道資訊", "渠道创建成功!": "管道建立成功!", @@ -2328,6 +2420,7 @@ "渠道额外设置": "管道額外設定", "源地址": "源位址", "满足任一条件(OR)": "", + "满足条件时,整单价格乘以 X;如果有多条同时命中,会继续相乘。": "", "演示站点": "演示站點", "演示站点模式": "演示站點模式", "点击 + 按钮添加图片URL进行多模态对话": "點擊 + 按鈕添加圖片URL進行多模態對話", @@ -2365,7 +2458,13 @@ "生成音乐": "生成音樂", "生效": "生效", "用于API调用的身份验证令牌,请妥善保管": "用於API調用的身份驗證令牌,請妥善保管", + "用于切换当前下单和回调校验所使用的环境": "", "用于唯一标识用户的字段路径": "", + "用于校验 Stripe Webhook 签名,保存后不会回显": "", + "用于校验测试环境的 Waffo Pancake Webhook 签名": "", + "用于校验测试环境的 Waffo 回调签名": "", + "用于校验生产环境的 Waffo Pancake Webhook 签名": "", + "用于校验生产环境的 Waffo 回调签名": "", "用于配置网络代理,支持 socks5 协议": "用於設定網路代理,支援 socks5 協議", "用于验证回调 new-api 的 webhook 请求的密钥,敏感信息不显示": "用於驗證回調 new-api 的 webhook 請求的密鑰,敏感資訊不顯示", "用以支持基于 WebAuthn 的无密码登录注册": "用以支援基於 WebAuthn 的無密碼登錄註冊", @@ -2381,6 +2480,7 @@ "用户": "使用者", "用户 ID 字段": "使用者 ID 字段", "用户 ID 字段(可选)": "", + "用户ID": "", "用户个人功能": "使用者個人功能", "用户主页,展示系统信息": "使用者首頁,展示系統訊息", "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "使用者優先:如果使用者在請求中指定了系統提示詞,將優先使用使用者的設定", @@ -2388,7 +2488,6 @@ "用户信息更新成功!": "使用者資訊更新成功!", "用户信息端点": "使用者資訊端點", "用户信息缺失": "", - "用户最大令牌数量": "", "用户分组": "使用者分組", "用户分组和额度管理": "使用者分組和額度管理", "用户分组的联动作用": "使用者分組的連動作用", @@ -2398,6 +2497,7 @@ "用户协议": "使用者協議", "用户协议已更新": "使用者協議已更新", "用户协议更新失败": "使用者協議更新失敗", + "用户单次最少可充值的美元数量": "", "用户可选": "使用者可選", "用户可选分组": "使用者可選分組", "用户可选分组的名称和描述(只包含勾选了用户可选的分组)": "使用者可選分組的名稱和描述(只包含勾選了使用者可選的分組)", @@ -2409,6 +2509,7 @@ "用户在充值页面看到的支付方式名称,例如:Credit Card": "", "用户控制面板,管理账户": "使用者控制面板,管理帳號", "用户新建令牌时可选的分组,格式为 JSON 字符串,例如:{\"vip\": \"VIP 用户\", \"test\": \"测试\"},表示用户可以选择 vip 分组和 test 分组": "使用者新建令牌時可選的分組,格式為 JSON 字符串,例如:{\"vip\": \"VIP 使用者\", \"test\": \"測試\"},表示使用者可以選擇 vip 分組和 test 分組", + "用户最大令牌数量": "", "用户每周期最多请求完成次数": "使用者每週期最多請求完成次數", "用户每周期最多请求次数": "使用者每週期最多請求次數", "用户注册时看到的网站名称,比如'我的网站'": "使用者註冊時看到的網站名稱,比如'我的網站'", @@ -2430,8 +2531,10 @@ "留空则使用默认端点;支持 {path, method}": "留空則使用預設端點;支援 {path, method}", "留空则保持原有密钥": "", "留空则自动使用 服务器地址 + /api/waffo/webhook": "", + "留空则自动使用当前站点的默认充值页地址": "", "留空则自动使用当前站点的默认回调地址": "留空則自動使用目前站點的預設回調位址", "留空则默认使用服务器地址,注意不能携带http://或者https://": "留空則預設使用伺服器位址,注意不能攜帶http://或者https://", + "留空时默认使用服务器地址作为回调地址,填写后将覆盖默认值": "", "登 录": "登 錄", "登录": "登錄", "登录成功!": "登錄成功!", @@ -2458,7 +2561,6 @@ "硬件类型": "硬體類型", "硬件配置": "硬體設定", "确定": "確定", - "确定?": "確定?", "确定删除此组?": "確定刪除此組?", "确定导入": "確定導入", "确定是否要修复数据库一致性?": "確定是否要修復資料庫一致性?", @@ -2501,6 +2603,7 @@ "确定要降级此用户吗?": "確定要降級此使用者嗎?", "确定重置": "確定重置", "确定重置模型倍率吗?": "確定重置模型倍率嗎?", + "确定?": "確定?", "确认": "確認", "确认作废": "確認作廢", "确认关闭提示": "確認關閉提示", @@ -2508,8 +2611,8 @@ "确认切换": "確認切換", "确认删除": "確認刪除", "确认删除模型": "確認刪除模型", - "确认删除该分组?": "確認刪除該分組?", "确认删除该分组的所有规则?": "確認刪除該分組的所有規則?", + "确认删除该分组?": "確認刪除該分組?", "确认删除该规则?": "確認刪除該規則?", "确认前,支付、兑换码、订阅计划和邀请返利功能将保持锁定。": "确认前,支付、兑换码、订阅计划和邀请返利功能将保持锁定。", "确认取消密码登录": "確認取消密碼登錄", @@ -2542,16 +2645,13 @@ "磁盘使用率超过此值时拒绝请求": "磁碟使用率超過此值時拒絕請求", "磁盘可用空间小于缓存最大总量设置": "磁碟可用空間小於快取最大總量設定", "磁盘命中": "磁碟命中", - "磁盘缓存最大总量 (MB)": "磁碟快取最大總量 (MB)", "磁盘缓存占用的最大空间": "磁碟快取佔用的最大空間", "磁盘缓存已清理": "磁碟快取已清理", + "磁盘缓存最大总量 (MB)": "磁碟快取最大總量 (MB)", "磁盘缓存设置(磁盘换内存)": "磁碟快取設定(磁碟換記憶體)", "磁盘缓存阈值 (MB)": "磁碟快取閾值 (MB)", "示例": "示例", "示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。": "示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。", - "视频": "影片", - "视频Remix": "影片 Remix", - "视频无法在当前浏览器中播放,这可能是由于:": "影片無法在當前瀏覽器中播放,這可能是由於:", "禁用": "禁用", "禁用 store 透传": "禁用 store 透傳", "禁用2FA失败": "禁用2FA失敗", @@ -2573,8 +2673,8 @@ "移动请求头": "", "移除": "移除", "移除 (-:)": "移除 (-:)", - "移除 functionResponse.id 字段": "移除 functionResponse.id 字段", "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "移除 One API 的版權標識必須首先獲得授權,項目維護需要花費大量精力,如果本項目對你有意義,請主動支援本項目", + "移除 functionResponse.id 字段": "移除 functionResponse.id 字段", "空": "空", "窗口处理": "窗口處理", "窗口等待": "窗口等待", @@ -2602,25 +2702,28 @@ "第 {{line}} 条操作缺少目标路径": "", "第 {{line}} 条请求头透传格式无效": "", "第 {{line}} 条请求头透传缺少请求头名称": "", + "第 {{n}} 档": "", + "第 {{n}} 组": "", "第三方支付配置": "第三方支付設定", "第三方账户绑定状态(只读)": "第三方帳號綁定狀態(唯讀)", + "等于": "", "等价金额:": "等價金額:", "等待中": "等待中", "等待获取邮箱信息...": "等待獲取信箱資訊...", "筛选": "篩選", - "签到最大额度": "簽到最大額度", - "签到最小额度": "簽到最小額度", "签到功能允许用户每日签到获取随机额度奖励": "簽到功能允許使用者每日簽到獲取隨機額度獎勵", "签到失败": "簽到失敗", "签到奖励将直接添加到您的账户余额": "簽到獎勵將直接添加到您的帳號餘額", "签到奖励的最大额度": "簽到獎勵的最大額度", "签到奖励的最小额度": "簽到獎勵的最小額度", "签到成功!获得": "簽到成功!獲得", + "签到最大额度": "簽到最大額度", + "签到最小额度": "簽到最小額度", "签到设置": "簽到設定", "简单来说:同一个令牌分组,不同等级的用户可以享受不同的价格。": "簡單來說:同一個令牌分組,不同等級的使用者可以享受不同的價格。", "简洁": "", - "简洁模式:按 type 全量清理对象,例如 redacted_thinking。": "", "简洁模式仅返回 message;状态码和错误类型将使用系统默认值。": "", + "简洁模式:按 type 全量清理对象,例如 redacted_thinking。": "", "管理": "管理", "管理 Ollama 模型的拉取和删除": "管理 Ollama 模型的拉取和刪除", "管理你的 LinuxDO OAuth App": "管理你的 LinuxDO OAuth App", @@ -2630,9 +2733,11 @@ "管理员区域": "管理員區域", "管理员暂时未设置任何关于内容": "管理員暫時未設定任何關於內容", "管理员未开启 Creem 充值!": "管理員未開啟 Creem 儲值!", + "管理员未开启 Waffo Pancake 充值!": "", + "管理员未开启 Waffo 充值!": "", "管理员未开启Stripe充值!": "管理員未開啟Stripe儲值!", - "管理员未开启在线充值!": "管理員未開啟在線儲值!", "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "管理員未開啟在線儲值功能,請聯繫管理員開啟或使用兌換碼儲值。", + "管理员未开启在线充值!": "管理員未開啟在線儲值!", "管理员未开启在线支付功能,请联系管理员配置。": "管理員未開啟在線支付功能,請聯繫管理員設定。", "管理员未设置用户可选分组": "管理員未設定使用者可選分組", "管理员给用户分配的分组(如 vip)不仅决定用户身份,还会影响后续两个功能:": "管理員給使用者分配的分組(如 vip)不僅決定使用者身份,還會影響後續兩個功能:", @@ -2676,12 +2781,12 @@ "系统管理功能": "系統管理功能", "系统设置": "系統設定", "系统访问令牌": "系統訪問令牌", - "约": "約", "索引": "索引", "紧凑列表": "緊湊列表", - "纯字符串会直接覆盖整条请求头,或者点击“查看 JSON 示例”按 token 规则处理。": "", "累计签到": "累計簽到", "累计获得": "累計獲得", + "约": "約", + "纯字符串会直接覆盖整条请求头,或者点击“查看 JSON 示例”按 token 规则处理。": "", "线路描述": "線路描述", "组列表": "組列表", "组名": "組名", @@ -2711,45 +2816,48 @@ "统计次数": "統計次數", "统计额度": "統計額度", "继续": "繼續", + "缓存 Tokens": "快取 Tokens", "缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "快取 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "快取 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})", - "缓存 Tokens": "快取 Tokens", "缓存: {{cacheRatio}}": "快取: {{cacheRatio}}", "缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "快取價格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (快取倍率: {{cacheRatio}})", "缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "快取價格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (快取倍率: {{cacheRatio}})", "缓存倍率": "快取倍率", "缓存倍率 {{cacheRatio}}": "快取倍率 {{cacheRatio}}", "缓存写": "快取寫", + "缓存创建": "", + "缓存创建 Tokens": "快取建立 Tokens", "缓存创建 {{price}} / 1M tokens": "快取建立 {{price}} / 1M tokens", "缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "快取建立 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}", "缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "快取建立 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})", - "缓存创建 Tokens": "快取建立 Tokens", - "缓存创建: {{cacheCreationRatio}}": "快取建立: {{cacheCreationRatio}}", + "缓存创建-1小时": "", "缓存创建: 1h {{cacheCreationRatio1h}}": "快取建立: 1h {{cacheCreationRatio1h}}", "缓存创建: 5m {{cacheCreationRatio5m}}": "快取建立: 5m {{cacheCreationRatio5m}}", "缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "快取建立: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}", - "缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 快取建立倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "缓存创建: {{cacheCreationRatio}}": "快取建立: {{cacheCreationRatio}}", "缓存创建价格": "快取建立價格", "缓存创建价格 {{symbol}}{{price}} / 1M tokens": "快取建立價格 {{symbol}}{{price}} / 1M tokens", + "缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens": "快取建立價格合計:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens", "缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "快取建立價格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (快取建立倍率: {{cacheCreationRatio}})", "缓存创建价格:{{symbol}}{{price}} / 1M tokens": "快取建立價格:{{symbol}}{{price}} / 1M tokens", - "缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens": "快取建立價格合計:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens", "缓存创建倍率": "快取建立倍率", - "缓存创建倍率 {{cacheCreationRatio}}": "快取建立倍率 {{cacheCreationRatio}}", "缓存创建倍率 1h {{cacheCreationRatio1h}}": "快取建立倍率 1h {{cacheCreationRatio1h}}", "缓存创建倍率 5m {{cacheCreationRatio5m}}": "快取建立倍率 5m {{cacheCreationRatio5m}}", "缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "快取建立倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}", + "缓存创建倍率 {{cacheCreationRatio}}": "快取建立倍率 {{cacheCreationRatio}}", + "缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 快取建立倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "缓存条目数": "", "缓存目录": "快取目錄", "缓存目录磁盘空间": "快取目錄磁碟空間", "缓存读": "快取讀", "缓存读 {{price}} / 1M tokens": "快取讀 {{price}} / 1M tokens", - "缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "快取讀取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 快取倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "缓存读取": "", "缓存读取价格": "快取讀取價格", "缓存读取价格 {{symbol}}{{price}} / 1M tokens": "快取讀取價格 {{symbol}}{{price}} / 1M tokens", "缓存读取价格:{{symbol}}{{price}} / 1M tokens": "快取讀取價格:{{symbol}}{{price}} / 1M tokens", "缓存读取价格:{{symbol}}{{total}} / 1M tokens": "快取讀取價格:{{symbol}}{{total}} / 1M tokens", "缓存读取倍率": "", + "缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "快取讀取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 快取倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "快取輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 快取倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "编辑": "編輯", "编辑 OAuth 提供商": "", @@ -2832,10 +2940,10 @@ "若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置": "若你的 OIDC Provider 支援 Discovery Endpoint,你可以僅填寫 OIDC Well-Known URL,系統會自動獲取 OIDC 設定", "获取 Discovery 配置": "", "获取 Discovery 配置失败:": "", - "获取 io.net API Key": "獲取 io.net API Key", "获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确": "獲取 OIDC 設定失敗,請檢查網路狀況和 Well-Known URL 是否正確", "获取 OIDC 配置成功!": "獲取 OIDC 設定成功!", "获取 Ollama 版本失败": "獲取 Ollama 版本失敗", + "获取 io.net API Key": "獲取 io.net API Key", "获取2FA状态失败": "獲取2FA狀態失敗", "获取令牌密钥失败": "", "获取充值配置失败": "取得儲值設定失敗", @@ -2869,9 +2977,9 @@ "补全 {{completion}} tokens * 输出倍率 {{completionRatio}}": "補全 {{completion}} tokens * 輸出倍率 {{completionRatio}}", "补全 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}": "補全 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}", "补全价格": "補全價格", + "补全价格已锁定": "補全價格已鎖定", "补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "補全價格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (補全倍率: {{completionRatio}})", "补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens": "補全價格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens", - "补全价格已锁定": "補全價格已鎖定", "补全倍率": "補全倍率", "补全倍率 {{completionRatio}}": "補全倍率 {{completionRatio}}", "补全倍率值": "補全倍率值", @@ -2880,10 +2988,15 @@ "补单成功": "補單成功", "表单引用错误,请刷新页面重试": "表單引用錯誤,請刷新頁面重試", "表格视图": "表格視圖", + "表达式/阶梯计费": "", + "表达式编辑": "", + "表达式计费": "", + "表达式错误": "", "覆盖": "覆蓋", "覆盖模式:将完全替换现有的所有密钥": "覆蓋模式:將完全替換現有的所有密鑰", "覆盖模板": "", "覆盖现有密钥": "覆蓋現有密鑰", + "见上方动态计费详情": "", "规则": "規則", "规则 JSON": "", "规则 JSON 格式不正确": "", @@ -2893,6 +3006,9 @@ "规则导航": "", "规则描述(可选)": "", "规则未找到,请刷新后重试": "", + "视频": "影片", + "视频Remix": "影片 Remix", + "视频无法在当前浏览器中播放,这可能是由于:": "影片無法在當前瀏覽器中播放,這可能是由於:", "角色": "角色", "解析响应数据时发生错误": "解析響應數據時發生錯誤", "解析密钥文件失败: {{msg}}": "解析密鑰檔案失敗: {{msg}}", @@ -2901,6 +3017,9 @@ "解绑 Passkey": "解綁 Passkey", "解绑后将无法使用 Passkey 登录,确定要继续吗?": "解綁後將無法使用 Passkey 登錄,確定要繼續嗎?", "解绑成功": "解綁成功", + "警告": "警告", + "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "警告:啟用保活後,如果已經寫入保活數據後管道出錯,系統無法重試,如果必須開啟,推薦設定儘可能大的Ping間隔", + "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "警告:禁用兩步驗證將永久刪除您的驗證設定和所有備用碼,此操作不可撤銷!", "计价币种": "計價幣種", "计算中": "計算中", "计算成本": "計算成本", @@ -2991,10 +3110,10 @@ "语音输入": "語音輸入", "语音输出": "語音輸出", "说明": "說明", + "说明信息": "說明資訊", "说明:": "說明:", "说明:本页测试为非流式请求;若渠道仅支持流式返回,可能出现测试失败,请以实际使用为准。": "說明:本頁測試為非流式請求;若管道僅支援流式返回,可能出現測試失敗,請以實際使用為準。", "说明:生成结果是可直接粘贴到渠道密钥里的 JSON(包含 access_token / refresh_token / account_id)。": "", - "说明信息": "說明資訊", "请上传密钥文件": "請上傳密鑰檔案", "请上传密钥文件!": "請上傳密鑰檔案!", "请为渠道命名": "請為管道命名", @@ -3029,6 +3148,9 @@ "请填写完整的产品信息": "請填寫完整的產品資訊", "请填写完整的管理员账号信息": "請填寫完整的管理員帳號資訊", "请填写密钥": "請填寫密鑰", + "请填写当前环境对应的 Product ID": "", + "请填写当前环境对应的 Store ID": "", + "请填写当前环境对应的商户 ID": "", "请填写渠道名称和渠道密钥!": "請填寫管道名稱和管道密鑰!", "请填写部署地区": "請填寫部署地區", "请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "請妥善保管密鑰資訊,不要洩露給他人。如有安全疑慮,請及時更換密鑰。", @@ -3040,16 +3162,20 @@ "请求体内存缓存": "請求體記憶體快取", "请求体磁盘缓存": "請求體磁碟快取", "请求体超过此大小时使用磁盘缓存": "請求體超過此大小時使用磁碟快取", + "请求参数": "", "请求参数无效": "請求參數無效", "请求发生错误": "請求發生錯誤", "请求发生错误: ": "請求發生錯誤: ", "请求后端接口失败:": "請求後端接口失敗:", "请求失败": "請求失敗", + "请求头": "", "请求头覆盖": "請求頭覆蓋", "请求并计费模型": "請求並計費模型", "请求时长: ${time}s": "請求時長: ${time}s", + "请求条件调价": "", "请求次数": "請求次數", "请求结束后多退少补": "請求結束後多退少補", + "请求规则": "", "请求超时,请刷新页面后重新发起 GitHub 登录": "請求超時,請刷新頁面後重新發起 GitHub 登錄", "请求路径": "請求路徑", "请求转换": "請求轉換", @@ -3075,18 +3201,20 @@ "请至少选择一个渠道": "請至少選擇一個管道", "请输入 API Key,一行一个,格式:APIKey|Region": "請輸入 API Key,一行一個,格式:APIKey|Region", "请输入 API Key,格式:APIKey|Region": "請輸入 API Key,格式:APIKey|Region", - "请输入 Authorization Endpoint": "", "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "請輸入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com", + "请输入 Authorization Endpoint": "", "请输入 Client ID": "", "请输入 Client Secret": "", - "请输入 io.net API Key": "請輸入 io.net API Key", - "请输入 io.net API Key(敏感信息不显示)": "請輸入 io.net API Key(敏感資訊不顯示)", "请输入 JSON 格式的 OAuth 凭据,例如:\n{\n \"access_token\": \"...\",\n \"account_id\": \"...\" \n}": "", "请输入 JSON 格式的密钥内容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}": "請輸入 JSON 格式的密鑰內容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}", "请输入 OIDC 的 Well-Known URL": "請輸入 OIDC 的 Well-Known URL", + "请输入 Product ID": "", "请输入 Slug": "", + "请输入 Store ID": "", "请输入 Token Endpoint": "", "请输入 User Info Endpoint": "", + "请输入 io.net API Key": "請輸入 io.net API Key", + "请输入 io.net API Key(敏感信息不显示)": "請輸入 io.net API Key(敏感資訊不顯示)", "请输入6位验证码或8位备用码": "請輸入6位驗證碼或8位備用碼", "请输入API地址": "請輸入API位址", "请输入API地址!": "請輸入API位址!", @@ -3096,9 +3224,9 @@ "请输入Gotify服务器地址": "請輸入Gotify伺服器位址", "请输入Gotify服务器地址,例如: https://gotify.example.com": "請輸入Gotify伺服器位址,例如: https://gotify.example.com", "请输入JSON数组,如 [\"model-a\",\"model-b\"]": "請輸入JSON陣列,如 [\"model-a\",\"model-b\"]", + "请输入URL链接": "請輸入URL連結", "请输入Uptime Kuma地址": "請輸入Uptime Kuma位址", "请输入Uptime Kuma服务地址,如:https://status.example.com": "請輸入Uptime Kuma服務位址,如:https://status.example.com", - "请输入URL链接": "請輸入URL連結", "请输入Webhook地址": "請輸入Webhook位址", "请输入Webhook地址,例如: https://example.com/webhook": "請輸入Webhook位址,例如: https://example.com/webhook", "请输入以下文字以确认:": "请输入以下文字以确认:", @@ -3117,6 +3245,7 @@ "请输入原密码": "請輸入原密碼", "请输入原密码!": "請輸入原密碼!", "请输入名称": "請輸入名稱", + "请输入商户 ID": "", "请输入回答内容": "請輸入回答內容", "请输入回答内容(支持 Markdown/HTML)": "請輸入回答內容(支援 Markdown/HTML)", "请输入图标名称": "請輸入圖示名稱", @@ -3124,14 +3253,14 @@ "请输入备注(仅管理员可见)": "請輸入備註(僅管理員可見)", "请输入套餐标题": "請輸入訂閱標題", "请输入完整的 JSON 格式密钥内容": "請輸入完整的 JSON 格式密鑰內容", - "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "請輸入完整的URL,例如:https://api.openai.com/v1/chat/completions", "请输入完整的URL链接": "請輸入完整的URL連結", + "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "請輸入完整的URL,例如:https://api.openai.com/v1/chat/completions", "请输入容器名称": "請輸入容器名稱", "请输入密码": "請輸入密碼", "请输入密钥": "請輸入密鑰", + "请输入密钥!": "請輸入密鑰!", "请输入密钥,一行一个": "請輸入密鑰,一行一個", "请输入密钥,一行一个,格式:AccessKey|SecretAccessKey|Region": "請輸入密鑰,一行一個,格式:AccessKey|SecretAccessKey|Region", - "请输入密钥!": "請輸入密鑰!", "请输入延长时长": "請輸入延長時長", "请输入总额度": "請輸入總額度", "请输入您的密码": "請輸入您的密碼", @@ -3185,9 +3314,9 @@ "请输入认证器验证码或备用码": "請輸入認證器驗證碼或備用碼", "请输入说明": "請輸入說明", "请输入运行时长": "請輸入運行時長", - "请输入邮箱!": "請輸入信箱!", "请输入邮箱地址": "請輸入信箱位址", "请输入邮箱验证码!": "請輸入信箱驗證碼!", + "请输入邮箱!": "請輸入信箱!", "请输入部署名称": "請輸入部署名稱", "请输入部署名称以完成二次确认": "請輸入部署名稱以完成二次確認", "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "請輸入部署地區,例如:us-central1\n支援使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}", @@ -3239,9 +3368,6 @@ "调用趋势": "調用趨勢", "调试信息": "除錯訊息", "谨慎": "謹慎", - "警告": "警告", - "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "警告:啟用保活後,如果已經寫入保活數據後管道出錯,系統無法重試,如果必須開啟,推薦設定儘可能大的Ping間隔", - "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "警告:禁用兩步驗證將永久刪除您的驗證設定和所有備用碼,此操作不可撤銷!", "豆包": "豆包", "账单": "帳單", "账户充值": "帳號儲值", @@ -3273,6 +3399,7 @@ "跨分组": "跨分組", "跨分组特殊倍率": "跨分組特殊倍率", "跨分组重试": "跨分組重試", + "跨夜范围": "", "路径正则": "", "路径正则(每行一个)": "", "跳转": "跳轉", @@ -3284,12 +3411,15 @@ "轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能": "輪詢模式必須搭配Redis和記憶體快取功能使用,否則性能將大幅降低,並且無法實現輪詢功能", "软错误": "", "输入": "輸入", - "输入 {{price}} / 1M tokens": "輸入 {{price}} / 1M tokens", "输入 $/1M tokens": "", "输入 OIDC 的 Authorization Endpoint": "輸入 OIDC 的 Authorization Endpoint", "输入 OIDC 的 Client ID": "輸入 OIDC 的 Client ID", "输入 OIDC 的 Token Endpoint": "輸入 OIDC 的 Token Endpoint", "输入 OIDC 的 Userinfo Endpoint": "輸入 OIDC 的 Userinfo Endpoint", + "输入 Token": "", + "输入 Token 数": "", + "输入 Token 数量,查看按当前配置的预计费用(不含分组倍率)。": "", + "输入 {{price}} / 1M tokens": "輸入 {{price}} / 1M tokens", "输入IP地址后回车,如:8.8.8.8": "輸入IP位址後回車,如:8.8.8.8", "输入JSON对象": "輸入JSON對象", "输入价格": "輸入價格", @@ -3316,6 +3446,7 @@ "输入补全价格": "輸入補全價格", "输入补全倍率": "輸入補全倍率", "输入要添加的邮箱域名": "輸入要添加的信箱域名", + "输入计费表达式...": "", "输入认证器应用显示的6位数字验证码": "輸入認證器應用顯示的6位數位驗證碼", "输入邮箱地址": "輸入信箱位址", "输入金额": "", @@ -3324,15 +3455,17 @@ "输入验证码": "輸入驗證碼", "输入验证码完成设置": "輸入驗證碼完成設定", "输出": "輸出", + "输出 Token": "", + "输出 Token 数": "", "输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}": "輸出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}", - "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 補全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", - "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 輸出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "输出价格": "輸出價格", "输出价格 {{symbol}}{{price}} / 1M tokens": "輸出價格 {{symbol}}{{price}} / 1M tokens", "输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "輸出價格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (補全倍率: {{completionRatio}})", "输出价格:{{symbol}}{{price}} / 1M tokens": "輸出價格:{{symbol}}{{price}} / 1M tokens", "输出价格:{{symbol}}{{total}} / 1M tokens": "輸出價格:{{symbol}}{{total}} / 1M tokens", "输出倍率 {{completionRatio}}": "輸出倍率 {{completionRatio}}", + "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 補全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 輸出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "边栏设置": "邊欄設定", "过期于": "過期於", "过期时间": "過期時間", @@ -3348,12 +3481,14 @@ "返回修改": "返回修改", "返回登录": "返回登錄", "返回错误": "", + "这个公式比较复杂,下面的简化表单没法完整还原,请在表达式编辑模式下修改。": "", "这个界面默认按价格填写,保存时会自动换算回后端需要的倍率 JSON。": "這個介面預設按價格填寫,儲存時會自動換算回後端需要的倍率 JSON。", "这些价格都是可选项,不填也可以。": "這些價格都是可選項,不填也可以。", "这将删除超过 10 分钟未使用的临时缓存文件": "這將刪除超過 10 分鐘未使用的臨時快取檔案", "这是基础金额,实际扣费 = 基础金额 x 系统分组倍率。": "這是基礎金額,實際扣費 = 基礎金額 x 系統分組倍率。", "这是重复键中的最后一个,其值将被使用": "這是重複鍵中的最後一個,其值將被使用", "这里直接编辑 JSON 对象。适合简单覆盖参数的场景。": "", + "这里配置 Waffo 下展示给用户的 Card、Apple Pay、Google Pay 等子支付方式。": "", "进度": "進度", "进行中": "進行中", "进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "進行該操作時,可能導致管道訪問錯誤,請僅在資料庫出現問題時使用", @@ -3411,7 +3546,9 @@ "透传请求头": "", "递归": "", "递归策略": "", + "逗号分隔的通配符模式。设置后,仅匹配的客户端 User-Agent 可使用此渠道。留空则允许所有客户端。": "", "通义千问": "通義千問", + "通用缓存": "", "通用设置": "通用設定", "通知": "通知", "通知、价格和隐私相关设置": "通知、價格和隱私相關設定", @@ -3482,7 +3619,7 @@ "配置 Turnstile": "設定 Turnstile", "配置 Waffo 充值时可用的支付方式,保存后在充值页面展示给用户。": "", "配置 WeChat Server": "設定 WeChat Server", - "配置:": "配置:", + "配置各工具的调用价格($/1K次调用)。按次计费模型不额外收取工具费用。": "", "配置后的效果:": "配置後的效果:", "配置和消息已全部重置": "設定和消息已全部重置", "配置套餐的有效时长": "設定訂閱的有效時長", @@ -3501,6 +3638,7 @@ "配置自定义 OAuth 提供商,支持 GitHub Enterprise、GitLab、Gitea、Nextcloud、Keycloak、ORY 等兼容 OAuth 2.0 协议的身份提供商": "設定自訂 OAuth 提供商,支援 GitHub Enterprise、GitLab、Gitea、Nextcloud、Keycloak、ORY 等兼容 OAuth 2.0 協議的身份提供商", "配置说明": "設定說明", "配置邮箱域名白名单": "設定信箱域名白名單", + "配置:": "配置:", "重启部署失败": "重啟部署失敗", "重命名部署": "重命名部署", "重复提交": "重複提交", @@ -3553,6 +3691,7 @@ "镜像配置": "鏡像設定", "问题标题": "問題標題", "队列中": "隊列中", + "阶梯计费": "", "阶梯计费(未匹配到对应阶梯)": "階梯計費(未匹配到對應階梯)", "阶梯计费(表达式解析失败)": "階梯計費(表達式解析失敗)", "附加条件": "", @@ -3562,15 +3701,15 @@ "限制周期统一使用上方配置的“限制周期”值。": "限制週期統一使用上方設定的「限制週期」值。", "限流": "", "限购": "限購", + "随机": "隨機", + "随机模式": "隨機模式", + "随机种子 (留空为随机)": "隨機種子 (留空為隨機)", "隐私政策": "隱私政策", "隐私政策已更新": "隱私政策已更新", "隐私政策更新失败": "隱私政策更新失敗", "隐私设置": "隱私設定", "隐藏操作项": "隱藏操作項", "隐藏调试": "隱藏除錯", - "随机": "隨機", - "随机模式": "隨機模式", - "随机种子 (留空为随机)": "隨機種子 (留空為隨機)", "零一万物": "零一萬物", "需要安全验证": "需要安全驗證", "需要添加的额度(支持负数)": "需要添加的額度(支援負數)", @@ -3592,13 +3731,13 @@ "音频补全价格:{{symbol}}{{price}} / 1M tokens": "音訊補全價格:{{symbol}}{{price}} / 1M tokens", "音频补全倍率": "", "音频补全倍率(仅部分模型支持该计费)": "音訊補全倍率(僅部分模型支援該計費)", - "音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音訊輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音訊倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "音频输入价格": "音訊輸入價格", "音频输入价格:{{symbol}}{{price}} / 1M tokens": "音訊輸入價格:{{symbol}}{{price}} / 1M tokens", "音频输入倍率": "", "音频输入相关的倍率设置,键为模型名称,值为倍率": "音訊輸入相關的倍率設定,鍵為模型名稱,值為倍率", - "音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音訊輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音訊倍率 {{audioRatio}} * 音訊補全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", + "音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音訊輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音訊倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "音频输出补全相关的倍率设置,键为模型名称,值为倍率": "音訊輸出補全相關的倍率設定,鍵為模型名稱,值為倍率", + "音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音訊輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音訊倍率 {{audioRatio}} * 音訊補全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}", "页脚": "頁腳", "页面未找到,请检查您的浏览器地址是否正确": "頁面未找到,請檢查您的瀏覽器位址是否正確", "页面渲染出错,请刷新页面重试": "頁面渲染出錯,請重新整理頁面重試", @@ -3616,10 +3755,11 @@ "预览更新": "預覽更新", "预览模板": "", "预览请求体": "預覽請求體", + "预警阈值必须为正数": "預警閾值必須為正數", "预计结束": "預計結束", "预计结果": "預計結果", + "预计费用": "", "预设模板": "", - "预警阈值必须为正数": "預警閾值必須為正數", "频率惩罚,减少重复词汇的出现": "頻率懲罰,減少重複詞彙的出現", "频率限制的周期(分钟)": "頻率限制的週期(分鐘)", "颜色": "顏色", @@ -3668,6 +3808,7 @@ "默认 Responses API 版本,为空则使用上方版本": "預設 Responses API 版本,為空則使用上方版本", "默认 TTL(秒)": "", "默认为 5m 缓存创建倍率;1h 缓存创建倍率按固定乘法自动计算(当前为 1.6x)": "預設為 5m 快取建立倍率;1h 快取建立倍率按固定乘法自動計算(當前為 1.6x)", + "默认使用 USD 结算": "", "默认使用auto分组": "預設使用 auto 分組", "默认使用系统名称": "預設使用系統名稱", "默认助手消息": "你好!有什麼我可以幫助你的嗎?", @@ -3677,6 +3818,12 @@ "默认折叠侧边栏": "預設摺疊側邊欄", "默认测试模型": "預設測試模型", "默认用户消息": "你好", - "默认补全倍率": "預設補全倍率" + "默认补全倍率": "預設補全倍率", + "(共 {{total}} 个)": "", + "(共 {{total}} 个,省略 {{omit}} 个)": "", + ",": ",", + ",当前无生效订阅,将自动使用钱包": ",當前無生效訂閱,將自動使用錢包", + ",时间:": ",時間:", + ",点击更新": ",點擊更新" } } diff --git a/web/classic/src/pages/Setting/Operation/SettingsGeneral.jsx b/web/classic/src/pages/Setting/Operation/SettingsGeneral.jsx index 80810da068c..0504f6e0102 100644 --- a/web/classic/src/pages/Setting/Operation/SettingsGeneral.jsx +++ b/web/classic/src/pages/Setting/Operation/SettingsGeneral.jsx @@ -52,6 +52,9 @@ export default function GeneralSettings(props) { 'general_setting.custom_currency_exchange_rate': '', QuotaPerUnit: '', RetryTimes: '', + SameChannelRetryTimes: '', + SameChannelRetryBaseDelayMs: '', + SameChannelRetryMaxDelayMs: '', USDExchangeRate: '', DisplayTokenStatEnabled: false, DefaultCollapseSidebar: false, @@ -273,6 +276,36 @@ export default function GeneralSettings(props) { showClear /> + + + + + + + + + @@ -248,16 +278,38 @@ export default function SettingsPaymentGateway(props) { extraText={t('用户单次最少可充值的美元数量')} /> - - - +
+ + + +
diff --git a/web/default/package.json b/web/default/package.json index 67906f26f47..85ba11c64ad 100644 --- a/web/default/package.json +++ b/web/default/package.json @@ -35,6 +35,7 @@ "@tanstack/react-router": "^1.170.17", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.14.5", + "@uiw/react-json-view": "^2.0.0-alpha.42", "@visactor/react-vchart": "^2.1.2", "@visactor/vchart": "^2.1.2", "ai": "^7.0.14", diff --git a/web/default/scripts/add-missing-keys.mjs b/web/default/scripts/add-missing-keys.mjs new file mode 100644 index 00000000000..c62c1e9e47f --- /dev/null +++ b/web/default/scripts/add-missing-keys.mjs @@ -0,0 +1,80 @@ +import fs from 'node:fs/promises' +import path from 'node:path' + +const LOCALES_DIR = path.resolve('src/i18n/locales') + +function stableStringify(obj) { + return JSON.stringify(obj, null, 2) + '\n' +} + +const newKeys = { + en: { + 'Capacity soft-fail keywords': 'Capacity soft-fail keywords', + 'If an upstream error contains any of these keywords (case insensitive) and has a null/unknown error code or a 429/5xx status, treat it as temporary capacity overload: retry/switch channels without auto-disabling, and clear channel affinity.': + 'If an upstream error contains any of these keywords (case insensitive) and has a null/unknown error code or a 429/5xx status, treat it as temporary capacity overload: retry/switch channels without auto-disabling, and clear channel affinity.', + }, + zh: { + 'Capacity soft-fail keywords': '容量软失败关键词', + 'If an upstream error contains any of these keywords (case insensitive) and has a null/unknown error code or a 429/5xx status, treat it as temporary capacity overload: retry/switch channels without auto-disabling, and clear channel affinity.': + '当上游错误包含任一关键词(不区分大小写),且错误码为 null/unknown 或状态码为 429/5xx 时,视为临时容量过载:重试/切换渠道且不自动禁用,并清除渠道亲和。', + }, + fr: { + 'Capacity soft-fail keywords': 'Mots-clés de soft-fail capacité', + 'If an upstream error contains any of these keywords (case insensitive) and has a null/unknown error code or a 429/5xx status, treat it as temporary capacity overload: retry/switch channels without auto-disabling, and clear channel affinity.': + "Si une erreur amont contient l'un de ces mots-clés (insensible à la casse) avec un code null/inconnu ou un statut 429/5xx, la traiter comme une surcharge temporaire : réessayer/changer de canal sans désactivation auto, et effacer l'affinité de canal.", + }, + ja: { + 'Capacity soft-fail keywords': '容量ソフトフェイルキーワード', + 'If an upstream error contains any of these keywords (case insensitive) and has a null/unknown error code or a 429/5xx status, treat it as temporary capacity overload: retry/switch channels without auto-disabling, and clear channel affinity.': + '上流エラーにこれらのキーワード(大文字小文字を区別しない)のいずれかが含まれ、エラーコードが null/unknown またはステータスが 429/5xx の場合、一時的な容量超過として扱い、自動無効化せずに再試行/チャネル切替し、チャネル親和性をクリアします。', + }, + ru: { + 'Capacity soft-fail keywords': 'Ключевые слова soft-fail по ёмкости', + 'If an upstream error contains any of these keywords (case insensitive) and has a null/unknown error code or a 429/5xx status, treat it as temporary capacity overload: retry/switch channels without auto-disabling, and clear channel affinity.': + 'Если ошибка upstream содержит любое из этих ключевых слов (без учёта регистра) и имеет код null/unknown или статус 429/5xx, считать это временной перегрузкой: повторять/переключать канал без авто-отключения и сбрасывать affinity канала.', + }, + vi: { + 'Capacity soft-fail keywords': 'Từ khóa soft-fail dung lượng', + 'If an upstream error contains any of these keywords (case insensitive) and has a null/unknown error code or a 429/5xx status, treat it as temporary capacity overload: retry/switch channels without auto-disabling, and clear channel affinity.': + 'Nếu lỗi upstream chứa bất kỳ từ khóa nào (không phân biệt hoa thường) và có mã null/unknown hoặc trạng thái 429/5xx, coi là quá tải tạm thời: thử lại/chuyển kênh mà không tự tắt, đồng thời xóa channel affinity.', + }, +} + +async function main() { + let totalAdded = 0 + + for (const [locale, trans] of Object.entries(newKeys)) { + const filePath = path.join(LOCALES_DIR, `${locale}.json`) + const json = JSON.parse(await fs.readFile(filePath, 'utf8')) + + let count = 0 + for (const [key, value] of Object.entries(trans)) { + if (!Object.prototype.hasOwnProperty.call(json.translation, key)) { + json.translation[key] = value + count++ + } else if (json.translation[key] !== value) { + json.translation[key] = value + count++ + } + } + + const sorted = Object.keys(json.translation) + .sort((a, b) => a.localeCompare(b)) + .reduce((acc, key) => { + acc[key] = json.translation[key] + return acc + }, {}) + json.translation = sorted + + await fs.writeFile(filePath, stableStringify(json), 'utf8') + console.log(`${locale}: updated ${count} keys`) + totalAdded += count + } + + console.log(`Done. Total key writes: ${totalAdded}`) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/web/default/src/components/json-editor.tsx b/web/default/src/components/json-editor.tsx index 034155f3f32..24623657725 100644 --- a/web/default/src/components/json-editor.tsx +++ b/web/default/src/components/json-editor.tsx @@ -44,7 +44,7 @@ type EditorRow = { value: string } -function parseJsonRows(json: string): EditorRow[] { +function parseJsonRows(json: string): EditorRow[] | null { try { if (!json.trim()) return [] const parsed = JSON.parse(json) @@ -57,7 +57,7 @@ function parseJsonRows(json: string): EditorRow[] { value: typeof val === 'object' ? JSON.stringify(val) : String(val), })) } catch { - return [] + return null } } @@ -81,11 +81,16 @@ export function JsonEditor({ const resolvedKeyLabel = keyLabel ?? t('Key') const resolvedValueLabel = valueLabel ?? t('Value') const [mode, setMode] = useState<'visual' | 'json'>('visual') - const [rows, setRows] = useState(() => parseJsonRows(value)) + const [rows, setRows] = useState( + () => parseJsonRows(value) ?? [] + ) const [jsonValue, setJsonValue] = useState(value) const parseJsonToRows = (json: string) => { - setRows(parseJsonRows(json)) + const parsedRows = parseJsonRows(json) + if (parsedRows) { + setRows(parsedRows) + } } // Parse JSON to rows when value changes externally diff --git a/web/default/src/features/channels/components/channels-columns.tsx b/web/default/src/features/channels/components/channels-columns.tsx index 4fcc8c02e04..3c963d07602 100644 --- a/web/default/src/features/channels/components/channels-columns.tsx +++ b/web/default/src/features/channels/components/channels-columns.tsx @@ -187,6 +187,7 @@ function PriorityCell({ channel }: { channel: Channel }) { return ( <> { setPendingValue(value) @@ -217,6 +218,7 @@ function PriorityCell({ channel }: { channel: Channel }) { // Regular channel row - editable return ( { handleUpdateChannelField(channel.id, 'priority', value, queryClient) @@ -245,6 +247,7 @@ function WeightCell({ channel }: { channel: Channel }) { return ( <> { setPendingValue(value) @@ -275,6 +278,7 @@ function WeightCell({ channel }: { channel: Channel }) { // Regular channel row - editable return ( { handleUpdateChannelField(channel.id, 'weight', value, queryClient) diff --git a/web/default/src/features/channels/components/channels-table.tsx b/web/default/src/features/channels/components/channels-table.tsx index e9d058bbace..5f44fbd2db6 100644 --- a/web/default/src/features/channels/components/channels-table.tsx +++ b/web/default/src/features/channels/components/channels-table.tsx @@ -326,6 +326,10 @@ export function ChannelsTable() { enableRowSelection: batchMode ? (row: Row) => !isTagAggregateRow(row.original) : false, + // Stable ids keep row state (selection, expansion, spinner local values) + // attached to the channel after priority-driven list reordering. + getRowId: (row) => + isTagAggregateRow(row) ? `tag:${row.tag ?? ''}` : String(row.id), onSortingChange: handleSortingChange, onColumnFiltersChange: handleColumnFiltersChange, onPaginationChange, diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx index b3aa0672983..28554398c92 100644 --- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -330,6 +330,7 @@ function hasAdvancedSettingsValues(values: ChannelFormValues): boolean { hasConfiguredOverrideValue(values.status_code_mapping) || values.tag?.trim() || values.remark?.trim() || + values.user_agent?.trim() || values.priority || values.weight || values.proxy?.trim() || @@ -730,6 +731,7 @@ export function ChannelMutateDrawer({ const currentAdvancedCustom = form.watch('advanced_custom') const currentPriority = form.watch('priority') const currentWeight = form.watch('weight') + const currentUserAgent = form.watch('user_agent') const currentTestModel = form.watch('test_model') const currentAutoBan = form.watch('auto_ban') const currentTag = form.watch('tag') @@ -992,6 +994,7 @@ export function ChannelMutateDrawer({ const routingStrategyConfigured = Boolean( currentPriority || currentWeight || + currentUserAgent?.trim() || currentTestModel?.trim() || (currentAutoBan ?? 1) !== 1 ) @@ -3349,6 +3352,7 @@ export function ChannelMutateDrawer({ /> {t('Fetch from Upstream')} + {!isEditing && !canEditSensitive && ( {t( @@ -3660,6 +3664,30 @@ export function ChannelMutateDrawer({ /> + ( + + + {t('User-Agent Filter')} + + + + + + {t(FIELD_DESCRIPTIONS.USER_AGENT)} + + + + )} + /> + { @@ -345,6 +346,7 @@ export const FIELD_PLACEHOLDERS = { PARAM_OVERRIDE: '{"temperature": 0.7}', HEADER_OVERRIDE: '{"X-Custom-Header": "value"}', STATUS_CODE_MAPPING: '{"400": "500"}', + USER_AGENT: 'codex*,claude-code*', } as const export const FIELD_DESCRIPTIONS = { @@ -367,6 +369,8 @@ export const FIELD_DESCRIPTIONS = { SETTING: 'Channel-specific settings (JSON format)', PARAM_OVERRIDE: 'Override request parameters (JSON format)', HEADER_OVERRIDE: 'Override request headers (JSON format)', + USER_AGENT: + 'Comma-separated wildcard patterns. When set, only matching client User-Agents can use this channel.', MULTI_KEY_MODE: 'How to select keys: random or sequential polling', BATCH_ADD: 'Create multiple channels from multiple keys', OPENAI_ORG: 'OpenAI Organization ID (optional)', @@ -377,7 +381,7 @@ export const FIELD_DESCRIPTIONS = { // ============================================================================ export const MODEL_FETCHABLE_TYPES = new Set([ - 1, 4, 14, 17, 20, 23, 24, 25, 26, 27, 31, 34, 35, 40, 42, 43, 47, 48, + 1, 4, 14, 17, 20, 23, 24, 25, 26, 27, 31, 34, 35, 40, 42, 43, 47, 48, 59, ]) export const TYPE_TO_KEY_PROMPT: Record = { diff --git a/web/default/src/features/channels/lib/channel-form.ts b/web/default/src/features/channels/lib/channel-form.ts index 33a5fb707a9..dae06545bc1 100644 --- a/web/default/src/features/channels/lib/channel-form.ts +++ b/web/default/src/features/channels/lib/channel-form.ts @@ -173,6 +173,7 @@ export const channelFormSchema = z .string() .optional() .refine(isOptionalJsonObject, ERROR_MESSAGES.INVALID_JSON), + user_agent: z.string().optional(), settings: z .string() .optional() @@ -318,6 +319,7 @@ export const CHANNEL_FORM_DEFAULT_VALUES: ChannelFormValues = { setting: '', param_override: '', header_override: '', + user_agent: '', settings: '{}', other: '', multi_key_mode: 'single', @@ -459,6 +461,7 @@ export function transformChannelToFormDefaults( setting: channel.setting || '', param_override: channel.param_override || '', header_override: channel.header_override || '', + user_agent: channel.user_agent || '', settings: channel.settings || '{}', other: channel.other || '', multi_key_mode: 'single', @@ -664,6 +667,7 @@ export function transformFormDataToCreatePayload(formData: ChannelFormValues): { header_override: formData.header_override || null, settings: buildSettingsJSON(formData), other: formData.other || '', + user_agent: formData.user_agent || null, } // Clean up empty strings to null for optional fields @@ -711,6 +715,7 @@ export function transformFormDataToUpdatePayload( header_override: formData.header_override || null, settings: buildSettingsJSON(formData), other: formData.other || '', + user_agent: formData.user_agent || null, } // Only include key if it was changed (not empty) @@ -735,6 +740,7 @@ export function transformFormDataToUpdatePayload( payload.status_code_mapping = formData.status_code_mapping || '' payload.param_override = formData.param_override || '' payload.header_override = formData.header_override || '' + payload.user_agent = formData.user_agent || '' return payload } diff --git a/web/default/src/features/channels/lib/channel-utils.ts b/web/default/src/features/channels/lib/channel-utils.ts index dc143a66d97..12634d6dfe7 100644 --- a/web/default/src/features/channels/lib/channel-utils.ts +++ b/web/default/src/features/channels/lib/channel-utils.ts @@ -91,6 +91,7 @@ export function getChannelTypeIcon(type: number): string { 40: 'SiliconCloud', // SiliconFlow 44: 'OpenAI', // MokaAI 20: 'OpenRouter', // OpenRouter + 59: 'OpenCode', // OpenCode Go // Image/Video generation 2: 'Midjourney', // MjProxy diff --git a/web/default/src/features/channels/types.ts b/web/default/src/features/channels/types.ts index 96bd5b2cfd1..23278380083 100644 --- a/web/default/src/features/channels/types.ts +++ b/web/default/src/features/channels/types.ts @@ -62,6 +62,7 @@ export const channelSchema = z.object({ setting: z.string().nullish(), param_override: z.string().nullish(), header_override: z.string().nullish(), + user_agent: z.string().nullish(), remark: z.string().default(''), max_input_tokens: z.number().default(0), channel_info: channelInfoSchema.default({ diff --git a/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx b/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx index db8330ef20b..0bd0355586a 100644 --- a/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx +++ b/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx @@ -203,10 +203,19 @@ export function ModelMutateDrawer({ 'grok.violation_deduction_enabled': false, 'grok.violation_deduction_amount': 0, RetryTimes: 0, + SameChannelRetryTimes: 0, + SameChannelRetryBaseDelayMs: 200, + SameChannelRetryMaxDelayMs: 2000, + TtftRoutingEnabled: false, + TtftRoutingMinSamples: 20, + TtftRoutingMinFactor: 0.5, + TtftRoutingMaxFactor: 2.0, + TtftRoutingRefMs: 500, ChannelDisableThreshold: '', AutomaticDisableChannelEnabled: false, AutomaticEnableChannelEnabled: false, AutomaticDisableKeywords: '', + UpstreamCapacityKeywords: '', AutomaticDisableStatusCodes: '401', AutomaticRetryStatusCodes: '100-199,300-399,401-407,409-499,500-503,505-523,525-599', diff --git a/web/default/src/features/system-settings/maintenance/log-settings-section.tsx b/web/default/src/features/system-settings/maintenance/log-settings-section.tsx index 6767224d4b4..360b7557f69 100644 --- a/web/default/src/features/system-settings/maintenance/log-settings-section.tsx +++ b/web/default/src/features/system-settings/maintenance/log-settings-section.tsx @@ -80,12 +80,14 @@ import type { LogCleanupTask } from '../types' const logSettingsSchema = z.object({ LogConsumeEnabled: z.boolean(), + RequestDetailLogEnabled: z.boolean(), }) type LogSettingsFormValues = z.infer type LogSettingsSectionProps = { defaultEnabled: boolean + defaultRequestDetailEnabled: boolean } type ServerLogInfo = { @@ -97,6 +99,12 @@ type ServerLogInfo = { newest_time?: string } +type RequestDetailStorageInfo = { + dir: string + file_count: number + total_size: number +} + const HOURS_IN_DAY = 24 function formatBytes(bytes: number, decimals = 2): string { @@ -141,6 +149,7 @@ function isActiveLogCleanupTask(task: LogCleanupTask | null) { export function LogSettingsSection({ defaultEnabled, + defaultRequestDetailEnabled, }: LogSettingsSectionProps) { const { t } = useTranslation() const updateOption = useUpdateOption() @@ -148,6 +157,7 @@ export function LogSettingsSection({ resolver: zodResolver(logSettingsSchema), defaultValues: { LogConsumeEnabled: defaultEnabled, + RequestDetailLogEnabled: defaultRequestDetailEnabled, }, }) @@ -163,6 +173,10 @@ export function LogSettingsSection({ const [serverLogCleanupMode, setServerLogCleanupMode] = useState('by_count') const [serverLogCleanupValue, setServerLogCleanupValue] = useState(10) const [serverLogCleanupLoading, setServerLogCleanupLoading] = useState(false) + const [requestDetailInfo, setRequestDetailInfo] = + useState(null) + const [requestDetailClearLoading, setRequestDetailClearLoading] = + useState(false) const fetchServerLogInfo = useCallback(async () => { try { @@ -173,13 +187,26 @@ export function LogSettingsSection({ } }, []) + const fetchRequestDetailInfo = useCallback(async () => { + try { + const res = await api.get('/api/log/request-detail/storage') + if (res.data.success) setRequestDetailInfo(res.data.data) + } catch { + /* ignore */ + } + }, []) + useEffect(() => { - form.reset({ LogConsumeEnabled: defaultEnabled }) - }, [defaultEnabled, form]) + form.reset({ + LogConsumeEnabled: defaultEnabled, + RequestDetailLogEnabled: defaultRequestDetailEnabled, + }) + }, [defaultEnabled, defaultRequestDetailEnabled, form]) useEffect(() => { fetchServerLogInfo() - }, [fetchServerLogInfo]) + fetchRequestDetailInfo() + }, [fetchServerLogInfo, fetchRequestDetailInfo]) useEffect(() => { let cancelled = false @@ -257,11 +284,25 @@ export function LogSettingsSection({ }, [logCleanupActive, logCleanupTaskId, t]) const onSubmit = async (values: LogSettingsFormValues) => { - if (values.LogConsumeEnabled === defaultEnabled) return - await updateOption.mutateAsync({ - key: 'LogConsumeEnabled', - value: values.LogConsumeEnabled, - }) + const updates: Array> = [] + if (values.LogConsumeEnabled !== defaultEnabled) { + updates.push( + updateOption.mutateAsync({ + key: 'LogConsumeEnabled', + value: values.LogConsumeEnabled, + }) + ) + } + if (values.RequestDetailLogEnabled !== defaultRequestDetailEnabled) { + updates.push( + updateOption.mutateAsync({ + key: 'RequestDetailLogEnabled', + value: values.RequestDetailLogEnabled, + }) + ) + } + if (updates.length === 0) return + await Promise.all(updates) } const handleRequestCleanLogs = () => { @@ -300,6 +341,29 @@ export function LogSettingsSection({ } } + const clearRequestDetailFiles = async () => { + setRequestDetailClearLoading(true) + try { + const res = await api.delete('/api/log/request-detail') + if (!res.data.success) { + throw new Error(res.data.message || t('Failed to clear request detail files')) + } + const removed = res.data.data?.removed ?? 0 + toast.success( + t('Cleared {{count}} request detail directories.', { count: removed }) + ) + await fetchRequestDetailInfo() + } catch (error) { + const message = + error instanceof Error + ? error.message + : t('Failed to clear request detail files') + toast.error(message) + } finally { + setRequestDetailClearLoading(false) + } + } + const cleanupServerLogFiles = async () => { if ( !serverLogCleanupValue || @@ -367,6 +431,106 @@ export function LogSettingsSection({ )} /> + ( + + + {t('Record request/response body')} + + {t( + 'Store full request JSON and upstream response bodies as files under log-dir/request-detail/{request_id}/ for usage log details. Stream responses are stored as newline-delimited upstream data payloads. Not written to the database. Intended for temporary debugging; multi-node deployments need a shared log-dir.' + )} + + + + + + + + )} + /> + + +
+

+ {t('Request detail body files')} +

+

+ {t( + 'Clear all request/response body files stored for usage log details on this node.' + )} +

+
+ {requestDetailInfo && ( +
+
+
+ + {t('Directory')}: + {' '} + + {requestDetailInfo.dir} + +
+
+ + {t('File Count')}: + {' '} + {requestDetailInfo.file_count} +
+
+ + {t('Total Size')}: + {' '} + {formatBytes(requestDetailInfo.total_size)} +
+
+
+ )} + + + } + > + {requestDetailClearLoading + ? t('Clearing...') + : t('Clear request detail files')} + + + + + {t('Clear all request detail body files?')} + + + {t( + 'This permanently deletes all request/response body files under the request-detail directory on this node. Usage log rows are not affected.' + )} + + + + {t('Cancel')} + + {t('Clear')} + + + + +
+

{t('Clean history logs')}

diff --git a/web/default/src/features/system-settings/models/index.tsx b/web/default/src/features/system-settings/models/index.tsx index e7a1ee1c490..c9771f4bd2b 100644 --- a/web/default/src/features/system-settings/models/index.tsx +++ b/web/default/src/features/system-settings/models/index.tsx @@ -63,10 +63,19 @@ const defaultModelSettings: ModelSettings = { DefaultUseAutoGroup: false, 'group_ratio_setting.group_special_usable_group': '{}', RetryTimes: 0, + SameChannelRetryTimes: 0, + SameChannelRetryBaseDelayMs: 200, + SameChannelRetryMaxDelayMs: 2000, + TtftRoutingEnabled: false, + TtftRoutingMinSamples: 20, + TtftRoutingMinFactor: 0.5, + TtftRoutingMaxFactor: 2.0, + TtftRoutingRefMs: 500, ChannelDisableThreshold: '', AutomaticDisableChannelEnabled: false, AutomaticEnableChannelEnabled: false, AutomaticDisableKeywords: '', + UpstreamCapacityKeywords: '', AutomaticDisableStatusCodes: '401', AutomaticRetryStatusCodes: '100-199,300-399,401-407,409-499,500-503,505-523,525-599', diff --git a/web/default/src/features/system-settings/models/routing-reliability-section.tsx b/web/default/src/features/system-settings/models/routing-reliability-section.tsx index efc8092a1c4..1e1ad0ff11a 100644 --- a/web/default/src/features/system-settings/models/routing-reliability-section.tsx +++ b/web/default/src/features/system-settings/models/routing-reliability-section.tsx @@ -69,10 +69,19 @@ type ChannelTestMode = (typeof channelTestModes)[number] const routingReliabilitySchema = z .object({ RetryTimes: z.coerce.number().min(0).max(10), + SameChannelRetryTimes: z.coerce.number().min(0).max(5), + SameChannelRetryBaseDelayMs: z.coerce.number().min(0).max(60000), + SameChannelRetryMaxDelayMs: z.coerce.number().min(0).max(60000), + TtftRoutingEnabled: z.boolean(), + TtftRoutingMinSamples: z.coerce.number().int().min(1).max(10000), + TtftRoutingMinFactor: z.coerce.number().min(0.1).max(10), + TtftRoutingMaxFactor: z.coerce.number().min(0.1).max(10), + TtftRoutingRefMs: z.coerce.number().int().min(1).max(600000), ChannelDisableThreshold: numericString, AutomaticDisableChannelEnabled: z.boolean(), AutomaticEnableChannelEnabled: z.boolean(), AutomaticDisableKeywords: z.string(), + UpstreamCapacityKeywords: z.string(), AutomaticDisableStatusCodes: z.string(), AutomaticRetryStatusCodes: z.string(), monitor_setting: z.object({ @@ -110,6 +119,14 @@ const routingReliabilitySchema = z )}`, }) } + + if (values.TtftRoutingMaxFactor < values.TtftRoutingMinFactor) { + ctx.addIssue({ + code: 'custom', + path: ['TtftRoutingMaxFactor'], + message: 'Max factor must be greater than or equal to min factor', + }) + } }) type RoutingReliabilityFormValues = z.output @@ -118,10 +135,19 @@ type RoutingReliabilityFormInput = z.input type RoutingReliabilitySectionProps = { defaultValues: { RetryTimes: number + SameChannelRetryTimes: number + SameChannelRetryBaseDelayMs: number + SameChannelRetryMaxDelayMs: number + TtftRoutingEnabled: boolean + TtftRoutingMinSamples: number + TtftRoutingMinFactor: number + TtftRoutingMaxFactor: number + TtftRoutingRefMs: number ChannelDisableThreshold: string AutomaticDisableChannelEnabled: boolean AutomaticEnableChannelEnabled: boolean AutomaticDisableKeywords: string + UpstreamCapacityKeywords: string AutomaticDisableStatusCodes: string AutomaticRetryStatusCodes: string 'monitor_setting.auto_test_channel_enabled': boolean @@ -136,10 +162,19 @@ function normalizeLineEndings(value: string) { type NormalizedRoutingReliabilityValues = { RetryTimes: number + SameChannelRetryTimes: number + SameChannelRetryBaseDelayMs: number + SameChannelRetryMaxDelayMs: number + TtftRoutingEnabled: boolean + TtftRoutingMinSamples: number + TtftRoutingMinFactor: number + TtftRoutingMaxFactor: number + TtftRoutingRefMs: number ChannelDisableThreshold: string AutomaticDisableChannelEnabled: boolean AutomaticEnableChannelEnabled: boolean AutomaticDisableKeywords: string + UpstreamCapacityKeywords: string AutomaticDisableStatusCodes: string AutomaticRetryStatusCodes: string 'monitor_setting.auto_test_channel_enabled': boolean @@ -155,12 +190,23 @@ const buildFormDefaults = ( defaults: RoutingReliabilitySectionProps['defaultValues'] ): RoutingReliabilityFormInput => ({ RetryTimes: defaults.RetryTimes ?? 0, + SameChannelRetryTimes: defaults.SameChannelRetryTimes ?? 0, + SameChannelRetryBaseDelayMs: defaults.SameChannelRetryBaseDelayMs ?? 200, + SameChannelRetryMaxDelayMs: defaults.SameChannelRetryMaxDelayMs ?? 2000, + TtftRoutingEnabled: defaults.TtftRoutingEnabled ?? false, + TtftRoutingMinSamples: defaults.TtftRoutingMinSamples ?? 20, + TtftRoutingMinFactor: defaults.TtftRoutingMinFactor ?? 0.5, + TtftRoutingMaxFactor: defaults.TtftRoutingMaxFactor ?? 2.0, + TtftRoutingRefMs: defaults.TtftRoutingRefMs ?? 500, ChannelDisableThreshold: defaults.ChannelDisableThreshold ?? '', AutomaticDisableChannelEnabled: defaults.AutomaticDisableChannelEnabled, AutomaticEnableChannelEnabled: defaults.AutomaticEnableChannelEnabled, AutomaticDisableKeywords: normalizeLineEndings( defaults.AutomaticDisableKeywords ?? '' ), + UpstreamCapacityKeywords: normalizeLineEndings( + defaults.UpstreamCapacityKeywords ?? '' + ), AutomaticDisableStatusCodes: defaults.AutomaticDisableStatusCodes ?? '', AutomaticRetryStatusCodes: defaults.AutomaticRetryStatusCodes ?? '', monitor_setting: { @@ -178,12 +224,23 @@ const normalizeDefaults = ( defaults: RoutingReliabilitySectionProps['defaultValues'] ): NormalizedRoutingReliabilityValues => ({ RetryTimes: defaults.RetryTimes ?? 0, + SameChannelRetryTimes: defaults.SameChannelRetryTimes ?? 0, + SameChannelRetryBaseDelayMs: defaults.SameChannelRetryBaseDelayMs ?? 200, + SameChannelRetryMaxDelayMs: defaults.SameChannelRetryMaxDelayMs ?? 2000, + TtftRoutingEnabled: defaults.TtftRoutingEnabled ?? false, + TtftRoutingMinSamples: defaults.TtftRoutingMinSamples ?? 20, + TtftRoutingMinFactor: defaults.TtftRoutingMinFactor ?? 0.5, + TtftRoutingMaxFactor: defaults.TtftRoutingMaxFactor ?? 2.0, + TtftRoutingRefMs: defaults.TtftRoutingRefMs ?? 500, ChannelDisableThreshold: (defaults.ChannelDisableThreshold ?? '').trim(), AutomaticDisableChannelEnabled: defaults.AutomaticDisableChannelEnabled, AutomaticEnableChannelEnabled: defaults.AutomaticEnableChannelEnabled, AutomaticDisableKeywords: normalizeLineEndings( defaults.AutomaticDisableKeywords ?? '' ), + UpstreamCapacityKeywords: normalizeLineEndings( + defaults.UpstreamCapacityKeywords ?? '' + ), AutomaticDisableStatusCodes: parseHttpStatusCodeRules( defaults.AutomaticDisableStatusCodes ?? '' ).normalized, @@ -203,12 +260,23 @@ const normalizeFormValues = ( values: RoutingReliabilityFormValues ): NormalizedRoutingReliabilityValues => ({ RetryTimes: values.RetryTimes, + SameChannelRetryTimes: values.SameChannelRetryTimes, + SameChannelRetryBaseDelayMs: values.SameChannelRetryBaseDelayMs, + SameChannelRetryMaxDelayMs: values.SameChannelRetryMaxDelayMs, + TtftRoutingEnabled: values.TtftRoutingEnabled, + TtftRoutingMinSamples: values.TtftRoutingMinSamples, + TtftRoutingMinFactor: values.TtftRoutingMinFactor, + TtftRoutingMaxFactor: values.TtftRoutingMaxFactor, + TtftRoutingRefMs: values.TtftRoutingRefMs, ChannelDisableThreshold: values.ChannelDisableThreshold.trim(), AutomaticDisableChannelEnabled: values.AutomaticDisableChannelEnabled, AutomaticEnableChannelEnabled: values.AutomaticEnableChannelEnabled, AutomaticDisableKeywords: normalizeLineEndings( values.AutomaticDisableKeywords ), + UpstreamCapacityKeywords: normalizeLineEndings( + values.UpstreamCapacityKeywords + ), AutomaticDisableStatusCodes: parseHttpStatusCodeRules( values.AutomaticDisableStatusCodes ).normalized, @@ -310,7 +378,81 @@ export function RoutingReliabilitySection({ /> - {t('Number of times to retry failed requests (0-10)')} + {t( + 'Number of channel switches after failure (0-10). Same-channel retries do not consume this budget.' + )} + + + + )} + /> + + ( + + {t('Same-channel retry times')} + + + + + {t( + 'Extra attempts on the same channel with exponential backoff for transient errors (network, 429, 502, 503). 0 disables.' + )} + + + + )} + /> + + ( + + {t('Same-channel base delay (ms)')} + + + + + {t( + 'Initial backoff delay in milliseconds before the first same-channel retry.' + )} + + + + )} + /> + + ( + + {t('Same-channel max delay (ms)')} + + + + + {t( + 'Maximum exponential backoff delay in milliseconds for same-channel retries.' + )} @@ -346,6 +488,168 @@ export function RoutingReliabilitySection({ )} /> + + ( + + {t('Capacity soft-fail keywords')} + +