feat: publish v1.0.0-rc.36-custom.2
Fixed-group tokens with a backup group now take precedence over the session affinity skip-retry-on-failure rule, so retryable upstream errors (503) fall through to the backup group instead of stopping on the primary channel. Non-retryable errors, explicit channel pins, and auto groups are unchanged. Adds controller regression tests for the affinity override and a build-time Go test gate.
This commit is contained in:
1 parent
3a7e8f3b54
commit
d5fb4ce8d1
6 files changed
+108
-4
No files matched your search
+3
-2
@@ -1,11 +1,12 @@
|
||||
# 自定义版本说明
|
||||
|
||||
当前生产版本基于官方 `v1.0.0-rc.36`(`ea7cb0ba4e0f82e2bfa5e55752eb68bdf902f71b`),版本号为 `v1.0.0-rc.36-custom.1`。
|
||||
当前生产版本基于官方 `v1.0.0-rc.36`(`ea7cb0ba4e0f82e2bfa5e55752eb68bdf902f71b`),版本号为 `v1.0.0-rc.36-custom.2`。
|
||||
|
||||
包含以下生产定制:
|
||||
|
||||
- 仅展示存在可用渠道的分组;禁用、无渠道和不可用分组不进入用户筛选。
|
||||
- 固定令牌分组可配置一个备用分组;主分组无路由或发生可重试上游错误时按顺序切换,实际命中的分组用于计费和日志。
|
||||
- 固定令牌配置了备用分组时,备用分组切换优先于会话亲和规则的“失败后不重试”;上游返回可重试错误(如 503)时不再停在主分组渠道。不可重试错误、显式指定渠道和 auto 分组行为保持不变。
|
||||
- 分组状态页展示请求成功率、首字延迟、平均延迟、TPS、请求缓存和令牌缓存指标。
|
||||
- 支持按分组配置动态计费表达式,并在价格页按所选分组展示。
|
||||
- 用户侧隐藏上游错误细节,完整错误仅保留在 root 日志范围;客户端断开不计为上游失败。
|
||||
@@ -13,7 +14,7 @@
|
||||
生产镜像通过以下命令构建:
|
||||
|
||||
```sh
|
||||
docker build --pull=false -f Dockerfile.request-status -t local/new-api:v1.0.0-rc.36-custom-20260911 .
|
||||
docker build --pull=false -f Dockerfile.request-status -t local/new-api:v1.0.0-rc.36-custom.2-20260911 .
|
||||
```
|
||||
|
||||
`Dockerfile.request-status` 会执行前端类型检查、相关 Vitest 与 oxlint 检查、Go 回归测试和生产构建。
|
||||
@@ -14,7 +14,7 @@ RUN npx --no-install oxlint -c .oxlintrc.json src/features/group-status src/feat
|
||||
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat /build/VERSION) npm run build
|
||||
|
||||
FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS build
|
||||
ENV GO111MODULE=on CGO_ENABLED=0 GOWORK=off GOEXPERIMENT=greenteagc
|
||||
ENV GOMAXPROCS=2 GOFLAGS=-p=2 GO111MODULE=on CGO_ENABLED=0 GOWORK=off GOEXPERIMENT=greenteagc
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64}
|
||||
@@ -25,6 +25,7 @@ RUN go mod download
|
||||
COPY . .
|
||||
COPY --from=webbuild /build/web/dist ./web/dist
|
||||
RUN go test ./pkg/perf_metrics ./model ./controller ./service ./relay/helper -run 'TestGroupBuckets|TestRequestStatus|TestGroupModelPricing|TestGetEnabledGroupSetRequiresEnabledAbilityAndChannel|TestSelectableGroupsRequireEnabledChannel|TestAddTokenPersistsFixedBackupGroup|TestUpdateTokenAutoGroupsTriStateAndFixedBackup|TestAddTokenRejectsInvalidFixedBackupGroup|TestCacheGetRandomSatisfiedChannelUsesTokenAutoGroupsWhenGlobalAutoIsEmpty|TestCacheGetRandomSatisfiedChannelUsesFixedTokenBackupGroup|TestCacheGetRandomSatisfiedChannelSkipsUnavailableFixedPrimary|TestAppendStreamStatusTreatsClientGoneAsDisplayNormal|TestClientFacingOpenAIErrorHidesUpstreamDetails|TestClientFacingClaudeErrorHidesUpstreamDetails|TestClientFacingErrorKeepsLocalValidationMessage|TestPublicUpstreamErrorContent' -count=1
|
||||
RUN go test ./controller ./service -run 'TestFixedBackup|TestShouldSkipRetryAfterChannelAffinityFailure|TestShouldRetryHonorsPinRetryMode' -count=1
|
||||
RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api
|
||||
|
||||
FROM debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/relaykit/types"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFixedBackupRetryOverridesAffinityOnly(t *testing.T) {
|
||||
configureTokenAutoGroupsTest(t, "2", "[]")
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
group string
|
||||
backups []string
|
||||
cross bool
|
||||
status int
|
||||
skip bool
|
||||
pinned bool
|
||||
want bool
|
||||
}{
|
||||
{"503 with backup", "default", []string{"vip"}, true, 503, false, false, true},
|
||||
{"no backup", "default", nil, true, 503, false, false, false},
|
||||
{"same group", "default", []string{"default"}, true, 503, false, false, false},
|
||||
{"revoked backup", "default", []string{"unavailable"}, true, 503, false, false, false},
|
||||
{"retry disabled", "default", []string{"vip"}, false, 503, false, false, false},
|
||||
{"auto unchanged", "auto", []string{"vip"}, true, 503, false, false, false},
|
||||
{"invalid request", "default", []string{"vip"}, true, 400, true, false, false},
|
||||
{"skip retry", "default", []string{"vip"}, true, 503, true, false, false},
|
||||
{"explicit channel pin", "default", []string{"vip"}, true, 503, false, true, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := newPinRetryContext()
|
||||
c.Set("channel_affinity_skip_retry_on_failure", true)
|
||||
common.SetContextKey(c, constant.ContextKeyUserGroup, "default")
|
||||
common.SetContextKey(c, constant.ContextKeyTokenGroup, tc.group)
|
||||
common.SetContextKey(c, constant.ContextKeyTokenAutoGroups, tc.backups)
|
||||
common.SetContextKey(c, constant.ContextKeyTokenCrossGroupRetry, tc.cross)
|
||||
if tc.pinned {
|
||||
service.GetChannelConstraints(c).AddPin(dto.ChannelPin{
|
||||
ChannelId: 13, Source: dto.PinSourceToken,
|
||||
Rank: dto.PinRankToken, RetryMode: dto.PinRetrySingleAttempt,
|
||||
})
|
||||
}
|
||||
err := types.NewOpenAIError(errors.New("upstream unavailable"), types.ErrorCodeBadResponseStatusCode, tc.status)
|
||||
if tc.skip {
|
||||
err = types.NewOpenAIError(errors.New("do not replay"), types.ErrorCodeBadResponseStatusCode, tc.status, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
require.Equal(t, tc.want, shouldRetry(c, err, 1))
|
||||
require.False(t, shouldRetry(c, err, 0), "exhausted retry budget stays bounded")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/pkg/cachex"
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
@@ -627,6 +628,17 @@ func ShouldSkipRetryAfterChannelAffinityFailure(c *gin.Context) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
// Explicit fixed-token fallback takes precedence over cache affinity.
|
||||
primary := common.GetContextKeyString(c, constant.ContextKeyTokenGroup)
|
||||
if primary != "" && primary != "auto" && common.GetContextKeyBool(c, constant.ContextKeyTokenCrossGroupRetry) {
|
||||
if groups, ok := common.GetContextKeyType[[]string](c, constant.ContextKeyTokenAutoGroups); ok {
|
||||
for _, group := range FilterUserTokenAutoGroups(common.GetContextKeyString(c, constant.ContextKeyUserGroup), groups) {
|
||||
if group != primary {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
v, ok := c.Get(ginKeyChannelAffinitySkipRetry)
|
||||
if ok {
|
||||
b, ok := v.(bool)
|
||||
|
||||
@@ -193,3 +193,35 @@ func TestCacheGetRandomSatisfiedChannelSkipsUnavailableFixedPrimary(t *testing.T
|
||||
assert.Equal(t, "vip", selectedGroup)
|
||||
assert.Equal(t, "vip", common.GetContextKeyString(ctx, constant.ContextKeyAutoGroup))
|
||||
}
|
||||
|
||||
func TestFixedBackupAfterAffinityFailureWithOneRetry(t *testing.T) {
|
||||
db := setupChannelSelectAutoGroupsTest(t)
|
||||
common.RetryTimes = 1
|
||||
const modelName = "fixed-backup-affinity"
|
||||
createChannelSelectAutoGroupsChannel(t, db, 13, "default", modelName)
|
||||
createChannelSelectAutoGroupsChannel(t, db, 14, "vip", modelName)
|
||||
model.InitChannelCache()
|
||||
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
common.SetContextKey(ctx, constant.ContextKeyUserGroup, "default")
|
||||
common.SetContextKey(ctx, constant.ContextKeyTokenGroup, "default")
|
||||
common.SetContextKey(ctx, constant.ContextKeyTokenAutoGroups, []string{"vip"})
|
||||
common.SetContextKey(ctx, constant.ContextKeyTokenCrossGroupRetry, true)
|
||||
ctx.Set(ginKeyChannelAffinitySkipRetry, true)
|
||||
require.False(t, ShouldSkipRetryAfterChannelAffinityFailure(ctx))
|
||||
|
||||
// The distributor's affinity hit bypasses the ordered group selector.
|
||||
param := &RetryParam{Ctx: ctx, TokenGroup: "default", ModelName: modelName}
|
||||
param.SetRetry(1)
|
||||
first, _, err := CacheGetRandomSatisfiedChannel(param)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, first)
|
||||
require.Equal(t, 13, first.Id)
|
||||
require.Equal(t, 0, param.GetRetry())
|
||||
param.IncreaseRetry()
|
||||
backup, group, err := CacheGetRandomSatisfiedChannel(param)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, backup)
|
||||
require.Equal(t, 14, backup.Id)
|
||||
require.Equal(t, "vip", group)
|
||||
require.Equal(t, "vip", common.GetContextKeyString(ctx, constant.ContextKeyAutoGroup))
|
||||
}
|
||||
Reference in new issue
Block a user