mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:48:43 +08:00
Merge pull request #5938 from Hakunm/fix/google-one-model-catalog
fix(gemini): 限制 Google One OAuth 模型目录 / constrain Google One model catalog
This commit is contained in:
@@ -2614,8 +2614,14 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) {
|
||||
|
||||
// Handle Gemini accounts
|
||||
if account.IsGemini() {
|
||||
// For OAuth accounts: return default Gemini models
|
||||
// Consumer Google One OAuth still uses the legacy Gemini CLI / Code
|
||||
// Assist channel. Do not advertise newer 3.x or image models that the
|
||||
// channel cannot serve.
|
||||
if account.IsOAuth() {
|
||||
if account.IsGeminiGoogleOne() {
|
||||
response.Success(c, geminicli.GoogleOneModels)
|
||||
return
|
||||
}
|
||||
response.Success(c, geminicli.DefaultModels)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -287,6 +287,42 @@ func TestAccountHandlerGetAvailableModels_OpenAISparkShadowReturnsMappingModels(
|
||||
}, ids, "影子可用模型由 model_mapping 派生(非写死)")
|
||||
}
|
||||
|
||||
func TestAccountHandlerGetAvailableModels_GeminiGoogleOneUsesConservativeCatalog(t *testing.T) {
|
||||
svc := &availableModelsAdminService{
|
||||
stubAdminService: newStubAdminService(),
|
||||
account: service.Account{
|
||||
ID: 45,
|
||||
Name: "google-one",
|
||||
Platform: service.PlatformGemini,
|
||||
Type: service.AccountTypeOAuth,
|
||||
Status: service.StatusActive,
|
||||
Credentials: map[string]any{
|
||||
"oauth_type": "google_one",
|
||||
},
|
||||
},
|
||||
}
|
||||
router := setupAvailableModelsRouter(svc)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/45/models", nil)
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
var resp struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||
ids := make([]string, 0, len(resp.Data))
|
||||
for _, model := range resp.Data {
|
||||
ids = append(ids, model.ID)
|
||||
}
|
||||
require.ElementsMatch(t, []string{"gemini-2.0-flash", "gemini-2.5-flash", "gemini-2.5-pro"}, ids)
|
||||
require.NotContains(t, ids, "gemini-3.5-flash")
|
||||
require.NotContains(t, ids, "gemini-2.5-flash-image")
|
||||
}
|
||||
|
||||
func TestAccountHandlerSyncUpstreamModels_ConfigErrorReturnsBadRequest(t *testing.T) {
|
||||
svc := &availableModelsAdminService{
|
||||
stubAdminService: newStubAdminService(),
|
||||
|
||||
@@ -22,5 +22,24 @@ var DefaultModels = []Model{
|
||||
{ID: "gemini-3.1-flash-image", Type: "model", DisplayName: "Gemini 3.1 Flash Image", CreatedAt: ""},
|
||||
}
|
||||
|
||||
// GoogleOneModels is the conservative model set exposed for legacy Google One
|
||||
// OAuth accounts. Newer subscription models are served through Antigravity
|
||||
// OAuth rather than the retired consumer Gemini CLI / Code Assist channel.
|
||||
var GoogleOneModels = []Model{
|
||||
{ID: "gemini-2.5-flash", Type: "model", DisplayName: "Gemini 2.5 Flash", CreatedAt: ""},
|
||||
{ID: "gemini-2.5-pro", Type: "model", DisplayName: "Gemini 2.5 Pro", CreatedAt: ""},
|
||||
{ID: "gemini-2.0-flash", Type: "model", DisplayName: "Gemini 2.0 Flash", CreatedAt: ""},
|
||||
}
|
||||
|
||||
// GoogleOneModelMapping returns a new whitelist map for each account so callers
|
||||
// cannot mutate the package-level catalog.
|
||||
func GoogleOneModelMapping() map[string]string {
|
||||
mapping := make(map[string]string, len(GoogleOneModels))
|
||||
for _, model := range GoogleOneModels {
|
||||
mapping[model.ID] = model.ID
|
||||
}
|
||||
return mapping
|
||||
}
|
||||
|
||||
// DefaultTestModel is the default model to preselect in test flows.
|
||||
const DefaultTestModel = "gemini-2.0-flash"
|
||||
|
||||
@@ -21,3 +21,24 @@ func TestDefaultModels_ContainsImageModels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleOneModels_ExcludeUnsupportedNewAndImageModels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mapping := GoogleOneModelMapping()
|
||||
for _, id := range []string{"gemini-2.0-flash", "gemini-2.5-flash", "gemini-2.5-pro"} {
|
||||
if mapping[id] != id {
|
||||
t.Fatalf("expected Google One model %q to map to itself", id)
|
||||
}
|
||||
}
|
||||
for _, id := range []string{"gemini-2.5-flash-image", "gemini-3.1-flash-image", "gemini-3.5-flash"} {
|
||||
if _, ok := mapping[id]; ok {
|
||||
t.Fatalf("did not expect unsupported Google One model %q", id)
|
||||
}
|
||||
}
|
||||
|
||||
mapping["gemini-2.5-flash"] = "mutated"
|
||||
if GoogleOneModelMapping()["gemini-2.5-flash"] != "gemini-2.5-flash" {
|
||||
t.Fatal("GoogleOneModelMapping must return a defensive copy")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai_compat"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
|
||||
)
|
||||
@@ -324,6 +325,12 @@ func (a *Account) IsGeminiCodeAssist() bool {
|
||||
return oauthType == "code_assist"
|
||||
}
|
||||
|
||||
// IsGeminiGoogleOne reports whether this account uses the legacy consumer
|
||||
// Gemini CLI / Code Assist OAuth channel.
|
||||
func (a *Account) IsGeminiGoogleOne() bool {
|
||||
return a.Platform == PlatformGemini && a.Type == AccountTypeOAuth && a.GeminiOAuthType() == "google_one"
|
||||
}
|
||||
|
||||
func (a *Account) CanGetUsage() bool {
|
||||
return a.Type == AccountTypeOAuth
|
||||
}
|
||||
@@ -625,6 +632,9 @@ func (a *Account) resolveModelMapping(rawMapping map[string]any) map[string]stri
|
||||
return nil
|
||||
}
|
||||
if len(rawMapping) == 0 {
|
||||
if a.IsGeminiGoogleOne() {
|
||||
return geminicli.GoogleOneModelMapping()
|
||||
}
|
||||
// Antigravity 平台使用默认映射
|
||||
if a.Platform == domain.PlatformAntigravity {
|
||||
return domain.DefaultAntigravityModelMapping
|
||||
@@ -659,6 +669,9 @@ func (a *Account) resolveModelMapping(rawMapping map[string]any) map[string]stri
|
||||
}
|
||||
|
||||
// Antigravity 平台使用默认映射
|
||||
if a.IsGeminiGoogleOne() {
|
||||
return geminicli.GoogleOneModelMapping()
|
||||
}
|
||||
if a.Platform == domain.PlatformAntigravity {
|
||||
return domain.DefaultAntigravityModelMapping
|
||||
}
|
||||
|
||||
@@ -537,6 +537,52 @@ func TestAccountGetModelMapping_AntigravityEnsuresGeminiDefaultPassthroughs(t *t
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountGetModelMapping_GoogleOneUsesConservativeDefaults(t *testing.T) {
|
||||
account := &Account{
|
||||
Platform: PlatformGemini,
|
||||
Type: AccountTypeOAuth,
|
||||
Credentials: map[string]any{
|
||||
"oauth_type": "google_one",
|
||||
},
|
||||
}
|
||||
|
||||
mapping := account.GetModelMapping()
|
||||
for _, model := range []string{"gemini-2.0-flash", "gemini-2.5-flash", "gemini-2.5-pro"} {
|
||||
if mapping[model] != model {
|
||||
t.Fatalf("expected Google One model %q to map to itself, got %q", model, mapping[model])
|
||||
}
|
||||
}
|
||||
for _, model := range []string{"gemini-2.5-flash-image", "gemini-3.1-flash-image", "gemini-3.5-flash"} {
|
||||
if _, ok := mapping[model]; ok {
|
||||
t.Fatalf("did not expect unsupported Google One model %q", model)
|
||||
}
|
||||
}
|
||||
if account.IsModelSupported("gemini-3.5-flash") {
|
||||
t.Fatal("Google One defaults must not treat unsupported models as eligible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountGetModelMapping_GoogleOnePreservesExplicitMapping(t *testing.T) {
|
||||
account := &Account{
|
||||
Platform: PlatformGemini,
|
||||
Type: AccountTypeOAuth,
|
||||
Credentials: map[string]any{
|
||||
"oauth_type": "google_one",
|
||||
"model_mapping": map[string]any{
|
||||
"custom-model": "gemini-2.5-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mapping := account.GetModelMapping()
|
||||
if mapping["custom-model"] != "gemini-2.5-flash" {
|
||||
t.Fatalf("expected explicit Google One mapping to be preserved, got %v", mapping)
|
||||
}
|
||||
if _, ok := mapping["gemini-2.5-flash"]; ok {
|
||||
t.Fatalf("did not expect defaults to overwrite an explicit mapping: %v", mapping)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountGetModelMapping_AntigravityRespectsWildcardOverride(t *testing.T) {
|
||||
account := &Account{
|
||||
Platform: PlatformAntigravity,
|
||||
|
||||
Reference in New Issue
Block a user