mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-03 08:40:36 +00:00
Merge pull request #7169 from HuntercodeT/fix/gemini-mixed-model-listing
fix(gateway): list mixed Antigravity models for Gemini groups
This commit is contained in:
2 files changed
+189
-6
No files matched your search
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||||
@@ -1315,3 +1316,166 @@ func modelIDsForTest(models []gatewayModelItemForTest) []string {
|
|||||||
}
|
}
|
||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gemini 分组混合调度:组内 antigravity 账号的 gemini-* 映射也应出现在 /v1/models,
|
||||||
|
// 与路由层(listSchedulableAccountsOnce 的 useMixedScheduling)同源;claude-* 不带入,
|
||||||
|
// 非混合调度的异平台账号(anthropic)仍被过滤。
|
||||||
|
func TestGatewayModels_GeminiGroupIncludesAntigravityGeminiMappings(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
groupID := int64(22)
|
||||||
|
h := newGatewayModelsHandlerForTest(
|
||||||
|
&gatewayModelsAccountRepoStub{
|
||||||
|
byGroup: map[int64][]service.Account{
|
||||||
|
groupID: {
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Platform: service.PlatformAntigravity,
|
||||||
|
Extra: map[string]any{"mixed_scheduling": true},
|
||||||
|
Credentials: map[string]any{
|
||||||
|
"model_mapping": map[string]any{
|
||||||
|
"gemini-3.8-flash-high": "gemini-3.8-flash-high",
|
||||||
|
"gemini-synced-custom": "gemini-3.8-flash-high",
|
||||||
|
"gemini-3.8-flash-low": "gemini-3.8-flash-low",
|
||||||
|
"claude-sonnet-4-6": "claude-sonnet-4-6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ID: 3, Platform: service.PlatformAntigravity,
|
||||||
|
Extra: map[string]any{"mixed_scheduling": false},
|
||||||
|
Credentials: map[string]any{"model_mapping": map[string]any{"gemini-disabled-only": "gemini-3.8-flash-high"}}},
|
||||||
|
{ID: 4, Platform: service.PlatformAntigravity,
|
||||||
|
Credentials: map[string]any{"model_mapping": map[string]any{"gemini-unset-only": "gemini-3.8-flash-high"}}},
|
||||||
|
{ID: 5, Platform: service.PlatformGemini,
|
||||||
|
Credentials: map[string]any{"model_mapping": map[string]any{"gemini-native-only": "gemini-2.5-pro"}}},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Platform: service.PlatformAnthropic,
|
||||||
|
Credentials: map[string]any{
|
||||||
|
"model_mapping": map[string]any{
|
||||||
|
"claude-opus-4-6": "claude-opus-4-6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(rec)
|
||||||
|
c.Request = httptest.NewRequest(http.MethodGet, "/v1/models", nil)
|
||||||
|
c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{
|
||||||
|
Group: &service.Group{ID: groupID, Platform: service.PlatformGemini},
|
||||||
|
})
|
||||||
|
|
||||||
|
h.Models(c)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
|
var got gatewayModelsResponseForTest
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
||||||
|
ids := modelIDsForTest(got.Data)
|
||||||
|
// 账号显式配置的 gemini-* 映射必须出现(resolveModelMapping 还会补一组 antigravity
|
||||||
|
// 默认透传别名,这里只断言显式条目与排除项,不锁定补齐后的完整集合)。
|
||||||
|
require.Contains(t, ids, "gemini-3.8-flash-high")
|
||||||
|
require.Contains(t, ids, "gemini-synced-custom")
|
||||||
|
require.Contains(t, ids, "gemini-3.8-flash-low")
|
||||||
|
require.Contains(t, ids, "gemini-native-only")
|
||||||
|
require.NotContains(t, ids, "gemini-disabled-only")
|
||||||
|
require.NotContains(t, ids, "gemini-unset-only")
|
||||||
|
// antigravity 账号的 claude-* 映射与 anthropic 账号的映射都不应出现在 gemini 分组。
|
||||||
|
require.NotContains(t, ids, "claude-sonnet-4-6")
|
||||||
|
require.NotContains(t, ids, "claude-opus-4-6")
|
||||||
|
for _, id := range ids {
|
||||||
|
require.True(t, strings.HasPrefix(id, "gemini-"), "unexpected non-gemini model on gemini group: %s", id)
|
||||||
|
}
|
||||||
|
// 没有回落到 geminicli 静态表。
|
||||||
|
require.NotContains(t, ids, "gemini-2.0-flash")
|
||||||
|
}
|
||||||
|
|
||||||
|
// antigravity 账号未配置 model_mapping 时使用 DefaultAntigravityModelMapping:
|
||||||
|
// gemini 分组应列出其中的 gemini-* 条目,而不是回落到 geminicli 静态表;claude-* 仍不出现。
|
||||||
|
func TestGatewayModels_GeminiGroupUsesAntigravityDefaultMappingWhenUnset(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
groupID := int64(23)
|
||||||
|
h := newGatewayModelsHandlerForTest(
|
||||||
|
&gatewayModelsAccountRepoStub{
|
||||||
|
byGroup: map[int64][]service.Account{
|
||||||
|
groupID: {
|
||||||
|
{ID: 1, Platform: service.PlatformAntigravity, Extra: map[string]any{"mixed_scheduling": true}, Credentials: map[string]any{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(rec)
|
||||||
|
c.Request = httptest.NewRequest(http.MethodGet, "/v1/models", nil)
|
||||||
|
c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{
|
||||||
|
Group: &service.Group{ID: groupID, Platform: service.PlatformGemini},
|
||||||
|
})
|
||||||
|
|
||||||
|
h.Models(c)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
|
var got gatewayModelsResponseForTest
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
||||||
|
ids := modelIDsForTest(got.Data)
|
||||||
|
require.Contains(t, ids, "gemini-3.8-flash-high")
|
||||||
|
for _, id := range ids {
|
||||||
|
require.True(t, strings.HasPrefix(id, "gemini-"), "unexpected non-gemini model on gemini group: %s", id)
|
||||||
|
}
|
||||||
|
// geminicli 静态表独有条目不应出现(说明没有回落到默认列表)。
|
||||||
|
require.NotContains(t, ids, "gemini-2.0-flash")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Codex 通过 /models?client_version= 走 CodexModels,同样应看到混合调度账号的 gemini-* 映射。
|
||||||
|
func TestGatewayModels_CodexGeminiGroupListsAntigravityGeminiMappings(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
groupID := int64(24)
|
||||||
|
h := newGatewayModelsHandlerForTest(
|
||||||
|
&gatewayModelsAccountRepoStub{
|
||||||
|
byGroup: map[int64][]service.Account{
|
||||||
|
groupID: {
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Platform: service.PlatformAntigravity,
|
||||||
|
Extra: map[string]any{"mixed_scheduling": true},
|
||||||
|
Credentials: map[string]any{
|
||||||
|
"model_mapping": map[string]any{
|
||||||
|
"gemini-3.8-flash-high": "gemini-3.8-flash-high",
|
||||||
|
"gemini-synced-custom": "gemini-3.8-flash-high",
|
||||||
|
"claude-sonnet-4-6": "claude-sonnet-4-6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(rec)
|
||||||
|
c.Request = httptest.NewRequest(http.MethodGet, "/models?client_version=0.147.0", nil)
|
||||||
|
c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{
|
||||||
|
Group: &service.Group{ID: groupID, Platform: service.PlatformGemini},
|
||||||
|
})
|
||||||
|
|
||||||
|
h.CodexModels(c)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
var got codexModelsResponseForTest
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
||||||
|
slugs := codexModelSlugsForTest(got.Models)
|
||||||
|
require.Contains(t, slugs, "gemini-3.8-flash-high")
|
||||||
|
require.Contains(t, slugs, "gemini-synced-custom")
|
||||||
|
require.NotContains(t, slugs, "claude-sonnet-4-6")
|
||||||
|
require.NotContains(t, slugs, "gemini-2.0-flash")
|
||||||
|
for _, slug := range slugs {
|
||||||
|
require.True(t, strings.HasPrefix(slug, "gemini-"), "unexpected non-gemini model on gemini group: %s", slug)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1377,6 +1377,19 @@ func (s *GatewayService) DoGrokNativeResponsesJSON(ctx context.Context, account
|
|||||||
return respBytes, nil
|
return respBytes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mixedListingAccountAllowed mirrors the mixed-scheduling rule in
|
||||||
|
// GeminiMessagesCompatService.listSchedulableAccountsOnce: a gemini group may be
|
||||||
|
// served by antigravity accounts, so model listing must consider them too.
|
||||||
|
func mixedListingAccountAllowed(groupPlatform string, account *Account) bool {
|
||||||
|
return groupPlatform == PlatformGemini && account.IsMixedSchedulingEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
// mixedListingModelAllowed limits what a mixed-scheduling account may advertise
|
||||||
|
// on the group's platform: only gemini-* wire IDs are meaningful on a gemini group.
|
||||||
|
func mixedListingModelAllowed(groupPlatform, model string) bool {
|
||||||
|
return groupPlatform == PlatformGemini && isAntigravityGeminiModel(model)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *GatewayService) GetAvailableModels(ctx context.Context, groupID *int64, platform string) []string {
|
func (s *GatewayService) GetAvailableModels(ctx context.Context, groupID *int64, platform string) []string {
|
||||||
cacheKey := modelsListCacheKey(groupID, platform)
|
cacheKey := modelsListCacheKey(groupID, platform)
|
||||||
if s.modelsListCache != nil {
|
if s.modelsListCache != nil {
|
||||||
@@ -1402,11 +1415,13 @@ func (s *GatewayService) GetAvailableModels(ctx context.Context, groupID *int64,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by platform if specified
|
// Filter by platform if specified. Mixed scheduling (a gemini group routing
|
||||||
|
// to antigravity accounts) is honoured here as well, so the advertised list
|
||||||
|
// stays in sync with what the request path can actually serve.
|
||||||
if platform != "" {
|
if platform != "" {
|
||||||
filtered := make([]Account, 0)
|
filtered := make([]Account, 0)
|
||||||
for _, acc := range accounts {
|
for _, acc := range accounts {
|
||||||
if acc.Platform == platform {
|
if acc.Platform == platform || mixedListingAccountAllowed(platform, &acc) {
|
||||||
filtered = append(filtered, acc)
|
filtered = append(filtered, acc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1430,11 +1445,15 @@ func (s *GatewayService) GetAvailableModels(ctx context.Context, groupID *int64,
|
|||||||
}
|
}
|
||||||
|
|
||||||
mapping := acc.GetModelMapping()
|
mapping := acc.GetModelMapping()
|
||||||
if len(mapping) > 0 {
|
for model := range mapping {
|
||||||
hasAnyMapping = true
|
// Accounts pulled in through mixed scheduling only contribute the
|
||||||
for model := range mapping {
|
// models that belong to the listing platform (e.g. an antigravity
|
||||||
modelSet[model] = struct{}{}
|
// account's claude-* mappings must not surface on a gemini group).
|
||||||
|
if platform != "" && acc.Platform != platform && !mixedListingModelAllowed(platform, model) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
modelSet[model] = struct{}{}
|
||||||
|
hasAnyMapping = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user