mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
fix(gateway): list mixed antigravity models for gemini groups
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||
@@ -1315,3 +1316,166 @@ func modelIDsForTest(models []gatewayModelItemForTest) []string {
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1375,6 +1375,19 @@ func (s *GatewayService) DoGrokNativeResponsesJSON(ctx context.Context, account
|
||||
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 {
|
||||
cacheKey := modelsListCacheKey(groupID, platform)
|
||||
if s.modelsListCache != nil {
|
||||
@@ -1400,11 +1413,13 @@ func (s *GatewayService) GetAvailableModels(ctx context.Context, groupID *int64,
|
||||
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 != "" {
|
||||
filtered := make([]Account, 0)
|
||||
for _, acc := range accounts {
|
||||
if acc.Platform == platform {
|
||||
if acc.Platform == platform || mixedListingAccountAllowed(platform, &acc) {
|
||||
filtered = append(filtered, acc)
|
||||
}
|
||||
}
|
||||
@@ -1428,11 +1443,15 @@ func (s *GatewayService) GetAvailableModels(ctx context.Context, groupID *int64,
|
||||
}
|
||||
|
||||
mapping := acc.GetModelMapping()
|
||||
if len(mapping) > 0 {
|
||||
hasAnyMapping = true
|
||||
for model := range mapping {
|
||||
modelSet[model] = struct{}{}
|
||||
for model := range mapping {
|
||||
// Accounts pulled in through mixed scheduling only contribute the
|
||||
// models that belong to the listing platform (e.g. an antigravity
|
||||
// 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