mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3614 from heathermhuang/codex/grok-media-group-gate-fix
fix: enable Grok media generation groups
This commit is contained in:
@@ -1814,6 +1814,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")
|
||||
@@ -1879,6 +1885,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 {
|
||||
@@ -1922,7 +1930,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,
|
||||
PeakRateEnabled: peakRateEnabled,
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}`)))
|
||||
|
||||
@@ -182,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)
|
||||
|
||||
@@ -219,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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user