mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
137 lines
6.4 KiB
Go
137 lines
6.4 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/gemini"
|
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestGeminiNativeModelsUsesAccountMappings(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
forced, mixed, allowlist bool
|
|
status int
|
|
}{
|
|
{"mixed", false, true, false, 200},
|
|
{"mixed allowlist", false, true, true, 200},
|
|
{"disabled mixed", false, false, false, 503},
|
|
{"forced without mixed opt-in", true, false, false, 200},
|
|
{"forced allowlist", true, false, true, 200},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
groupID := int64(45)
|
|
repo := &geminiAllowlistAccountRepoStub{gatewayModelsAccountRepoStub: gatewayModelsAccountRepoStub{byGroup: map[int64][]service.Account{
|
|
groupID: {{ID: 1, Platform: service.PlatformAntigravity,
|
|
Extra: map[string]any{"mixed_scheduling": tt.mixed},
|
|
Credentials: map[string]any{"model_mapping": map[string]any{"gemini-synced-custom": "gemini-3.8-flash-high", "claude-custom": "claude-sonnet-4-6"}}}},
|
|
}}}
|
|
h := &GatewayHandler{geminiCompatService: service.NewGeminiMessagesCompatService(repo, nil, nil, nil, nil, nil, nil, nil, nil)}
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/v1beta/models", nil)
|
|
c.Set(string(middleware.ContextKeyAPIKey), &service.APIKey{GroupID: &groupID, Group: &service.Group{ID: groupID, Platform: service.PlatformGemini,
|
|
ModelAllowlist: service.GroupModelAllowlist{Enabled: tt.allowlist, Models: []string{"gemini-synced-custom"}},
|
|
}})
|
|
if tt.forced {
|
|
c.Set(string(middleware.ContextKeyForcePlatform), service.PlatformAntigravity)
|
|
}
|
|
h.GeminiV1BetaListModels(c)
|
|
require.Equal(t, tt.status, rec.Code, rec.Body.String())
|
|
if tt.status != 200 {
|
|
return
|
|
}
|
|
var got gemini.ModelsListResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
|
names := []string{}
|
|
for _, model := range got.Models {
|
|
names = append(names, model.Name)
|
|
require.Contains(t, model.SupportedGenerationMethods, "generateContent")
|
|
}
|
|
require.Contains(t, names, "models/gemini-synced-custom")
|
|
require.NotContains(t, names, "models/claude-custom")
|
|
require.NotContains(t, names, "models/gemini-2.0-flash")
|
|
if tt.allowlist {
|
|
require.Equal(t, []string{"models/gemini-synced-custom"}, names)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAppendUpstreamGeminiModelsPreservesMetadata(t *testing.T) {
|
|
body := []byte(`{"models":[{"name":"models/gemini-native","inputTokenLimit":123,"custom":{"a":true}}],"nextPageToken":"next","other":42}`)
|
|
extra := []gemini.Model{gemini.FallbackModel("gemini-native"), gemini.FallbackModel("gemini-synced-custom"), gemini.FallbackModel("gemini-synced-custom")}
|
|
merged, ok := appendUpstreamGeminiModels(body, extra)
|
|
require.True(t, ok)
|
|
require.JSONEq(t, `{"models":[{"name":"models/gemini-native","inputTokenLimit":123,"custom":{"a":true}},{"name":"models/gemini-synced-custom","supportedGenerationMethods":["generateContent","streamGenerateContent"]}],"nextPageToken":"next","other":42}`, string(merged))
|
|
filtered, dropped, ok := filterUpstreamGeminiModelsBody(merged, service.GroupModelAllowlist{Enabled: true, Models: []string{"gemini-synced-*"}})
|
|
require.True(t, ok)
|
|
require.True(t, dropped)
|
|
require.NotContains(t, string(filtered), "gemini-native")
|
|
require.Contains(t, string(filtered), "gemini-synced-custom")
|
|
for _, invalid := range []string{`null`, `not-json`, `{"error":"bad"}`, `{"models":{}}`} {
|
|
got, ok := appendUpstreamGeminiModels([]byte(invalid), extra)
|
|
require.False(t, ok)
|
|
require.Equal(t, invalid, string(got))
|
|
}
|
|
}
|
|
|
|
// Exercise the real handler's native-upstream branch, including scope fallback.
|
|
type geminiMixedModelsUpstream struct {
|
|
service.HTTPUpstream
|
|
status int
|
|
body string
|
|
}
|
|
|
|
func (u *geminiMixedModelsUpstream) Do(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
|
|
return &http.Response{StatusCode: u.status, Header: http.Header{"Content-Type": {"application/json"}, "X-Request-Id": {"native-id"}}, Body: io.NopCloser(strings.NewReader(u.body))}, nil
|
|
}
|
|
func TestGeminiNativeModelsMergesNativeUpstream(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
status int
|
|
body string
|
|
expectedNative string
|
|
}{
|
|
{"native", 200, `{"models":[{"name":"models/gemini-native","inputTokenLimit":123}],"nextPageToken":"next"}`, "models/gemini-native"},
|
|
{"scope fallback", 403, `{"error":"insufficient authentication scopes"}`, "models/gemini-2.5-pro"},
|
|
{"upstream error", 429, `{"error":"rate limited"}`, ""},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
id := int64(46)
|
|
repo := &geminiAllowlistAccountRepoStub{gatewayModelsAccountRepoStub: gatewayModelsAccountRepoStub{byGroup: map[int64][]service.Account{id: {
|
|
{ID: 1, Platform: service.PlatformGemini, Type: service.AccountTypeAPIKey, Credentials: map[string]any{"api_key": "test"}},
|
|
{ID: 2, Platform: service.PlatformAntigravity, Extra: map[string]any{"mixed_scheduling": true}, Credentials: map[string]any{"model_mapping": map[string]any{"gemini-synced-custom": "gemini-3.8-flash-high"}}},
|
|
}}}}
|
|
upstream := &geminiMixedModelsUpstream{status: tt.status, body: tt.body}
|
|
h := &GatewayHandler{geminiCompatService: service.NewGeminiMessagesCompatService(repo, nil, nil, nil, nil, nil, upstream, nil, &config.Config{})}
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/v1beta/models", nil)
|
|
c.Set(string(middleware.ContextKeyAPIKey), &service.APIKey{GroupID: &id, Group: &service.Group{ID: id, Platform: service.PlatformGemini}})
|
|
h.GeminiV1BetaListModels(c)
|
|
if tt.status == 429 {
|
|
require.Equal(t, 429, rec.Code)
|
|
require.JSONEq(t, tt.body, rec.Body.String())
|
|
return
|
|
}
|
|
require.Equal(t, 200, rec.Code, rec.Body.String())
|
|
require.Contains(t, rec.Body.String(), tt.expectedNative)
|
|
require.Contains(t, rec.Body.String(), "models/gemini-synced-custom")
|
|
if tt.status == 200 {
|
|
require.Contains(t, rec.Body.String(), `"inputTokenLimit":123`)
|
|
require.Contains(t, rec.Body.String(), `"nextPageToken":"next"`)
|
|
require.Equal(t, "native-id", rec.Header().Get("X-Request-Id"))
|
|
}
|
|
})
|
|
}
|
|
}
|