From ecba49f711d5f514cbc302a8c36d5c55a48c348c Mon Sep 17 00:00:00 2001 From: HuntercodeT <61233559+HuntercodeT@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:51:06 +0800 Subject: [PATCH] fix(gateway): let OpenAI passthrough accounts supplement model discovery instead of hiding mapped models A single passthrough account made GetAvailableModels return nil for the whole group, so every other account's model_mapping was dropped from /v1/models and the Codex catalog. Treat passthrough accounts like unmapped ones: skip their (possibly stale) mapping and let supplementUnmappedOpenAIModels add the default set, while ordinary accounts' mappings still reach the list. Refs #6647, #7517 --- backend/internal/handler/gateway_models_test.go | 12 ++++++++++++ .../service/gateway_hotpath_optimization_test.go | 11 ++++++++--- backend/internal/service/gateway_service.go | 14 ++++++-------- backend/internal/service/group_model_allowlist.go | 8 +++++--- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/backend/internal/handler/gateway_models_test.go b/backend/internal/handler/gateway_models_test.go index 81df742e9b..08da8e97c3 100644 --- a/backend/internal/handler/gateway_models_test.go +++ b/backend/internal/handler/gateway_models_test.go @@ -287,6 +287,18 @@ func TestGatewayModels_UnmappedOpenAIAccountsSupplementMappedModels(t *testing.T accounts: accounts[1:], want: []string{sparkModel, alias}, }, + { + // A passthrough account with a stale mapping behaves like an unmapped + // one: it adds the defaults but never its own mapping keys, and it no + // longer hides the aliases declared on ordinary accounts. + name: "passthrough account contributes defaults without hiding mapped aliases", + accounts: append([]service.Account{{ + ID: 5, Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth, + Credentials: map[string]any{"model_mapping": map[string]any{"stale-model": "stale-model"}}, + Extra: map[string]any{"openai_passthrough": true}, + }}, accounts[1:]...), + want: append(openai.DefaultModelIDs(), alias), + }, { name: "unmapped accounts from another platform do not add defaults", accounts: append([]service.Account{{ID: 4, Platform: service.PlatformAnthropic}}, accounts[1:]...), diff --git a/backend/internal/service/gateway_hotpath_optimization_test.go b/backend/internal/service/gateway_hotpath_optimization_test.go index 8855b13e52..7a44456102 100644 --- a/backend/internal/service/gateway_hotpath_optimization_test.go +++ b/backend/internal/service/gateway_hotpath_optimization_test.go @@ -10,6 +10,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey" + "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" gocache "github.com/patrickmn/go-cache" "github.com/stretchr/testify/require" @@ -670,7 +671,9 @@ func TestGetAvailableModels_OpenAIPassthroughUsesDefaultFallback(t *testing.T) { want: nil, }, { - name: "passthrough wins over ordinary account mapping", + // The passthrough account serves the default set (its stale mapping is + // ignored), while the ordinary account's mapping still reaches the list. + name: "passthrough contributes defaults alongside ordinary account mapping", accounts: []Account{ { ID: 2, @@ -684,7 +687,7 @@ func TestGetAvailableModels_OpenAIPassthroughUsesDefaultFallback(t *testing.T) { Extra: map[string]any{"openai_passthrough": true}, }, }, - want: nil, + want: dedupeAndSortModelIDs(append([]string{"configured-model"}, openai.DefaultModelIDs()...)), }, { name: "ordinary accounts preserve mapped whitelist", @@ -708,7 +711,9 @@ func TestGetAvailableModels_OpenAIPassthroughUsesDefaultFallback(t *testing.T) { modelsListCacheTTL: time.Minute, } - require.Equal(t, tt.want, svc.GetAvailableModels(context.Background(), &groupID, PlatformOpenAI)) + got := svc.GetAvailableModels(context.Background(), &groupID, PlatformOpenAI) + require.Equal(t, tt.want, got) + require.NotContains(t, got, "stale-model", "passthrough mapping must never reach the public list") }) } } diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 26a2c5f90b..eb782d9a1e 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -1433,15 +1433,13 @@ func (s *GatewayService) GetAvailableModels(ctx context.Context, groupID *int64, hasAnyMapping := false for _, acc := range accounts { - // Passthrough routing accepts models independently of model_mapping. A stale - // mapping on any eligible passthrough account therefore cannot define the - // public whitelist; return nil so the handler uses its default model set. + // Passthrough routing accepts models independently of model_mapping, so a + // stale mapping on a passthrough account must not narrow the public list. + // Treat it like an unmapped account: skip its mapping here and let + // supplementUnmappedOpenAIModels contribute the default set. Mappings on + // the ordinary accounts in the same group still count. if platform == PlatformOpenAI && acc.IsOpenAIPassthroughEnabled() { - if s.modelsListCache != nil { - s.modelsListCache.Set(cacheKey, []string(nil), s.modelsListCacheTTL) - modelsListCacheStoreTotal.Add(1) - } - return nil + continue } mapping := acc.GetModelMapping() diff --git a/backend/internal/service/group_model_allowlist.go b/backend/internal/service/group_model_allowlist.go index 6ef6b41a85..165fded4b8 100644 --- a/backend/internal/service/group_model_allowlist.go +++ b/backend/internal/service/group_model_allowlist.go @@ -29,15 +29,17 @@ func GroupModelAllowlistFromDomain(cfg domain.GroupModelAllowlist) GroupModelAll } // supplementUnmappedOpenAIModels ensures a partial mapping catalog does not -// hide models from unmapped OpenAI accounts. An empty catalog is left unchanged -// so callers retain their existing discovery fallback. +// hide models from unmapped or passthrough OpenAI accounts (passthrough routing +// ignores model_mapping, so it serves the same default set as an unmapped +// account). An empty catalog is left unchanged so callers retain their existing +// discovery fallback. func supplementUnmappedOpenAIModels(accounts []Account, models []string) []string { if len(models) == 0 { return models } for i := range accounts { account := &accounts[i] - if account.Platform == PlatformOpenAI && len(account.GetModelMapping()) == 0 { + if account.Platform == PlatformOpenAI && (account.IsOpenAIPassthroughEnabled() || len(account.GetModelMapping()) == 0) { return dedupeAndSortModelIDs(slices.Concat(models, openai.DefaultModelIDs())) } }