feat: add AWS credentials support for Bedrock provider

- Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock
- Show different credential fields based on provider type
- Update validation API to handle Bedrock with AWS credentials
- Add region selector with common AWS regions
This commit is contained in:
dayuan.jiang
2025-12-22 20:09:16 +09:00
parent e089702949
commit 56ca9d3f48
3 changed files with 400 additions and 116 deletions

View File

@@ -1,3 +1,4 @@
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
import { createAnthropic } from "@ai-sdk/anthropic"
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
import { createGateway } from "@ai-sdk/gateway"
@@ -15,12 +16,24 @@ interface ValidateRequest {
apiKey: string
baseUrl?: string
modelId: string
// AWS Bedrock specific
awsAccessKeyId?: string
awsSecretAccessKey?: string
awsRegion?: string
}
export async function POST(req: Request) {
try {
const body: ValidateRequest = await req.json()
const { provider, apiKey, baseUrl, modelId } = body
const {
provider,
apiKey,
baseUrl,
modelId,
awsAccessKeyId,
awsSecretAccessKey,
awsRegion,
} = body
if (!provider || !modelId) {
return NextResponse.json(
@@ -29,8 +42,18 @@ export async function POST(req: Request) {
)
}
// Ollama doesn't require API key
if (provider !== "ollama" && !apiKey) {
// Validate credentials based on provider
if (provider === "bedrock") {
if (!awsAccessKeyId || !awsSecretAccessKey || !awsRegion) {
return NextResponse.json(
{
valid: false,
error: "AWS credentials (Access Key ID, Secret Access Key, Region) are required",
},
{ status: 400 },
)
}
} else if (provider !== "ollama" && !apiKey) {
return NextResponse.json(
{ valid: false, error: "API key is required" },
{ status: 400 },
@@ -76,6 +99,16 @@ export async function POST(req: Request) {
break
}
case "bedrock": {
const bedrock = createAmazonBedrock({
accessKeyId: awsAccessKeyId,
secretAccessKey: awsSecretAccessKey,
region: awsRegion,
})
model = bedrock(modelId)
break
}
case "openrouter": {
const openrouter = createOpenRouter({
apiKey,

View File

@@ -175,8 +175,15 @@ export function ModelConfigDialog({
) => {
if (!selectedProviderId) return
updateProvider(selectedProviderId, { [field]: value })
// Reset validation when API key or base URL changes
if (field === "apiKey" || field === "baseUrl") {
// Reset validation when credentials change
const credentialFields = [
"apiKey",
"baseUrl",
"awsAccessKeyId",
"awsSecretAccessKey",
"awsRegion",
]
if (credentialFields.includes(field)) {
setValidationStatus("idle")
updateProvider(selectedProviderId, { validated: false })
}
@@ -205,7 +212,21 @@ export function ModelConfigDialog({
// Validate all models
const handleValidate = useCallback(async () => {
if (!selectedProvider || !selectedProvider.apiKey) return
if (!selectedProvider) return
// Check credentials based on provider type
const isBedrock = selectedProvider.provider === "bedrock"
if (isBedrock) {
if (
!selectedProvider.awsAccessKeyId ||
!selectedProvider.awsSecretAccessKey ||
!selectedProvider.awsRegion
) {
return
}
} else if (!selectedProvider.apiKey) {
return
}
// Need at least one model to validate
if (selectedProvider.models.length === 0) {
@@ -234,6 +255,10 @@ export function ModelConfigDialog({
apiKey: selectedProvider.apiKey,
baseUrl: selectedProvider.baseUrl,
modelId: model.modelId,
// AWS Bedrock credentials
awsAccessKeyId: selectedProvider.awsAccessKeyId,
awsSecretAccessKey: selectedProvider.awsSecretAccessKey,
awsRegion: selectedProvider.awsRegion,
}),
})
@@ -499,136 +524,347 @@ export function ModelConfigDialog({
/>
</div>
{/* API Key */}
<div className="space-y-2">
<Label
htmlFor="api-key"
className="text-xs font-medium flex items-center gap-1.5"
>
<Key className="h-3.5 w-3.5 text-muted-foreground" />
API Key
</Label>
<div className="flex gap-2">
<div className="relative flex-1">
{/* Credentials - different for Bedrock vs other providers */}
{selectedProvider.provider ===
"bedrock" ? (
<>
{/* AWS Access Key ID */}
<div className="space-y-2">
<Label
htmlFor="aws-access-key-id"
className="text-xs font-medium flex items-center gap-1.5"
>
<Key className="h-3.5 w-3.5 text-muted-foreground" />
AWS Access Key
ID
</Label>
<Input
id="api-key"
id="aws-access-key-id"
type={
showApiKey
? "text"
: "password"
}
value={
selectedProvider.apiKey
selectedProvider.awsAccessKeyId ||
""
}
onChange={(e) =>
handleProviderUpdate(
"apiKey",
"awsAccessKeyId",
e.target
.value,
)
}
placeholder="Enter your API key"
className="h-9 pr-10 font-mono text-xs"
placeholder="AKIA..."
className="h-9 font-mono text-xs"
/>
<button
type="button"
onClick={() =>
setShowApiKey(
!showApiKey,
</div>
{/* AWS Secret Access Key */}
<div className="space-y-2">
<Label
htmlFor="aws-secret-access-key"
className="text-xs font-medium flex items-center gap-1.5"
>
<Key className="h-3.5 w-3.5 text-muted-foreground" />
AWS Secret
Access Key
</Label>
<div className="relative">
<Input
id="aws-secret-access-key"
type={
showApiKey
? "text"
: "password"
}
value={
selectedProvider.awsSecretAccessKey ||
""
}
onChange={(
e,
) =>
handleProviderUpdate(
"awsSecretAccessKey",
e
.target
.value,
)
}
placeholder="Enter your secret access key"
className="h-9 pr-10 font-mono text-xs"
/>
<button
type="button"
onClick={() =>
setShowApiKey(
!showApiKey,
)
}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
>
{showApiKey ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
</div>
{/* AWS Region */}
<div className="space-y-2">
<Label
htmlFor="aws-region"
className="text-xs font-medium flex items-center gap-1.5"
>
<Link2 className="h-3.5 w-3.5 text-muted-foreground" />
AWS Region
</Label>
<Select
value={
selectedProvider.awsRegion ||
""
}
onValueChange={(
v,
) =>
handleProviderUpdate(
"awsRegion",
v,
)
}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
>
{showApiKey ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
<SelectTrigger className="h-9 font-mono text-xs hover:bg-accent">
<SelectValue placeholder="Select region" />
</SelectTrigger>
<SelectContent>
<SelectItem value="us-east-1">
us-east-1
(N.
Virginia)
</SelectItem>
<SelectItem value="us-west-2">
us-west-2
(Oregon)
</SelectItem>
<SelectItem value="eu-west-1">
eu-west-1
(Ireland)
</SelectItem>
<SelectItem value="eu-central-1">
eu-central-1
(Frankfurt)
</SelectItem>
<SelectItem value="ap-northeast-1">
ap-northeast-1
(Tokyo)
</SelectItem>
<SelectItem value="ap-southeast-1">
ap-southeast-1
(Singapore)
</SelectItem>
<SelectItem value="ap-southeast-2">
ap-southeast-2
(Sydney)
</SelectItem>
</SelectContent>
</Select>
</div>
<Button
variant={
validationStatus ===
"success"
? "outline"
: "default"
}
size="sm"
onClick={
handleValidate
}
disabled={
!selectedProvider.apiKey ||
validationStatus ===
"validating"
}
className={cn(
"h-9 px-4",
validationStatus ===
"success" &&
"text-emerald-600 border-emerald-200 dark:border-emerald-800",
)}
>
{validationStatus ===
"validating" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : validationStatus ===
"success" ? (
<>
<Check className="h-4 w-4 mr-1.5" />
Verified
</>
) : (
"Test"
)}
</Button>
</div>
{validationStatus ===
"error" &&
validationError && (
<p className="text-xs text-destructive flex items-center gap-1">
<X className="h-3 w-3" />
{
validationError
}
</p>
)}
</div>
{/* Base URL */}
<div className="space-y-2">
<Label
htmlFor="base-url"
className="text-xs font-medium flex items-center gap-1.5"
>
<Link2 className="h-3.5 w-3.5 text-muted-foreground" />
Base URL
<span className="text-muted-foreground font-normal">
(optional)
</span>
</Label>
<Input
id="base-url"
value={
selectedProvider.baseUrl ||
""
}
onChange={(e) =>
handleProviderUpdate(
"baseUrl",
e.target.value,
)
}
placeholder={
PROVIDER_INFO[
selectedProvider
.provider
].defaultBaseUrl ||
"Custom endpoint URL"
}
className="h-9 font-mono text-xs"
/>
</div>
{/* Test Button for Bedrock */}
<div className="flex items-center gap-2">
<Button
variant={
validationStatus ===
"success"
? "outline"
: "default"
}
size="sm"
onClick={
handleValidate
}
disabled={
!selectedProvider.awsAccessKeyId ||
!selectedProvider.awsSecretAccessKey ||
!selectedProvider.awsRegion ||
validationStatus ===
"validating"
}
className={cn(
"h-9 px-4",
validationStatus ===
"success" &&
"text-emerald-600 border-emerald-200 dark:border-emerald-800",
)}
>
{validationStatus ===
"validating" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : validationStatus ===
"success" ? (
<>
<Check className="h-4 w-4 mr-1.5" />
Verified
</>
) : (
"Test"
)}
</Button>
{validationStatus ===
"error" &&
validationError && (
<p className="text-xs text-destructive flex items-center gap-1">
<X className="h-3 w-3" />
{
validationError
}
</p>
)}
</div>
</>
) : (
<>
{/* API Key */}
<div className="space-y-2">
<Label
htmlFor="api-key"
className="text-xs font-medium flex items-center gap-1.5"
>
<Key className="h-3.5 w-3.5 text-muted-foreground" />
API Key
</Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="api-key"
type={
showApiKey
? "text"
: "password"
}
value={
selectedProvider.apiKey
}
onChange={(
e,
) =>
handleProviderUpdate(
"apiKey",
e
.target
.value,
)
}
placeholder="Enter your API key"
className="h-9 pr-10 font-mono text-xs"
/>
<button
type="button"
onClick={() =>
setShowApiKey(
!showApiKey,
)
}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
>
{showApiKey ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
<Button
variant={
validationStatus ===
"success"
? "outline"
: "default"
}
size="sm"
onClick={
handleValidate
}
disabled={
!selectedProvider.apiKey ||
validationStatus ===
"validating"
}
className={cn(
"h-9 px-4",
validationStatus ===
"success" &&
"text-emerald-600 border-emerald-200 dark:border-emerald-800",
)}
>
{validationStatus ===
"validating" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : validationStatus ===
"success" ? (
<>
<Check className="h-4 w-4 mr-1.5" />
Verified
</>
) : (
"Test"
)}
</Button>
</div>
{validationStatus ===
"error" &&
validationError && (
<p className="text-xs text-destructive flex items-center gap-1">
<X className="h-3 w-3" />
{
validationError
}
</p>
)}
</div>
{/* Base URL */}
<div className="space-y-2">
<Label
htmlFor="base-url"
className="text-xs font-medium flex items-center gap-1.5"
>
<Link2 className="h-3.5 w-3.5 text-muted-foreground" />
Base URL
<span className="text-muted-foreground font-normal">
(optional)
</span>
</Label>
<Input
id="base-url"
value={
selectedProvider.baseUrl ||
""
}
onChange={(e) =>
handleProviderUpdate(
"baseUrl",
e.target
.value,
)
}
placeholder={
PROVIDER_INFO[
selectedProvider
.provider
]
.defaultBaseUrl ||
"Custom endpoint URL"
}
className="h-9 font-mono text-xs"
/>
</div>
</>
)}
</div>
</div>

View File

@@ -26,6 +26,11 @@ export interface ProviderConfig {
name?: string // Custom display name (e.g., "OpenAI Production")
apiKey: string
baseUrl?: string
// AWS Bedrock specific fields
awsAccessKeyId?: string
awsSecretAccessKey?: string
awsRegion?: string
awsSessionToken?: string // Optional, for temporary credentials
models: ModelConfig[]
validated?: boolean // Has API key been validated
}
@@ -45,6 +50,11 @@ export interface FlattenedModel {
providerLabel: string // Provider display name
apiKey: string
baseUrl?: string
// AWS Bedrock specific fields
awsAccessKeyId?: string
awsSecretAccessKey?: string
awsRegion?: string
awsSessionToken?: string
validated?: boolean // Has this model been validated
}
@@ -252,6 +262,11 @@ export function flattenModels(config: MultiModelConfig): FlattenedModel[] {
providerLabel,
apiKey: provider.apiKey,
baseUrl: provider.baseUrl,
// AWS Bedrock fields
awsAccessKeyId: provider.awsAccessKeyId,
awsSecretAccessKey: provider.awsSecretAccessKey,
awsRegion: provider.awsRegion,
awsSessionToken: provider.awsSessionToken,
validated: model.validated,
})
}