feat(api, xai): integrate xAI Grok image models and extend API endpoints for image support
- Added new xAI Grok image models (`grok-imagine-image`, `grok-imagine-image-quality`) with high-fidelity and aspect ratio configurations. - Extended `isSupportedImagesModel` logic to validate xAI models. - Implemented API request builders for image generation/editing with customizable options (e.g., resolution, aspect ratio, response format). - Enhanced `/v1/images` endpoints to handle xAI model capabilities, including response normalization and model-specific handlers. - Updated unit tests to validate xAI model validation, request structure, and API integration.
This commit is contained in:
@@ -40,7 +40,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 " + defaultImagesToolModel + "."
|
||||
expectedMessage := "Model " + model + " is not supported on " + imagesGenerationsPath + " or " + imagesEditsPath + ". Use " + defaultImagesToolModel + ", " + defaultXAIImagesModel + ", or " + xaiImagesQualityModel + "."
|
||||
if message != expectedMessage {
|
||||
t.Fatalf("error message = %q, want %q", message, expectedMessage)
|
||||
}
|
||||
@@ -49,8 +49,8 @@ func assertUnsupportedImagesModelResponse(t *testing.T, resp *httptest.ResponseR
|
||||
}
|
||||
}
|
||||
|
||||
func TestImagesModelValidationAllowsGPTImage2WithOptionalPrefix(t *testing.T) {
|
||||
for _, model := range []string{"gpt-image-2", "codex/gpt-image-2"} {
|
||||
func TestImagesModelValidationAllowsGPTImage2AndXAIModels(t *testing.T) {
|
||||
for _, model := range []string{"gpt-image-2", "codex/gpt-image-2", "grok-imagine-image", "xai/grok-imagine-image", "grok-imagine-image-quality", "xai/grok-imagine-image-quality"} {
|
||||
if !isSupportedImagesModel(model) {
|
||||
t.Fatalf("expected %s to be supported", model)
|
||||
}
|
||||
@@ -58,6 +58,90 @@ func TestImagesModelValidationAllowsGPTImage2WithOptionalPrefix(t *testing.T) {
|
||||
if isSupportedImagesModel("gpt-5.4-mini") {
|
||||
t.Fatal("expected gpt-5.4-mini to be rejected")
|
||||
}
|
||||
if isSupportedImagesModel("codex/grok-imagine-image") {
|
||||
t.Fatal("expected codex/grok-imagine-image to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
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"}`)
|
||||
|
||||
req := buildXAIImagesGenerationsRequest(rawJSON, "xai/grok-imagine-image-quality", "url")
|
||||
|
||||
if got := gjson.GetBytes(req, "model").String(); got != "grok-imagine-image-quality" {
|
||||
t.Fatalf("model = %q, want grok-imagine-image-quality", got)
|
||||
}
|
||||
if got := gjson.GetBytes(req, "prompt").String(); got != "abstract art" {
|
||||
t.Fatalf("prompt = %q, want abstract art", got)
|
||||
}
|
||||
if got := gjson.GetBytes(req, "aspect_ratio").String(); got != "16:9" {
|
||||
t.Fatalf("aspect_ratio = %q, want 16:9", got)
|
||||
}
|
||||
if got := gjson.GetBytes(req, "resolution").String(); got != "2k" {
|
||||
t.Fatalf("resolution = %q, want 2k", got)
|
||||
}
|
||||
if got := gjson.GetBytes(req, "response_format").String(); got != "url" {
|
||||
t.Fatalf("response_format = %q, want url", got)
|
||||
}
|
||||
if got := gjson.GetBytes(req, "n").Int(); got != 2 {
|
||||
t.Fatalf("n = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildXAIImagesEditRequest(t *testing.T) {
|
||||
req := buildXAIImagesEditRequest("grok-imagine-image", "edit it", []string{"data:image/png;base64,AA==", "https://example.com/image.png"}, "b64_json", "3:2", "1k", 0)
|
||||
|
||||
if got := gjson.GetBytes(req, "model").String(); got != "grok-imagine-image" {
|
||||
t.Fatalf("model = %q, want grok-imagine-image", got)
|
||||
}
|
||||
if got := gjson.GetBytes(req, "images.0.type").String(); got != "image_url" {
|
||||
t.Fatalf("images.0.type = %q, want image_url", got)
|
||||
}
|
||||
if got := gjson.GetBytes(req, "images.0.url").String(); got != "data:image/png;base64,AA==" {
|
||||
t.Fatalf("images.0.url = %q", got)
|
||||
}
|
||||
if got := gjson.GetBytes(req, "images.1.url").String(); got != "https://example.com/image.png" {
|
||||
t.Fatalf("images.1.url = %q", got)
|
||||
}
|
||||
if gjson.GetBytes(req, "image").Exists() {
|
||||
t.Fatalf("multiple image edits must use images array: %s", string(req))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildXAIImagesEditRequestSingleImage(t *testing.T) {
|
||||
req := buildXAIImagesEditRequest("grok-imagine-image", "edit it", []string{"https://example.com/image.png"}, "url", "", "", 0)
|
||||
|
||||
if got := gjson.GetBytes(req, "image.type").String(); got != "image_url" {
|
||||
t.Fatalf("image.type = %q, want image_url", got)
|
||||
}
|
||||
if got := gjson.GetBytes(req, "image.url").String(); got != "https://example.com/image.png" {
|
||||
t.Fatalf("image.url = %q", got)
|
||||
}
|
||||
if gjson.GetBytes(req, "images").Exists() {
|
||||
t.Fatalf("single image edit must use image object: %s", string(req))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildImagesAPIResponseFromXAI(t *testing.T) {
|
||||
payload := []byte(`{"created":123,"data":[{"b64_json":"AA==","revised_prompt":"refined","mime_type":"image/png"}],"usage":{"total_tokens":0}}`)
|
||||
|
||||
out, err := buildImagesAPIResponseFromXAI(payload, "b64_json")
|
||||
if err != nil {
|
||||
t.Fatalf("buildImagesAPIResponseFromXAI() error = %v", err)
|
||||
}
|
||||
|
||||
if got := gjson.GetBytes(out, "created").Int(); got != 123 {
|
||||
t.Fatalf("created = %d, want 123", got)
|
||||
}
|
||||
if got := gjson.GetBytes(out, "data.0.b64_json").String(); got != "AA==" {
|
||||
t.Fatalf("data.0.b64_json = %q, want AA==", got)
|
||||
}
|
||||
if got := gjson.GetBytes(out, "data.0.revised_prompt").String(); got != "refined" {
|
||||
t.Fatalf("data.0.revised_prompt = %q, want refined", got)
|
||||
}
|
||||
if !gjson.GetBytes(out, "usage").Exists() {
|
||||
t.Fatalf("usage missing: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestImagesGenerationsRejectsUnsupportedModel(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user