Skip to content

Commit 05f7f5f

Browse files
committed
Add Vertex AI backend support and UI
Introduce Vertex AI support across the InferPage UI and settings plumbing. Changes include: - Add a new BackendOption (id 9) for Vertex AI and corresponding selection/lookup logic in Settings.razor and Program.cs; Program warns that Vertex requires service account credentials. - Extend the settings UI to collect Vertex auth fields (Project ID, Client Email, Private Key, Location), with toggleable key visibility and required-field validation to enable saving. - Persist Vertex auth separately via SettingsService.SaveVertexAuthAsync/GetVertexAuthAsync and a VertexAuthStorage record; private_key is stored outside general settings. Also add VertexLocation to InferPageSettings. - Pass a GoogleServiceAccountConfig (vertexAuth) through Utils.ApplySettings so MaINSettings.GoogleServiceAccountAuth can be set when selecting Vertex. - Add model identifier constant for gemini-2.5-pro and fix a CloudModel reference to use Models.Gemini.Gemini2_5Pro. These changes wire up UI, storage, and application settings so Vertex can be configured from the Settings page and applied at runtime.
1 parent 34f7ed5 commit 05f7f5f

9 files changed

Lines changed: 172 additions & 10 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public sealed record Gemini2_0Flash() : CloudModel(
9494
}
9595

9696
public sealed record Gemini2_5Pro() : CloudModel(
97-
Models.Vertex.Gemini2_5Pro,
97+
Models.Gemini.Gemini2_5Pro,
9898
BackendType.Gemini,
9999
"Gemini 2.5 Pro",
100100
1000000,

src/MaIN.Domain/Models/Models.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public static class Anthropic
2323

2424
public static class Gemini
2525
{
26+
public const string Gemini2_5Pro = "gemini-2.5-pro";
2627
public const string Gemini2_5Flash = "gemini-2.5-flash";
2728
public const string Gemini2_0Flash = "gemini-2.0-flash";
2829
}

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,11 +428,28 @@
428428
{
429429
var backendKey = backendType == BackendType.Ollama
430430
? (settings.IsOllamaCloud ? "OllamaCloud" : "OllamaLocal")
431+
: backendType == BackendType.Vertex ? "Vertex"
431432
: backendType.ToString();
432433

433434
apiKey = await SettingsStorage.GetApiKeyForBackendAsync(backendKey);
434435
}
435436

437+
// Load Vertex auth from localStorage if applicable
438+
Domain.Configuration.Vertex.GoogleServiceAccountConfig? vertexAuth = null;
439+
if (backendType == BackendType.Vertex)
440+
{
441+
var stored = await SettingsStorage.GetVertexAuthAsync();
442+
if (stored != null)
443+
{
444+
vertexAuth = new Domain.Configuration.Vertex.GoogleServiceAccountConfig
445+
{
446+
ProjectId = stored.ProjectId,
447+
ClientEmail = stored.ClientEmail,
448+
PrivateKey = stored.PrivateKey
449+
};
450+
}
451+
}
452+
436453
Utils.ApplySettings(
437454
backendType,
438455
settings.Model!,
@@ -442,7 +459,8 @@
442459
settings.HasImageGen,
443460
settings.MmProjName,
444461
MaINSettings,
445-
apiKey);
462+
apiKey,
463+
vertexAuth);
446464
}
447465

448466
private void ShowSettingsFromGear()

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

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@using MaIN.Domain.Configuration
2+
@using MaIN.Domain.Configuration.Vertex
23
@using MaIN.Domain.Models.Abstract
34
@inject SettingsService SettingsStorage
45
@inject MaINSettings MaINSettings
@@ -64,6 +65,49 @@
6465
</div>
6566
}
6667

