mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-20 00:18:55 +00:00
fix(codex): ignore persistently disabled catalog accounts
This commit is contained in:
@@ -72,6 +72,13 @@ func (s *gatewayModelsAccountRepoStub) ListByGroup(ctx context.Context, groupID
|
||||
return s.ListSchedulableByGroupID(ctx, groupID)
|
||||
}
|
||||
|
||||
func (s *gatewayModelsAccountRepoStub) ListModelAvailabilityCandidates(ctx context.Context, groupID *int64, _ []string, _ bool) ([]service.Account, error) {
|
||||
if groupID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return s.ListSchedulableByGroupID(ctx, *groupID)
|
||||
}
|
||||
|
||||
func newGatewayModelsHandlerForTest(repo service.AccountRepository) *GatewayHandler {
|
||||
return &GatewayHandler{
|
||||
gatewayService: service.NewGatewayService(
|
||||
|
||||
@@ -53,6 +53,10 @@ func (r codexModelsFailoverAccountRepo) ListByGroup(_ context.Context, _ int64)
|
||||
return append([]service.Account(nil), r.accounts...), nil
|
||||
}
|
||||
|
||||
func (r codexModelsFailoverAccountRepo) ListModelAvailabilityCandidates(_ context.Context, _ *int64, _ []string, _ bool) ([]service.Account, error) {
|
||||
return append([]service.Account(nil), r.accounts...), nil
|
||||
}
|
||||
|
||||
type codexModelsFailoverHTTPUpstream struct {
|
||||
service.HTTPUpstream
|
||||
mu sync.Mutex
|
||||
|
||||
@@ -198,8 +198,9 @@ func TestBuildCodexModelsManifestForGroupIntersectsDifferentMappedTargetsWithout
|
||||
}
|
||||
}
|
||||
|
||||
// Scenario: temporarily unschedulable mapped accounts still participate in capability intersection.
|
||||
func TestBuildCodexModelsManifestForGroupIntersectsUnschedulableMappedAccounts(t *testing.T) {
|
||||
// Scenario: accounts that leave the current scheduling pool only because of
|
||||
// transient state still participate in capability intersection.
|
||||
func TestBuildCodexModelsManifestForGroupIntersectsTransientlyUnschedulableMappedAccounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const groupID int64 = 741
|
||||
@@ -213,19 +214,19 @@ func TestBuildCodexModelsManifestForGroupIntersectsUnschedulableMappedAccounts(t
|
||||
true,
|
||||
nil,
|
||||
)
|
||||
unschedulable := newCodexCatalogMappedAccount(
|
||||
transientlyUnschedulable := newCodexCatalogMappedAccount(
|
||||
42,
|
||||
"glm-5.3",
|
||||
"GLM 5.3",
|
||||
[]string{"low", "medium", "high"},
|
||||
[]string{"text"},
|
||||
272_000,
|
||||
false,
|
||||
true,
|
||||
map[string]any{"exclusive-model": "exclusive-upstream"},
|
||||
)
|
||||
svc := &GatewayService{accountRepo: splitCodexModelsAccountRepo{
|
||||
schedulable: map[int64][]Account{groupID: {schedulable}},
|
||||
catalog: map[int64][]Account{groupID: {schedulable, unschedulable}},
|
||||
catalog: map[int64][]Account{groupID: {schedulable, transientlyUnschedulable}},
|
||||
}}
|
||||
|
||||
body, err := svc.BuildCodexModelsManifestForGroup(
|
||||
@@ -241,8 +242,8 @@ func TestBuildCodexModelsManifestForGroupIntersectsUnschedulableMappedAccounts(t
|
||||
require.EqualValues(t, 272_000, models[0]["context_window"])
|
||||
}
|
||||
|
||||
// Scenario: deleting an account can widen the advertised contract.
|
||||
func TestBuildCodexModelsManifestForGroupWidensAfterUnschedulableAccountIsRemoved(t *testing.T) {
|
||||
// Scenario: a persistently disabled account cannot narrow the advertised contract.
|
||||
func TestBuildCodexModelsManifestForGroupIgnoresPersistentlyDisabledMappedAccounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const groupID int64 = 742
|
||||
@@ -256,9 +257,20 @@ func TestBuildCodexModelsManifestForGroupWidensAfterUnschedulableAccountIsRemove
|
||||
true,
|
||||
nil,
|
||||
)
|
||||
disabled := newCodexCatalogMappedAccount(
|
||||
42,
|
||||
"glm-5.3",
|
||||
"GLM 5.3",
|
||||
[]string{"low", "medium", "high"},
|
||||
[]string{"text"},
|
||||
272_000,
|
||||
false,
|
||||
nil,
|
||||
)
|
||||
svc := &GatewayService{accountRepo: splitCodexModelsAccountRepo{
|
||||
schedulable: map[int64][]Account{groupID: {remaining}},
|
||||
catalog: map[int64][]Account{groupID: {remaining}},
|
||||
all: map[int64][]Account{groupID: {remaining, disabled}},
|
||||
}}
|
||||
|
||||
body, err := svc.BuildCodexModelsManifestForGroup(
|
||||
@@ -271,7 +283,7 @@ func TestBuildCodexModelsManifestForGroupWidensAfterUnschedulableAccountIsRemove
|
||||
require.EqualValues(t, 1_000_000, models[0]["context_window"])
|
||||
}
|
||||
|
||||
func TestBuildCodexModelsManifestForGroupFallsBackToSchedulableWhenListByGroupFails(t *testing.T) {
|
||||
func TestBuildCodexModelsManifestForGroupFallsBackToSchedulableWhenAvailabilityLookupFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const groupID int64 = 743
|
||||
@@ -286,7 +298,7 @@ func TestBuildCodexModelsManifestForGroupFallsBackToSchedulableWhenListByGroupFa
|
||||
true,
|
||||
nil,
|
||||
)},
|
||||
listByGroupErr: errors.New("group listing unavailable"),
|
||||
availabilityErr: errors.New("group listing unavailable"),
|
||||
}
|
||||
svc := &GatewayService{accountRepo: repo}
|
||||
|
||||
|
||||
@@ -208,11 +208,12 @@ func (s *OpenAIGatewayService) groupConfiguredCodexModelIDs(ctx context.Context,
|
||||
|
||||
// loadCodexGroupCatalogAccounts separates picker membership from capability
|
||||
// intersection. visible accounts are currently schedulable and decide which
|
||||
// public aliases appear. catalog accounts are all non-deleted active group
|
||||
// members; their snapshots keep advertised capabilities from widening when a
|
||||
// mapped account is only temporarily unschedulable. If ListByGroup fails, the
|
||||
// catalog falls back to the schedulable set so a listing error does not fail
|
||||
// the client request.
|
||||
// public aliases appear. catalog accounts are persistently enabled group
|
||||
// members; the availability query ignores transient rate-limit, overload, and
|
||||
// temporary-unschedulable state so those conditions cannot widen advertised
|
||||
// capabilities. Persistently disabled accounts are excluded because routing
|
||||
// cannot select them. If the availability query fails, the catalog falls back
|
||||
// to the schedulable set so a listing error does not fail the client request.
|
||||
func loadCodexGroupCatalogAccounts(ctx context.Context, repo AccountRepository, groupID int64) (visible []Account, catalog []Account, err error) {
|
||||
if repo == nil {
|
||||
return nil, nil, nil
|
||||
@@ -222,7 +223,21 @@ func loadCodexGroupCatalogAccounts(ctx context.Context, repo AccountRepository,
|
||||
return nil, nil, err
|
||||
}
|
||||
catalog = visible
|
||||
groupAccounts, listErr := repo.ListByGroup(ctx, groupID)
|
||||
groupAccounts, listErr := repo.ListModelAvailabilityCandidates(
|
||||
ctx,
|
||||
&groupID,
|
||||
[]string{
|
||||
PlatformAnthropic,
|
||||
PlatformOpenAI,
|
||||
PlatformGemini,
|
||||
PlatformAntigravity,
|
||||
PlatformGrok,
|
||||
PlatformKimi,
|
||||
PlatformZhipu,
|
||||
PlatformDeepseek,
|
||||
},
|
||||
false,
|
||||
)
|
||||
if listErr != nil {
|
||||
return visible, catalog, nil
|
||||
}
|
||||
|
||||
@@ -39,17 +39,23 @@ func (r codexModelsVisibilityAccountRepo) ListSchedulableByGroupID(_ context.Con
|
||||
return append([]Account(nil), accounts...), nil
|
||||
}
|
||||
|
||||
func (r codexModelsVisibilityAccountRepo) ListByGroup(_ context.Context, groupID int64) ([]Account, error) {
|
||||
accounts := r.byGroup[groupID]
|
||||
func (r codexModelsVisibilityAccountRepo) ListModelAvailabilityCandidates(_ context.Context, groupID *int64, _ []string, _ bool) ([]Account, error) {
|
||||
if groupID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
accounts := r.byGroup[*groupID]
|
||||
return append([]Account(nil), accounts...), nil
|
||||
}
|
||||
|
||||
type countingCodexModelsAccountRepo struct {
|
||||
AccountRepository
|
||||
accounts []Account
|
||||
err error
|
||||
listByGroupErr error
|
||||
calls atomic.Int32
|
||||
accounts []Account
|
||||
err error
|
||||
availabilityErr error
|
||||
groupID *int64
|
||||
platforms []string
|
||||
includeGrouped bool
|
||||
calls atomic.Int32
|
||||
}
|
||||
|
||||
func (r *countingCodexModelsAccountRepo) ListSchedulableByGroupID(_ context.Context, _ int64) ([]Account, error) {
|
||||
@@ -60,9 +66,15 @@ func (r *countingCodexModelsAccountRepo) ListSchedulableByGroupID(_ context.Cont
|
||||
return append([]Account(nil), r.accounts...), nil
|
||||
}
|
||||
|
||||
func (r *countingCodexModelsAccountRepo) ListByGroup(_ context.Context, _ int64) ([]Account, error) {
|
||||
if r.listByGroupErr != nil {
|
||||
return nil, r.listByGroupErr
|
||||
func (r *countingCodexModelsAccountRepo) ListModelAvailabilityCandidates(_ context.Context, groupID *int64, platforms []string, includeGrouped bool) ([]Account, error) {
|
||||
if groupID != nil {
|
||||
value := *groupID
|
||||
r.groupID = &value
|
||||
}
|
||||
r.platforms = append([]string(nil), platforms...)
|
||||
r.includeGrouped = includeGrouped
|
||||
if r.availabilityErr != nil {
|
||||
return nil, r.availabilityErr
|
||||
}
|
||||
return append([]Account(nil), r.accounts...), nil
|
||||
}
|
||||
@@ -71,6 +83,7 @@ type splitCodexModelsAccountRepo struct {
|
||||
AccountRepository
|
||||
schedulable map[int64][]Account
|
||||
catalog map[int64][]Account
|
||||
all map[int64][]Account
|
||||
}
|
||||
|
||||
func (r splitCodexModelsAccountRepo) ListSchedulableByGroupID(_ context.Context, groupID int64) ([]Account, error) {
|
||||
@@ -78,7 +91,18 @@ func (r splitCodexModelsAccountRepo) ListSchedulableByGroupID(_ context.Context,
|
||||
}
|
||||
|
||||
func (r splitCodexModelsAccountRepo) ListByGroup(_ context.Context, groupID int64) ([]Account, error) {
|
||||
return append([]Account(nil), r.catalog[groupID]...), nil
|
||||
accounts := r.all[groupID]
|
||||
if accounts == nil {
|
||||
accounts = r.catalog[groupID]
|
||||
}
|
||||
return append([]Account(nil), accounts...), nil
|
||||
}
|
||||
|
||||
func (r splitCodexModelsAccountRepo) ListModelAvailabilityCandidates(_ context.Context, groupID *int64, _ []string, _ bool) ([]Account, error) {
|
||||
if groupID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return append([]Account(nil), r.catalog[*groupID]...), nil
|
||||
}
|
||||
|
||||
func newCodexCatalogMappedAccount(
|
||||
@@ -814,6 +838,13 @@ func TestBuildCodexModelsManifestForGroupLoadsAccountsOnce(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(1), repo.calls.Load())
|
||||
require.NotNil(t, repo.groupID)
|
||||
require.Equal(t, groupID, *repo.groupID)
|
||||
require.False(t, repo.includeGrouped)
|
||||
require.Contains(t, repo.platforms, PlatformOpenAI)
|
||||
require.Contains(t, repo.platforms, PlatformGrok)
|
||||
require.Contains(t, repo.platforms, PlatformDeepseek)
|
||||
require.NotContains(t, repo.platforms, PlatformComposite)
|
||||
}
|
||||
|
||||
func TestBuildCodexModelsManifestForGroupUsesFallbackWhenTextOnlyPlatformHasNoSnapshot(t *testing.T) {
|
||||
@@ -1003,8 +1034,9 @@ func TestBuildGroupConfiguredCodexModelsManifestExpandsSelectedModelCoveredByWil
|
||||
require.NotContains(t, string(manifest.Body), "gpt-*")
|
||||
}
|
||||
|
||||
// Scenario: OpenAI 配置目录对暂时不可调度账号取能力交集,且不发布其独有模型。
|
||||
func TestBuildGroupConfiguredCodexModelsManifestIntersectsUnschedulableMappedAccounts(t *testing.T) {
|
||||
// Scenario: OpenAI 配置目录对仅因瞬态状态退出当前调度池的账号取能力交集,
|
||||
// 且不发布其独有模型。持久 schedulable 仍为 true。
|
||||
func TestBuildGroupConfiguredCodexModelsManifestIntersectsTransientlyUnschedulableMappedAccounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const groupID int64 = 79
|
||||
@@ -1018,19 +1050,19 @@ func TestBuildGroupConfiguredCodexModelsManifestIntersectsUnschedulableMappedAcc
|
||||
true,
|
||||
nil,
|
||||
)
|
||||
unschedulable := newCodexCatalogMappedAccount(
|
||||
transientlyUnschedulable := newCodexCatalogMappedAccount(
|
||||
42,
|
||||
"glm-5.3",
|
||||
"GLM 5.3",
|
||||
[]string{"low", "medium", "high"},
|
||||
[]string{"text"},
|
||||
272_000,
|
||||
false,
|
||||
true,
|
||||
map[string]any{"exclusive-model": "exclusive-upstream"},
|
||||
)
|
||||
svc := &OpenAIGatewayService{accountRepo: splitCodexModelsAccountRepo{
|
||||
schedulable: map[int64][]Account{groupID: {schedulable}},
|
||||
catalog: map[int64][]Account{groupID: {schedulable, unschedulable}},
|
||||
catalog: map[int64][]Account{groupID: {schedulable, transientlyUnschedulable}},
|
||||
}}
|
||||
|
||||
manifest, configured, err := svc.BuildGroupConfiguredCodexModelsManifest(
|
||||
@@ -1049,6 +1081,52 @@ func TestBuildGroupConfiguredCodexModelsManifestIntersectsUnschedulableMappedAcc
|
||||
require.EqualValues(t, 272_000, models[0]["context_window"])
|
||||
}
|
||||
|
||||
// Scenario: 管理员持久禁用的账号不能继续收窄 Codex 能力目录。
|
||||
func TestBuildGroupConfiguredCodexModelsManifestIgnoresPersistentlyDisabledMappedAccounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const groupID int64 = 80
|
||||
enabled := newCodexCatalogMappedAccount(
|
||||
51,
|
||||
"gpt-5.6-sol",
|
||||
"GPT-5.6 Sol",
|
||||
[]string{"low", "medium", "high", "xhigh"},
|
||||
[]string{"text", "image"},
|
||||
1_000_000,
|
||||
true,
|
||||
nil,
|
||||
)
|
||||
disabled := newCodexCatalogMappedAccount(
|
||||
52,
|
||||
"glm-5.3",
|
||||
"GLM 5.3",
|
||||
[]string{"low", "medium", "high"},
|
||||
[]string{"text"},
|
||||
272_000,
|
||||
false,
|
||||
nil,
|
||||
)
|
||||
svc := &OpenAIGatewayService{accountRepo: splitCodexModelsAccountRepo{
|
||||
schedulable: map[int64][]Account{groupID: {enabled}},
|
||||
catalog: map[int64][]Account{groupID: {enabled}},
|
||||
all: map[int64][]Account{groupID: {enabled, disabled}},
|
||||
}}
|
||||
|
||||
manifest, configured, err := svc.BuildGroupConfiguredCodexModelsManifest(
|
||||
context.Background(),
|
||||
&Group{ID: groupID, Platform: PlatformOpenAI},
|
||||
"",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.True(t, configured)
|
||||
models := decodeCodexManifestModels(t, manifest.Body)
|
||||
require.Len(t, models, 1)
|
||||
require.Equal(t, "my-coder", models[0]["slug"])
|
||||
require.Equal(t, []string{"low", "medium", "high", "xhigh"}, effortsFromManifestModel(t, models[0]))
|
||||
require.Equal(t, []any{"text", "image"}, models[0]["input_modalities"])
|
||||
require.EqualValues(t, 1_000_000, models[0]["context_window"])
|
||||
}
|
||||
|
||||
// Scenario: 没有管理员模型配置时保留现有上游发现路径。
|
||||
func TestBuildGroupConfiguredCodexModelsManifestFallsThroughWithoutConfiguration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
Reference in New Issue
Block a user