From db35b91e2a4607df5b8e7030d73935076031773f Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Thu, 13 Aug 2026 14:37:29 +0800 Subject: [PATCH] feat(openai): add xAI Grok Imagine Image 2.0 image model support - Added `grok-imagine-image-2.0` as a first-class xAI image base model across validation, canonicalization, and routing checks. - Registered the model in built-in model definitions so it appears in model metadata. - Updated image model allowlists and request validation error messaging to include the new model. - Marked the new model as hidden in client visibility override handling. --- internal/api/server_test.go | 2 ++ internal/client/codex/models/models.go | 2 +- internal/registry/model_definitions.go | 16 ++++++++++++- internal/registry/model_definitions_test.go | 13 +++++++++++ .../handlers/handlers_request_details_test.go | 8 +++++++ sdk/api/handlers/handlers_routing.go | 2 +- .../handlers/openai/openai_images_handlers.go | 23 +++++++++++++++---- .../openai/openai_images_handlers_test.go | 12 ++++++++-- 8 files changed, 68 insertions(+), 10 deletions(-) diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 65a9ac49b..21f78281c 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -1855,6 +1855,7 @@ func TestModelsWithClientVersionReturnsCodexCatalog(t *testing.T) { {ID: "grok-imagine-image-quality", Object: "model", OwnedBy: "xai", Type: "openai"}, {ID: "gpt-image-2", Object: "model", OwnedBy: "openai", Type: "openai"}, {ID: "grok-imagine-image", Object: "model", OwnedBy: "xai", Type: "openai"}, + {ID: "grok-imagine-image-2.0", 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"}, @@ -1955,6 +1956,7 @@ func TestModelsWithClientVersionReturnsCodexCatalog(t *testing.T) { "grok-imagine-image-quality": false, "gpt-image-2": false, "grok-imagine-image": false, + "grok-imagine-image-2.0": false, "grok-imagine-video": false, "grok-imagine-video-1.5": false, "grok-imagine-video-1.5-preview": false, diff --git a/internal/client/codex/models/models.go b/internal/client/codex/models/models.go index 81767e920..3dd684f57 100644 --- a/internal/client/codex/models/models.go +++ b/internal/client/codex/models/models.go @@ -286,7 +286,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", "grok-imagine-video-1.5-preview": + case "grok-imagine-image-quality", "gpt-image-1.5", "gpt-image-2", "grok-imagine-image", "grok-imagine-image-2.0", "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 3567f67ca..408cf6e58 100644 --- a/internal/registry/model_definitions.go +++ b/internal/registry/model_definitions.go @@ -11,6 +11,7 @@ const ( codexBuiltinImageModelID = "gpt-image-2" xaiBuiltinImageModelID = "grok-imagine-image" xaiBuiltinImageQualityModelID = "grok-imagine-image-quality" + xaiBuiltinImage20ModelID = "grok-imagine-image-2.0" xaiBuiltinVideoModelID = "grok-imagine-video" xaiBuiltinVideo15ModelID = "grok-imagine-video-1.5" xaiBuiltinVideo15PreviewID = "grok-imagine-video-1.5-preview" @@ -121,7 +122,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(), xaiBuiltinVideo15PreviewModelInfo()) + return upsertModelInfos(models, xaiBuiltinImageModelInfo(), xaiBuiltinImageQualityModelInfo(), xaiBuiltinImage20ModelInfo(), xaiBuiltinVideoModelInfo(), xaiBuiltinVideo15ModelInfo(), xaiBuiltinVideo15PreviewModelInfo()) } func normalizeAntigravityCapabilityModelID(modelID string) string { @@ -182,6 +183,19 @@ func xaiBuiltinImageQualityModelInfo() *ModelInfo { } } +func xaiBuiltinImage20ModelInfo() *ModelInfo { + return &ModelInfo{ + ID: xaiBuiltinImage20ModelID, + Object: "model", + Created: 1786060800, // 2026-08-07 + OwnedBy: "xai", + Type: "xai", + DisplayName: "Grok Imagine Image 2.0", + Name: xaiBuiltinImage20ModelID, + Description: "xAI Grok image generation model.", + } +} + func xaiBuiltinVideoModelInfo() *ModelInfo { return &ModelInfo{ ID: xaiBuiltinVideoModelID, diff --git a/internal/registry/model_definitions_test.go b/internal/registry/model_definitions_test.go index 2c5777736..2ef087e9e 100644 --- a/internal/registry/model_definitions_test.go +++ b/internal/registry/model_definitions_test.go @@ -35,6 +35,19 @@ func TestGeminiVertexModelsUseFlashLiteReleaseID(t *testing.T) { t.Fatalf("Vertex models do not contain %q", releaseID) } +func TestWithXAIBuiltinsIncludesImage20(t *testing.T) { + models := WithXAIBuiltins(nil) + for _, model := range models { + if model != nil && model.ID == xaiBuiltinImage20ModelID { + if model.Created != 1786060800 { + t.Fatalf("created = %d, want 1786060800 (2026-08-07)", model.Created) + } + return + } + } + t.Fatalf("expected xAI builtin model %s", xaiBuiltinImage20ModelID) +} + func TestWithXAIBuiltinsIncludesVideo15GAAndPreviewAlias(t *testing.T) { models := WithXAIBuiltins(nil) foundGA := false diff --git a/sdk/api/handlers/handlers_request_details_test.go b/sdk/api/handlers/handlers_request_details_test.go index 266bcee08..33d4ca604 100644 --- a/sdk/api/handlers/handlers_request_details_test.go +++ b/sdk/api/handlers/handlers_request_details_test.go @@ -171,6 +171,8 @@ func TestGetRequestDetails_ImageModelReturns503(t *testing.T) { "xai/grok-imagine-image", "grok-imagine-image-quality", "xai/grok-imagine-image-quality", + "grok-imagine-image-2.0", + "xai/grok-imagine-image-2.0", } for _, model := range imageOnlyModels { t.Run(model, func(t *testing.T) { @@ -203,6 +205,8 @@ func TestValidateImageOnlyModel_AllowsImageEndpoints(t *testing.T) { "xai/grok-imagine-image", "grok-imagine-image-quality", "xai/grok-imagine-image-quality", + "grok-imagine-image-2.0", + "xai/grok-imagine-image-2.0", } for _, model := range imageOnlyModels { t.Run(model, func(t *testing.T) { @@ -230,6 +234,8 @@ func TestIsOpenAIImageOnlyModel(t *testing.T) { {model: "xai/grok-imagine-image", want: true}, {model: "XAI/Grok-Imagine-Image-Quality", want: true}, {model: "grok-imagine-image-quality", want: true}, + {model: "grok-imagine-image-2.0", want: true}, + {model: "xai/grok-imagine-image-2.0", want: true}, {model: "grok-3", want: false}, {model: "gpt-5.2", want: false}, {model: "grok-imagine-video", want: false}, @@ -252,6 +258,8 @@ func TestExecuteImageWithAuthManager_AllowsImageOnlyModels(t *testing.T) { "grok-imagine-image", "grok-imagine-image-quality", "xai/grok-imagine-image-quality", + "grok-imagine-image-2.0", + "xai/grok-imagine-image-2.0", } for _, model := range imageOnlyModels { t.Run(model, func(t *testing.T) { diff --git a/sdk/api/handlers/handlers_routing.go b/sdk/api/handlers/handlers_routing.go index 04e8e6025..fe1ec6fd1 100644 --- a/sdk/api/handlers/handlers_routing.go +++ b/sdk/api/handlers/handlers_routing.go @@ -239,7 +239,7 @@ func (h *BaseAPIHandler) validateImageOnlyModel(modelName string, allowImageMode func isOpenAIImageOnlyModel(model string) bool { switch strings.ToLower(strings.TrimSpace(routeModelBaseName(model))) { - case "gpt-image-1.5", "gpt-image-2", "grok-imagine-image", "grok-imagine-image-quality": + case "gpt-image-1.5", "gpt-image-2", "grok-imagine-image", "grok-imagine-image-quality", "grok-imagine-image-2.0": return true default: return false diff --git a/sdk/api/handlers/openai/openai_images_handlers.go b/sdk/api/handlers/openai/openai_images_handlers.go index bc5597779..1d31da2ef 100644 --- a/sdk/api/handlers/openai/openai_images_handlers.go +++ b/sdk/api/handlers/openai/openai_images_handlers.go @@ -31,6 +31,7 @@ const ( defaultImagesToolModel = "gpt-image-2" defaultXAIImagesModel = "grok-imagine-image" xaiImagesQualityModel = "grok-imagine-image-quality" + xaiImages20Model = "grok-imagine-image-2.0" xaiImagesHandlerType = "openai-image" xaiImagesDefaultAspectRatio = "1:1" xaiImagesDefaultResolution = "1k" @@ -210,10 +211,18 @@ func imagesModelBase(model string) string { return strings.ToLower(strings.TrimSpace(baseModel)) } +func isXAIImagesBaseModel(baseModel string) bool { + switch strings.ToLower(strings.TrimSpace(baseModel)) { + case defaultXAIImagesModel, xaiImagesQualityModel, xaiImages20Model: + return true + default: + return false + } +} + func isXAIImagesModel(model string) bool { prefix, baseModel := imagesModelParts(model) - baseModel = strings.ToLower(strings.TrimSpace(baseModel)) - if baseModel != defaultXAIImagesModel && baseModel != xaiImagesQualityModel { + if !isXAIImagesBaseModel(baseModel) { return false } @@ -249,7 +258,7 @@ func rejectUnsupportedImagesModel(c *gin.Context, model string) bool { c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ Error: handlers.ErrorDetail{ - Message: fmt.Sprintf("Model %s is not supported on %s or %s. Use %s, %s, %s, %s, or a configured openai-compatibility image model.", model, imagesGenerationsPath, imagesEditsPath, gptImage15Model, defaultImagesToolModel, defaultXAIImagesModel, xaiImagesQualityModel), + Message: fmt.Sprintf("Model %s is not supported on %s or %s. Use %s, %s, %s, %s, %s, or a configured openai-compatibility image model.", model, imagesGenerationsPath, imagesEditsPath, gptImage15Model, defaultImagesToolModel, defaultXAIImagesModel, xaiImagesQualityModel, xaiImages20Model), Type: "invalid_request_error", }, }) @@ -265,10 +274,14 @@ func normalizeImagesResponseFormat(responseFormat string) string { func canonicalXAIImagesModel(model string) string { baseModel := imagesModelBase(model) - if baseModel == xaiImagesQualityModel { + switch baseModel { + case xaiImagesQualityModel: return xaiImagesQualityModel + case xaiImages20Model: + return xaiImages20Model + default: + return defaultXAIImagesModel } - return defaultXAIImagesModel } func xaiImagesAspectRatio(raw string, fallback string) string { diff --git a/sdk/api/handlers/openai/openai_images_handlers_test.go b/sdk/api/handlers/openai/openai_images_handlers_test.go index 525ad54ed..5bfa8ca3e 100644 --- a/sdk/api/handlers/openai/openai_images_handlers_test.go +++ b/sdk/api/handlers/openai/openai_images_handlers_test.go @@ -46,7 +46,7 @@ func assertUnsupportedImagesModelResponse(t *testing.T, resp *httptest.ResponseR } message := gjson.GetBytes(resp.Body.Bytes(), "error.message").String() - expectedMessage := "Model " + model + " is not supported on " + imagesGenerationsPath + " or " + imagesEditsPath + ". Use " + gptImage15Model + ", " + defaultImagesToolModel + ", " + defaultXAIImagesModel + ", " + xaiImagesQualityModel + ", or a configured openai-compatibility image model." + expectedMessage := "Model " + model + " is not supported on " + imagesGenerationsPath + " or " + imagesEditsPath + ". Use " + gptImage15Model + ", " + defaultImagesToolModel + ", " + defaultXAIImagesModel + ", " + xaiImagesQualityModel + ", " + xaiImages20Model + ", or a configured openai-compatibility image model." if message != expectedMessage { t.Fatalf("error message = %q, want %q", message, expectedMessage) } @@ -56,7 +56,7 @@ func assertUnsupportedImagesModelResponse(t *testing.T, resp *httptest.ResponseR } func TestImagesModelValidationAllowsGPTImageAndXAIModels(t *testing.T) { - for _, model := range []string{"gpt-image-1.5", "codex/gpt-image-1.5", "gpt-image-2", "codex/gpt-image-2", "grok-imagine-image", "xai/grok-imagine-image", "grok-imagine-image-quality", "xai/grok-imagine-image-quality"} { + for _, model := range []string{"gpt-image-1.5", "codex/gpt-image-1.5", "gpt-image-2", "codex/gpt-image-2", "grok-imagine-image", "xai/grok-imagine-image", "grok-imagine-image-quality", "xai/grok-imagine-image-quality", "grok-imagine-image-2.0", "xai/grok-imagine-image-2.0"} { if !isSupportedImagesModel(model) { t.Fatalf("expected %s to be supported", model) } @@ -88,6 +88,14 @@ func TestImagesModelValidationAllowsOpenAICompatImageModels(t *testing.T) { } } +func TestCanonicalXAIImagesModelPreservesImage20(t *testing.T) { + for _, model := range []string{"grok-imagine-image-2.0", "xai/grok-imagine-image-2.0", "XAI/Grok-Imagine-Image-2.0"} { + if got := canonicalXAIImagesModel(model); got != xaiImages20Model { + t.Fatalf("canonicalXAIImagesModel(%q) = %q, want %s", model, got, xaiImages20Model) + } + } +} + func TestBuildXAIImagesGenerationsRequest(t *testing.T) { rawJSON := []byte(`{"model":"xai/grok-imagine-image-quality","prompt":"abstract art","aspect_ratio":"landscape","resolution":"2k","n":2,"response_format":"url"}`)