| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317 |
- package handler
- import (
- "context"
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
- "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
- middleware2 "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"
- )
- type gatewayModelsAccountRepoStub struct {
- service.AccountRepository
- byGroup map[int64][]service.Account
- }
- type gatewayModelsResponseForTest struct {
- Object string `json:"object"`
- Data []gatewayModelItemForTest `json:"data"`
- }
- type codexModelsResponseForTest struct {
- Models []struct {
- Slug string `json:"slug"`
- SupportedReasoningLevels []codexReasoningLevelForTest `json:"supported_reasoning_levels"`
- InputModalities []string `json:"input_modalities"`
- ModelMessages map[string]json.RawMessage `json:"model_messages"`
- TruncationPolicy map[string]json.RawMessage `json:"truncation_policy"`
- AvailabilityNUX json.RawMessage `json:"availability_nux"`
- Upgrade json.RawMessage `json:"upgrade"`
- } `json:"models"`
- }
- type codexReasoningLevelForTest struct {
- Effort string `json:"effort"`
- }
- type gatewayModelItemForTest struct {
- ID string `json:"id"`
- Object string `json:"object"`
- Created int64 `json:"created"`
- OwnedBy string `json:"owned_by"`
- CreatedAt string `json:"created_at"`
- SupportsReasoningEffort bool `json:"supportsReasoningEffort"`
- ReasoningEffort string `json:"reasoningEffort"`
- ReasoningEfforts []gatewayReasoningEffortOptionForTest `json:"reasoningEfforts"`
- }
- type gatewayReasoningEffortOptionForTest struct {
- Value string `json:"value"`
- Label string `json:"label"`
- Default bool `json:"default"`
- }
- func (s *gatewayModelsAccountRepoStub) ListSchedulableByGroupID(ctx context.Context, groupID int64) ([]service.Account, error) {
- accounts, ok := s.byGroup[groupID]
- if !ok {
- return nil, nil
- }
- out := make([]service.Account, len(accounts))
- copy(out, accounts)
- return out, nil
- }
- func (s *gatewayModelsAccountRepoStub) ListByGroup(ctx context.Context, groupID int64) ([]service.Account, error) {
- return s.ListSchedulableByGroupID(ctx, groupID)
- }
- func (s *gatewayModelsAccountRepoStub) ListModelAvailabilityCandidates(ctx context.Context, groupID *int64, _ []string, _ bool) ([]service.Account, error) {
- if groupID == nil {
- return nil, nil
- }
- return s.ListSchedulableByGroupID(ctx, *groupID)
- }
- func newGatewayModelsHandlerForTest(repo service.AccountRepository) *GatewayHandler {
- return &GatewayHandler{
- gatewayService: service.NewGatewayService(
- repo,
- nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
- nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
- ),
- }
- }
- func TestDefaultModelIDsForCompositeIncludesAntigravityDefaults(t *testing.T) {
- antigravityIDs := defaultModelIDsForPlatform(service.PlatformAntigravity)
- require.NotEmpty(t, antigravityIDs)
- compositeIDs := defaultModelIDsForPlatform(service.PlatformComposite)
- require.Contains(t, compositeIDs, antigravityIDs[0])
- }
- // Scenario: Anthropic defaults contain only Claude while Antigravity keeps its own Gemini models.
- func TestDefaultModelIDsForAnthropicExcludeAntigravityGemini(t *testing.T) {
- anthropicIDs := defaultModelIDsForPlatform(service.PlatformAnthropic)
- require.Contains(t, anthropicIDs, "claude-opus-4-6")
- require.NotContains(t, anthropicIDs, "gemini-2.5-flash")
- antigravityIDs := defaultModelIDsForPlatform(service.PlatformAntigravity)
- require.Contains(t, antigravityIDs, "gemini-2.5-flash")
- }
- // Scenario: non-OpenAI groups return a Codex manifest instead of a standard model list.
- func TestGatewayCodexModels_NonOpenAIGroupsUseMappedModels(t *testing.T) {
- tests := []struct {
- name string
- platform string
- model string
- efforts []string
- modalities []string
- }{
- {
- name: "Grok",
- platform: service.PlatformGrok,
- model: "grok-4.6",
- efforts: []string{"low", "medium", "high", "xhigh"},
- modalities: []string{"text", "image"},
- },
- {
- name: "DeepSeek",
- platform: service.PlatformDeepseek,
- model: "deepseek-v4-pro",
- efforts: []string{"low", "high", "max"},
- modalities: []string{"text"},
- },
- {
- name: "provider-qualified Claude",
- platform: service.PlatformAnthropic,
- model: "anthropic/claude-sonnet-4-6",
- efforts: []string{"low", "medium", "high", "max"},
- modalities: []string{"text"},
- },
- }
- for index, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(100 + index)
- h := newGatewayModelsHandlerForTest(&gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: tt.platform,
- Credentials: map[string]any{
- "model_mapping": map[string]any{tt.model: tt.model},
- },
- },
- },
- },
- })
- 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: tt.platform},
- })
- h.CodexModels(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got codexModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Len(t, got.Models, 1)
- require.Equal(t, tt.model, got.Models[0].Slug)
- require.NotEmpty(t, got.Models[0].ModelMessages)
- require.NotEmpty(t, got.Models[0].TruncationPolicy)
- require.NotNil(t, got.Models[0].AvailabilityNUX)
- require.NotNil(t, got.Models[0].Upgrade)
- require.Equal(t, tt.efforts, codexReasoningEffortsForTest(got.Models[0].SupportedReasoningLevels))
- require.Equal(t, tt.modalities, got.Models[0].InputModalities)
- })
- }
- }
- // Composite manifests include defaults from unmapped accounts and explicit mappings.
- func TestGatewayCodexModels_CompositeUsesCompleteEffectiveModelList(t *testing.T) {
- gin.SetMode(gin.TestMode)
- const groupID int64 = 120
- h := newGatewayModelsHandlerForTest(&gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 3,
- Platform: service.PlatformOpenAI,
- Status: service.StatusActive,
- Schedulable: true,
- Credentials: map[string]any{},
- },
- {
- ID: 1,
- Platform: service.PlatformOpenAI,
- Credentials: map[string]any{
- "model_mapping": map[string]any{"gpt-5.5": "gpt-5.5"},
- },
- },
- {
- ID: 2,
- Platform: service.PlatformGrok,
- Credentials: map[string]any{
- "model_mapping": map[string]any{"grok-4.6": "grok-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.PlatformComposite},
- })
- h.CodexModels(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got codexModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- want := service.FilterCodexModelIDsForGroup(openai.DefaultModelIDs(), nil)
- require.ElementsMatch(t, append(want, "grok-4.6"), codexModelSlugsForTest(got.Models))
- }
- func TestGatewayModels_UnmappedOpenAIAccountsSupplementMappedModels(t *testing.T) {
- gin.SetMode(gin.TestMode)
- const groupID int64 = 28
- const sparkModel = "gpt-5.3-codex-spark"
- const alias = "team-coder"
- parentID := int64(1)
- accounts := []service.Account{
- {ID: parentID, Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth},
- {
- ID: 2, Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth,
- ParentAccountID: &parentID, QuotaDimension: "spark",
- Credentials: map[string]any{"model_mapping": map[string]any{sparkModel: sparkModel}},
- },
- {
- ID: 3, Platform: service.PlatformOpenAI, Type: service.AccountTypeAPIKey,
- Credentials: map[string]any{"model_mapping": map[string]any{alias: "gpt-5.6-sol"}},
- },
- }
- tests := []struct {
- name string
- accounts []service.Account
- config service.GroupModelAllowlist
- want []string
- }{
- {
- name: "unmapped parent and Spark shadow retain defaults and aliases",
- accounts: accounts,
- want: append(openai.DefaultModelIDs(), alias),
- },
- {
- name: "unmapped API key account also contributes defaults",
- accounts: append([]service.Account{{ID: 4, Platform: service.PlatformOpenAI, Type: service.AccountTypeAPIKey}}, accounts[1:]...),
- want: append(openai.DefaultModelIDs(), alias),
- },
- {
- name: "unmapped accounts alone retain default response shape",
- accounts: accounts[:1],
- want: openai.DefaultModelIDs(),
- },
- {
- name: "custom list can select defaults and aliases",
- accounts: accounts,
- config: service.GroupModelAllowlist{Enabled: true, Models: []string{alias, "gpt-5.6-sol", sparkModel, "unknown-model"}},
- want: []string{alias, "gpt-5.6-sol", sparkModel},
- },
- {
- name: "unavailable custom selection remains empty",
- accounts: accounts,
- config: service.GroupModelAllowlist{Enabled: true, Models: []string{"unknown-model"}},
- want: []string{},
- },
- {
- name: "mapped accounts alone do not gain defaults",
- accounts: accounts[1:],
- want: []string{sparkModel, alias},
- },
- {
- name: "unmapped accounts from another platform do not add defaults",
- accounts: append([]service.Account{{ID: 4, Platform: service.PlatformAnthropic}}, accounts[1:]...),
- want: []string{sparkModel, alias},
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- h := newGatewayModelsHandlerForTest(&gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{groupID: tt.accounts},
- })
- for range 2 {
- 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.PlatformOpenAI, ModelAllowlist: tt.config},
- })
- h.Models(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got gatewayModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Equal(t, "list", got.Object)
- require.ElementsMatch(t, tt.want, modelIDsForTest(got.Data))
- for _, model := range got.Data {
- require.Equal(t, "model", model.Object, model.ID)
- require.Positive(t, model.Created, model.ID)
- require.Equal(t, "openai", model.OwnedBy, model.ID)
- require.Empty(t, model.CreatedAt, model.ID)
- }
- if tt.config.Enabled {
- require.Equal(t, tt.want, modelIDsForTest(got.Data))
- }
- }
- })
- }
- require.Empty(t, accounts[0].GetModelMapping())
- require.True(t, accounts[0].IsModelSupported("gpt-future-model"))
- require.False(t, accounts[1].IsModelSupported("gpt-5.6-sol"))
- }
- func TestGatewayCodexModels_GeneratedManifestUsesFinalBodyETag(t *testing.T) {
- gin.SetMode(gin.TestMode)
- const groupID int64 = 122
- h := newGatewayModelsHandlerForTest(&gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {{
- ID: 1,
- Platform: service.PlatformDeepseek,
- Credentials: map[string]any{
- "model_mapping": map[string]any{"deepseek-v4-pro": "deepseek-v4-pro"},
- },
- }},
- },
- })
- group := &service.Group{ID: groupID, Platform: service.PlatformDeepseek}
- first := httptest.NewRecorder()
- firstContext, _ := gin.CreateTestContext(first)
- firstContext.Request = httptest.NewRequest(http.MethodGet, "/models?client_version=0.147.0", nil)
- firstContext.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{Group: group})
- h.CodexModels(firstContext)
- require.Equal(t, http.StatusOK, first.Code)
- etag := first.Header().Get("ETag")
- require.NotEmpty(t, etag)
- require.Equal(t, service.CodexModelsManifestETag(first.Body.Bytes()), etag)
- second := httptest.NewRecorder()
- secondContext, _ := gin.CreateTestContext(second)
- secondContext.Request = httptest.NewRequest(http.MethodGet, "/models?client_version=0.147.0", nil)
- secondContext.Request.Header.Set("If-None-Match", "W/"+etag)
- secondContext.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{Group: group})
- h.CodexModels(secondContext)
- require.Equal(t, http.StatusNotModified, second.Code)
- require.Empty(t, second.Body.Bytes())
- require.Equal(t, etag, second.Header().Get("ETag"))
- }
- // Scenario: group model_allowlist limits the generated Codex manifest.
- func TestGatewayCodexModels_CustomModelsListFiltersCompositeManifest(t *testing.T) {
- gin.SetMode(gin.TestMode)
- const groupID int64 = 121
- h := newGatewayModelsHandlerForTest(&gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: service.PlatformOpenAI,
- Credentials: map[string]any{
- "model_mapping": map[string]any{"gpt-5.5": "gpt-5.5"},
- },
- },
- {
- ID: 2,
- Platform: service.PlatformGrok,
- Credentials: map[string]any{
- "model_mapping": map[string]any{"grok-4.6": "grok-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.PlatformComposite,
- ModelAllowlist: service.GroupModelAllowlist{
- Enabled: true,
- Models: []string{"grok-4.6"},
- },
- },
- })
- h.CodexModels(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got codexModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Equal(t, []string{"grok-4.6"}, codexModelSlugsForTest(got.Models))
- }
- func codexModelSlugsForTest(models []struct {
- Slug string `json:"slug"`
- SupportedReasoningLevels []codexReasoningLevelForTest `json:"supported_reasoning_levels"`
- InputModalities []string `json:"input_modalities"`
- ModelMessages map[string]json.RawMessage `json:"model_messages"`
- TruncationPolicy map[string]json.RawMessage `json:"truncation_policy"`
- AvailabilityNUX json.RawMessage `json:"availability_nux"`
- Upgrade json.RawMessage `json:"upgrade"`
- }) []string {
- slugs := make([]string, 0, len(models))
- for _, model := range models {
- slugs = append(slugs, model.Slug)
- }
- return slugs
- }
- func codexReasoningEffortsForTest(levels []codexReasoningLevelForTest) []string {
- efforts := make([]string, 0, len(levels))
- for _, level := range levels {
- efforts = append(efforts, level.Effort)
- }
- return efforts
- }
- func TestGatewayModels_GeminiGroupFallsBackToGeminiModels(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(20)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {ID: 1, Platform: service.PlatformGemini},
- },
- },
- },
- )
- 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))
- require.Equal(t, "list", got.Object)
- require.Contains(t, modelIDsForTest(got.Data), "gemini-2.5-flash")
- require.NotContains(t, modelIDsForTest(got.Data), "claude-sonnet-4-6")
- }
- func TestGatewayModels_Grok45AdvertisesReasoningEffortForGrokBuild(t *testing.T) {
- assertGrokGatewayReasoningEfforts(t, 4409, "grok-4.5", []gatewayReasoningEffortOptionForTest{
- {Value: "low", Label: "Low"},
- {Value: "medium", Label: "Medium"},
- {Value: "high", Label: "High", Default: true},
- })
- }
- func TestGatewayModels_Grok46AdvertisesXHighReasoningEffortForGrokBuild(t *testing.T) {
- xhighEfforts := []gatewayReasoningEffortOptionForTest{
- {Value: "low", Label: "Low"},
- {Value: "medium", Label: "Medium"},
- {Value: "high", Label: "High", Default: true},
- {Value: "xhigh", Label: "xHigh"},
- }
- tests := []struct {
- groupID int64
- model string
- }{
- {groupID: 4410, model: "grok-4.6"},
- {groupID: 4411, model: "grok-4.6-latest"},
- }
- for _, tt := range tests {
- t.Run(tt.model, func(t *testing.T) {
- assertGrokGatewayReasoningEfforts(t, tt.groupID, tt.model, xhighEfforts)
- })
- }
- }
- func assertGrokGatewayReasoningEfforts(t *testing.T, groupID int64, modelID string, want []gatewayReasoningEffortOptionForTest) {
- t.Helper()
- gin.SetMode(gin.TestMode)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: service.PlatformGrok,
- Credentials: map[string]any{
- "model_mapping": map[string]any{modelID: modelID},
- },
- },
- },
- },
- },
- )
- 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.PlatformGrok},
- })
- h.Models(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got gatewayModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Len(t, got.Data, 1)
- model := got.Data[0]
- require.Equal(t, modelID, model.ID)
- require.True(t, model.SupportsReasoningEffort)
- require.Equal(t, "high", model.ReasoningEffort)
- require.Equal(t, want, model.ReasoningEfforts)
- }
- func TestGatewayModels_GeminiGroupFiltersMappedModelsByPlatform(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(21)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: service.PlatformAnthropic,
- Credentials: map[string]any{
- "model_mapping": map[string]any{
- "claude-sonnet-4-6": "claude-sonnet-4-6",
- },
- },
- },
- {
- ID: 2,
- Platform: service.PlatformGemini,
- Credentials: map[string]any{
- "model_mapping": map[string]any{
- "gemini-2.5-flash": "gemini-2.5-flash",
- },
- },
- },
- },
- },
- },
- )
- 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))
- require.Equal(t, []string{"gemini-2.5-flash"}, modelIDsForTest(got.Data))
- }
- // Scenario: a Composite group with only Anthropic accounts must not inherit Antigravity Gemini defaults.
- func TestGatewayCodexModels_CompositeAnthropicDoesNotAdvertiseAntigravityDefaults(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(64)
- h := newGatewayModelsHandlerForTest(&gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {{ID: 1, Platform: service.PlatformAnthropic}},
- },
- })
- 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.PlatformComposite},
- })
- 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, "claude-opus-4-6")
- require.NotContains(t, slugs, "gemini-2.5-flash")
- }
- // Scenario: Antigravity retains its own Claude and Gemini defaults inside Composite groups.
- func TestGatewayModels_CompositeAntigravityAdvertisesAntigravityDefaults(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(65)
- h := newGatewayModelsHandlerForTest(&gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {{ID: 1, Platform: service.PlatformAntigravity}},
- },
- })
- 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.PlatformComposite},
- })
- 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, "claude-opus-4-6")
- require.Contains(t, ids, "gemini-2.5-flash")
- }
- func TestGatewayModels_CustomModelsListDisabledKeepsOriginalModels(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(22)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: service.PlatformOpenAI,
- Credentials: map[string]any{
- "model_mapping": map[string]any{
- "gpt-5.5": "gpt-5.5",
- "gpt-5.4": "gpt-5.4",
- },
- },
- },
- },
- },
- },
- )
- 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.PlatformOpenAI,
- ModelAllowlist: service.GroupModelAllowlist{
- Enabled: false,
- Models: []string{"gpt-5.5"},
- },
- },
- })
- h.Models(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got gatewayModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Equal(t, []string{"gpt-5.4", "gpt-5.5"}, modelIDsForTest(got.Data))
- }
- func TestGatewayModels_CustomModelsListFiltersAndOrdersMappedModels(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(23)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: service.PlatformOpenAI,
- Credentials: map[string]any{
- "model_mapping": map[string]any{
- "gpt-5.4": "gpt-5.4",
- "gpt-5.5": "gpt-5.5",
- "legacy-gpt-2024": "legacy-gpt-2024",
- },
- },
- },
- },
- },
- },
- )
- 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.PlatformOpenAI,
- ModelAllowlist: service.GroupModelAllowlist{
- Enabled: true,
- Models: []string{"gpt-5.5", "missing-model", "gpt-5.4"},
- },
- },
- })
- h.Models(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got gatewayModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Equal(t, []string{"gpt-5.5", "gpt-5.4"}, modelIDsForTest(got.Data))
- }
- func TestGatewayModels_CompositeCustomModelsListFiltersAcrossConcretePlatforms(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(33)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: service.PlatformOpenAI,
- Credentials: map[string]any{
- "model_mapping": map[string]any{
- "gpt-5.4": "gpt-5.4",
- "gpt-5.5": "gpt-5.5",
- },
- },
- },
- {
- ID: 2,
- Platform: service.PlatformGemini,
- Credentials: map[string]any{
- "model_mapping": map[string]any{
- "gemini-2.5-flash": "gemini-2.5-flash",
- },
- },
- },
- {
- ID: 3,
- Platform: service.PlatformAntigravity,
- Credentials: map[string]any{
- "model_mapping": map[string]any{
- "ag-custom-model": "ag-custom-model",
- },
- },
- },
- {
- ID: 4,
- Platform: service.PlatformKimi,
- Credentials: map[string]any{
- "model_mapping": map[string]any{"kimi-custom": "kimi-upstream"},
- },
- },
- {
- ID: 5,
- Platform: service.PlatformZhipu,
- Credentials: map[string]any{
- "model_mapping": map[string]any{"glm-custom": "glm-upstream"},
- },
- },
- {
- ID: 6,
- Platform: service.PlatformDeepseek,
- Credentials: map[string]any{
- "model_mapping": map[string]any{"deepseek-custom": "deepseek-upstream"},
- },
- },
- {
- ID: 7,
- Platform: service.PlatformMiniMax,
- Credentials: map[string]any{
- "model_mapping": map[string]any{"minimax-custom": "MiniMax-M3"},
- },
- },
- },
- },
- },
- )
- 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.PlatformComposite,
- ModelAllowlist: service.GroupModelAllowlist{
- Enabled: true,
- Models: []string{"gemini-2.5-flash", "missing-model", "ag-custom-model", "gpt-5.5", "kimi-custom", "glm-custom", "deepseek-custom", "minimax-custom"},
- },
- },
- })
- h.Models(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got gatewayModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Equal(t, []string{"gemini-2.5-flash", "ag-custom-model", "gpt-5.5", "kimi-custom", "glm-custom", "deepseek-custom", "minimax-custom"}, modelIDsForTest(got.Data))
- }
- func TestGatewayModels_CompositeUnmappedAccountsFallbackToLinkedPlatformsOnly(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(34)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {ID: 1, Platform: service.PlatformOpenAI},
- {ID: 2, Platform: service.PlatformGrok},
- },
- },
- },
- )
- 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.PlatformComposite},
- })
- 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, "gpt-5.5")
- require.Contains(t, ids, "grok-4.3")
- require.NotContains(t, ids, "claude-sonnet-4-6")
- require.NotContains(t, ids, "gemini-2.5-flash")
- }
- // CN 供应商没有静态默认模型列表:composite 下无映射的可调度 CN 账号不得把
- // defaultModelIDsForPlatform default 分支的 Claude 列表挂到 CN 平台名下。
- func TestGatewayModels_CompositeUnmappedCNAccountsContributeNoDefaults(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(35)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {ID: 1, Platform: service.PlatformOpenAI},
- {ID: 2, Platform: service.PlatformKimi},
- {ID: 3, Platform: service.PlatformZhipu},
- {ID: 4, Platform: service.PlatformDeepseek},
- {ID: 5, Platform: service.PlatformMiniMax},
- },
- },
- },
- )
- 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.PlatformComposite},
- })
- 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, "gpt-5.5")
- require.NotContains(t, ids, "claude-sonnet-4-6")
- }
- // 独立 CN 分组沿用 default 分支的 Claude 默认列表(Claude Code 客户端请求的
- // 就是这些模型名并经账号 model_mapping 转换),composite 支持不得改变该回退。
- func TestDefaultModelIDsForPlatform_CNProvidersKeepClaudeDefaults(t *testing.T) {
- want := make([]string, 0, len(claude.DefaultModels))
- for _, model := range claude.DefaultModels {
- want = append(want, model.ID)
- }
- for _, platform := range []string{service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax} {
- require.Equal(t, want, defaultModelIDsForPlatform(platform), "platform=%s", platform)
- }
- }
- func TestDefaultCodexModelIDsForPlatform_DeepSeekUsesDeepSeekModels(t *testing.T) {
- require.Equal(t, []string{"deepseek-v4-pro", "deepseek-v4-flash"}, defaultCodexModelIDsForPlatform(service.PlatformDeepseek))
- require.Equal(t, []string{"MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"}, defaultCodexModelIDsForPlatform(service.PlatformMiniMax))
- require.Equal(t, defaultModelIDsForPlatform(service.PlatformAnthropic), defaultCodexModelIDsForPlatform(service.PlatformAnthropic))
- }
- func TestGatewayCodexModels_DeepSeekWithoutMappingUsesDeepSeekDefaults(t *testing.T) {
- gin.SetMode(gin.TestMode)
- const groupID int64 = 130
- h := newGatewayModelsHandlerForTest(&gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: service.PlatformDeepseek,
- Status: service.StatusActive,
- Schedulable: true,
- Credentials: map[string]any{},
- },
- },
- },
- })
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodGet, "/models?client_version=0.150.0", nil)
- c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{
- Group: &service.Group{ID: groupID, Platform: service.PlatformDeepseek},
- })
- h.CodexModels(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got codexModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- slugs := make([]string, 0, len(got.Models))
- for _, model := range got.Models {
- slugs = append(slugs, model.Slug)
- }
- require.Contains(t, slugs, "deepseek-v4-pro")
- require.Contains(t, slugs, "deepseek-v4-flash")
- require.NotContains(t, slugs, "claude-sonnet-4-6")
- require.NotContains(t, slugs, "claude-opus-4-6")
- }
- func TestGatewayCodexModels_OmitsWildcardMappingKeys(t *testing.T) {
- gin.SetMode(gin.TestMode)
- const groupID int64 = 131
- h := newGatewayModelsHandlerForTest(&gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: service.PlatformDeepseek,
- Credentials: map[string]any{
- "model_mapping": map[string]any{
- "foo-*": "deepseek-v4-pro",
- "deepseek-v4-pro": "deepseek-v4-pro",
- },
- },
- },
- },
- },
- })
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodGet, "/models?client_version=0.150.0", nil)
- c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{
- Group: &service.Group{ID: groupID, Platform: service.PlatformDeepseek},
- })
- h.CodexModels(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got codexModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- slugs := make([]string, 0, len(got.Models))
- for _, model := range got.Models {
- slugs = append(slugs, model.Slug)
- }
- require.Equal(t, []string{"deepseek-v4-pro"}, slugs)
- }
- func TestGatewayModels_CustomModelsListKeepsConcreteModelAllowedByWildcardMapping(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(26)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: service.PlatformAnthropic,
- Credentials: map[string]any{
- "model_mapping": map[string]any{
- "claude-*": "claude-sonnet-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.PlatformAnthropic,
- ModelAllowlist: service.GroupModelAllowlist{
- Enabled: true,
- Models: []string{"claude-sonnet-4-6"},
- },
- },
- })
- h.Models(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got gatewayModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Equal(t, []string{"claude-sonnet-4-6"}, modelIDsForTest(got.Data))
- }
- func TestGatewayModels_AnthropicCustomModelsListIncludesOAuthClaudeAndMappedDeepSeek(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(28)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: service.PlatformAnthropic,
- Type: service.AccountTypeOAuth,
- },
- {
- ID: 2,
- Platform: service.PlatformAnthropic,
- Type: service.AccountTypeAPIKey,
- Credentials: map[string]any{
- "model_mapping": map[string]any{
- "deepseek-v4-pro": "deepseek-v4-pro",
- },
- },
- },
- },
- },
- },
- )
- 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.PlatformAnthropic,
- ModelAllowlist: service.GroupModelAllowlist{
- Enabled: true,
- Models: []string{"claude-fable-5", "claude-opus-4-8", "deepseek-v4-pro"},
- },
- },
- })
- h.Models(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got gatewayModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Equal(t, []string{"claude-fable-5", "claude-opus-4-8", "deepseek-v4-pro"}, modelIDsForTest(got.Data))
- }
- func TestGatewayModels_AnthropicCustomModelsListDisabledKeepsMappedModelList(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(29)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: service.PlatformAnthropic,
- Type: service.AccountTypeOAuth,
- },
- {
- ID: 2,
- Platform: service.PlatformAnthropic,
- Type: service.AccountTypeAPIKey,
- Credentials: map[string]any{
- "model_mapping": map[string]any{
- "deepseek-v4-pro": "deepseek-v4-pro",
- },
- },
- },
- },
- },
- },
- )
- 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.PlatformAnthropic,
- ModelAllowlist: service.GroupModelAllowlist{
- Enabled: false,
- Models: []string{"claude-fable-5", "deepseek-v4-pro"},
- },
- },
- })
- h.Models(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got gatewayModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Equal(t, []string{"deepseek-v4-pro"}, modelIDsForTest(got.Data))
- }
- func TestGatewayModels_AnthropicCustomModelsListIncludesOAuthClaudeWithoutMappings(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(30)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: service.PlatformAnthropic,
- Type: service.AccountTypeOAuth,
- },
- },
- },
- },
- )
- 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.PlatformAnthropic,
- ModelAllowlist: service.GroupModelAllowlist{
- Enabled: true,
- Models: []string{"claude-opus-4-6-thinking", "claude-sonnet-4-5"},
- },
- },
- })
- h.Models(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got gatewayModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Equal(t, []string{"claude-opus-4-6-thinking", "claude-sonnet-4-5"}, modelIDsForTest(got.Data))
- }
- func TestGatewayModels_CustomModelsListCanReturnEmptyWhenSelectionsUnavailable(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(24)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {
- ID: 1,
- Platform: service.PlatformOpenAI,
- Credentials: map[string]any{
- "model_mapping": map[string]any{
- "gpt-5.4": "gpt-5.4",
- },
- },
- },
- },
- },
- },
- )
- 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.PlatformOpenAI,
- ModelAllowlist: service.GroupModelAllowlist{
- Enabled: true,
- Models: []string{"gpt-5.5"},
- },
- },
- })
- h.Models(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got gatewayModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Empty(t, modelIDsForTest(got.Data))
- }
- func TestGatewayModels_CustomModelsListFiltersDefaultFallbackModels(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(25)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {ID: 1, Platform: service.PlatformOpenAI},
- },
- },
- },
- )
- 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.PlatformOpenAI,
- ModelAllowlist: service.GroupModelAllowlist{
- Enabled: true,
- Models: []string{"gpt-5.5", "legacy-gpt-2024", "gpt-5.4"},
- },
- },
- })
- h.Models(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got gatewayModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Equal(t, []string{"gpt-5.5", "gpt-5.4"}, modelIDsForTest(got.Data))
- }
- func TestGatewayModels_OpenAICustomModelsListKeepsOpenAIResponseShapeForDefaultFallback(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(27)
- h := newGatewayModelsHandlerForTest(
- &gatewayModelsAccountRepoStub{
- byGroup: map[int64][]service.Account{
- groupID: {
- {ID: 1, Platform: service.PlatformOpenAI},
- },
- },
- },
- )
- 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.PlatformOpenAI,
- ModelAllowlist: service.GroupModelAllowlist{
- Enabled: true,
- Models: []string{"gpt-5.5", "gpt-5.4"},
- },
- },
- })
- h.Models(c)
- require.Equal(t, http.StatusOK, rec.Code)
- var got gatewayModelsResponseForTest
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- require.Equal(t, []string{"gpt-5.5", "gpt-5.4"}, modelIDsForTest(got.Data))
- require.Equal(t, "model", got.Data[0].Object)
- require.NotZero(t, got.Data[0].Created)
- require.Equal(t, "openai", got.Data[0].OwnedBy)
- require.Empty(t, got.Data[0].CreatedAt)
- }
- func modelIDsForTest(models []gatewayModelItemForTest) []string {
- ids := make([]string, 0, len(models))
- for _, model := range models {
- ids = append(ids, model.ID)
- }
- return ids
- }
|