From abaeb55bb20e9a346cae7002c2ed8af5d82c2790 Mon Sep 17 00:00:00 2001 From: oscarbrey <188580177+oscarbrey@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:06:30 +0200 Subject: [PATCH 1/3] fix(xai): support Grok Imagine Video 1.5 GA --- internal/api/server_test.go | 2 + internal/client/codex/models/models.go | 2 +- internal/registry/model_definitions.go | 24 +++--- internal/registry/model_definitions_test.go | 14 +++- .../handlers/openai/openai_videos_handlers.go | 11 ++- .../openai/openai_videos_handlers_test.go | 79 ++++++++++++++----- 6 files changed, 90 insertions(+), 42 deletions(-) diff --git a/internal/api/server_test.go b/internal/api/server_test.go index fb1c202a3..dd54f4949 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -1514,6 +1514,7 @@ func TestModelsWithClientVersionReturnsCodexCatalog(t *testing.T) { {ID: "gpt-image-2", Object: "model", OwnedBy: "openai", Type: "openai"}, {ID: "grok-imagine-image", Object: "model", OwnedBy: "xai", Type: "openai"}, {ID: "grok-imagine-video", Object: "model", OwnedBy: "xai", Type: "openai"}, + {ID: "grok-imagine-video-1.5", Object: "model", OwnedBy: "xai", Type: "openai"}, {ID: "grok-imagine-video-1.5-preview", Object: "model", OwnedBy: "xai", Type: "openai"}, }) t.Cleanup(func() { @@ -1613,6 +1614,7 @@ func TestModelsWithClientVersionReturnsCodexCatalog(t *testing.T) { "gpt-image-2": false, "grok-imagine-image": false, "grok-imagine-video": false, + "grok-imagine-video-1.5": false, "grok-imagine-video-1.5-preview": false, } for _, model := range resp.Models { diff --git a/internal/client/codex/models/models.go b/internal/client/codex/models/models.go index 289ca448f..d1d76b670 100644 --- a/internal/client/codex/models/models.go +++ b/internal/client/codex/models/models.go @@ -271,7 +271,7 @@ func applyCodexClientModelMetadata(entry map[string]any, id string, model map[st func applyCodexClientVisibilityOverride(entry map[string]any, id string) { switch strings.TrimSpace(id) { - case "grok-imagine-image-quality", "gpt-image-1.5", "gpt-image-2", "grok-imagine-image", "grok-imagine-video", "grok-imagine-video-1.5-preview": + case "grok-imagine-image-quality", "gpt-image-1.5", "gpt-image-2", "grok-imagine-image", "grok-imagine-video", "grok-imagine-video-1.5", "grok-imagine-video-1.5-preview": entry["visibility"] = "hide" } } diff --git a/internal/registry/model_definitions.go b/internal/registry/model_definitions.go index ef1ce7952..41e52de22 100644 --- a/internal/registry/model_definitions.go +++ b/internal/registry/model_definitions.go @@ -7,12 +7,12 @@ import ( ) const ( - codexBuiltinImage15ModelID = "gpt-image-1.5" - codexBuiltinImageModelID = "gpt-image-2" - xaiBuiltinImageModelID = "grok-imagine-image" - xaiBuiltinImageQualityModelID = "grok-imagine-image-quality" - xaiBuiltinVideoModelID = "grok-imagine-video" - xaiBuiltinVideo15PreviewModelID = "grok-imagine-video-1.5-preview" + codexBuiltinImage15ModelID = "gpt-image-1.5" + codexBuiltinImageModelID = "gpt-image-2" + xaiBuiltinImageModelID = "grok-imagine-image" + xaiBuiltinImageQualityModelID = "grok-imagine-image-quality" + xaiBuiltinVideoModelID = "grok-imagine-video" + xaiBuiltinVideo15ModelID = "grok-imagine-video-1.5" ) // staticModelsJSON mirrors the top-level structure of models.json. @@ -120,7 +120,7 @@ func WithCodexBuiltins(models []*ModelInfo) []*ModelInfo { // WithXAIBuiltins injects hard-coded xAI image/video model definitions that should // not depend on remote models.json updates. func WithXAIBuiltins(models []*ModelInfo) []*ModelInfo { - return upsertModelInfos(models, xaiBuiltinImageModelInfo(), xaiBuiltinImageQualityModelInfo(), xaiBuiltinVideoModelInfo(), xaiBuiltinVideo15PreviewModelInfo()) + return upsertModelInfos(models, xaiBuiltinImageModelInfo(), xaiBuiltinImageQualityModelInfo(), xaiBuiltinVideoModelInfo(), xaiBuiltinVideo15ModelInfo()) } func normalizeAntigravityCapabilityModelID(modelID string) string { @@ -194,16 +194,16 @@ func xaiBuiltinVideoModelInfo() *ModelInfo { } } -func xaiBuiltinVideo15PreviewModelInfo() *ModelInfo { +func xaiBuiltinVideo15ModelInfo() *ModelInfo { return &ModelInfo{ - ID: xaiBuiltinVideo15PreviewModelID, + ID: xaiBuiltinVideo15ModelID, Object: "model", Created: 1735689600, // 2025-01-01 OwnedBy: "xai", Type: "xai", - DisplayName: "Grok Imagine Video 1.5 Preview", - Name: xaiBuiltinVideo15PreviewModelID, - Description: "xAI Grok preview video generation model.", + DisplayName: "Grok Imagine Video 1.5", + Name: xaiBuiltinVideo15ModelID, + Description: "xAI Grok video generation model.", } } diff --git a/internal/registry/model_definitions_test.go b/internal/registry/model_definitions_test.go index 196c26536..c256e0dc7 100644 --- a/internal/registry/model_definitions_test.go +++ b/internal/registry/model_definitions_test.go @@ -35,19 +35,25 @@ func TestGeminiVertexModelsUseFlashLiteReleaseID(t *testing.T) { t.Fatalf("Vertex models do not contain %q", releaseID) } -func TestWithXAIBuiltinsIncludesVideoPreviewModel(t *testing.T) { +func TestWithXAIBuiltinsUsesVideo15GAModel(t *testing.T) { models := WithXAIBuiltins(nil) + foundGA := false for _, model := range models { if model == nil { continue } - if model.ID == xaiBuiltinVideo15PreviewModelID { - return + if model.ID == xaiBuiltinVideo15ModelID { + foundGA = true + } + if model.ID == "grok-imagine-video-1.5-preview" { + t.Fatal("xAI builtins must not advertise the retired preview alias") } } - t.Fatalf("expected xAI builtin model %s", xaiBuiltinVideo15PreviewModelID) + if !foundGA { + t.Fatalf("expected xAI builtin model %s", xaiBuiltinVideo15ModelID) + } } func TestAntigravityWebSearchModelForRequiresRequestedModelCapability(t *testing.T) { diff --git a/sdk/api/handlers/openai/openai_videos_handlers.go b/sdk/api/handlers/openai/openai_videos_handlers.go index e891dbe2d..76573ccf3 100644 --- a/sdk/api/handlers/openai/openai_videos_handlers.go +++ b/sdk/api/handlers/openai/openai_videos_handlers.go @@ -32,7 +32,8 @@ const ( xaiVideosExtensionsAPI = "/v1/videos/extensions" defaultOpenAIVideosModel = "sora-2" defaultXAIVideosModel = "grok-imagine-video" - xaiVideos15PreviewModel = "grok-imagine-video-1.5-preview" + xaiVideos15Model = "grok-imagine-video-1.5" + xaiVideos15PreviewAlias = "grok-imagine-video-1.5-preview" xaiVideosHandlerType = "openai-video" defaultVideosSeconds = "4" defaultVideosSize = "720x1280" @@ -147,7 +148,7 @@ func videosModelBase(model string) string { func isXAIVideosModel(model string) bool { prefix, baseModel := imagesModelParts(model) baseModel = strings.ToLower(strings.TrimSpace(baseModel)) - if baseModel != defaultXAIVideosModel && baseModel != xaiVideos15PreviewModel { + if baseModel != defaultXAIVideosModel && baseModel != xaiVideos15Model && baseModel != xaiVideos15PreviewAlias { return false } @@ -199,8 +200,8 @@ func canonicalXAIVideosModel(model string) string { switch videosModelBase(model) { case defaultXAIVideosModel: return defaultXAIVideosModel - case xaiVideos15PreviewModel: - return xaiVideos15PreviewModel + case xaiVideos15Model, xaiVideos15PreviewAlias: + return xaiVideos15Model } return defaultXAIVideosModel } @@ -732,6 +733,8 @@ func (h *OpenAIAPIHandler) handleXAIVideosNativePost(c *gin.Context) { return } + videoModel = canonicalXAIVideosModel(videoModel) + rawJSON, _ = sjson.SetBytes(rawJSON, "model", videoModel) h.collectXAIVideosNative(c, rawJSON, videoModel, true) } diff --git a/sdk/api/handlers/openai/openai_videos_handlers_test.go b/sdk/api/handlers/openai/openai_videos_handlers_test.go index 52f6ca092..c4a82c29e 100644 --- a/sdk/api/handlers/openai/openai_videos_handlers_test.go +++ b/sdk/api/handlers/openai/openai_videos_handlers_test.go @@ -63,11 +63,12 @@ func performVideosRouteRequest(t *testing.T, method string, routePath string, re } type videoAuthCaptureExecutor struct { - mu sync.Mutex - requestID string - contentURL string - authIDs []string - models []string + mu sync.Mutex + requestID string + contentURL string + authIDs []string + models []string + payloadModels []string } func (e *videoAuthCaptureExecutor) Identifier() string { return "xai" } @@ -80,6 +81,7 @@ func (e *videoAuthCaptureExecutor) Execute(_ context.Context, auth *coreauth.Aut e.mu.Lock() e.authIDs = append(e.authIDs, authID) e.models = append(e.models, req.Model) + e.payloadModels = append(e.payloadModels, strings.TrimSpace(gjson.GetBytes(req.Payload, "model").String())) e.mu.Unlock() requestID := strings.TrimSpace(gjson.GetBytes(req.Payload, "request_id").String()) @@ -126,6 +128,14 @@ func (e *videoAuthCaptureExecutor) Models() []string { return out } +func (e *videoAuthCaptureExecutor) PayloadModels() []string { + e.mu.Lock() + defer e.mu.Unlock() + out := make([]string, len(e.payloadModels)) + copy(out, e.payloadModels) + return out +} + func resetVideoAuthBindingsForTest(t *testing.T) { t.Helper() previous := videoAuthBindings @@ -170,6 +180,10 @@ func TestVideosModelValidationAllowsXAIVideoModel(t *testing.T) { "xai/grok-imagine-video", "x-ai/grok-imagine-video", "grok/grok-imagine-video", + "grok-imagine-video-1.5", + "xai/grok-imagine-video-1.5", + "x-ai/grok-imagine-video-1.5", + "grok/grok-imagine-video-1.5", "grok-imagine-video-1.5-preview", "xai/grok-imagine-video-1.5-preview", "x-ai/grok-imagine-video-1.5-preview", @@ -188,6 +202,9 @@ func TestVideosModelValidationAllowsXAIVideoModel(t *testing.T) { if isSupportedVideosModel("codex/grok-imagine-video") { t.Fatal("expected codex/grok-imagine-video to be rejected") } + if isSupportedVideosModel("codex/grok-imagine-video-1.5") { + t.Fatal("expected codex/grok-imagine-video-1.5 to be rejected") + } if isSupportedVideosModel("codex/grok-imagine-video-1.5-preview") { t.Fatal("expected codex/grok-imagine-video-1.5-preview to be rejected") } @@ -240,7 +257,23 @@ func TestBuildXAIVideosCreateRequest(t *testing.T) { } } -func TestBuildXAIVideosCreateRequestAllowsPreviewModel(t *testing.T) { +func TestBuildXAIVideosCreateRequestAllowsVideo15Model(t *testing.T) { + rawJSON := []byte(`{"model":"xai/grok-imagine-video-1.5","prompt":"a cat playing piano","seconds":"8"}`) + + req, meta, err := buildXAIVideosCreateRequest(rawJSON, "xai/grok-imagine-video-1.5") + if err != nil { + t.Fatalf("buildXAIVideosCreateRequest() error = %v", err) + } + + if got := gjson.GetBytes(req, "model").String(); got != xaiVideos15Model { + t.Fatalf("model = %q, want %s", got, xaiVideos15Model) + } + if meta.Model != xaiVideos15Model { + t.Fatalf("meta model = %q, want %s", meta.Model, xaiVideos15Model) + } +} + +func TestBuildXAIVideosCreateRequestNormalizesVideo15PreviewAlias(t *testing.T) { rawJSON := []byte(`{"model":"xai/grok-imagine-video-1.5-preview","prompt":"a cat playing piano","seconds":"8"}`) req, meta, err := buildXAIVideosCreateRequest(rawJSON, "xai/grok-imagine-video-1.5-preview") @@ -248,11 +281,11 @@ func TestBuildXAIVideosCreateRequestAllowsPreviewModel(t *testing.T) { t.Fatalf("buildXAIVideosCreateRequest() error = %v", err) } - if got := gjson.GetBytes(req, "model").String(); got != xaiVideos15PreviewModel { - t.Fatalf("model = %q, want %s", got, xaiVideos15PreviewModel) + if got := gjson.GetBytes(req, "model").String(); got != xaiVideos15Model { + t.Fatalf("model = %q, want %s", got, xaiVideos15Model) } - if meta.Model != xaiVideos15PreviewModel { - t.Fatalf("meta model = %q, want %s", meta.Model, xaiVideos15PreviewModel) + if meta.Model != xaiVideos15Model { + t.Fatalf("meta model = %q, want %s", meta.Model, xaiVideos15Model) } } @@ -734,9 +767,9 @@ func TestXAIVideosNativeCreateBindsRetrieveToSelectedAuth(t *testing.T) { } } -func TestXAIVideosNativeRetrieveUsesBoundModel(t *testing.T) { +func TestXAIVideosNativeRetrieveUsesCanonicalBoundModel(t *testing.T) { resetVideoAuthBindingsForTest(t) - executor := &videoAuthCaptureExecutor{requestID: "video-xai-preview-bound"} + executor := &videoAuthCaptureExecutor{requestID: "video-xai-1.5-bound"} manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil) manager.RegisterExecutor(executor) @@ -744,8 +777,8 @@ func TestXAIVideosNativeRetrieveUsesBoundModel(t *testing.T) { authID string model string }{ - {authID: "video-xai-preview-default-auth", model: defaultXAIVideosModel}, - {authID: "video-xai-preview-auth", model: xaiVideos15PreviewModel}, + {authID: "video-xai-1.5-default-auth", model: defaultXAIVideosModel}, + {authID: "video-xai-1.5-auth", model: xaiVideos15Model}, } for _, entry := range authModels { auth := &coreauth.Auth{ @@ -768,7 +801,7 @@ func TestXAIVideosNativeRetrieveUsesBoundModel(t *testing.T) { base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager) handler := NewOpenAIAPIHandler(base) - createResp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosGenerationsAPI, "application/json", strings.NewReader(`{"model":"grok-imagine-video-1.5-preview","prompt":"make a video"}`), handler.XAIVideosGenerations) + createResp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosGenerationsAPI, "application/json", strings.NewReader(`{"model":"grok-imagine-video-1.5","prompt":"make a video"}`), handler.XAIVideosGenerations) if createResp.Code != http.StatusOK { t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String()) } @@ -786,22 +819,26 @@ func TestXAIVideosNativeRetrieveUsesBoundModel(t *testing.T) { if len(authIDs) != 2 { t.Fatalf("authIDs = %v, want two calls", authIDs) } - if authIDs[0] != "video-xai-preview-auth" || authIDs[1] != authIDs[0] { - t.Fatalf("authIDs = %v, want both calls to use video-xai-preview-auth", authIDs) + if authIDs[0] != "video-xai-1.5-auth" || authIDs[1] != authIDs[0] { + t.Fatalf("authIDs = %v, want both calls to use video-xai-1.5-auth", authIDs) } models := executor.Models() if len(models) != 2 { t.Fatalf("models = %v, want two calls", models) } - if models[0] != xaiVideos15PreviewModel || models[1] != xaiVideos15PreviewModel { - t.Fatalf("models = %v, want both calls to use %s", models, xaiVideos15PreviewModel) + if models[0] != xaiVideos15Model || models[1] != xaiVideos15Model { + t.Fatalf("models = %v, want both calls to use %s", models, xaiVideos15Model) + } + payloadModels := executor.PayloadModels() + if len(payloadModels) != 2 || payloadModels[0] != xaiVideos15Model { + t.Fatalf("payload models = %v, want create payload model %s", payloadModels, xaiVideos15Model) } binding, ok := videoAuthBindings.getBinding(videoID) if !ok { t.Fatal("video auth binding was not stored") } - if binding.authID != "video-xai-preview-auth" || binding.model != xaiVideos15PreviewModel { - t.Fatalf("binding = {authID:%q model:%q}, want {authID:%q model:%q}", binding.authID, binding.model, "video-xai-preview-auth", xaiVideos15PreviewModel) + if binding.authID != "video-xai-1.5-auth" || binding.model != xaiVideos15Model { + t.Fatalf("binding = {authID:%q model:%q}, want {authID:%q model:%q}", binding.authID, binding.model, "video-xai-1.5-auth", xaiVideos15Model) } } From 61d9a30d126ed99a8ff4a12fbc80d11cbc9ea36b Mon Sep 17 00:00:00 2001 From: oscarbrey <188580177+oscarbrey@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:19:16 +0200 Subject: [PATCH 2/3] fix(xai): preserve preview alias auth routing --- .../handlers/openai/openai_videos_handlers.go | 59 +++++++----- .../openai/openai_videos_handlers_test.go | 94 +++++++++++++++++++ 2 files changed, 131 insertions(+), 22 deletions(-) diff --git a/sdk/api/handlers/openai/openai_videos_handlers.go b/sdk/api/handlers/openai/openai_videos_handlers.go index 76573ccf3..6f9e1c3ac 100644 --- a/sdk/api/handlers/openai/openai_videos_handlers.go +++ b/sdk/api/handlers/openai/openai_videos_handlers.go @@ -46,12 +46,12 @@ const defaultVideoAuthBindingTTL = 3 * time.Hour var videoAuthBindings = newVideoAuthBindingStore() type xaiVideoCreateMetadata struct { - Model string - UpstreamModel string - Prompt string - Seconds string - Size string - CreatedAt int64 + Model string + RoutingModel string + Prompt string + Seconds string + Size string + CreatedAt int64 } type videoAuthBinding struct { @@ -206,6 +206,21 @@ func canonicalXAIVideosModel(model string) string { return defaultXAIVideosModel } +func routingXAIVideosModel(model string) string { + if isSoraVideosModel(model) { + return defaultXAIVideosModel + } + switch videosModelBase(model) { + case defaultXAIVideosModel: + return defaultXAIVideosModel + case xaiVideos15Model: + return xaiVideos15Model + case xaiVideos15PreviewAlias: + return xaiVideos15PreviewAlias + } + return defaultXAIVideosModel +} + func responseVideosModel(model string) string { return canonicalXAIVideosModel(model) } @@ -299,11 +314,11 @@ func (h *OpenAIAPIHandler) bindVideoAuthIDAndModelFromPayload(payload []byte, au if videoID == "" { return } - videoAuthBindings.setWithModel(videoID, authID, canonicalXAIVideosModel(model), h.videoAuthBindingTTL()) + videoAuthBindings.setWithModel(videoID, authID, routingXAIVideosModel(model), h.videoAuthBindingTTL()) } func (h *OpenAIAPIHandler) bindVideoAuthID(videoID string, authID string, model string) { - videoAuthBindings.setWithModel(videoID, authID, canonicalXAIVideosModel(model), h.videoAuthBindingTTL()) + videoAuthBindings.setWithModel(videoID, authID, routingXAIVideosModel(model), h.videoAuthBindingTTL()) } func (h *OpenAIAPIHandler) contextWithVideoAuthBinding(ctx context.Context, videoID string) context.Context { @@ -375,12 +390,12 @@ func buildXAIVideosCreateRequest(rawJSON []byte, model string) ([]byte, xaiVideo } meta := xaiVideoCreateMetadata{ - Model: responseVideosModel(model), - UpstreamModel: videoModel, - Prompt: prompt, - Seconds: seconds, - Size: size, - CreatedAt: time.Now().Unix(), + Model: responseVideosModel(model), + RoutingModel: routingXAIVideosModel(model), + Prompt: prompt, + Seconds: seconds, + Size: size, + CreatedAt: time.Now().Unix(), } return req, meta, nil } @@ -733,9 +748,9 @@ func (h *OpenAIAPIHandler) handleXAIVideosNativePost(c *gin.Context) { return } - videoModel = canonicalXAIVideosModel(videoModel) - rawJSON, _ = sjson.SetBytes(rawJSON, "model", videoModel) - h.collectXAIVideosNative(c, rawJSON, videoModel, true) + routingModel := routingXAIVideosModel(videoModel) + rawJSON, _ = sjson.SetBytes(rawJSON, "model", canonicalXAIVideosModel(videoModel)) + h.collectXAIVideosNative(c, rawJSON, routingModel, true) } func (h *OpenAIAPIHandler) XAIVideosRetrieve(c *gin.Context) { @@ -998,12 +1013,12 @@ func (h *OpenAIAPIHandler) collectXAIVideosCreate(c *gin.Context, xaiReq []byte, cliCtx = handlers.WithSelectedAuthIDCallback(cliCtx, func(authID string) { selectedAuthID = authID }) - upstreamModel := strings.TrimSpace(meta.UpstreamModel) - if upstreamModel == "" { - upstreamModel = meta.Model + routingModel := strings.TrimSpace(meta.RoutingModel) + if routingModel == "" { + routingModel = routingXAIVideosModel(meta.Model) } stopKeepAlive := h.StartNonStreamingKeepAlive(c, cliCtx) - resp, upstreamHeaders, errMsg := h.ExecuteWithAuthManager(cliCtx, xaiVideosHandlerType, upstreamModel, xaiReq, "") + resp, upstreamHeaders, errMsg := h.ExecuteWithAuthManager(cliCtx, xaiVideosHandlerType, routingModel, xaiReq, "") stopKeepAlive() if errMsg != nil { h.WriteErrorResponse(c, errMsg) @@ -1023,7 +1038,7 @@ func (h *OpenAIAPIHandler) collectXAIVideosCreate(c *gin.Context, xaiReq []byte, return } - h.bindVideoAuthIDFromPayload(out, selectedAuthID) + h.bindVideoAuthIDAndModelFromPayload(out, selectedAuthID, routingModel) handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders) _, _ = c.Writer.Write(out) cliCancel(nil) diff --git a/sdk/api/handlers/openai/openai_videos_handlers_test.go b/sdk/api/handlers/openai/openai_videos_handlers_test.go index c4a82c29e..8b2a7afa9 100644 --- a/sdk/api/handlers/openai/openai_videos_handlers_test.go +++ b/sdk/api/handlers/openai/openai_videos_handlers_test.go @@ -271,6 +271,9 @@ func TestBuildXAIVideosCreateRequestAllowsVideo15Model(t *testing.T) { if meta.Model != xaiVideos15Model { t.Fatalf("meta model = %q, want %s", meta.Model, xaiVideos15Model) } + if meta.RoutingModel != xaiVideos15Model { + t.Fatalf("routing model = %q, want %s", meta.RoutingModel, xaiVideos15Model) + } } func TestBuildXAIVideosCreateRequestNormalizesVideo15PreviewAlias(t *testing.T) { @@ -287,6 +290,9 @@ func TestBuildXAIVideosCreateRequestNormalizesVideo15PreviewAlias(t *testing.T) if meta.Model != xaiVideos15Model { t.Fatalf("meta model = %q, want %s", meta.Model, xaiVideos15Model) } + if meta.RoutingModel != xaiVideos15PreviewAlias { + t.Fatalf("routing model = %q, want %s", meta.RoutingModel, xaiVideos15PreviewAlias) + } } func TestBuildXAIVideosCreateRequestAllowsCustomSeconds(t *testing.T) { @@ -842,6 +848,94 @@ func TestXAIVideosNativeRetrieveUsesCanonicalBoundModel(t *testing.T) { } } +func TestVideosCreatePreviewAliasUsesPreviewAuthWithGAPayload(t *testing.T) { + resetVideoAuthBindingsForTest(t) + executor := &videoAuthCaptureExecutor{requestID: "video-openai-preview-alias"} + handler := newVideoSingleModelAuthTestHandler(t, executor, "video-openai-preview-auth", xaiVideos15PreviewAlias) + + createResp := performVideosEndpointRequest(t, http.MethodPost, openAIVideosPath, "application/json", strings.NewReader(`{"model":"grok-imagine-video-1.5-preview","prompt":"make a video"}`), handler.VideosCreate) + if createResp.Code != http.StatusOK { + t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String()) + } + videoID := gjson.GetBytes(createResp.Body.Bytes(), "id").String() + if got := gjson.GetBytes(createResp.Body.Bytes(), "model").String(); got != xaiVideos15Model { + t.Fatalf("response model = %q, want %s", got, xaiVideos15Model) + } + + retrieveResp := performVideosRouteRequest(t, http.MethodGet, openAIVideosPath+"/:video_id", openAIVideosPath+"/"+videoID, "", nil, handler.VideosRetrieve) + if retrieveResp.Code != http.StatusOK { + t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String()) + } + + assertPreviewAliasRouting(t, executor, videoID, "video-openai-preview-auth") +} + +func TestXAIVideosNativePreviewAliasUsesPreviewAuthWithGAPayload(t *testing.T) { + resetVideoAuthBindingsForTest(t) + executor := &videoAuthCaptureExecutor{requestID: "video-native-preview-alias"} + handler := newVideoSingleModelAuthTestHandler(t, executor, "video-native-preview-auth", xaiVideos15PreviewAlias) + + createResp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosGenerationsAPI, "application/json", strings.NewReader(`{"model":"grok-imagine-video-1.5-preview","prompt":"make a video"}`), handler.XAIVideosGenerations) + if createResp.Code != http.StatusOK { + t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String()) + } + videoID := gjson.GetBytes(createResp.Body.Bytes(), "request_id").String() + + retrieveResp := performVideosRouteRequest(t, http.MethodGet, videosPath+"/:request_id", videosPath+"/"+videoID, "", nil, handler.XAIVideosRetrieve) + if retrieveResp.Code != http.StatusOK { + t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String()) + } + + assertPreviewAliasRouting(t, executor, videoID, "video-native-preview-auth") +} + +func newVideoSingleModelAuthTestHandler(t *testing.T, executor *videoAuthCaptureExecutor, authID string, model string) *OpenAIAPIHandler { + t.Helper() + + manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil) + manager.RegisterExecutor(executor) + auth := &coreauth.Auth{ + ID: authID, + Provider: "xai", + Status: coreauth.StatusActive, + } + if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil { + t.Fatalf("manager.Register(%s): %v", authID, errRegister) + } + registry.GetGlobalRegistry().RegisterClient(authID, auth.Provider, []*registry.ModelInfo{{ID: model}}) + manager.RefreshSchedulerEntry(authID) + t.Cleanup(func() { + registry.GetGlobalRegistry().UnregisterClient(authID) + }) + + base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager) + return NewOpenAIAPIHandler(base) +} + +func assertPreviewAliasRouting(t *testing.T, executor *videoAuthCaptureExecutor, videoID string, authID string) { + t.Helper() + + authIDs := executor.AuthIDs() + if len(authIDs) != 2 || authIDs[0] != authID || authIDs[1] != authID { + t.Fatalf("authIDs = %v, want both calls to use %s", authIDs, authID) + } + models := executor.Models() + if len(models) != 2 || models[0] != xaiVideos15PreviewAlias || models[1] != xaiVideos15PreviewAlias { + t.Fatalf("models = %v, want both calls to route with %s", models, xaiVideos15PreviewAlias) + } + payloadModels := executor.PayloadModels() + if len(payloadModels) != 2 || payloadModels[0] != xaiVideos15Model { + t.Fatalf("payload models = %v, want create payload model %s", payloadModels, xaiVideos15Model) + } + binding, ok := videoAuthBindings.getBinding(videoID) + if !ok { + t.Fatal("video auth binding was not stored") + } + if binding.authID != authID || binding.model != xaiVideos15PreviewAlias { + t.Fatalf("binding = {authID:%q model:%q}, want {authID:%q model:%q}", binding.authID, binding.model, authID, xaiVideos15PreviewAlias) + } +} + func TestVideoAuthBindingTTLUsesConfig(t *testing.T) { base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{VideoResultAuthCacheTTL: "45m"}, nil) handler := NewOpenAIAPIHandler(base) From 84232747e20e09871f9996601dad9e627a47448f Mon Sep 17 00:00:00 2001 From: oscarbrey <188580177+oscarbrey@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:28:53 +0200 Subject: [PATCH 3/3] fix(xai): register video preview alias --- internal/registry/model_definitions.go | 16 +++++++++- internal/registry/model_definitions_test.go | 10 +++++-- .../openai/openai_videos_handlers_test.go | 30 ++++++++++++++++++- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/internal/registry/model_definitions.go b/internal/registry/model_definitions.go index 41e52de22..3567f67ca 100644 --- a/internal/registry/model_definitions.go +++ b/internal/registry/model_definitions.go @@ -13,6 +13,7 @@ const ( xaiBuiltinImageQualityModelID = "grok-imagine-image-quality" xaiBuiltinVideoModelID = "grok-imagine-video" xaiBuiltinVideo15ModelID = "grok-imagine-video-1.5" + xaiBuiltinVideo15PreviewID = "grok-imagine-video-1.5-preview" ) // staticModelsJSON mirrors the top-level structure of models.json. @@ -120,7 +121,7 @@ func WithCodexBuiltins(models []*ModelInfo) []*ModelInfo { // WithXAIBuiltins injects hard-coded xAI image/video model definitions that should // not depend on remote models.json updates. func WithXAIBuiltins(models []*ModelInfo) []*ModelInfo { - return upsertModelInfos(models, xaiBuiltinImageModelInfo(), xaiBuiltinImageQualityModelInfo(), xaiBuiltinVideoModelInfo(), xaiBuiltinVideo15ModelInfo()) + return upsertModelInfos(models, xaiBuiltinImageModelInfo(), xaiBuiltinImageQualityModelInfo(), xaiBuiltinVideoModelInfo(), xaiBuiltinVideo15ModelInfo(), xaiBuiltinVideo15PreviewModelInfo()) } func normalizeAntigravityCapabilityModelID(modelID string) string { @@ -207,6 +208,19 @@ func xaiBuiltinVideo15ModelInfo() *ModelInfo { } } +func xaiBuiltinVideo15PreviewModelInfo() *ModelInfo { + return &ModelInfo{ + ID: xaiBuiltinVideo15PreviewID, + Object: "model", + Created: 1735689600, // 2025-01-01 + OwnedBy: "xai", + Type: "xai", + DisplayName: "Grok Imagine Video 1.5 Preview", + Name: xaiBuiltinVideo15PreviewID, + Description: "Compatibility alias for the xAI Grok video generation model.", + } +} + func upsertModelInfos(models []*ModelInfo, extras ...*ModelInfo) []*ModelInfo { if len(extras) == 0 { return models diff --git a/internal/registry/model_definitions_test.go b/internal/registry/model_definitions_test.go index c256e0dc7..2c5777736 100644 --- a/internal/registry/model_definitions_test.go +++ b/internal/registry/model_definitions_test.go @@ -35,9 +35,10 @@ func TestGeminiVertexModelsUseFlashLiteReleaseID(t *testing.T) { t.Fatalf("Vertex models do not contain %q", releaseID) } -func TestWithXAIBuiltinsUsesVideo15GAModel(t *testing.T) { +func TestWithXAIBuiltinsIncludesVideo15GAAndPreviewAlias(t *testing.T) { models := WithXAIBuiltins(nil) foundGA := false + foundPreviewAlias := false for _, model := range models { if model == nil { @@ -46,14 +47,17 @@ func TestWithXAIBuiltinsUsesVideo15GAModel(t *testing.T) { if model.ID == xaiBuiltinVideo15ModelID { foundGA = true } - if model.ID == "grok-imagine-video-1.5-preview" { - t.Fatal("xAI builtins must not advertise the retired preview alias") + if model.ID == xaiBuiltinVideo15PreviewID { + foundPreviewAlias = true } } if !foundGA { t.Fatalf("expected xAI builtin model %s", xaiBuiltinVideo15ModelID) } + if !foundPreviewAlias { + t.Fatalf("expected xAI builtin compatibility alias %s", xaiBuiltinVideo15PreviewID) + } } func TestAntigravityWebSearchModelForRequiresRequestedModelCapability(t *testing.T) { diff --git a/sdk/api/handlers/openai/openai_videos_handlers_test.go b/sdk/api/handlers/openai/openai_videos_handlers_test.go index 8b2a7afa9..29666d881 100644 --- a/sdk/api/handlers/openai/openai_videos_handlers_test.go +++ b/sdk/api/handlers/openai/openai_videos_handlers_test.go @@ -870,6 +870,28 @@ func TestVideosCreatePreviewAliasUsesPreviewAuthWithGAPayload(t *testing.T) { assertPreviewAliasRouting(t, executor, videoID, "video-openai-preview-auth") } +func TestVideosCreatePreviewAliasUsesDefaultXAIModelsWithGAPayload(t *testing.T) { + resetVideoAuthBindingsForTest(t) + executor := &videoAuthCaptureExecutor{requestID: "video-openai-preview-default-models"} + handler := newVideoAuthTestHandler(t, executor, "video-openai-preview-default-auth", registry.GetXAIModels()) + + createResp := performVideosEndpointRequest(t, http.MethodPost, openAIVideosPath, "application/json", strings.NewReader(`{"model":"grok-imagine-video-1.5-preview","prompt":"make a video"}`), handler.VideosCreate) + if createResp.Code != http.StatusOK { + t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String()) + } + videoID := gjson.GetBytes(createResp.Body.Bytes(), "id").String() + if got := gjson.GetBytes(createResp.Body.Bytes(), "model").String(); got != xaiVideos15Model { + t.Fatalf("response model = %q, want %s", got, xaiVideos15Model) + } + + retrieveResp := performVideosRouteRequest(t, http.MethodGet, openAIVideosPath+"/:video_id", openAIVideosPath+"/"+videoID, "", nil, handler.VideosRetrieve) + if retrieveResp.Code != http.StatusOK { + t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String()) + } + + assertPreviewAliasRouting(t, executor, videoID, "video-openai-preview-default-auth") +} + func TestXAIVideosNativePreviewAliasUsesPreviewAuthWithGAPayload(t *testing.T) { resetVideoAuthBindingsForTest(t) executor := &videoAuthCaptureExecutor{requestID: "video-native-preview-alias"} @@ -892,6 +914,12 @@ func TestXAIVideosNativePreviewAliasUsesPreviewAuthWithGAPayload(t *testing.T) { func newVideoSingleModelAuthTestHandler(t *testing.T, executor *videoAuthCaptureExecutor, authID string, model string) *OpenAIAPIHandler { t.Helper() + return newVideoAuthTestHandler(t, executor, authID, []*registry.ModelInfo{{ID: model}}) +} + +func newVideoAuthTestHandler(t *testing.T, executor *videoAuthCaptureExecutor, authID string, models []*registry.ModelInfo) *OpenAIAPIHandler { + t.Helper() + manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil) manager.RegisterExecutor(executor) auth := &coreauth.Auth{ @@ -902,7 +930,7 @@ func newVideoSingleModelAuthTestHandler(t *testing.T, executor *videoAuthCapture if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil { t.Fatalf("manager.Register(%s): %v", authID, errRegister) } - registry.GetGlobalRegistry().RegisterClient(authID, auth.Provider, []*registry.ModelInfo{{ID: model}}) + registry.GetGlobalRegistry().RegisterClient(authID, auth.Provider, models) manager.RefreshSchedulerEntry(authID) t.Cleanup(func() { registry.GetGlobalRegistry().UnregisterClient(authID)