fix(xai): support Grok Imagine Video 1.5 GA

This commit is contained in:
oscarbrey
2026-07-29 23:06:30 +02:00
parent 2b63d6bcda
commit abaeb55bb2
6 changed files with 90 additions and 42 deletions

View File

@@ -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 {

View File

@@ -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"
}
}

View File

@@ -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.",
}
}

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -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)
}
}