From 674d065a69453dd03eb96dd10fe571cc8db80782 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:56:59 +0000 Subject: [PATCH 1/7] Add IImageGenerator implementation with tests Co-authored-by: kzu <169707+kzu@users.noreply.github.com> --- src/xAI.Tests/ImageGeneratorTests.cs | 146 +++++++++++++++++++++++ src/xAI/GrokClientExtensions.cs | 8 ++ src/xAI/GrokImageGenerator.cs | 172 +++++++++++++++++++++++++++ 3 files changed, 326 insertions(+) create mode 100644 src/xAI.Tests/ImageGeneratorTests.cs create mode 100644 src/xAI/GrokImageGenerator.cs diff --git a/src/xAI.Tests/ImageGeneratorTests.cs b/src/xAI.Tests/ImageGeneratorTests.cs new file mode 100644 index 0000000..2005c07 --- /dev/null +++ b/src/xAI.Tests/ImageGeneratorTests.cs @@ -0,0 +1,146 @@ +using Microsoft.Extensions.AI; +using Tests.Client.Helpers; +using xAI; +using static ConfigurationExtensions; + +namespace xAI.Tests; + +public class ImageGeneratorTests(ITestOutputHelper output) +{ + [SecretsFact("XAI_API_KEY")] + public async Task GenerateImage_WithPrompt_ReturnsImageContent() + { + var imageGenerator = new GrokClient(Configuration["XAI_API_KEY"]!) + .AsIImageGenerator("grok-2-image"); + + var request = new ImageGenerationRequest("A cat sitting on a tree branch"); + var options = new ImageGenerationOptions + { + ResponseFormat = ImageGenerationResponseFormat.Uri, + Count = 1 + }; + + var response = await imageGenerator.GenerateAsync(request, options); + + Assert.NotNull(response); + Assert.NotEmpty(response.Images); + Assert.Single(response.Images); + + var image = response.Images.First(); + Assert.True(image is UriContent); + + var uriContent = (UriContent)image; + Assert.NotNull(uriContent.Uri); + + output.WriteLine($"Generated image URL: {uriContent.Uri}"); + } + + [SecretsFact("XAI_API_KEY")] + public async Task GenerateImage_WithBase64Response_ReturnsDataContent() + { + var imageGenerator = new GrokClient(Configuration["XAI_API_KEY"]!) + .AsIImageGenerator("grok-2-image"); + + var request = new ImageGenerationRequest("A sunset over mountains"); + var options = new ImageGenerationOptions + { + ResponseFormat = ImageGenerationResponseFormat.Data, + Count = 1 + }; + + var response = await imageGenerator.GenerateAsync(request, options); + + Assert.NotNull(response); + Assert.NotEmpty(response.Images); + Assert.Single(response.Images); + + var image = response.Images.First(); + Assert.True(image is DataContent); + + var dataContent = (DataContent)image; + Assert.True(dataContent.Data.Length > 0); + Assert.Equal("image/jpeg", dataContent.MediaType); + + output.WriteLine($"Generated image size: {dataContent.Data.Length} bytes"); + } + + [SecretsFact("XAI_API_KEY")] + public async Task GenerateMultipleImages_ReturnsCorrectCount() + { + var imageGenerator = new GrokClient(Configuration["XAI_API_KEY"]!) + .AsIImageGenerator("grok-2-image"); + + var request = new ImageGenerationRequest("A robot reading a book"); + var options = new ImageGenerationOptions + { + ResponseFormat = ImageGenerationResponseFormat.Uri, + Count = 3 + }; + + var response = await imageGenerator.GenerateAsync(request, options); + + Assert.NotNull(response); + Assert.NotEmpty(response.Images); + Assert.Equal(3, response.Images.Count); + + foreach (var image in response.Images) + { + Assert.True(image is UriContent); + output.WriteLine($"Image URL: {((UriContent)image).Uri}"); + } + } + + [SecretsFact("XAI_API_KEY")] + public async Task GenerateImage_ResponseContainsModelId() + { + var imageGenerator = new GrokClient(Configuration["XAI_API_KEY"]!) + .AsIImageGenerator("grok-2-image"); + + var request = new ImageGenerationRequest("A futuristic cityscape"); + var options = new ImageGenerationOptions + { + ResponseFormat = ImageGenerationResponseFormat.Uri + }; + + var response = await imageGenerator.GenerateAsync(request, options); + + Assert.NotNull(response); + Assert.NotNull(response.ModelId); + output.WriteLine($"Model used: {response.ModelId}"); + } + + [Fact] + public async Task GenerateImage_WithNullRequest_ThrowsArgumentNullException() + { + var imageGenerator = new GrokClient("test-api-key") + .AsIImageGenerator("grok-2-image"); + + await Assert.ThrowsAsync( + async () => await imageGenerator.GenerateAsync(null!, null)); + } + + [Fact] + public async Task GenerateImage_WithNullPrompt_ThrowsArgumentNullException() + { + var imageGenerator = new GrokClient("test-api-key") + .AsIImageGenerator("grok-2-image"); + + var request = new ImageGenerationRequest(null!); + + await Assert.ThrowsAsync( + async () => await imageGenerator.GenerateAsync(request, null)); + } + + [Fact] + public void GetService_ReturnsImageGeneratorMetadata() + { + var imageGenerator = new GrokClient("test-api-key") + .AsIImageGenerator("grok-2-image"); + + var metadata = imageGenerator.GetService(); + + Assert.NotNull(metadata); + Assert.Equal("xai", metadata.ProviderName); + Assert.Equal("grok-2-image", metadata.ModelId); + } +} diff --git a/src/xAI/GrokClientExtensions.cs b/src/xAI/GrokClientExtensions.cs index 49910d7..db8d556 100644 --- a/src/xAI/GrokClientExtensions.cs +++ b/src/xAI/GrokClientExtensions.cs @@ -15,4 +15,12 @@ public static IChatClient AsIChatClient(this GrokClient client, string defaultMo /// Creates a new from the specified using the given model as the default. public static IChatClient AsIChatClient(this Chat.ChatClient client, string defaultModelId) => new GrokChatClient(client, defaultModelId); + + /// Creates a new from the specified using the given model as the default. + public static IImageGenerator AsIImageGenerator(this GrokClient client, string defaultModelId) + => new GrokImageGenerator(client.Channel, defaultModelId); + + /// Creates a new from the specified using the given model as the default. + public static IImageGenerator AsIImageGenerator(this Image.ImageClient client, string defaultModelId) + => new GrokImageGenerator(client, defaultModelId); } \ No newline at end of file diff --git a/src/xAI/GrokImageGenerator.cs b/src/xAI/GrokImageGenerator.cs new file mode 100644 index 0000000..ada11c6 --- /dev/null +++ b/src/xAI/GrokImageGenerator.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Net.Client; +using Microsoft.Extensions.AI; +using xAI.Protocol; +using static xAI.Protocol.Image; + +namespace xAI; + +/// +/// Represents an for xAI's Grok image generation service. +/// +internal sealed class GrokImageGenerator : IImageGenerator +{ + /// Metadata about the image generator. + private readonly ImageGeneratorMetadata _metadata; + + /// The underlying . + private readonly ImageClient _imageClient; + + /// The default model ID to use for image generation. + private readonly string _defaultModelId; + + /// + /// Initializes a new instance of the class for the specified . + /// + /// The gRPC channel to use for communication. + /// The default model ID to use for image generation. + internal GrokImageGenerator(GrpcChannel channel, string defaultModelId) + : this(new ImageClient(channel), defaultModelId) + { + } + + /// + /// Initializes a new instance of the class for the specified . + /// + /// The underlying image client. + /// The default model ID to use for image generation. + /// is . + public GrokImageGenerator(ImageClient imageClient, string defaultModelId) + { + _imageClient = Throw.IfNull(imageClient); + _defaultModelId = Throw.IfNullOrWhitespace(defaultModelId); + _metadata = new ImageGeneratorMetadata("xai", null, defaultModelId); + } + + /// + public async Task GenerateAsync( + ImageGenerationRequest request, + ImageGenerationOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(request); + + string? prompt = request.Prompt; + _ = Throw.IfNull(prompt); + + // Build the protocol request + var protocolRequest = new GenerateImageRequest + { + Prompt = prompt, + Model = options?.ModelId ?? _defaultModelId, + }; + + // Set the number of images to generate + if (options?.Count is { } count) + { + protocolRequest.N = count; + } + + // Set the response format (URL or base64) + if (options?.ResponseFormat is { } responseFormat) + { + protocolRequest.Format = responseFormat switch + { + ImageGenerationResponseFormat.Uri => ImageFormat.ImgFormatUrl, + ImageGenerationResponseFormat.Data => ImageFormat.ImgFormatBase64, + _ => ImageFormat.ImgFormatInvalid + }; + } + + // Handle image editing if original images are provided + if (request.OriginalImages is not null && request.OriginalImages.Any()) + { + var originalImage = request.OriginalImages.FirstOrDefault(); + if (originalImage is DataContent dataContent) + { + // Convert the data content to a base64 string or URL for the API + var imageUrl = dataContent.Uri?.ToString(); + if (imageUrl == null && dataContent.Data.Length > 0) + { + // Convert to base64 if we have raw data + imageUrl = $"data:{dataContent.MediaType ?? "image/png"};base64,{Convert.ToBase64String(dataContent.Data.ToArray())}"; + } + + if (imageUrl != null) + { + protocolRequest.Image = new ImageUrlContent + { + ImageUrl = imageUrl + }; + } + } + else if (originalImage is UriContent uriContent) + { + protocolRequest.Image = new ImageUrlContent + { + ImageUrl = uriContent.Uri.ToString() + }; + } + } + + // Call the gRPC API + var response = await _imageClient.GenerateImageAsync(protocolRequest, cancellationToken: cancellationToken).ConfigureAwait(false); + + // Convert the response to the Microsoft.Extensions.AI format + return ToImageGenerationResponse(response, options?.MediaType); + } + + /// + public object? GetService(Type serviceType, object? serviceKey = null) => + serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) : + serviceKey is not null ? null : + serviceType == typeof(ImageGeneratorMetadata) ? _metadata : + serviceType == typeof(ImageClient) ? _imageClient : + serviceType.IsInstanceOfType(this) ? this : + null; + + /// + void IDisposable.Dispose() + { + // Nothing to dispose. Implementation required for the IImageGenerator interface. + } + + /// + /// Converts an xAI to a . + /// + private static ImageGenerationResponse ToImageGenerationResponse(ImageResponse response, string? mediaType) + { + var contents = new List(); + var contentType = mediaType ?? "image/jpeg"; // xAI returns JPG by default + + foreach (var image in response.Images) + { + switch (image.ImageCase) + { + case GeneratedImage.ImageOneofCase.Base64: + { + var imageBytes = Convert.FromBase64String(image.Base64); + contents.Add(new DataContent(imageBytes, contentType)); + break; + } + case GeneratedImage.ImageOneofCase.Url: + { + contents.Add(new UriContent(new Uri(image.Url), contentType)); + break; + } + default: + throw new InvalidOperationException("Generated image does not contain a valid URL or base64 data."); + } + } + + return new ImageGenerationResponse(contents) + { + ModelId = response.Model, + RawRepresentation = response, + }; + } +} From 057dd5ed9838366823ad476f4dbbe38e8cfbf223 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:04:53 +0000 Subject: [PATCH 2/7] Fix code style: remove underscore prefix and redundant visibility modifiers Co-authored-by: kzu <169707+kzu@users.noreply.github.com> --- src/xAI/GrokClientExtensions.cs | 2 +- src/xAI/GrokImageGenerator.cs | 63 +++++++++++++++------------------ 2 files changed, 29 insertions(+), 36 deletions(-) diff --git a/src/xAI/GrokClientExtensions.cs b/src/xAI/GrokClientExtensions.cs index db8d556..9bf53fb 100644 --- a/src/xAI/GrokClientExtensions.cs +++ b/src/xAI/GrokClientExtensions.cs @@ -18,7 +18,7 @@ public static IChatClient AsIChatClient(this Chat.ChatClient client, string defa /// Creates a new from the specified using the given model as the default. public static IImageGenerator AsIImageGenerator(this GrokClient client, string defaultModelId) - => new GrokImageGenerator(client.Channel, defaultModelId); + => new GrokImageGenerator(client.Channel, client.Options, defaultModelId); /// Creates a new from the specified using the given model as the default. public static IImageGenerator AsIImageGenerator(this Image.ImageClient client, string defaultModelId) diff --git a/src/xAI/GrokImageGenerator.cs b/src/xAI/GrokImageGenerator.cs index ada11c6..f217fbf 100644 --- a/src/xAI/GrokImageGenerator.cs +++ b/src/xAI/GrokImageGenerator.cs @@ -13,38 +13,32 @@ namespace xAI; /// /// Represents an for xAI's Grok image generation service. /// -internal sealed class GrokImageGenerator : IImageGenerator +sealed class GrokImageGenerator : IImageGenerator { - /// Metadata about the image generator. - private readonly ImageGeneratorMetadata _metadata; + const string DefaultContentType = "image/png"; - /// The underlying . - private readonly ImageClient _imageClient; + readonly ImageGeneratorMetadata metadata; + readonly ImageClient imageClient; + readonly string defaultModelId; + readonly GrokClientOptions clientOptions; - /// The default model ID to use for image generation. - private readonly string _defaultModelId; + internal GrokImageGenerator(GrpcChannel channel, GrokClientOptions clientOptions, string defaultModelId) + : this(new ImageClient(channel), clientOptions, defaultModelId) + { } /// - /// Initializes a new instance of the class for the specified . + /// Test constructor. /// - /// The gRPC channel to use for communication. - /// The default model ID to use for image generation. - internal GrokImageGenerator(GrpcChannel channel, string defaultModelId) - : this(new ImageClient(channel), defaultModelId) - { - } + internal GrokImageGenerator(ImageClient imageClient, string defaultModelId) + : this(imageClient, new(), defaultModelId) + { } - /// - /// Initializes a new instance of the class for the specified . - /// - /// The underlying image client. - /// The default model ID to use for image generation. - /// is . - public GrokImageGenerator(ImageClient imageClient, string defaultModelId) + GrokImageGenerator(ImageClient imageClient, GrokClientOptions clientOptions, string defaultModelId) { - _imageClient = Throw.IfNull(imageClient); - _defaultModelId = Throw.IfNullOrWhitespace(defaultModelId); - _metadata = new ImageGeneratorMetadata("xai", null, defaultModelId); + this.imageClient = imageClient; + this.clientOptions = clientOptions; + this.defaultModelId = defaultModelId; + metadata = new ImageGeneratorMetadata("xai", clientOptions.Endpoint, defaultModelId); } /// @@ -62,7 +56,7 @@ public async Task GenerateAsync( var protocolRequest = new GenerateImageRequest { Prompt = prompt, - Model = options?.ModelId ?? _defaultModelId, + Model = options?.ModelId ?? defaultModelId, }; // Set the number of images to generate @@ -78,7 +72,7 @@ public async Task GenerateAsync( { ImageGenerationResponseFormat.Uri => ImageFormat.ImgFormatUrl, ImageGenerationResponseFormat.Data => ImageFormat.ImgFormatBase64, - _ => ImageFormat.ImgFormatInvalid + _ => throw new ArgumentException($"Unsupported response format: {responseFormat}", nameof(options)) }; } @@ -93,7 +87,7 @@ public async Task GenerateAsync( if (imageUrl == null && dataContent.Data.Length > 0) { // Convert to base64 if we have raw data - imageUrl = $"data:{dataContent.MediaType ?? "image/png"};base64,{Convert.ToBase64String(dataContent.Data.ToArray())}"; + imageUrl = $"data:{dataContent.MediaType ?? DefaultContentType};base64,{Convert.ToBase64String(dataContent.Data.ToArray())}"; } if (imageUrl != null) @@ -114,20 +108,19 @@ public async Task GenerateAsync( } // Call the gRPC API - var response = await _imageClient.GenerateImageAsync(protocolRequest, cancellationToken: cancellationToken).ConfigureAwait(false); + var response = await imageClient.GenerateImageAsync(protocolRequest, cancellationToken: cancellationToken).ConfigureAwait(false); // Convert the response to the Microsoft.Extensions.AI format return ToImageGenerationResponse(response, options?.MediaType); } /// - public object? GetService(Type serviceType, object? serviceKey = null) => - serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) : - serviceKey is not null ? null : - serviceType == typeof(ImageGeneratorMetadata) ? _metadata : - serviceType == typeof(ImageClient) ? _imageClient : - serviceType.IsInstanceOfType(this) ? this : - null; + public object? GetService(Type serviceType, object? serviceKey = null) => serviceType switch + { + Type t when t == typeof(ImageGeneratorMetadata) => metadata, + Type t when t == typeof(GrokImageGenerator) => this, + _ => null + }; /// void IDisposable.Dispose() From 34ee0e92cf8ccfa853f276c17addf8bbac83fe36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:48 +0000 Subject: [PATCH 3/7] Apply additional code style fixes: remove private keyword, separate input/output content type constants Co-authored-by: kzu <169707+kzu@users.noreply.github.com> --- src/xAI/GrokImageGenerator.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/xAI/GrokImageGenerator.cs b/src/xAI/GrokImageGenerator.cs index f217fbf..ccc9f0b 100644 --- a/src/xAI/GrokImageGenerator.cs +++ b/src/xAI/GrokImageGenerator.cs @@ -15,7 +15,8 @@ namespace xAI; /// sealed class GrokImageGenerator : IImageGenerator { - const string DefaultContentType = "image/png"; + const string DefaultInputContentType = "image/png"; + const string DefaultOutputContentType = "image/jpeg"; readonly ImageGeneratorMetadata metadata; readonly ImageClient imageClient; @@ -61,9 +62,7 @@ public async Task GenerateAsync( // Set the number of images to generate if (options?.Count is { } count) - { protocolRequest.N = count; - } // Set the response format (URL or base64) if (options?.ResponseFormat is { } responseFormat) @@ -85,10 +84,8 @@ public async Task GenerateAsync( // Convert the data content to a base64 string or URL for the API var imageUrl = dataContent.Uri?.ToString(); if (imageUrl == null && dataContent.Data.Length > 0) - { // Convert to base64 if we have raw data - imageUrl = $"data:{dataContent.MediaType ?? DefaultContentType};base64,{Convert.ToBase64String(dataContent.Data.ToArray())}"; - } + imageUrl = $"data:{dataContent.MediaType ?? DefaultInputContentType};base64,{Convert.ToBase64String(dataContent.Data.ToArray())}"; if (imageUrl != null) { @@ -131,10 +128,10 @@ void IDisposable.Dispose() /// /// Converts an xAI to a . /// - private static ImageGenerationResponse ToImageGenerationResponse(ImageResponse response, string? mediaType) + static ImageGenerationResponse ToImageGenerationResponse(ImageResponse response, string? mediaType) { var contents = new List(); - var contentType = mediaType ?? "image/jpeg"; // xAI returns JPG by default + var contentType = mediaType ?? DefaultOutputContentType; // xAI returns JPG by default foreach (var image in response.Images) { From 338da97a98025b95008cb927c17863d0c89ef6e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:50:36 +0000 Subject: [PATCH 4/7] Initial plan From 2aa4f9289c15d6558fbbab4b77fb669ba045ce90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:25:27 +0000 Subject: [PATCH 5/7] Fix build: use Contents instead of Images, DefaultModelId instead of ModelId, remove unsupported ModelId property Co-authored-by: kzu <169707+kzu@users.noreply.github.com> --- src/xAI.Tests/ImageGeneratorTests.cs | 30 ++++++++++++++++------------ src/xAI/GrokImageGenerator.cs | 1 - 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/xAI.Tests/ImageGeneratorTests.cs b/src/xAI.Tests/ImageGeneratorTests.cs index 2005c07..4c74c60 100644 --- a/src/xAI.Tests/ImageGeneratorTests.cs +++ b/src/xAI.Tests/ImageGeneratorTests.cs @@ -23,10 +23,10 @@ public async Task GenerateImage_WithPrompt_ReturnsImageContent() var response = await imageGenerator.GenerateAsync(request, options); Assert.NotNull(response); - Assert.NotEmpty(response.Images); - Assert.Single(response.Images); + Assert.NotEmpty(response.Contents); + Assert.Single(response.Contents); - var image = response.Images.First(); + var image = response.Contents.First(); Assert.True(image is UriContent); var uriContent = (UriContent)image; @@ -51,10 +51,10 @@ public async Task GenerateImage_WithBase64Response_ReturnsDataContent() var response = await imageGenerator.GenerateAsync(request, options); Assert.NotNull(response); - Assert.NotEmpty(response.Images); - Assert.Single(response.Images); + Assert.NotEmpty(response.Contents); + Assert.Single(response.Contents); - var image = response.Images.First(); + var image = response.Contents.First(); Assert.True(image is DataContent); var dataContent = (DataContent)image; @@ -80,10 +80,10 @@ public async Task GenerateMultipleImages_ReturnsCorrectCount() var response = await imageGenerator.GenerateAsync(request, options); Assert.NotNull(response); - Assert.NotEmpty(response.Images); - Assert.Equal(3, response.Images.Count); + Assert.NotEmpty(response.Contents); + Assert.Equal(3, response.Contents.Count); - foreach (var image in response.Images) + foreach (var image in response.Contents) { Assert.True(image is UriContent); output.WriteLine($"Image URL: {((UriContent)image).Uri}"); @@ -91,7 +91,7 @@ public async Task GenerateMultipleImages_ReturnsCorrectCount() } [SecretsFact("XAI_API_KEY")] - public async Task GenerateImage_ResponseContainsModelId() + public async Task GenerateImage_ResponseContainsRawRepresentation() { var imageGenerator = new GrokClient(Configuration["XAI_API_KEY"]!) .AsIImageGenerator("grok-2-image"); @@ -105,8 +105,12 @@ public async Task GenerateImage_ResponseContainsModelId() var response = await imageGenerator.GenerateAsync(request, options); Assert.NotNull(response); - Assert.NotNull(response.ModelId); - output.WriteLine($"Model used: {response.ModelId}"); + Assert.NotNull(response.RawRepresentation); + + // The raw representation should be an ImageResponse from the protocol + var rawResponse = Assert.IsType(response.RawRepresentation); + Assert.NotNull(rawResponse.Model); + output.WriteLine($"Model used: {rawResponse.Model}"); } [Fact] @@ -141,6 +145,6 @@ public void GetService_ReturnsImageGeneratorMetadata() Assert.NotNull(metadata); Assert.Equal("xai", metadata.ProviderName); - Assert.Equal("grok-2-image", metadata.ModelId); + Assert.Equal("grok-2-image", metadata.DefaultModelId); } } diff --git a/src/xAI/GrokImageGenerator.cs b/src/xAI/GrokImageGenerator.cs index ccc9f0b..8d94bed 100644 --- a/src/xAI/GrokImageGenerator.cs +++ b/src/xAI/GrokImageGenerator.cs @@ -155,7 +155,6 @@ static ImageGenerationResponse ToImageGenerationResponse(ImageResponse response, return new ImageGenerationResponse(contents) { - ModelId = response.Model, RawRepresentation = response, }; } From 0133551ddf4617d56f72a216b2e3b2b56c29bbcd Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Mon, 5 Jan 2026 16:28:04 -0300 Subject: [PATCH 6/7] Add image editing test case --- src/xAI.Tests/ImageGeneratorTests.cs | 49 +++++++++++++++++++++++----- src/xAI/GrokImageGenerator.cs | 19 +---------- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/xAI.Tests/ImageGeneratorTests.cs b/src/xAI.Tests/ImageGeneratorTests.cs index 4c74c60..5dfd103 100644 --- a/src/xAI.Tests/ImageGeneratorTests.cs +++ b/src/xAI.Tests/ImageGeneratorTests.cs @@ -11,7 +11,7 @@ public class ImageGeneratorTests(ITestOutputHelper output) public async Task GenerateImage_WithPrompt_ReturnsImageContent() { var imageGenerator = new GrokClient(Configuration["XAI_API_KEY"]!) - .AsIImageGenerator("grok-2-image"); + .AsIImageGenerator("grok-imagine-image-beta"); var request = new ImageGenerationRequest("A cat sitting on a tree branch"); var options = new ImageGenerationOptions @@ -25,16 +25,47 @@ public async Task GenerateImage_WithPrompt_ReturnsImageContent() Assert.NotNull(response); Assert.NotEmpty(response.Contents); Assert.Single(response.Contents); - + var image = response.Contents.First(); Assert.True(image is UriContent); - + var uriContent = (UriContent)image; Assert.NotNull(uriContent.Uri); - + output.WriteLine($"Generated image URL: {uriContent.Uri}"); } + [SecretsFact("XAI_API_KEY")] + public async Task GenerateImage_WithEditsToPreviousImage() + { + var imageGenerator = new GrokClient(Configuration["XAI_API_KEY"]!) + .AsIImageGenerator("grok-imagine-image-beta"); + + var request = new ImageGenerationRequest("A cat sitting on a tree branch"); + var options = new ImageGenerationOptions + { + ResponseFormat = ImageGenerationResponseFormat.Uri, + Count = 1 + }; + + var response = await imageGenerator.GenerateAsync(request, options); + + Assert.NotNull(response); + Assert.NotEmpty(response.Contents); + Assert.Single(response.Contents); + var image = Assert.IsType(response.Contents.First()); + output.WriteLine($"Generated image URL: {image.Uri}"); + + var edit = await imageGenerator.GenerateAsync(new ImageGenerationRequest("Edit provided image by adding a batman mask", [image]), options); + + Assert.NotNull(edit); + Assert.NotEmpty(edit.Contents); + Assert.Single(edit.Contents); + image = Assert.IsType(edit.Contents.First()); + + output.WriteLine($"Edited image URL: {image.Uri}"); + } + [SecretsFact("XAI_API_KEY")] public async Task GenerateImage_WithBase64Response_ReturnsDataContent() { @@ -53,14 +84,14 @@ public async Task GenerateImage_WithBase64Response_ReturnsDataContent() Assert.NotNull(response); Assert.NotEmpty(response.Contents); Assert.Single(response.Contents); - + var image = response.Contents.First(); Assert.True(image is DataContent); - + var dataContent = (DataContent)image; Assert.True(dataContent.Data.Length > 0); Assert.Equal("image/jpeg", dataContent.MediaType); - + output.WriteLine($"Generated image size: {dataContent.Data.Length} bytes"); } @@ -82,7 +113,7 @@ public async Task GenerateMultipleImages_ReturnsCorrectCount() Assert.NotNull(response); Assert.NotEmpty(response.Contents); Assert.Equal(3, response.Contents.Count); - + foreach (var image in response.Contents) { Assert.True(image is UriContent); @@ -106,7 +137,7 @@ public async Task GenerateImage_ResponseContainsRawRepresentation() Assert.NotNull(response); Assert.NotNull(response.RawRepresentation); - + // The raw representation should be an ImageResponse from the protocol var rawResponse = Assert.IsType(response.RawRepresentation); Assert.NotNull(rawResponse.Model); diff --git a/src/xAI/GrokImageGenerator.cs b/src/xAI/GrokImageGenerator.cs index 8d94bed..88ae984 100644 --- a/src/xAI/GrokImageGenerator.cs +++ b/src/xAI/GrokImageGenerator.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Grpc.Net.Client; using Microsoft.Extensions.AI; using xAI.Protocol; @@ -48,23 +43,15 @@ public async Task GenerateAsync( ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) { - _ = Throw.IfNull(request); - - string? prompt = request.Prompt; - _ = Throw.IfNull(prompt); - - // Build the protocol request var protocolRequest = new GenerateImageRequest { - Prompt = prompt, + Prompt = Throw.IfNull(Throw.IfNull(request).Prompt, "request.Prompt"), Model = options?.ModelId ?? defaultModelId, }; - // Set the number of images to generate if (options?.Count is { } count) protocolRequest.N = count; - // Set the response format (URL or base64) if (options?.ResponseFormat is { } responseFormat) { protocolRequest.Format = responseFormat switch @@ -81,10 +68,8 @@ public async Task GenerateAsync( var originalImage = request.OriginalImages.FirstOrDefault(); if (originalImage is DataContent dataContent) { - // Convert the data content to a base64 string or URL for the API var imageUrl = dataContent.Uri?.ToString(); if (imageUrl == null && dataContent.Data.Length > 0) - // Convert to base64 if we have raw data imageUrl = $"data:{dataContent.MediaType ?? DefaultInputContentType};base64,{Convert.ToBase64String(dataContent.Data.ToArray())}"; if (imageUrl != null) @@ -104,10 +89,8 @@ public async Task GenerateAsync( } } - // Call the gRPC API var response = await imageClient.GenerateImageAsync(protocolRequest, cancellationToken: cancellationToken).ConfigureAwait(false); - // Convert the response to the Microsoft.Extensions.AI format return ToImageGenerationResponse(response, options?.MediaType); } From 78b8c06db48dbc05a7388bb92810fee25df70e91 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Mon, 5 Jan 2026 16:33:46 -0300 Subject: [PATCH 7/7] Document the image generation feature --- readme.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/readme.md b/readme.md index ab94165..e128fb7 100644 --- a/readme.md +++ b/readme.md @@ -250,6 +250,59 @@ var options = new GrokChatOptions ``` Learn more about [Remote MCP tools](https://docs.x.ai/docs/guides/tools/remote-mcp-tools). + +## Image Generation + +Grok also supports image generation using the `IImageGenerator` abstraction from +Microsoft.Extensions.AI. Use the `AsIImageGenerator` extension method to get an +image generator client: + +```csharp +var imageGenerator = new GrokClient(Environment.GetEnvironmentVariable("XAI_API_KEY")!) + .AsIImageGenerator("grok-imagine-image-beta"); + +var request = new ImageGenerationRequest("A cat sitting on a tree branch"); +var options = new ImageGenerationOptions +{ + ResponseFormat = ImageGenerationResponseFormat.Uri, + Count = 1 +}; + +var response = await imageGenerator.GenerateAsync(request, options); + +var image = (UriContent)response.Contents.First(); +Console.WriteLine($"Generated image URL: {image.Uri}"); +``` + +### Editing Images + +You can also edit previously generated images by passing them as input to a new +generation request: + +```csharp +var imageGenerator = new GrokClient(Environment.GetEnvironmentVariable("XAI_API_KEY")!) + .AsIImageGenerator("grok-imagine-image-beta"); + +// First, generate the original image +var request = new ImageGenerationRequest("A cat sitting on a tree branch"); +var options = new ImageGenerationOptions +{ + ResponseFormat = ImageGenerationResponseFormat.Uri, + Count = 1 +}; + +var response = await imageGenerator.GenerateAsync(request, options); +var image = (UriContent)response.Contents.First(); + +// Now edit the image by providing it as input along with the edit instructions +var edit = await imageGenerator.GenerateAsync( + new ImageGenerationRequest("Edit provided image by adding a batman mask", [image]), + options); + +var editedImage = (UriContent)edit.Contents.First(); +Console.WriteLine($"Edited image URL: {editedImage.Uri}"); +``` + # xAI.Protocol