68+
@if (_selectedBackend?.BackendType == BackendType.Vertex)
69+
{
70+
<div class="settings-field">
71+
<label class="settings-label">Project ID</label>
72+
<FluentTextField @bind-Value="_vertexProjectId"
73+
Placeholder="my-google-cloud-project"
74+
Style="width: 100%;" />
75+
</div>
76+
77+
<div class="settings-field">
78+
<label class="settings-label">Client Email</label>
79+
<FluentTextField @bind-Value="_vertexClientEmail"
80+
Placeholder="name@project.iam.gserviceaccount.com"
81+
Style="width: 100%;" />
82+
</div>
83+
84+
<div class="settings-field">
85+
<label class="settings-label">Private Key</label>
86+
<div class="api-key-row">
87+
<FluentTextField @bind-Value="_vertexPrivateKey"
88+
Placeholder="Private key from service account JSON"
89+
TextFieldType="@(_showVertexKey ? TextFieldType.Text : TextFieldType.Password)"
90+
Style="flex: 1;" />
91+
<FluentButton Appearance="Appearance.Lightweight"
92+
Style="--neutral-fill-stealth-rest: transparent; --neutral-fill-stealth-hover: rgba(128,128,128,0.15); --neutral-fill-stealth-active: rgba(128,128,128,0.25);"
93+
OnClick="@(() => _showVertexKey = !_showVertexKey)"
94+
IconStart="@(_showVertexKey
95+
? new Icons.Regular.Size20.EyeOff()
96+
: (Icon)new Icons.Regular.Size20.Eye())">
97+
</FluentButton>
98+
</div>
99+
<span class="api-key-hint">private_key field from the service account JSON file</span>
100+
</div>
101+
102+
<div class="settings-field">
103+
<label class="settings-label">Location</label>
104+
<FluentTextField @bind-Value="_vertexLocation"
105+
Placeholder="us-central1"
106+
Style="width: 100%;" />
107+
<span class="api-key-hint">Optionaldefaults to us-central1</span>
108+
</div>
109+
}
110+
67111
@if (_selectedBackend?.BackendType == BackendType.Self)
68112
{
69113
<div class="settings-field">
@@ -155,6 +199,13 @@
155199
private string? _savedKeyPreview;
156200
private bool _showApiKey;
157201

202+
// Vertex AI auth fields
203+
private string? _vertexProjectId;
204+
private string? _vertexClientEmail;
205+
private string? _vertexPrivateKey;
206+
private string? _vertexLocation;
207+
private bool _showVertexKey;
208+
158209
// "Will load:" path preview shown below the model path field (Self backend only)
159210
private string? ResolvedModelPathPreview
160211
{
@@ -236,9 +287,14 @@
236287
private string? _mmProjName;
237288

238289
private bool RequiresApiKey => _selectedBackend?.RequiresApiKey == true;
290+
private bool IsVertexBackend => _selectedBackend?.BackendType == BackendType.Vertex;
291+
private bool HasVertexRequiredFields => !string.IsNullOrWhiteSpace(_vertexProjectId)
292+
&& !string.IsNullOrWhiteSpace(_vertexClientEmail)
293+
&& !string.IsNullOrWhiteSpace(_vertexPrivateKey);
239294
private bool CanSave => !string.IsNullOrWhiteSpace(_modelName)
240295
&& _selectedBackend != null
241-
&& (!RequiresApiKey || !string.IsNullOrEmpty(_apiKeyInput) || !string.IsNullOrEmpty(_savedKeyPreview));
296+
&& (!RequiresApiKey || !string.IsNullOrEmpty(_apiKeyInput) || !string.IsNullOrEmpty(_savedKeyPreview))
297+
&& (!IsVertexBackend || HasVertexRequiredFields);
242298

243299
protected override async Task OnInitializedAsync()
244300
{
@@ -251,7 +307,8 @@
251307
new(5, "Anthropic", BackendType.Anthropic, true),
252308
new(6, "xAI", BackendType.Xai, true),
253309
new(7, "Ollama (Local)", BackendType.Ollama, false),
254-
new(8, "Ollama (Cloud)", BackendType.Ollama, true)
310+
new(8, "Ollama (Cloud)", BackendType.Ollama, true),
311+
new(9, "Vertex AI", BackendType.Vertex, false)
255312
}.OrderBy(x => x.DisplayName)
256313
.Prepend(new BackendOption(0, "Local", BackendType.Self, false))
257314
.ToList();
@@ -281,6 +338,10 @@
281338
? _backendOptions.First(o => o.Id == 8)
282339
: _backendOptions.First(o => o.Id == 7);
283340
}
341+
else if (backendType == BackendType.Vertex)
342+
{
343+
_selectedBackend = _backendOptions.First(o => o.Id == 9);
344+
}
284345
else
285346
{
286347
_selectedBackend = _backendOptions.FirstOrDefault(o => o.BackendType == backendType && o.Id < 7);
@@ -293,6 +354,18 @@
293354
_manualImageGen = settings.HasImageGen;
294355
_mmProjName = settings.MmProjName;
295356

357+
if (backendType == BackendType.Vertex)
358+
{
359+
var vertexAuth = await SettingsStorage.GetVertexAuthAsync();
360+
if (vertexAuth != null)
361+
{
362+
_vertexProjectId = vertexAuth.ProjectId;
363+
_vertexClientEmail = vertexAuth.ClientEmail;
364+
_vertexPrivateKey = vertexAuth.PrivateKey;
365+
}
366+
_vertexLocation = settings.VertexLocation;
367+
}
368+
296369
OnModelNameChanged();
297370
}
298371
else if (!Utils.NeedsConfiguration)
@@ -302,7 +375,9 @@
302375
? (Utils.HasApiKey
303376
? _backendOptions.First(o => o.Id == 8)
304377
: _backendOptions.First(o => o.Id == 7))
305-
: _backendOptions.FirstOrDefault(o => o.BackendType == Utils.BackendType && o.Id < 7);
378+
: Utils.BackendType == BackendType.Vertex
379+
? _backendOptions.First(o => o.Id == 9)
380+
: _backendOptions.FirstOrDefault(o => o.BackendType == Utils.BackendType && o.Id < 7);
306381

307382
_modelName = Utils.Model;
308383
_modelPath = Utils.Path;
@@ -343,6 +418,26 @@
343418
}
344419
}
345420

