mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
fix(grok): normalize media reference image payloads
This commit is contained in:
@@ -147,7 +147,7 @@ func parseGrokMediaJSONRequest(body []byte, info *GrokMediaRequestInfo) {
|
||||
switch {
|
||||
case value.IsArray():
|
||||
for _, item := range value.Array() {
|
||||
if imageURL := strings.TrimSpace(item.Get("image_url").String()); imageURL != "" {
|
||||
if imageURL := grokMediaJSONImageURL(item); imageURL != "" {
|
||||
info.InputImageURLs = append(info.InputImageURLs, imageURL)
|
||||
continue
|
||||
}
|
||||
@@ -160,7 +160,7 @@ func parseGrokMediaJSONRequest(body []byte, info *GrokMediaRequestInfo) {
|
||||
}
|
||||
}
|
||||
default:
|
||||
if imageURL := strings.TrimSpace(value.Get("image_url").String()); imageURL != "" {
|
||||
if imageURL := grokMediaJSONImageURL(value); imageURL != "" {
|
||||
info.InputImageURLs = append(info.InputImageURLs, imageURL)
|
||||
return
|
||||
}
|
||||
@@ -175,7 +175,15 @@ func parseGrokMediaJSONRequest(body []byte, info *GrokMediaRequestInfo) {
|
||||
}
|
||||
appendJSONImageURLs(gjson.GetBytes(body, "image"))
|
||||
appendJSONImageURLs(gjson.GetBytes(body, "images"))
|
||||
info.MaskImageURL = strings.TrimSpace(gjson.GetBytes(body, "mask.image_url").String())
|
||||
appendJSONImageURLs(gjson.GetBytes(body, "reference_images"))
|
||||
info.MaskImageURL = grokMediaJSONImageURL(gjson.GetBytes(body, "mask"))
|
||||
}
|
||||
|
||||
func grokMediaJSONImageURL(value gjson.Result) string {
|
||||
if imageURL := strings.TrimSpace(value.Get("url").String()); imageURL != "" {
|
||||
return imageURL
|
||||
}
|
||||
return strings.TrimSpace(value.Get("image_url").String())
|
||||
}
|
||||
|
||||
func parseGrokMediaMultipartRequest(contentType string, body []byte, info *GrokMediaRequestInfo) {
|
||||
@@ -409,7 +417,7 @@ func prepareGrokMediaForwardBody(endpoint GrokMediaEndpoint, body []byte, conten
|
||||
images := make([]map[string]string, 0, len(info.InputImageURLs)+len(info.Uploads))
|
||||
for _, imageURL := range info.InputImageURLs {
|
||||
if imageURL = strings.TrimSpace(imageURL); imageURL != "" {
|
||||
images = append(images, map[string]string{"image_url": imageURL})
|
||||
images = append(images, map[string]string{"url": imageURL})
|
||||
}
|
||||
}
|
||||
for _, upload := range info.Uploads {
|
||||
@@ -417,7 +425,7 @@ func prepareGrokMediaForwardBody(endpoint GrokMediaEndpoint, body []byte, conten
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
images = append(images, map[string]string{"image_url": dataURL})
|
||||
images = append(images, map[string]string{"url": dataURL})
|
||||
}
|
||||
if len(images) > 0 {
|
||||
payload["image"] = images[0]
|
||||
@@ -435,7 +443,7 @@ func prepareGrokMediaForwardBody(endpoint GrokMediaEndpoint, body []byte, conten
|
||||
maskImageURL = dataURL
|
||||
}
|
||||
if maskImageURL != "" {
|
||||
payload["mask"] = map[string]string{"image_url": maskImageURL}
|
||||
payload["mask"] = map[string]string{"url": maskImageURL}
|
||||
}
|
||||
|
||||
out, err := marshalOpenAIUpstreamJSON(payload)
|
||||
@@ -449,6 +457,18 @@ func normalizeGrokMediaForwardBody(endpoint GrokMediaEndpoint, body []byte, cont
|
||||
if !endpoint.RequiresRequestBody() || !gjson.ValidBytes(body) {
|
||||
return body, contentType, nil
|
||||
}
|
||||
var imageFields []string
|
||||
switch endpoint {
|
||||
case GrokMediaEndpointImagesEdits:
|
||||
imageFields = []string{"image", "images", "mask"}
|
||||
case GrokMediaEndpointVideosGenerations:
|
||||
imageFields = []string{"image", "images", "reference_images"}
|
||||
}
|
||||
var err error
|
||||
body, err = canonicalizeGrokMediaImageURLFields(body, imageFields...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
info := ParseGrokMediaRequest(contentType, body)
|
||||
upstreamModel := normalizeGrokMediaModelForEndpoint(endpoint, info.Model, info.HasInputImage())
|
||||
if upstreamModel == "" || upstreamModel == info.Model {
|
||||
@@ -461,6 +481,54 @@ func normalizeGrokMediaForwardBody(endpoint GrokMediaEndpoint, body []byte, cont
|
||||
return out, contentType, nil
|
||||
}
|
||||
|
||||
func canonicalizeGrokMediaImageURLFields(body []byte, fields ...string) ([]byte, error) {
|
||||
out := body
|
||||
for _, field := range fields {
|
||||
value := gjson.GetBytes(out, field)
|
||||
if !value.Exists() {
|
||||
continue
|
||||
}
|
||||
if value.IsArray() {
|
||||
for index := range value.Array() {
|
||||
var err error
|
||||
out, err = canonicalizeGrokMediaImageURLObject(out, fmt.Sprintf("%s.%d", field, index))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
out, err = canonicalizeGrokMediaImageURLObject(out, field)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func canonicalizeGrokMediaImageURLObject(body []byte, path string) ([]byte, error) {
|
||||
legacyPath := path + ".image_url"
|
||||
legacy := gjson.GetBytes(body, legacyPath)
|
||||
if !legacy.Exists() {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
out := body
|
||||
if !gjson.GetBytes(out, path+".url").Exists() {
|
||||
var err error
|
||||
out, err = sjson.SetBytes(out, path+".url", legacy.Value())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("normalize grok media image url: %w", err)
|
||||
}
|
||||
}
|
||||
out, err := sjson.DeleteBytes(out, legacyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("remove legacy grok media image url: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func sanitizeGrokMediaForwardBody(endpoint GrokMediaEndpoint, body []byte, contentType string) ([]byte, string, error) {
|
||||
if !endpoint.RequiresRequestBody() || !gjson.ValidBytes(body) {
|
||||
return body, contentType, nil
|
||||
|
||||
@@ -418,6 +418,72 @@ func TestParseGrokMediaVideoRequestResolution(t *testing.T) {
|
||||
require.Equal(t, "720p", info.Resolution)
|
||||
}
|
||||
|
||||
func TestParseGrokMediaRequestAcceptsOfficialImageURLFields(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"model":"grok-imagine-video-1.5",
|
||||
"image":{"url":"https://example.com/source.png"},
|
||||
"reference_images":[{"url":"https://example.com/reference.png"}]
|
||||
}`)
|
||||
|
||||
info := ParseGrokMediaRequest("application/json", body)
|
||||
|
||||
require.Equal(t, []string{
|
||||
"https://example.com/source.png",
|
||||
"https://example.com/reference.png",
|
||||
}, info.InputImageURLs)
|
||||
require.True(t, info.HasInputImage())
|
||||
}
|
||||
|
||||
func TestNormalizeGrokMediaForwardBodyCanonicalizesImageURLAlias(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"model":"grok-imagine-video-1.5",
|
||||
"prompt":"animate",
|
||||
"image":{"image_url":"https://example.com/source.png"},
|
||||
"duration":8
|
||||
}`)
|
||||
|
||||
out, contentType, err := normalizeGrokMediaForwardBody(GrokMediaEndpointVideosGenerations, body, "application/json")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "application/json", contentType)
|
||||
require.Equal(t, "grok-imagine-video-1.5", gjson.GetBytes(out, "model").String())
|
||||
require.Equal(t, "https://example.com/source.png", gjson.GetBytes(out, "image.url").String())
|
||||
require.False(t, gjson.GetBytes(out, "image.image_url").Exists())
|
||||
}
|
||||
|
||||
func TestNormalizeGrokMediaForwardBodyPreservesImageToVideoModelForOfficialURL(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"model":"grok-imagine-video-1.5",
|
||||
"prompt":"animate",
|
||||
"image":{"url":"https://example.com/source.png"}
|
||||
}`)
|
||||
|
||||
out, _, err := normalizeGrokMediaForwardBody(GrokMediaEndpointVideosGenerations, body, "application/json")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "grok-imagine-video-1.5", gjson.GetBytes(out, "model").String())
|
||||
require.Equal(t, "https://example.com/source.png", gjson.GetBytes(out, "image.url").String())
|
||||
}
|
||||
|
||||
func TestCanonicalizeGrokMediaImageURLFieldsPreservesOfficialURL(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"image":{"url":"https://example.com/official.png","image_url":"https://example.com/legacy.png"},
|
||||
"images":[
|
||||
{"image_url":"https://example.com/first.png"},
|
||||
{"url":"https://example.com/second.png"}
|
||||
]
|
||||
}`)
|
||||
|
||||
out, err := canonicalizeGrokMediaImageURLFields(body, "image", "images")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "https://example.com/official.png", gjson.GetBytes(out, "image.url").String())
|
||||
require.False(t, gjson.GetBytes(out, "image.image_url").Exists())
|
||||
require.Equal(t, "https://example.com/first.png", gjson.GetBytes(out, "images.0.url").String())
|
||||
require.False(t, gjson.GetBytes(out, "images.0.image_url").Exists())
|
||||
require.Equal(t, "https://example.com/second.png", gjson.GetBytes(out, "images.1.url").String())
|
||||
}
|
||||
|
||||
func TestNormalizeGrokMediaModelForEndpoint(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -577,7 +643,8 @@ func TestForwardGrokMediaImagesEditMultipartConvertsToJSON(t *testing.T) {
|
||||
require.True(t, json.Valid(upstream.lastBody))
|
||||
require.Equal(t, "grok-imagine-edit", gjson.GetBytes(upstream.lastBody, "model").String())
|
||||
require.Equal(t, "edit this private image", gjson.GetBytes(upstream.lastBody, "prompt").String())
|
||||
require.True(t, strings.HasPrefix(gjson.GetBytes(upstream.lastBody, "image.image_url").String(), "data:image/png;base64,"))
|
||||
require.True(t, strings.HasPrefix(gjson.GetBytes(upstream.lastBody, "image.url").String(), "data:image/png;base64,"))
|
||||
require.False(t, gjson.GetBytes(upstream.lastBody, "image.image_url").Exists())
|
||||
}
|
||||
|
||||
func TestForwardGrokMediaVideoGenerationReturnsUsageAndResponseID(t *testing.T) {
|
||||
@@ -659,7 +726,7 @@ func TestForwardGrokMediaVideoGenerationPreservesImageToVideoModel(t *testing.T)
|
||||
result, err := svc.ForwardGrokMedia(context.Background(), c, account, GrokMediaEndpointVideosGenerations, "", body, "application/json")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "https://xai.test/v1/videos/generations", upstream.lastReq.URL.String())
|
||||
require.JSONEq(t, `{"model":"grok-imagine-video-1.5","prompt":"animate","image":{"image_url":"data:image/png;base64,aW1n"}}`, string(upstream.lastBody))
|
||||
require.JSONEq(t, `{"model":"grok-imagine-video-1.5","prompt":"animate","image":{"url":"data:image/png;base64,aW1n"}}`, string(upstream.lastBody))
|
||||
require.Equal(t, "video-request-456", result.ResponseID)
|
||||
require.Equal(t, "grok-imagine-video-1.5", result.BillingModel)
|
||||
// 未指定 duration 时按上游默认 8 秒计费。
|
||||
@@ -705,7 +772,8 @@ func TestForwardGrokMediaOAuthImageToVideoUsesOfficialAPIForLargeBody(t *testing
|
||||
require.Equal(t, xai.DefaultBaseURL+"/videos/generations", upstream.lastReq.URL.String())
|
||||
require.Empty(t, upstream.lastReq.Header.Get("X-XAI-Token-Auth"))
|
||||
require.Empty(t, upstream.lastReq.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, "data:image/png;base64,"+imageData, gjson.GetBytes(upstream.lastBody, "image.image_url").String())
|
||||
require.Equal(t, "data:image/png;base64,"+imageData, gjson.GetBytes(upstream.lastBody, "image.url").String())
|
||||
require.False(t, gjson.GetBytes(upstream.lastBody, "image.image_url").Exists())
|
||||
}
|
||||
|
||||
func TestForwardGrokMediaVideoStatusUsesGETWithoutBody(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user