Skip to content

Commit 8c26bf9

Browse files
committed
Add Vertex Imagen image generation service
Introduce support for Google Vertex Imagen image generation: add VertexImageGenService to call Vertex AI predict endpoints (uses service account token, default model/location, decodes base64 image into ChatResult), register BackendType.Vertex in ImageGenServiceFactory, add model constant (Imagen4_0_Generate) and CloudModel record (VertexImagen4_0Generate). Also adjust Home.razor injections and minor backendKey formatting. Includes basic response/error handling and model name extraction for publisher prefixes.
1 parent d7e2a5f commit 8c26bf9

5 files changed

Lines changed: 135 additions & 6 deletions

File tree

src/MaIN.Domain/Models/Concrete/CloudModels.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@ public sealed record VertexVeo2_0Generate() : CloudModel(
132132
4000,
133133
"Google's video generation model available through Vertex AI"), IImageGenerationModel;
134134

135+
public sealed record VertexImagen4_0Generate() : CloudModel(
136+
Models.Vertex.Imagen4_0_Generate,
137+
BackendType.Vertex,
138+
"Imagen 4.0 (Vertex)",
139+
4000,
140+
"Google's latest image generation model available through Vertex AI"), IImageGenerationModel;
141+
135142
// ===== xAI Models =====
136143

137144
public sealed record Grok3Beta() : CloudModel(

src/MaIN.Domain/Models/Models.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public static class Vertex
5555
public const string Gemini2_5Pro = "google/gemini-2.5-pro";
5656
public const string Gemini2_5Flash = "google/gemini-2.5-flash";
5757
public const string Veo2_0_Generate = "google/veo-2.0-generate-001";
58+
public const string Imagen4_0_Generate = "google/imagen-4.0-generate-001";
5859
}
5960

6061
public static class Local