421+
if (IsVertexBackend)
422+
{
423+
var vertexAuth = await SettingsStorage.GetVertexAuthAsync();
424+
if (vertexAuth != null)
425+
{
426+
_vertexProjectId = vertexAuth.ProjectId;
427+
_vertexClientEmail = vertexAuth.ClientEmail;
428+
_vertexPrivateKey = vertexAuth.PrivateKey;
429+
}
430+
else
431+
{
432+
_vertexProjectId = null;
433+
_vertexClientEmail = null;
434+
_vertexPrivateKey = null;
435+
}
436+
437+
var settings = await SettingsStorage.LoadSettingsAsync();
438+
_vertexLocation = settings?.VertexLocation;
439+
}
440+
346441
await LoadApiKeyPreview();
347442
_apiKeyInput = null;
348443
}
@@ -425,7 +520,8 @@
425520
HasReasoning = hasReasoning,
426521
HasImageGen = hasImageGen,
427522
ModelPath = _modelPath,
428-
MmProjName = _mmProjName
523+
MmProjName = _mmProjName,
524+
VertexLocation = IsVertexBackend ? _vertexLocation : null
429525
};
430526
await SettingsStorage.SaveSettingsAsync(settings);
431527

@@ -447,6 +543,19 @@
447543
}
448544
}
449545

546+
// Vertex AI: persist auth and build config
547+
GoogleServiceAccountConfig? vertexAuth = null;
548+
if (IsVertexBackend && HasVertexRequiredFields)
549+
{
550+
await SettingsStorage.SaveVertexAuthAsync(_vertexProjectId!, _vertexClientEmail!, _vertexPrivateKey!);
551+
vertexAuth = new GoogleServiceAccountConfig
552+
{
553+
ProjectId = _vertexProjectId!,
554+
ClientEmail = _vertexClientEmail!,
555+
PrivateKey = _vertexPrivateKey!
556+
};
557+
}
558+
450559
Utils.ApplySettings(
451560
_selectedBackend.BackendType,
452561
_modelName,
@@ -456,7 +565,8 @@
456565
hasImageGen,
457566
_mmProjName,
458567
MaINSettings,
459-
apiKey);
568+
apiKey,
569+
vertexAuth);
460570

461571
await OnSettingsApplied.InvokeAsync();
462572
}
@@ -466,6 +576,7 @@
466576
if (_selectedBackend == null) return "Self";
467577
if (_selectedBackend.Id == 7) return "OllamaLocal";
468578
if (_selectedBackend.Id == 8) return "OllamaCloud";
579+
if (_selectedBackend.Id == 9) return "Vertex";
469580
return _selectedBackend.BackendType.ToString();
470581
}
471582

src/MaIN.InferPage/Program.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,15 @@
3939
"anthropic" => BackendType.Anthropic,
4040
"xai" => BackendType.Xai,
4141
"ollama" => BackendType.Ollama,
42+
"vertex" => BackendType.Vertex,
4243
_ => BackendType.Self
4344
};
4445

