Skip to content

Commit 2192605

Browse files
feat: Bedrock API Keys & filter available models (#5343)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
1 parent d248d2f commit 2192605

File tree

7 files changed

+255
-8
lines changed

7 files changed

+255
-8
lines changed

backend/onyx/llm/llm_provider_options.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,21 +226,30 @@ def fetch_available_well_known_llms() -> list[WellKnownLLMProviderDescriptor]:
226226
name="AWS_ACCESS_KEY_ID",
227227
display_name="AWS Access Key ID",
228228
is_required=False,
229-
description="If using AWS IAM roles, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY can be left blank.",
229+
description="If using IAM role or a long-term API key, leave this field blank.",
230230
),
231231
CustomConfigKey(
232232
name="AWS_SECRET_ACCESS_KEY",
233233
display_name="AWS Secret Access Key",
234234
is_required=False,
235235
is_secret=True,
236-
description="If using AWS IAM roles, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY can be left blank.",
236+
description="If using IAM role or a long-term API key, leave this field blank.",
237+
),
238+
CustomConfigKey(
239+
name="AWS_BEARER_TOKEN_BEDROCK",
240+
display_name="AWS Bedrock Long-term API Key",
241+
is_required=False,
242+
is_secret=True,
243+
description=(
244+
"If using IAM role or access key, leave this field blank."
245+
),
237246
),
238247
],
239248
model_configurations=fetch_model_configurations_for_provider(
240249
BEDROCK_PROVIDER_NAME
241250
),
242251
default_model=BEDROCK_DEFAULT_MODEL,
243-
default_fast_model=BEDROCK_DEFAULT_MODEL,
252+
default_fast_model=None,
244253
),
245254
WellKnownLLMProviderDescriptor(
246255
name=VERTEXAI_PROVIDER_NAME,
@@ -304,6 +313,7 @@ def fetch_model_configurations_for_provider(
304313
visible_model_names = (
305314
fetch_visible_model_names_for_provider_as_set(provider_name) or set()
306315
)
316+
307317
return [
308318
ModelConfigurationView(
309319
name=model_name,

backend/onyx/server/manage/llm/api.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import os
12
from collections.abc import Callable
23
from datetime import datetime
34
from datetime import timezone
45

6+
import boto3
7+
from botocore.exceptions import BotoCoreError
8+
from botocore.exceptions import ClientError
9+
from botocore.exceptions import NoCredentialsError
510
from fastapi import APIRouter
611
from fastapi import Depends
712
from fastapi import HTTPException
@@ -22,12 +27,14 @@
2227
from onyx.llm.factory import get_default_llms
2328
from onyx.llm.factory import get_llm
2429
from onyx.llm.factory import get_max_input_tokens_from_llm_provider
30+
from onyx.llm.llm_provider_options import BEDROCK_MODEL_NAMES
2531
from onyx.llm.llm_provider_options import fetch_available_well_known_llms
2632
from onyx.llm.llm_provider_options import WellKnownLLMProviderDescriptor
2733
from onyx.llm.utils import get_llm_contextual_cost
2834
from onyx.llm.utils import litellm_exception_to_error_msg
2935
from onyx.llm.utils import model_supports_image_input
3036
from onyx.llm.utils import test_llm
37+
from onyx.server.manage.llm.models import BedrockModelsRequest
3138
from onyx.server.manage.llm.models import LLMCost
3239
from onyx.server.manage.llm.models import LLMProviderDescriptor
3340
from onyx.server.manage.llm.models import LLMProviderUpsertRequest
@@ -386,3 +393,84 @@ def get_provider_contextual_cost(
386393
)
387394

388395
return costs
396+
397+
398+
@admin_router.post("/bedrock/available-models")
399+
def get_bedrock_available_models(
400+
request: BedrockModelsRequest,
401+
_: User | None = Depends(current_admin_user),
402+
) -> list[str]:
403+
"""Fetch available Bedrock models for a specific region and credentials"""
404+
try:
405+
# Precedence: bearer → keys → IAM
406+
if request.aws_bearer_token_bedrock:
407+
os.environ["AWS_BEARER_TOKEN_BEDROCK"] = request.aws_bearer_token_bedrock
408+
session = boto3.Session(region_name=request.aws_region_name)
409+
elif request.aws_access_key_id and request.aws_secret_access_key:
410+
session = boto3.Session(
411+
aws_access_key_id=request.aws_access_key_id,
412+
aws_secret_access_key=request.aws_secret_access_key,
413+
region_name=request.aws_region_name,
414+
)
415+
else:
416+
session = boto3.Session(region_name=request.aws_region_name)
417+
418+
try:
419+
bedrock = session.client("bedrock")
420+
except Exception as e:
421+
raise HTTPException(
422+
status_code=400,
423+
detail=f"Failed to create Bedrock client: {e}. Check AWS credentials and region.",
424+
)
425+
426+
# Available Bedrock models: text-only, streaming supported
427+
model_summaries = bedrock.list_foundation_models().get("modelSummaries", [])
428+
available_models = {
429+
model.get("modelId", "")
430+
for model in model_summaries
431+
if model.get("modelId")
432+
and "embed" not in model.get("modelId", "").lower()
433+
and model.get("responseStreamingSupported", False)
434+
}
435+
436+
# Available inference profiles. Invoking these allows cross-region inference (preferred over base models).
437+
profile_ids: set[str] = set()
438+
cross_region_models: set[str] = set()
439+
try:
440+
inference_profiles = bedrock.list_inference_profiles(
441+
typeEquals="SYSTEM_DEFINED"
442+
).get("inferenceProfileSummaries", [])
443+
for profile in inference_profiles:
444+
if profile_id := profile.get("inferenceProfileId"):
445+
profile_ids.add(profile_id)
446+
447+
# The model id is everything after the first period in the profile id
448+
if "." in profile_id:
449+
model_id = profile_id.split(".", 1)[1]
450+
cross_region_models.add(model_id)
451+
except Exception as e:
452+
# Cross-region inference isn't guaranteed; ignore failures here.
453+
logger.warning(f"Couldn't fetch inference profiles for Bedrock: {e}")
454+
455+
# Prefer profiles: de-dupe available models, then add profile IDs
456+
candidates = (available_models - cross_region_models) | profile_ids
457+
458+
# Keep only models we support (compatibility with litellm)
459+
filtered = sorted(
460+
[model for model in candidates if model in BEDROCK_MODEL_NAMES],
461+
reverse=True,
462+
)
463+
464+
# Unset the environment variable, even though it is set again in DefaultMultiLLM init
465+
os.environ.pop("AWS_BEARER_TOKEN_BEDROCK", None)
466+
467+
return filtered
468+
469+
except (ClientError, NoCredentialsError, BotoCoreError) as e:
470+
raise HTTPException(
471+
status_code=400, detail=f"Failed to connect to AWS Bedrock: {e}"
472+
)
473+
except Exception as e:
474+
raise HTTPException(
475+
status_code=500, detail=f"Unexpected error fetching Bedrock models: {e}"
476+
)

backend/onyx/server/manage/llm/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,11 @@ class LLMCost(BaseModel):
188188
provider: str
189189
model_name: str
190190
cost: float
191+
192+
193+
class BedrockModelsRequest(BaseModel):
194+
aws_region_name: str
195+
aws_access_key_id: str | None = None
196+
aws_secret_access_key: str | None = None
197+
aws_bearer_token_bedrock: str | None = None
198+
provider_name: str | None = None # Optional: to save models to existing provider

backend/requirements/default.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
aioboto3==14.0.0
1+
aioboto3==15.1.0
22
aiohttp==3.11.16
33
alembic==1.10.4
44
asyncpg==0.30.0
55
atlassian-python-api==3.41.16
66
beautifulsoup4==4.12.3
7-
boto3==1.36.23
7+
boto3==1.39.11
88
celery==5.5.1
99
chardet==5.2.0
1010
chonkie==1.0.10
@@ -93,7 +93,7 @@ zulip==0.8.2
9393
hubspot-api-client==8.1.0
9494
asana==5.0.8
9595
dropbox==11.36.2
96-
boto3-stubs[s3]==1.34.133
96+
boto3-stubs[s3]==1.39.11
9797
shapely==2.0.6
9898
stripe==10.12.0
9999
urllib3==2.2.3

backend/requirements/dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
black==25.1.0
2-
boto3-stubs[s3]==1.34.133
2+
boto3-stubs[s3]==1.39.11
33
celery-types==0.19.0
44
cohere==5.6.1
55
faker==37.1.0

backend/requirements/model_server.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ uvicorn==0.35.0
1717
voyageai==0.2.3
1818
litellm==1.76.2
1919
sentry-sdk[fastapi,celery,starlette]==2.14.0
20-
aioboto3==14.0.0
20+
aioboto3==15.1.0
2121
prometheus_fastapi_instrumentator==7.1.0

web/src/app/admin/configuration/llm/LLMProviderUpdateForm.tsx

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export function LLMProviderUpdateForm({
4848

4949
const [isTesting, setIsTesting] = useState(false);
5050
const [testError, setTestError] = useState<string>("");
51+
const [isFetchingModels, setIsFetchingModels] = useState(false);
52+
const [fetchModelsError, setFetchModelsError] = useState<string>("");
5153

5254
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
5355

@@ -97,6 +99,9 @@ export function LLMProviderUpdateForm({
9799
(llmProviderDescriptor.model_configurations
98100
.filter((modelConfiguration) => modelConfiguration.is_visible)
99101
.map((modelConfiguration) => modelConfiguration.name) as string[]),
102+
103+
// Helper field to force re-renders when model list updates
104+
_modelListUpdated: 0,
100105
};
101106

102107
// Setup validation schema if required
@@ -179,6 +184,99 @@ export function LLMProviderUpdateForm({
179184
);
180185
};
181186

187+
const fetchBedrockModels = async (values: any, setFieldValue: any) => {
188+
if (llmProviderDescriptor.name !== "bedrock") {
189+
return;
190+
}
191+
192+
setIsFetchingModels(true);
193+
setFetchModelsError("");
194+
195+
try {
196+
const response = await fetch("/api/admin/llm/bedrock/available-models", {
197+
method: "POST",
198+
headers: {
199+
"Content-Type": "application/json",
200+
},
201+
body: JSON.stringify({
202+
aws_region_name: values.custom_config?.AWS_REGION_NAME,
203+
aws_access_key_id: values.custom_config?.AWS_ACCESS_KEY_ID,
204+
aws_secret_access_key: values.custom_config?.AWS_SECRET_ACCESS_KEY,
205+
aws_bearer_token_bedrock:
206+
values.custom_config?.AWS_BEARER_TOKEN_BEDROCK,
207+
provider_name: existingLlmProvider?.name, // Save models to existing provider if editing
208+
}),
209+
});
210+
211+
if (!response.ok) {
212+
const errorData = await response.json();
213+
throw new Error(errorData.detail || "Failed to fetch models");
214+
}
215+
216+
const availableModels: string[] = await response.json();
217+
218+
// Update the model configurations with the fetched models
219+
const updatedModelConfigs = availableModels.map((modelName) => {
220+
// Find existing configuration to preserve is_visible setting
221+
const existingConfig = llmProviderDescriptor.model_configurations.find(
222+
(config) => config.name === modelName
223+
);
224+
225+
return {
226+
name: modelName,
227+
is_visible: existingConfig?.is_visible ?? false, // Preserve existing visibility or default to false
228+
max_input_tokens: null,
229+
supports_image_input: false, // Will be determined by the backend
230+
};
231+
});
232+
233+
// Update the descriptor and form values
234+
llmProviderDescriptor.model_configurations = updatedModelConfigs;
235+
236+
// Update selected model names to only include previously visible models that are available
237+
const previouslySelectedModels = values.selected_model_names || [];
238+
const stillAvailableSelectedModels = previouslySelectedModels.filter(
239+
(modelName: string) => availableModels.includes(modelName)
240+
);
241+
setFieldValue("selected_model_names", stillAvailableSelectedModels);
242+
243+
// Set a default model if none is set
244+
if (
245+
(!values.default_model_name ||
246+
!availableModels.includes(values.default_model_name)) &&
247+
availableModels.length > 0
248+
) {
249+
setFieldValue("default_model_name", availableModels[0]);
250+
}
251+
252+
// Clear fast model if it's not in the new list
253+
if (
254+
values.fast_default_model_name &&
255+
!availableModels.includes(values.fast_default_model_name)
256+
) {
257+
setFieldValue("fast_default_model_name", null);
258+
}
259+
260+
// Force a re-render by updating a timestamp or counter
261+
setFieldValue("_modelListUpdated", Date.now());
262+
263+
setPopup?.({
264+
message: `Successfully fetched ${availableModels.length} models for the selected region (including cross-region inference models).`,
265+
type: "success",
266+
});
267+
} catch (error) {
268+
const errorMessage =
269+
error instanceof Error ? error.message : "Unknown error";
270+
setFetchModelsError(errorMessage);
271+
setPopup?.({
272+
message: `Failed to fetch models: ${errorMessage}`,
273+
type: "error",
274+
});
275+
} finally {
276+
setIsFetchingModels(false);
277+
}
278+
};
279+
182280
return (
183281
<Formik
184282
initialValues={initialValues}
@@ -191,6 +289,7 @@ export function LLMProviderUpdateForm({
191289
selected_model_names: visibleModels,
192290
model_configurations: modelConfigurations,
193291
target_uri,
292+
_modelListUpdated,
194293
...rest
195294
} = values;
196295

@@ -390,6 +489,7 @@ export function LLMProviderUpdateForm({
390489
</ReactMarkdown>
391490
}
392491
placeholder={customConfigKey.default_value || undefined}
492+
type={customConfigKey.is_secret ? "password" : "text"}
393493
/>
394494
</div>
395495
);
@@ -407,6 +507,47 @@ export function LLMProviderUpdateForm({
407507
}
408508
})}
409509

510+
{/* Bedrock-specific fetch models button */}
511+
{llmProviderDescriptor.name === "bedrock" && (
512+
<div className="flex flex-col gap-2">
513+
<Button
514+
type="button"
515+
onClick={() =>
516+
fetchBedrockModels(
517+
formikProps.values,
518+
formikProps.setFieldValue
519+
)
520+
}
521+
disabled={
522+
isFetchingModels ||
523+
!formikProps.values.custom_config?.AWS_REGION_NAME
524+
}
525+
className="w-fit"
526+
>
527+
{isFetchingModels ? (
528+
<>
529+
<LoadingAnimation size="text-sm" />
530+
<span className="ml-2">Fetching Models...</span>
531+
</>
532+
) : (
533+
"Fetch Available Models for Region"
534+
)}
535+
</Button>
536+
537+
{fetchModelsError && (
538+
<Text className="text-red-600 text-sm">{fetchModelsError}</Text>
539+
)}
540+
541+
<Text className="text-sm text-gray-600">
542+
Enter your AWS region, then click this button to fetch available
543+
Bedrock models.
544+
<br />
545+
If you&apos;re updating your existing provider, you&apos;ll need
546+
to click this button to fetch the latest models.
547+
</Text>
548+
</div>
549+
)}
550+
410551
{!firstTimeConfiguration && (
411552
<>
412553
<Separator />

0 commit comments

Comments
 (0)