src/MaIN.InferPage/Components/Pages/Home.razor

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
@inject IJSRuntime JS
44
@inject SettingsService SettingsStorage
55
@inject SettingsStateService SettingsState
6-
@inject MaIN.Domain.Configuration.MaINSettings MaINSettings
6+
@inject MaINSettings MaINSettings
77
@implements IDisposable
88
@using MaIN.Core.Hub
99
@using MaIN.Core.Hub.Contexts.Interfaces.ChatContext
@@ -427,9 +427,12 @@
427427
if (backendType != BackendType.Self)
428428
{
429429
var backendKey = backendType == BackendType.Ollama
430-
? (settings.IsOllamaCloud ? "OllamaCloud" : "OllamaLocal")
431-
: backendType == BackendType.Vertex ? "Vertex"
432-
: backendType.ToString();
430+
? (settings.IsOllamaCloud
431+
? "OllamaCloud"
432+
: "OllamaLocal")
433+
: backendType == BackendType.Vertex
434+
? "Vertex"
435+
: backendType.ToString();
433436

434437
apiKey = await SettingsStorage.GetApiKeyForBackendAsync(backendKey);
435438
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using MaIN.Domain.Configuration;
2+
using MaIN.Domain.Configuration.BackendInferenceParams;
3+
using MaIN.Domain.Entities;
4+
using MaIN.Services.Constants;
5+
using MaIN.Services.Services.Abstract;
6+
using MaIN.Services.Services.LLMService.Auth;
7+
using MaIN.Services.Services.Models;
8+
using System.Net.Http.Headers;
9+
using System.Net.Http.Json;
10+
using System.Text.Json.Serialization;
11+
12+
namespace MaIN.Services.Services.ImageGenServices;
13+
14+
internal class VertexImageGenService(IHttpClientFactory httpClientFactory, MaINSettings settings) : IImageGenService
15+
{
16+
private const string DefaultModel = "imagen-4.0-generate-001";
17+
private const string DefaultLocation = "us-central1";
18+
19+
public async Task<ChatResult?> Send(Chat chat)
20+
{
21+
var auth = settings.GoogleServiceAccountAuth
22+
?? throw new InvalidOperationException("Vertex AI service account is not configured.");
23+
24+
var location = chat.BackendParams is VertexInferenceParams vp
25+
? vp.Location
26+
: DefaultLocation;
27+
28+
using var tokenProvider = new GoogleServiceAccountTokenProvider(auth);
29+
var httpClient = httpClientFactory.CreateClient(ServiceConstants.HttpClients.VertexClient);
30+
var accessToken = await tokenProvider.GetAccessTokenAsync(httpClient);
31+
32+
var model = ExtractModelName(chat.ModelId);
33+
var endpoint = $"https://{location}-aiplatform.googleapis.com/v1/projects/{auth.ProjectId}/locations/{location}/publishers/google/models/{model}:predict";
34+
35+
var requestBody = new
36+
{
37+
instances = new[]
38+
{
39+
new { prompt = BuildPromptFromChat(chat) }
40+
},
41+
parameters = new
42+
{
43+
sampleCount = 1,
44+
aspectRatio = "1:1"
45+
}
46+
};
47+
48+
using var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
49+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
50+
request.Content = JsonContent.Create(requestBody);
51+
52+
using var response = await httpClient.SendAsync(request);
53+
54+
if (!response.IsSuccessStatusCode)
55+
{
56+
var error = await response.Content.ReadAsStringAsync();
57+
throw new InvalidOperationException(
58+
$"Vertex AI Imagen request failed ({response.StatusCode}): {error}");
59+
}
60+
61+
var result = await response.Content.ReadFromJsonAsync<ImagenResponse>();
62+
var base64Image = result?.Predictions?.FirstOrDefault()?.BytesBase64Encoded;
63+
64+
if (string.IsNullOrEmpty(base64Image))
65+
throw new InvalidOperationException("No image returned from Vertex AI Imagen.");
66+
67+
var imageBytes = Convert.FromBase64String(base64Image);
68+
69+
return new ChatResult
70+
{
71+
Done = true,
72+
Message = new Message
73+
{
74+
Content = ServiceConstants.Messages.GeneratedImageContent,
75+
Role = ServiceConstants.Roles.Assistant,
76+
Image = imageBytes,
77+
Type = MessageType.Image
78+
},
79+
Model = chat.ModelId ?? $"google/{DefaultModel}",
80+
CreatedAt = DateTime.UtcNow
81+
};
82+
}
83+
84+
private static string BuildPromptFromChat(Chat chat)
85+
{
86+
return chat.Messages
87+
.Select((msg, index) => index == 0 ? msg.Content : $"&& {msg.Content}")
88+
.Aggregate((current, next) => $"{current} {next}");
89+
}
90+
91+
/// <summary>
92+
/// Strips the "google/" publisher prefix if present (Vertex predict endpoint doesn't use it).
93+
/// </summary>
94+
private static string ExtractModelName(string? modelId)
95+
{
96+
if (string.IsNullOrEmpty(modelId))
97+
return DefaultModel;
98+
99+
return modelId.StartsWith("google/", StringComparison.OrdinalIgnoreCase)
100+
? modelId["google/".Length..]
101+
: modelId;
102+
}
103+
}
104+
105+
file class ImagenResponse
106+
{
107+
[JsonPropertyName("predictions")]
108+
public ImagenPrediction[]? Predictions { get; set; }
109+
}
110+
111+
file class ImagenPrediction
112+
{
113+
[JsonPropertyName("bytesBase64Encoded")]
114+
public string? BytesBase64Encoded { get; set; }
115+
116+
[JsonPropertyName("mimeType")]
117+
public string? MimeType { get; set; }
118+
}

src/MaIN.Services/Services/LLMService/Factory/ImageGenServiceFactory.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ public class ImageGenServiceFactory(IServiceProvider serviceProvider) : IImageGe
2020
BackendType.Anthropic => null,
2121
BackendType.Xai => new XaiImageGenService(serviceProvider.GetRequiredService<IHttpClientFactory>(),
2222
serviceProvider.GetRequiredService<MaINSettings>()),
23+
BackendType.Vertex => new VertexImageGenService(serviceProvider.GetRequiredService<IHttpClientFactory>(),
24+
serviceProvider.GetRequiredService<MaINSettings>()),
2325
BackendType.Ollama => null,
2426
BackendType.Self => new ImageGenService(serviceProvider.GetRequiredService<IHttpClientFactory>(),
2527
serviceProvider.GetRequiredService<MaINSettings>()),
26-
27-
// Add other backends as needed
2828
_ => throw new NotSupportedException("Not support image generation."),
2929
};
3030
}

0 commit comments

Comments
 (0)