diff --git a/config/ai_connectors.go b/config/ai_connectors.go index 3c7aae7..14aa70b 100644 --- a/config/ai_connectors.go +++ b/config/ai_connectors.go @@ -106,6 +106,16 @@ func RenderManagedAIConnectorsSection(connectors []uicfg.ConnectorRemote, sectio builder.WriteString(strconv.Quote(connector.BaseURL)) builder.WriteString("\n") } + if connector.GCPProjectID != "" { + builder.WriteString("gcp_project_id = ") + builder.WriteString(strconv.Quote(connector.GCPProjectID)) + builder.WriteString("\n") + } + if connector.GCPLocation != "" { + builder.WriteString("gcp_location = ") + builder.WriteString(strconv.Quote(connector.GCPLocation)) + builder.WriteString("\n") + } if connector.SelectedModel != "" { builder.WriteString("selected_model = ") builder.WriteString(strconv.Quote(connector.SelectedModel)) diff --git a/internal/staticserve/static/ui-connectors.js b/internal/staticserve/static/ui-connectors.js index f4d3f67..33c1727 100644 --- a/internal/staticserve/static/ui-connectors.js +++ b/internal/staticserve/static/ui-connectors.js @@ -372,6 +372,8 @@ function App() { api_key: connector.api_key || '', base_url: connector.base_url || '', selected_model: connector.selected_model || '', + gcp_project_id: connector.gcp_project_id || '', + gcp_location: connector.gcp_location || '', }); setStatus(`Editing connector #${connector.id}`); } @@ -398,6 +400,8 @@ function App() { base_url: provider.requiresBaseURL ? (provider.baseURLPresets?.length ? provider.baseURLPresets[0].value : '') : '', + gcp_project_id: '', + gcp_location: '', })); if (provider.id !== 'ollama') { setOllamaModels([]); @@ -426,6 +430,8 @@ function App() { api_key: connector.api_key || '', base_url: connector.base_url || '', selected_model: connector.selected_model || '', + gcp_project_id: connector.gcp_project_id || '', + gcp_location: connector.gcp_location || '', }); setStatus(`Editing connector #${connector.id}`); navigate(`/connectors/edit/${connector.id}`); @@ -531,6 +537,8 @@ function App() { api_key: apiKey, base_url: baseURL || undefined, model: selectedModel || apiDefaultModel || undefined, + gcp_project_id: (form.gcp_project_id || '').trim() || undefined, + gcp_location: (form.gcp_location || '').trim() || undefined, }), }); @@ -547,6 +555,8 @@ function App() { connector_name: connectorName, base_url: baseURL || undefined, selected_model: selectedModel || apiDefaultModel || undefined, + gcp_project_id: (form.gcp_project_id || '').trim() || undefined, + gcp_location: (form.gcp_location || '').trim() || undefined, display_order: 0, }; @@ -663,6 +673,12 @@ function App() { return !baseURL || !selectedModel; } + if (provider.id === 'gemini-enterprise') { + const gcpProjectID = (form.gcp_project_id || '').trim(); + const gcpLocation = (form.gcp_location || '').trim(); + return !apiKey || !gcpProjectID || !gcpLocation; + } + if (!apiKey) { return true; } diff --git a/internal/staticserve/static/ui-connectors/pages/ConnectorFormPage.js b/internal/staticserve/static/ui-connectors/pages/ConnectorFormPage.js index f478573..e309e15 100644 --- a/internal/staticserve/static/ui-connectors/pages/ConnectorFormPage.js +++ b/internal/staticserve/static/ui-connectors/pages/ConnectorFormPage.js @@ -1,4 +1,4 @@ -const { html } = window.preact; +const { html, useState, useEffect } = window.preact; export function ConnectorFormPage({ title, @@ -27,7 +27,15 @@ export function ConnectorFormPage({ connectorNamePlaceholder, apiDefaultModel = '', }) { + const [fileError, setFileError] = useState(''); + + // Clear file error when provider changes + useEffect(() => { + setFileError(''); + }, [form.provider_name]); + const isOllama = form.provider_name === 'ollama'; + const isGeminiEnterprise = form.provider_name === 'gemini-enterprise'; const showBaseURL = Boolean(selectedProvider.requiresBaseURL); const connectorName = (form.connector_name || '').trim(); const apiKey = (form.api_key || '').trim(); @@ -43,9 +51,40 @@ export function ConnectorFormPage({ } } + const handleFileChange = (e) => { + const file = e.target.files?.[0]; + if (!file) return; + + if (file.size > 1024 * 1024) { + setFileError('File is too large. Please upload a valid service account JSON under 1MB.'); + onFieldChange('api_key', ''); + return; + } + + const reader = new FileReader(); + reader.onload = (event) => { + const content = event.target.result; + onFieldChange('api_key', content); + + try { + const parsed = JSON.parse(content); + if (parsed.project_id) { + onFieldChange('gcp_project_id', parsed.project_id); + } + setFileError(''); + } catch (err) { + console.error('Failed to parse Service Account JSON:', err); + setFileError('Failed to parse Service Account JSON. Please upload a valid JSON file.'); + onFieldChange('api_key', ''); + } + }; + reader.readAsText(file); + }; + const hasClientValidationError = !connectorName || - (!isOllama && !apiKey) || + (isGeminiEnterprise && (!apiKey || !form.gcp_project_id || !form.gcp_location)) || + (!isOllama && !isGeminiEnterprise && !apiKey) || (showBaseURL && (!baseURL || !hasValidBaseURL)); const fetchModelsDisabled = fetchingModels || !baseURL || !hasValidBaseURL; @@ -74,16 +113,78 @@ export function ConnectorFormPage({ - - onFieldChange('api_key', event.target.value)} - /> + ${isGeminiEnterprise + ? html` + +
+ ${form.api_key + ? `Valid Google Cloud service account JSON configured (${(form.api_key.length / 1024).toFixed(2)} KB)` + : "Upload the Google Cloud IAM Service Account JSON keyfile" + } +
++ Follow this guide to create a service account JSON file and assign the Agent Platform user role. +
+ + + onFieldChange('gcp_project_id', event.target.value)} + /> + + + onFieldChange('gcp_location', event.target.value)} + /> + ` + : html` + + onFieldChange('api_key', event.target.value)} + /> + ` + } ${showBaseURL ? (() => { diff --git a/internal/staticserve/static/ui-connectors/providers.js b/internal/staticserve/static/ui-connectors/providers.js index 77e4e75..896ca63 100644 --- a/internal/staticserve/static/ui-connectors/providers.js +++ b/internal/staticserve/static/ui-connectors/providers.js @@ -63,6 +63,11 @@ export const providers = [ name: 'Atlas Cloud', apiKeyPlaceholder: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', baseURLPlaceholder: 'https://api.atlascloud.ai/v1', + }, + { + id: 'gemini-enterprise', + name: 'Gemini Enterprise', + apiKeyPlaceholder: 'Google Cloud Service Account JSON', } ]; @@ -75,5 +80,7 @@ export function defaultForm() { api_key: '', base_url: '', selected_model: '', + gcp_project_id: '', + gcp_location: '', }; } diff --git a/ui/types.go b/ui/types.go index 89d02d4..eb1300d 100644 --- a/ui/types.go +++ b/ui/types.go @@ -28,6 +28,8 @@ type ConnectorRemote struct { ConnectorName string `json:"connector_name"` APIKey string `json:"api_key"` BaseURL string `json:"base_url"` + GCPProjectID string `json:"gcp_project_id"` + GCPLocation string `json:"gcp_location"` SelectedModel string `json:"selected_model"` DisplayOrder int `json:"display_order"` CreatedAt string `json:"created_at"`