45-
if (Utils.BackendType != BackendType.Self)
46+
if (Utils.BackendType == BackendType.Vertex)
47+
{
48+
Console.WriteLine("Vertex AI requires service account credentials. Configure them via the Settings page.");
49+
}
50+
else if (Utils.BackendType != BackendType.Self)
4651
{
4752
var apiKeyVariable = LLMApiRegistry.GetEntry(Utils.BackendType)?.ApiKeyEnvName ?? string.Empty;
4853
var key = Environment.GetEnvironmentVariable(apiKeyVariable);

src/MaIN.InferPage/Services/InferPageSettings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ public class InferPageSettings
1010
public bool HasImageGen { get; set; }
1111
public string? ModelPath { get; set; }
1212
public string? MmProjName { get; set; }
13+
public string? VertexLocation { get; set; }
1314
}

src/MaIN.InferPage/Services/SettingsService.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ public async Task SaveProfileForBackendAsync(string backend, string model,
4141
return profiles?.GetValueOrDefault(backend);
4242
}
4343

44+
// Vertex AI auth (stored separately — PrivateKey should not be in general settings)
45+
private const string VertexAuthKey = "inferpage-vertex-auth";
46+
47+
public async Task SaveVertexAuthAsync(string projectId, string clientEmail, string privateKey)
48+
{
49+
var auth = new VertexAuthStorage(projectId, clientEmail, privateKey);
50+
await js.InvokeVoidAsync("settingsManager.save", VertexAuthKey, auth);
51+
}
52+
53+
public async Task<VertexAuthStorage?> GetVertexAuthAsync()
54+
=> await js.InvokeAsync<VertexAuthStorage?>("settingsManager.load", VertexAuthKey);
55+
4456
private async Task SetInDictAsync(string storageKey, string key, string value)
4557
{
4658
var dict = await LoadDictAsync(storageKey);
@@ -56,3 +68,5 @@ private async Task<Dictionary<string, string>> LoadDictAsync(string storageKey)
5668
}
5769

5870
public record BackendProfile(string Model, bool Vision, bool Reasoning, bool ImageGen, string? MmProjName = null);
71+
72+
public record VertexAuthStorage(string ProjectId, string ClientEmail, string PrivateKey);

src/MaIN.InferPage/Utils.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using MaIN.Domain.Configuration;
2+
using MaIN.Domain.Configuration.Vertex;
23
using MaIN.Domain.Entities;
34
using MaIN.Domain.Models.Abstract;
45
using MaIN.Domain.Models.Concrete;
@@ -45,7 +46,8 @@ public static void ApplySettings(
4546
bool hasImageGen,
4647
string? mmProjName,
4748
MaINSettings mainSettings,
48-
string? apiKey)
49+
string? apiKey,
50+
GoogleServiceAccountConfig? vertexAuth = null)
4951
{
5052
BackendType = backendType;
5153
Model = model;
@@ -89,6 +91,10 @@ public static void ApplySettings(
8991
case BackendType.GroqCloud: mainSettings.GroqCloudKey = apiKey; break;
9092
case BackendType.Ollama: mainSettings.OllamaKey = apiKey; break;
9193
case BackendType.Xai: mainSettings.XaiKey = apiKey; break;
94+
case BackendType.Vertex:
95+
if (vertexAuth != null)
96+
mainSettings.GoogleServiceAccountAuth = vertexAuth;
97+
break;
9298
}
9399
}
94100

src/MaIN.Services/Services/LLMService/VertexService.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,12 @@ protected override string GetApiKey()
5454

5555
_tokenProvider ??= new GoogleServiceAccountTokenProvider(auth);
5656

57+
logger?.LogInformation("Vertex: Requesting access token for {ClientEmail}...", auth.ClientEmail);
5758
var httpClient = _httpClientFactory.CreateClient(HttpClientName);
58-
return _tokenProvider.GetAccessTokenAsync(httpClient).GetAwaiter().GetResult();
59+
// Use Task.Run to avoid deadlocking on Blazor Server's SynchronizationContext
60+
var token = Task.Run(() => _tokenProvider.GetAccessTokenAsync(httpClient)).GetAwaiter().GetResult();
61+
logger?.LogInformation("Vertex: Access token obtained (length={Length})", token?.Length ?? 0);
62+
return token;
5963
}
6064

6165
protected override string GetApiName() => LLMApiRegistry.Vertex.ApiName;
@@ -88,6 +92,8 @@ protected override void ApplyBackendParams(Dictionary<string, object> requestBod
8892
CancellationToken cancellationToken = default)
8993
{
9094
ExtractLocation(chat);
95+
logger?.LogInformation("Vertex: Send called, model={Model}, location={Location}, url={Url}",
96+
chat.ModelId, _location, ChatCompletionsUrl);
9197
return await base.Send(chat, options, cancellationToken);
9298
}
9399

0 commit comments

Comments
 (0)