diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 9f426686..155f2fa4 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -269,6 +269,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-preview", Object: "model", OwnedBy: "xai", Type: "openai"}, }) t.Cleanup(func() { modelRegistry.UnregisterClient(clientID) @@ -355,10 +356,11 @@ func TestModelsWithClientVersionReturnsCodexCatalog(t *testing.T) { } hiddenModels := map[string]bool{ - "grok-imagine-image-quality": false, - "gpt-image-2": false, - "grok-imagine-image": false, - "grok-imagine-video": false, + "grok-imagine-image-quality": false, + "gpt-image-2": false, + "grok-imagine-image": false, + "grok-imagine-video": false, + "grok-imagine-video-1.5-preview": false, } for _, model := range resp.Models { slug, _ := model["slug"].(string) diff --git a/internal/registry/model_definitions.go b/internal/registry/model_definitions.go index f160325f..22fd15f3 100644 --- a/internal/registry/model_definitions.go +++ b/internal/registry/model_definitions.go @@ -7,10 +7,11 @@ import ( ) const ( - codexBuiltinImageModelID = "gpt-image-2" - xaiBuiltinImageModelID = "grok-imagine-image" - xaiBuiltinImageQualityModelID = "grok-imagine-image-quality" - xaiBuiltinVideoModelID = "grok-imagine-video" + codexBuiltinImageModelID = "gpt-image-2" + xaiBuiltinImageModelID = "grok-imagine-image" + xaiBuiltinImageQualityModelID = "grok-imagine-image-quality" + xaiBuiltinVideoModelID = "grok-imagine-video" + xaiBuiltinVideo15PreviewModelID = "grok-imagine-video-1.5-preview" ) // staticModelsJSON mirrors the top-level structure of models.json. @@ -99,7 +100,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()) + return upsertModelInfos(models, xaiBuiltinImageModelInfo(), xaiBuiltinImageQualityModelInfo(), xaiBuiltinVideoModelInfo(), xaiBuiltinVideo15PreviewModelInfo()) } func codexBuiltinImageModelInfo() *ModelInfo { @@ -153,6 +154,19 @@ func xaiBuiltinVideoModelInfo() *ModelInfo { } } +func xaiBuiltinVideo15PreviewModelInfo() *ModelInfo { + return &ModelInfo{ + ID: xaiBuiltinVideo15PreviewModelID, + 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.", + } +} + func upsertModelInfos(models []*ModelInfo, extras ...*ModelInfo) []*ModelInfo { if len(extras) == 0 { return models diff --git a/sdk/api/handlers/openai/codex_client_models.go b/sdk/api/handlers/openai/codex_client_models.go index 5f9a254e..cc894468 100644 --- a/sdk/api/handlers/openai/codex_client_models.go +++ b/sdk/api/handlers/openai/codex_client_models.go @@ -151,7 +151,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-2", "grok-imagine-image", "grok-imagine-video": + case "grok-imagine-image-quality", "gpt-image-2", "grok-imagine-image", "grok-imagine-video", "grok-imagine-video-1.5-preview": entry["visibility"] = "hide" } } diff --git a/sdk/api/handlers/openai/openai_videos_handlers.go b/sdk/api/handlers/openai/openai_videos_handlers.go index 15e69a68..2319c1e8 100644 --- a/sdk/api/handlers/openai/openai_videos_handlers.go +++ b/sdk/api/handlers/openai/openai_videos_handlers.go @@ -22,6 +22,7 @@ const ( xaiVideosEditsAPI = "/v1/videos/edits" xaiVideosExtensionsAPI = "/v1/videos/extensions" defaultXAIVideosModel = "grok-imagine-video" + xaiVideos15PreviewModel = "grok-imagine-video-1.5-preview" xaiVideosHandlerType = "openai-video" defaultVideosSeconds = "4" defaultVideosSize = "720x1280" @@ -45,7 +46,7 @@ func videosModelBase(model string) string { func isXAIVideosModel(model string) bool { prefix, baseModel := imagesModelParts(model) baseModel = strings.ToLower(strings.TrimSpace(baseModel)) - if baseModel != defaultXAIVideosModel { + if baseModel != defaultXAIVideosModel && baseModel != xaiVideos15PreviewModel { return false } @@ -86,8 +87,11 @@ func rejectUnsupportedNativeVideosModel(c *gin.Context, model string) bool { } func canonicalXAIVideosModel(model string) string { - if videosModelBase(model) == defaultXAIVideosModel { + switch videosModelBase(model) { + case defaultXAIVideosModel: return defaultXAIVideosModel + case xaiVideos15PreviewModel: + return xaiVideos15PreviewModel } return defaultXAIVideosModel } @@ -190,8 +194,9 @@ func buildXAIVideosCreateRequest(rawJSON []byte, model string) ([]byte, xaiVideo seconds = "10" } + videoModel := canonicalXAIVideosModel(model) req := []byte(`{}`) - req, _ = sjson.SetBytes(req, "model", canonicalXAIVideosModel(model)) + req, _ = sjson.SetBytes(req, "model", videoModel) req, _ = sjson.SetBytes(req, "prompt", prompt) req, _ = sjson.SetRawBytes(req, "duration", []byte(strconv.FormatInt(duration, 10))) req, _ = sjson.SetBytes(req, "aspect_ratio", aspectRatio) @@ -204,7 +209,7 @@ func buildXAIVideosCreateRequest(rawJSON []byte, model string) ([]byte, xaiVideo } meta := xaiVideoCreateMetadata{ - Model: defaultXAIVideosModel, + Model: videoModel, Prompt: prompt, Seconds: seconds, Size: size, diff --git a/sdk/api/handlers/openai/openai_videos_handlers_test.go b/sdk/api/handlers/openai/openai_videos_handlers_test.go index d4fed8b4..5e4568b4 100644 --- a/sdk/api/handlers/openai/openai_videos_handlers_test.go +++ b/sdk/api/handlers/openai/openai_videos_handlers_test.go @@ -33,7 +33,16 @@ func performVideosEndpointRequest(t *testing.T, method string, endpointPath stri } func TestVideosModelValidationAllowsXAIVideoModel(t *testing.T) { - for _, model := range []string{"grok-imagine-video", "xai/grok-imagine-video", "x-ai/grok-imagine-video", "grok/grok-imagine-video"} { + for _, model := range []string{ + "grok-imagine-video", + "xai/grok-imagine-video", + "x-ai/grok-imagine-video", + "grok/grok-imagine-video", + "grok-imagine-video-1.5-preview", + "xai/grok-imagine-video-1.5-preview", + "x-ai/grok-imagine-video-1.5-preview", + "grok/grok-imagine-video-1.5-preview", + } { if !isSupportedVideosModel(model) { t.Fatalf("expected %s to be supported", model) } @@ -44,6 +53,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-preview") { + t.Fatal("expected codex/grok-imagine-video-1.5-preview to be rejected") + } } func TestBuildXAIVideosCreateRequest(t *testing.T) { @@ -77,6 +89,22 @@ func TestBuildXAIVideosCreateRequest(t *testing.T) { } } +func TestBuildXAIVideosCreateRequestAllowsPreviewModel(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") + if err != nil { + t.Fatalf("buildXAIVideosCreateRequest() error = %v", err) + } + + if got := gjson.GetBytes(req, "model").String(); got != xaiVideos15PreviewModel { + t.Fatalf("model = %q, want %s", got, xaiVideos15PreviewModel) + } + if meta.Model != xaiVideos15PreviewModel { + t.Fatalf("meta model = %q, want %s", meta.Model, xaiVideos15PreviewModel) + } +} + func TestBuildXAIVideosCreateRequestAllowsCustomSeconds(t *testing.T) { rawJSON := []byte(`{"model":"grok-imagine-video","prompt":"a cat playing piano","seconds":"6"}`)