From 12db0f906a7faae9e818a481485e58e344487658 Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Sat, 8 Aug 2026 09:45:12 +0800 Subject: [PATCH] fix(grok): drop account-test ZDR path and align media CLI headers Remove optional upload_url / fake connectivity-only success from admin video tests. Stamp Grok CLI headers only on the CLI proxy so OAuth media against api.x.ai can complete and preview video like the gateway path. --- .../internal/handler/admin/account_handler.go | 11 +-- .../internal/service/account_test_service.go | 76 ++----------------- .../service/account_test_service_grok_test.go | 17 ++--- 3 files changed, 19 insertions(+), 85 deletions(-) diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 54271c17b..30fc6b982 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -1067,10 +1067,8 @@ type TestAccountRequest struct { Mode string `json:"mode"` // Optional media for Grok (and future) real generation tests. // ImageDataURL / AudioDataURL are data:;base64,... payloads. - // VideoUploadURL is a public HTTPS PUT URL for ZDR video output. - ImageDataURL string `json:"image_data_url"` - AudioDataURL string `json:"audio_data_url"` - VideoUploadURL string `json:"video_upload_url"` + ImageDataURL string `json:"image_data_url"` + AudioDataURL string `json:"audio_data_url"` } type SyncFromCRSRequest struct { @@ -1101,9 +1099,8 @@ func (h *AccountHandler) Test(c *gin.Context) { _ = c.ShouldBindJSON(&req) opts := service.AccountTestOptions{ - ImageDataURL: req.ImageDataURL, - AudioDataURL: req.AudioDataURL, - VideoUploadURL: req.VideoUploadURL, + ImageDataURL: req.ImageDataURL, + AudioDataURL: req.AudioDataURL, } // Use AccountTestService to test the account with SSE streaming diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index 6224feb6e..ed06bd51f 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -66,11 +66,9 @@ type TestEvent struct { // AccountTestOptions carries optional media for admin connectivity tests. // ImageDataURL / AudioDataURL are full data URLs (data:;base64,...). -// VideoUploadURL is an optional public HTTPS PUT URL for ZDR video output. type AccountTestOptions struct { - ImageDataURL string - AudioDataURL string - VideoUploadURL string + ImageDataURL string + AudioDataURL string } func firstAccountTestOptions(opts []AccountTestOptions) AccountTestOptions { @@ -137,22 +135,6 @@ func normalizeGrokAccountTestMode(mode string) string { } } -// isGrokVideoZDRUploadURLRequired reports when xAI rejects video create because -// Zero Data Retention teams must supply output.upload_url. For admin connectivity -// probes we treat this as a successful reachability signal (endpoint + auth work). -func isGrokVideoZDRUploadURLRequired(statusCode int, body []byte) bool { - if statusCode != http.StatusBadRequest && statusCode != http.StatusUnprocessableEntity { - return false - } - msg := strings.ToLower(string(body)) - if !strings.Contains(msg, "upload_url") { - return false - } - return strings.Contains(msg, "zero data retention") || - strings.Contains(msg, "zdr") || - strings.Contains(msg, "must provide output.upload_url") -} - // AccountTestService handles account testing operations type AccountTestService struct { accountRepo AccountRepository @@ -277,7 +259,7 @@ func createTestPayload(modelID string) (map[string]any, error) { // All account types use full Claude Code client characteristics, only auth header differs // modelID is optional - if empty, defaults to claude.DefaultTestModel // mode is optional - "compact" routes OpenAI accounts to the /responses/compact probe path -// opts is optional media (image/audio data URLs, video upload_url for ZDR). +// opts is optional media (image/audio data URLs for real generation / STT). func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int64, modelID string, prompt string, mode string, opts ...AccountTestOptions) error { ctx := c.Request.Context() testOpts := firstAccountTestOptions(opts) @@ -962,7 +944,10 @@ func (s *AccountTestService) applyGrokTestRequestHeaders(req *http.Request, acco req.Header.Set("Accept", accept) } req.Header.Set("Authorization", "Bearer "+authToken) - if account.IsGrokOAuth() { + // Match gateway media/voice: CLI identity headers only on the CLI chat proxy. + // api.x.ai media (images/videos) rejects or mistreats OAuth when CLI headers + // are stamped on the official API host (e.g. ZDR upload_url false positives). + if account.IsGrokOAuth() && req.URL != nil && isGrokCLIProxyTarget(req.URL.String()) { applyGrokCLIHeaders(req.Header) } account.ApplyHeaderOverrides(req.Header) @@ -1253,13 +1238,6 @@ func (s *AccountTestService) testGrokVideoGeneration(c *gin.Context, ctx context payload["image"] = grokMediaImageObject(normalized) s.sendEvent(c, TestEvent{Type: "content", Text: "using uploaded first-frame / reference image\n"}) } - if uploadURL := strings.TrimSpace(opts.VideoUploadURL); uploadURL != "" { - if err := validateAccountTestPublicUploadURL(uploadURL); err != nil { - return s.sendErrorAndEnd(c, err.Error()) - } - payload["output"] = map[string]any{"upload_url": uploadURL} - s.sendEvent(c, TestEvent{Type: "content", Text: "using client-provided output.upload_url for ZDR video output\n"}) - } payloadBytes, _ := json.Marshal(payload) req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(payloadBytes)) @@ -1280,25 +1258,6 @@ func (s *AccountTestService) testGrokVideoGeneration(c *gin.Context, ctx context return s.sendErrorAndEnd(c, fmt.Sprintf("Failed to read Grok video response: %s", err.Error())) } if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusCreated { - // ZDR teams require a client-hosted output.upload_url that xAI can PUT the - // finished MP4 into. Admin account tests do not expose a public upload sink - // (would need HTTPS public base + PUT receiver, like a media upload ticket). - // Reaching this validation still proves auth + /videos/generations routing. - // Mark success for connectivity, but state clearly that no video was produced. - if isGrokVideoZDRUploadURLRequired(resp.StatusCode, body) { - s.sendEvent(c, TestEvent{ - Type: "content", - Text: "" + - "NO VIDEO GENERATED — this is connectivity-only for ZDR accounts.\n" + - "xAI Zero Data Retention requires payload field output.upload_url (public HTTPS PUT URL);\n" + - "this admin probe does not host that receiver, so generation cannot complete here.\n" + - "What worked: auth token + POST /v1/videos/generations reached xAI and returned the ZDR rule.\n" + - "To actually generate video: call gateway /v1/videos/generations with a client-provided output.upload_url, or use a non-ZDR team.\n", - }) - s.sendEvent(c, TestEvent{Type: "status", Text: "Connectivity OK (no video file produced)"}) - s.sendEvent(c, TestEvent{Type: "test_complete", Success: true}) - return nil - } return s.sendErrorAndEnd(c, fmt.Sprintf("Grok videos API returned %d: %s", resp.StatusCode, string(body))) } @@ -1392,8 +1351,7 @@ func (s *AccountTestService) emitGrokVideoResult(c *gin.Context, ctx context.Con defer func() { _ = resp.Body.Close() }() body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<20)) // 64 MiB cap for admin preview if resp.StatusCode != http.StatusOK { - // Fall back to reporting status-only success if content is unavailable - // (e.g. ZDR already PUT to client upload_url). + // Fall back to status URL when binary content is unavailable. if videoURL != "" { s.sendEvent(c, TestEvent{Type: "content", Text: "video completed; content download unavailable, reported url=" + videoURL + "\n"}) s.sendEvent(c, TestEvent{Type: "video", VideoURL: videoURL, MimeType: "video/mp4"}) @@ -1895,24 +1853,6 @@ func decodeAccountTestDataURL(raw string) (data []byte, mime string, err error) return decoded, mime, nil } -func validateAccountTestPublicUploadURL(raw string) error { - raw = strings.TrimSpace(raw) - if raw == "" { - return fmt.Errorf("upload_url is empty") - } - u, err := url.Parse(raw) - if err != nil || u == nil { - return fmt.Errorf("upload_url is not a valid URL") - } - if !strings.EqualFold(u.Scheme, "https") { - return fmt.Errorf("upload_url must be https (xAI requires a public HTTPS PUT URL)") - } - if strings.TrimSpace(u.Host) == "" { - return fmt.Errorf("upload_url host is required") - } - return nil -} - func sttFilenameForMIME(mime string) string { switch strings.ToLower(strings.TrimSpace(mime)) { case "audio/mpeg", "audio/mp3": diff --git a/backend/internal/service/account_test_service_grok_test.go b/backend/internal/service/account_test_service_grok_test.go index 5226e802c..781e29e47 100644 --- a/backend/internal/service/account_test_service_grok_test.go +++ b/backend/internal/service/account_test_service_grok_test.go @@ -502,10 +502,10 @@ func TestAccountTestService_GrokExplicitImageModeDefaultsModel(t *testing.T) { require.Contains(t, rec.Body.String(), `"type":"test_complete"`) } -func TestAccountTestService_GrokVideoZDRUploadURLRequiredCountsAsConnectivityOK(t *testing.T) { +func TestAccountTestService_GrokVideoUpstreamErrorIsNotMaskedAsSuccess(t *testing.T) { gin.SetMode(gin.TestMode) account := &Account{ - ID: 21, Name: "grok-oauth-video-zdr", Platform: PlatformGrok, + ID: 21, Name: "grok-oauth-video-err", Platform: PlatformGrok, Type: AccountTypeOAuth, Status: StatusActive, Schedulable: true, Concurrency: 1, Credentials: map[string]any{ "access_token": "grok-access-token", @@ -518,7 +518,7 @@ func TestAccountTestService_GrokVideoZDRUploadURLRequiredCountsAsConnectivityOK( StatusCode: http.StatusBadRequest, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader( - `{"code":"invalid-argument","error":"Zero Data Retention teams must provide output.upload_url for video generation."}`, + `{"code":"invalid-argument","error":"bad video request"}`, )), }} svc := &AccountTestService{ @@ -532,14 +532,11 @@ func TestAccountTestService_GrokVideoZDRUploadURLRequiredCountsAsConnectivityOK( err := svc.TestAccountConnection(c, account.ID, "grok-imagine-video", "bounce ball", AccountTestModeGrokVideo) - require.NoError(t, err) + require.Error(t, err) require.Equal(t, "https://api.x.ai/v1/videos/generations", upstream.lastReq.URL.String()) - require.Contains(t, rec.Body.String(), "NO VIDEO GENERATED") - require.Contains(t, rec.Body.String(), "output.upload_url") - require.Contains(t, rec.Body.String(), "Connectivity OK") - require.Contains(t, rec.Body.String(), `"type":"test_complete"`) - require.Contains(t, rec.Body.String(), `"success":true`) - require.NotContains(t, rec.Body.String(), `"type":"error"`) + require.Contains(t, rec.Body.String(), `"type":"error"`) + require.Contains(t, rec.Body.String(), "Grok videos API returned 400") + require.NotContains(t, rec.Body.String(), `"success":true`) } type grokRealtimeTestConn struct {