Skip to content

Commit 1521ce4

Browse files
committed
Implement AWS Bedrock AI Connector
LiveReview Pre-Commit Check: ran (iter:1, coverage:0%)
1 parent d10bdee commit 1521ce4

8 files changed

Lines changed: 301 additions & 91 deletions

File tree

config/ai_connectors.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@ func RenderManagedAIConnectorsSection(connectors []uicfg.ConnectorRemote, sectio
123123
builder.WriteString(strconv.Quote(connector.GCPLocation))
124124
builder.WriteString("\n")
125125
}
126+
if connector.AWSAccessKeyID != "" {
127+
builder.WriteString("aws_access_key_id = ")
128+
builder.WriteString(strconv.Quote(connector.AWSAccessKeyID))
129+
builder.WriteString("\n")
130+
}
131+
if connector.AWSRegion != "" {
132+
builder.WriteString("aws_region = ")
133+
builder.WriteString(strconv.Quote(connector.AWSRegion))
134+
builder.WriteString("\n")
135+
}
126136
if connector.SelectedModel != "" {
127137
builder.WriteString("selected_model = ")
128138
builder.WriteString(strconv.Quote(connector.SelectedModel))

internal/appui/ui_connectors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func RunUI(c *cli.Context) error {
6262
mux.HandleFunc("/api/ui/connectors/settings", srv.handleHelperSettings)
6363
mux.HandleFunc("/api/ui/connectors/validate-key", srv.handleValidateKey)
6464
mux.HandleFunc("/api/ui/connectors/ollama/models", srv.handleOllamaModels)
65+
mux.HandleFunc("/api/ui/connectors/bedrock/models", srv.handleBedrockModels)
6566
mux.HandleFunc("/api/ui/connectors/providers/", srv.handleProviderModels)
6667
mux.HandleFunc("/api/ui/usage-chip", srv.handleUsageChip)
6768
mux.HandleFunc("/api/ui/connectors/", srv.handleConnectorByID)

internal/appui/ui_connectors_handlers.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,26 @@ func (s *connectorManagerServer) handleOllamaModels(w http.ResponseWriter, r *ht
408408
writeRawJSON(w, status, respBody)
409409
}
410410

411+
func (s *connectorManagerServer) handleBedrockModels(w http.ResponseWriter, r *http.Request) {
412+
if r.Method != http.MethodPost {
413+
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
414+
return
415+
}
416+
417+
body, err := io.ReadAll(r.Body)
418+
if err != nil {
419+
writeJSONError(w, http.StatusBadRequest, "failed to read request body")
420+
return
421+
}
422+
423+
status, respBody, err := s.proxyJSONRequest(http.MethodPost, "/api/v1/aiconnectors/bedrock/models", body)
424+
if err != nil {
425+
writeJSONError(w, http.StatusBadGateway, err.Error())
426+
return
427+
}
428+
writeRawJSON(w, status, respBody)
429+
}
430+
411431
func (s *connectorManagerServer) handleProviderModels(w http.ResponseWriter, r *http.Request) {
412432
if r.Method != http.MethodGet {
413433
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
@@ -431,4 +451,3 @@ func (s *connectorManagerServer) handleProviderModels(w http.ResponseWriter, r *
431451
}
432452
writeRawJSON(w, status, respBody)
433453
}
434-

internal/staticserve/static/ui-connectors.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@
423423
}
424424
button.tertiary-danger:hover { background: var(--status-error-bg); }
425425
button:disabled { opacity: 0.6; cursor: not-allowed; }
426-
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
426+
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 10px; }
427427
.connector-name-row {
428428
display: grid;
429429
grid-template-columns: minmax(0, 1fr) auto;
@@ -441,7 +441,7 @@
441441
white-space: nowrap;
442442
opacity: 0.92;
443443
}
444-
.status { margin-top: 8px; font-size: 13px; }
444+
.status { margin-top: 8px; margin-bottom: 10px; font-size: 13px; }
445445
.status.ok { color: var(--status-success-text); }
446446
.status.err { color: var(--status-error-text); }
447447
.connectors-content {

internal/staticserve/static/ui-connectors.js

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ function App() {
3333
const [modelsFetched, setModelsFetched] = useState(false);
3434
const [form, setForm] = useState(defaultForm());
3535
const [ollamaModels, setOllamaModels] = useState([]);
36+
const [bedrockModels, setBedrockModels] = useState([]);
3637
const [dynamicModels, setDynamicModels] = useState([]);
3738
const [apiDefaultModel, setApiDefaultModel] = useState('');
3839
const [session, setSession] = useState(null);
@@ -44,7 +45,7 @@ function App() {
4445
}, [form.provider_name]);
4546

4647
useEffect(() => {
47-
if (!form.provider_name || form.provider_name === 'ollama') {
48+
if (!form.provider_name || form.provider_name === 'ollama' || form.provider_name === 'bedrock') {
4849
setDynamicModels([]);
4950
setApiDefaultModel('');
5051
return;
@@ -93,11 +94,14 @@ function App() {
9394
if (form.provider_name === 'ollama' && ollamaModels.length > 0) {
9495
return ollamaModels;
9596
}
97+
if (form.provider_name === 'bedrock' && bedrockModels.length > 0) {
98+
return bedrockModels.map((model) => model.model_id);
99+
}
96100
if (dynamicModels.length > 0) {
97101
return dynamicModels;
98102
}
99103
return selectedProvider.models || [];
100-
}, [selectedProvider, form.provider_name, ollamaModels, dynamicModels]);
104+
}, [selectedProvider, form.provider_name, ollamaModels, bedrockModels, dynamicModels]);
101105

102106
const [searchQuery, setSearchQuery] = useState('');
103107
const [isOpen, setIsOpen] = useState(false);
@@ -431,6 +435,7 @@ function App() {
431435
const connector = connectors.find((entry) => String(entry.id) === String(route.connectorID));
432436
if (connector) {
433437
setOllamaModels([]);
438+
setBedrockModels([]);
434439
setModelsFetched(false);
435440
setFetchingModels(false);
436441
setForm({
@@ -442,6 +447,8 @@ function App() {
442447
selected_model: connector.selected_model || '',
443448
gcp_project_id: connector.gcp_project_id || '',
444449
gcp_location: connector.gcp_location || '',
450+
aws_access_key_id: connector.aws_access_key_id || '',
451+
aws_region: connector.aws_region || '',
445452
role: connector.role || 'leader',
446453
});
447454
setStatus(`Editing connector #${connector.id}`);
@@ -452,6 +459,7 @@ function App() {
452459
function resetFormState(role) {
453460
setForm({ ...defaultForm(), role: role || 'leader' });
454461
setOllamaModels([]);
462+
setBedrockModels([]);
455463
setApiDefaultModel('');
456464
setModelsFetched(false);
457465
setFetchingModels(false);
@@ -471,12 +479,21 @@ function App() {
471479
: '',
472480
gcp_project_id: '',
473481
gcp_location: '',
482+
aws_access_key_id: '',
483+
aws_region: '',
474484
}));
475485
if (provider.id !== 'ollama') {
476486
setOllamaModels([]);
487+
}
488+
if (provider.id !== 'bedrock') {
489+
setBedrockModels([]);
490+
}
491+
if (provider.id !== 'ollama' && provider.id !== 'bedrock') {
477492
setApiDefaultModel('');
478493
setModelsFetched(false);
479494
}
495+
setStatus('');
496+
setError('');
480497
}
481498

482499
function setFormField(field, value) {
@@ -501,8 +518,14 @@ function App() {
501518
selected_model: connector.selected_model || '',
502519
gcp_project_id: connector.gcp_project_id || '',
503520
gcp_location: connector.gcp_location || '',
521+
aws_access_key_id: connector.aws_access_key_id || '',
522+
aws_region: connector.aws_region || '',
504523
role: connector.role || 'leader',
505524
});
525+
setOllamaModels([]);
526+
setBedrockModels([]);
527+
setModelsFetched(false);
528+
setFetchingModels(false);
506529
setStatus(`Editing connector #${connector.id}`);
507530
navigate(`/connectors/edit/${connector.id}`);
508531
}
@@ -545,6 +568,51 @@ function App() {
545568
}
546569
}
547570

571+
async function fetchBedrockModels() {
572+
setFetchingModels(true);
573+
setError('');
574+
setStatus('');
575+
const awsRegion = (form.aws_region || '').trim();
576+
if (!awsRegion) {
577+
setError('Region is required for Bedrock model discovery');
578+
setFetchingModels(false);
579+
return;
580+
}
581+
try {
582+
const response = await api('/api/ui/connectors/bedrock/models', {
583+
method: 'POST',
584+
body: JSON.stringify({
585+
access_key_id: (form.aws_access_key_id || '').trim(),
586+
secret_access_key: form.api_key || '',
587+
region: awsRegion,
588+
}),
589+
});
590+
const models = response.models || [];
591+
setBedrockModels(models);
592+
setModelsFetched(true);
593+
const modelIds = models.map((model) => model.model_id);
594+
setForm((prev) => {
595+
if (prev.selected_model && modelIds.includes(prev.selected_model)) {
596+
return prev;
597+
}
598+
return { ...prev, selected_model: '' };
599+
});
600+
if (models.length === 0) {
601+
setError('No foundation models found for this region. Request model access in the AWS Bedrock console first.');
602+
} else {
603+
setStatus(`Fetched ${models.length} Bedrock model(s)`);
604+
}
605+
} catch (err) {
606+
setModelsFetched(false);
607+
if (await handleAuthError(err)) {
608+
return;
609+
}
610+
setError(err.message || String(err));
611+
} finally {
612+
setFetchingModels(false);
613+
}
614+
}
615+
548616
async function saveConnector() {
549617
const provider = providers.find((entry) => entry.id === form.provider_name) || providers[0];
550618
const connectorName = (form.connector_name || '').trim();
@@ -582,6 +650,27 @@ function App() {
582650
return;
583651
}
584652

653+
if (!selectedModel) {
654+
setError('Please select a model');
655+
setStatus('');
656+
return;
657+
}
658+
} else if (provider.id === 'bedrock') {
659+
if (!(form.aws_access_key_id || '').trim()) {
660+
setError('AWS Access Key ID is required');
661+
setStatus('');
662+
return;
663+
}
664+
if (!apiKey) {
665+
setError('AWS Secret Access Key is required');
666+
setStatus('');
667+
return;
668+
}
669+
if (!(form.aws_region || '').trim()) {
670+
setError('Region is required');
671+
setStatus('');
672+
return;
673+
}
585674
if (!selectedModel) {
586675
setError('Please select a model');
587676
setStatus('');
@@ -609,6 +698,8 @@ function App() {
609698
model: selectedModel || apiDefaultModel || undefined,
610699
gcp_project_id: (form.gcp_project_id || '').trim() || undefined,
611700
gcp_location: (form.gcp_location || '').trim() || undefined,
701+
aws_access_key_id: (form.aws_access_key_id || '').trim() || undefined,
702+
aws_region: (form.aws_region || '').trim() || undefined,
612703
}),
613704
});
614705

@@ -627,6 +718,8 @@ function App() {
627718
selected_model: selectedModel || apiDefaultModel || undefined,
628719
gcp_project_id: (form.gcp_project_id || '').trim() || undefined,
629720
gcp_location: (form.gcp_location || '').trim() || undefined,
721+
aws_access_key_id: (form.aws_access_key_id || '').trim() || undefined,
722+
aws_region: (form.aws_region || '').trim() || undefined,
630723
display_order: 0,
631724
role: form.role || 'leader',
632725
};
@@ -759,6 +852,12 @@ function App() {
759852
return !apiKey || !gcpProjectID || !gcpLocation;
760853
}
761854

855+
if (provider.id === 'bedrock') {
856+
const awsAccessKeyID = (form.aws_access_key_id || '').trim();
857+
const awsRegion = (form.aws_region || '').trim();
858+
return !apiKey || !awsAccessKeyID || !awsRegion || !selectedModel;
859+
}
860+
762861
if (!apiKey) {
763862
return true;
764863
}
@@ -838,6 +937,7 @@ function App() {
838937
onProviderChange=${setProvider}
839938
onFieldChange=${setFormField}
840939
onFetchOllamaModels=${fetchOllamaModels}
940+
onFetchBedrockModels=${fetchBedrockModels}
841941
onSave=${saveConnector}
842942
onGenerateName=${generateConnectorName}
843943
onCancel=${() => {

0 commit comments

Comments
 (0)