From 0435417f437ae196fc97b57f0df4e771f0316f40 Mon Sep 17 00:00:00 2001 From: Heatherm Huang Date: Wed, 1 Jul 2026 20:31:32 +0800 Subject: [PATCH 1/3] fix: enable grok media generation groups --- .../service/openai_gateway_grok_test.go | 19 +++++++++++++++++++ ...58_enable_grok_media_generation_groups.sql | 7 +++++++ ...tity_payment_migrations_regression_test.go | 11 +++++++++++ 3 files changed, 37 insertions(+) create mode 100644 backend/migrations/158_enable_grok_media_generation_groups.sql diff --git a/backend/internal/service/openai_gateway_grok_test.go b/backend/internal/service/openai_gateway_grok_test.go index ad088be725..feb2c95abf 100644 --- a/backend/internal/service/openai_gateway_grok_test.go +++ b/backend/internal/service/openai_gateway_grok_test.go @@ -146,6 +146,25 @@ func TestBuildGrokResponsesRequestRejectsUnsafeAccountBaseURL(t *testing.T) { require.Contains(t, err.Error(), "invalid base url") } +func TestGrokMediaGenerationGateCoversImagesAndVideo(t *testing.T) { + tests := []struct { + name string + endpoint GrokMediaEndpoint + want bool + }{ + {name: "image generation", endpoint: GrokMediaEndpointImagesGenerations, want: true}, + {name: "image edit", endpoint: GrokMediaEndpointImagesEdits, want: true}, + {name: "video generation", endpoint: GrokMediaEndpointVideosGenerations, want: true}, + {name: "video status", endpoint: GrokMediaEndpointVideoStatus, want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, tt.endpoint.IsGenerationRequest()) + }) + } +} + func TestExtractGrokMediaModelSupportsJSONAndMultipart(t *testing.T) { require.Equal(t, "grok-imagine", ExtractGrokMediaModel("application/json", []byte(`{"model":"grok-imagine"}`))) diff --git a/backend/migrations/158_enable_grok_media_generation_groups.sql b/backend/migrations/158_enable_grok_media_generation_groups.sql new file mode 100644 index 0000000000..db1fccb6b1 --- /dev/null +++ b/backend/migrations/158_enable_grok_media_generation_groups.sql @@ -0,0 +1,7 @@ +-- PR 3593 added Grok media routes for image generation, image edits, and video generation. +-- Existing Grok groups were created before the image-generation gate knew about +-- the Grok platform, so backfill them onto the same generation capability gate. +UPDATE groups +SET allow_image_generation = true +WHERE platform = 'grok' + AND allow_image_generation = false; diff --git a/backend/migrations/auth_identity_payment_migrations_regression_test.go b/backend/migrations/auth_identity_payment_migrations_regression_test.go index 7f2263e0f8..14dab99160 100644 --- a/backend/migrations/auth_identity_payment_migrations_regression_test.go +++ b/backend/migrations/auth_identity_payment_migrations_regression_test.go @@ -169,6 +169,17 @@ func TestMigration151AddsAccountAutoPauseExpiryPartialIndex(t *testing.T) { require.Contains(t, sql, "expires_at IS NOT NULL") } +func TestMigration158BackfillsGrokMediaGenerationGroups(t *testing.T) { + content, err := FS.ReadFile("158_enable_grok_media_generation_groups.sql") + require.NoError(t, err) + + sql := string(content) + require.Contains(t, sql, "UPDATE groups") + require.Contains(t, sql, "SET allow_image_generation = true") + require.Contains(t, sql, "WHERE platform = 'grok'") + require.Contains(t, sql, "AND allow_image_generation = false") +} + func TestMigration154AddsSparkShadowColumnsAndConstraintsWithoutHotIndexes(t *testing.T) { content, err := FS.ReadFile("154_account_spark_shadow.sql") require.NoError(t, err) From 9934bd25725f97d69db5c6a65312802885dce020 Mon Sep 17 00:00:00 2001 From: Heatherm Huang Date: Wed, 1 Jul 2026 20:55:53 +0800 Subject: [PATCH 2/3] fix: default grok group media generation --- backend/internal/service/admin_service.go | 10 +++++- .../service/admin_service_group_test.go | 34 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index 8e38080c68..29643f0cc0 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -1804,6 +1804,12 @@ func defaultModelsListCandidateIDs(platform string) []string { } } +func defaultAllowImageGenerationForPlatform(platform string) bool { + // Grok image and video generation routes share the legacy image-generation gate. + // Older clients send the false zero value, so Grok groups must default enabled. + return platform == PlatformGrok +} + func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupInput) (*Group, error) { if input.RateMultiplier <= 0 { return nil, errors.New("rate_multiplier must be > 0") @@ -1859,6 +1865,8 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn mcpXMLInject = *input.MCPXMLInject } + allowImageGeneration := input.AllowImageGeneration || defaultAllowImageGenerationForPlatform(platform) + // 如果指定了复制账号的源分组,先获取账号 ID 列表 var accountIDsToCopy []int64 if len(input.CopyAccountsFromGroupIDs) > 0 { @@ -1902,7 +1910,7 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn DailyLimitUSD: dailyLimit, WeeklyLimitUSD: weeklyLimit, MonthlyLimitUSD: monthlyLimit, - AllowImageGeneration: input.AllowImageGeneration, + AllowImageGeneration: allowImageGeneration, ImageRateIndependent: input.ImageRateIndependent, ImageRateMultiplier: imageRateMultiplier, ImagePrice1K: imagePrice1K, diff --git a/backend/internal/service/admin_service_group_test.go b/backend/internal/service/admin_service_group_test.go index eb3eff7f69..3f78fdcf59 100644 --- a/backend/internal/service/admin_service_group_test.go +++ b/backend/internal/service/admin_service_group_test.go @@ -198,6 +198,40 @@ func TestAdminService_CreateGroup_NilImagePricing(t *testing.T) { require.Nil(t, repo.created.ImagePrice4K) } +func TestAdminService_CreateGroup_DefaultsGrokMediaGenerationEnabled(t *testing.T) { + repo := &groupRepoStubForAdmin{} + svc := &adminServiceImpl{groupRepo: repo} + + group, err := svc.CreateGroup(context.Background(), &CreateGroupInput{ + Name: "grok-media", + Description: "Grok media group", + Platform: PlatformGrok, + RateMultiplier: 1.0, + }) + require.NoError(t, err) + require.NotNil(t, group) + require.NotNil(t, repo.created) + require.True(t, repo.created.AllowImageGeneration) + require.True(t, group.AllowImageGeneration) +} + +func TestAdminService_CreateGroup_PreservesNonGrokImageGenerationDisabled(t *testing.T) { + repo := &groupRepoStubForAdmin{} + svc := &adminServiceImpl{groupRepo: repo} + + group, err := svc.CreateGroup(context.Background(), &CreateGroupInput{ + Name: "anthropic-text", + Description: "Anthropic text group", + Platform: PlatformAnthropic, + RateMultiplier: 1.0, + }) + require.NoError(t, err) + require.NotNil(t, group) + require.NotNil(t, repo.created) + require.False(t, repo.created.AllowImageGeneration) + require.False(t, group.AllowImageGeneration) +} + // TestAdminService_UpdateGroup_WithImagePricing 测试更新分组时 ImagePrice 字段正确更新 func TestAdminService_UpdateGroup_WithImagePricing(t *testing.T) { existingGroup := &Group{ From 99a8d8ad28da194d93d771d2f8f7d77cd81937c3 Mon Sep 17 00:00:00 2001 From: Heatherm Huang Date: Thu, 2 Jul 2026 12:35:49 +0800 Subject: [PATCH 3/3] fix: normalize grok imagine image alias --- backend/internal/service/grok_media.go | 32 +++++++++++++++++++ .../service/openai_gateway_grok_test.go | 29 ++++++++++++++--- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/backend/internal/service/grok_media.go b/backend/internal/service/grok_media.go index fb05b64b01..8942404eaa 100644 --- a/backend/internal/service/grok_media.go +++ b/backend/internal/service/grok_media.go @@ -17,6 +17,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/util/responseheaders" "github.com/gin-gonic/gin" "github.com/tidwall/gjson" + "github.com/tidwall/sjson" ) type GrokMediaEndpoint string @@ -296,6 +297,10 @@ func (s *OpenAIGatewayService) ForwardGrokMedia( if err != nil { return nil, err } + body, contentType, err = normalizeGrokMediaForwardBody(endpoint, body, contentType) + if err != nil { + return nil, err + } var bodyReader io.Reader if endpoint.RequiresRequestBody() { @@ -424,6 +429,33 @@ func prepareGrokMediaForwardBody(endpoint GrokMediaEndpoint, body []byte, conten return out, "application/json", nil } +func normalizeGrokMediaForwardBody(endpoint GrokMediaEndpoint, body []byte, contentType string) ([]byte, string, error) { + if !endpoint.RequiresRequestBody() || !gjson.ValidBytes(body) { + return body, contentType, nil + } + model := strings.TrimSpace(gjson.GetBytes(body, "model").String()) + upstreamModel := normalizeGrokMediaModelForEndpoint(endpoint, model) + if upstreamModel == "" || upstreamModel == model { + return body, contentType, nil + } + out, err := sjson.SetBytes(body, "model", upstreamModel) + if err != nil { + return nil, "", fmt.Errorf("rewrite grok media model: %w", err) + } + return out, contentType, nil +} + +func normalizeGrokMediaModelForEndpoint(endpoint GrokMediaEndpoint, model string) string { + model = strings.TrimSpace(model) + switch endpoint { + case GrokMediaEndpointImagesGenerations, GrokMediaEndpointImagesEdits: + if model == "grok-imagine" { + return "grok-imagine-image-quality" + } + } + return model +} + type grokMediaUsageMetadata struct { ResponseID string Usage OpenAIUsage diff --git a/backend/internal/service/openai_gateway_grok_test.go b/backend/internal/service/openai_gateway_grok_test.go index feb2c95abf..eae424ce9a 100644 --- a/backend/internal/service/openai_gateway_grok_test.go +++ b/backend/internal/service/openai_gateway_grok_test.go @@ -201,7 +201,28 @@ func TestParseGrokMediaRequestBuildsMultipartModerationBody(t *testing.T) { require.True(t, strings.HasPrefix(gjson.GetBytes(moderationBody, "images.0.image_url").String(), "data:image/")) } -func TestForwardGrokMediaImagesGenerationPassthrough(t *testing.T) { +func TestNormalizeGrokMediaModelForEndpoint(t *testing.T) { + tests := []struct { + name string + endpoint GrokMediaEndpoint + model string + want string + }{ + {name: "image generation alias", endpoint: GrokMediaEndpointImagesGenerations, model: "grok-imagine", want: "grok-imagine-image-quality"}, + {name: "image edit alias", endpoint: GrokMediaEndpointImagesEdits, model: "grok-imagine", want: "grok-imagine-image-quality"}, + {name: "image quality passthrough", endpoint: GrokMediaEndpointImagesGenerations, model: "grok-imagine-image-quality", want: "grok-imagine-image-quality"}, + {name: "image fast passthrough", endpoint: GrokMediaEndpointImagesGenerations, model: "grok-imagine-image", want: "grok-imagine-image"}, + {name: "video passthrough", endpoint: GrokMediaEndpointVideosGenerations, model: "grok-imagine-video", want: "grok-imagine-video"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, normalizeGrokMediaModelForEndpoint(tt.endpoint, tt.model)) + }) + } +} + +func TestForwardGrokMediaImagesGenerationNormalizesImagineAlias(t *testing.T) { t.Setenv(xai.EnvAllowUnsafeURLOverrides, "true") gin.SetMode(gin.TestMode) @@ -238,12 +259,12 @@ func TestForwardGrokMediaImagesGenerationPassthrough(t *testing.T) { require.Equal(t, http.MethodPost, upstream.lastReq.Method) require.Equal(t, "Bearer api-key", upstream.lastReq.Header.Get("Authorization")) require.Equal(t, "application/json", upstream.lastReq.Header.Get("Content-Type")) - require.JSONEq(t, string(body), string(upstream.lastBody)) + require.JSONEq(t, `{"model":"grok-imagine-image-quality","prompt":"draw a cat"}`, string(upstream.lastBody)) require.Equal(t, http.StatusOK, recorder.Code) require.JSONEq(t, `{"data":[]}`, recorder.Body.String()) require.Equal(t, "xai-image-req", result.RequestID) - require.Equal(t, "grok-imagine", result.Model) - require.Equal(t, "grok-imagine", result.BillingModel) + require.Equal(t, "grok-imagine-image-quality", result.Model) + require.Equal(t, "grok-imagine-image-quality", result.BillingModel) require.Equal(t, 1, result.ImageCount) require.Equal(t, ImageBillingSize2K, result.ImageSize) }