2026-06-15 00:40:35 +09:00
|
|
|
"use client"
|
|
|
|
|
|
2026-10-08 18:10:07 +09:00
|
|
|
import { ChevronRight, Key, Link2, Tag } from "lucide-react"
|
|
|
|
|
import { type ReactNode, useState } from "react"
|
2026-06-15 00:40:35 +09:00
|
|
|
import { Input } from "@/components/ui/input"
|
|
|
|
|
import { Label } from "@/components/ui/label"
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@/components/ui/select"
|
|
|
|
|
import { useDictionary } from "@/hooks/use-dictionary"
|
|
|
|
|
import { formatMessage } from "@/lib/i18n/utils"
|
2026-10-08 18:10:07 +09:00
|
|
|
import { OPTIONS_OPEN } from "@/lib/provider-setup"
|
2026-10-05 22:39:32 +09:00
|
|
|
import {
|
|
|
|
|
chatRequestUrl,
|
|
|
|
|
normalizeBaseUrl,
|
|
|
|
|
PROVIDER_INFO,
|
|
|
|
|
type ProviderName,
|
|
|
|
|
} from "@/lib/types/model-config"
|
2026-10-08 18:10:07 +09:00
|
|
|
import { cn } from "@/lib/utils"
|
2026-06-15 00:40:35 +09:00
|
|
|
|
|
|
|
|
// Logical secret field. The caller owns the actual input — plaintext for the
|
|
|
|
|
// user dialog, write-only masked for the admin panel — supplied via
|
|
|
|
|
// renderSecret. That (and the optional test action) are the only genuine
|
|
|
|
|
// differences between the two screens; the field structure is shared here.
|
|
|
|
|
export type SecretField =
|
|
|
|
|
| "apiKey"
|
|
|
|
|
| "awsAccessKeyId"
|
|
|
|
|
| "awsSecretAccessKey"
|
|
|
|
|
| "vertexApiKey"
|
|
|
|
|
|
2026-10-08 18:10:07 +09:00
|
|
|
/** Bedrock signs in with an API key or with an access key pair */
|
|
|
|
|
export type BedrockAuth = "apiKey" | "accessKey"
|
|
|
|
|
|
2026-06-15 00:40:35 +09:00
|
|
|
// AWS regions offered for Bedrock (shared by both screens)
|
2026-10-05 22:39:32 +09:00
|
|
|
export const AWS_REGIONS: Array<[string, string]> = [
|
2026-06-15 00:40:35 +09:00
|
|
|
["us-east-1", "N. Virginia"],
|
|
|
|
|
["us-east-2", "Ohio"],
|
|
|
|
|
["us-west-2", "Oregon"],
|
|
|
|
|
["eu-west-1", "Ireland"],
|
|
|
|
|
["eu-west-2", "London"],
|
|
|
|
|
["eu-west-3", "Paris"],
|
|
|
|
|
["eu-central-1", "Frankfurt"],
|
|
|
|
|
["ap-south-1", "Mumbai"],
|
|
|
|
|
["ap-northeast-1", "Tokyo"],
|
|
|
|
|
["ap-northeast-2", "Seoul"],
|
|
|
|
|
["ap-southeast-1", "Singapore"],
|
|
|
|
|
["ap-southeast-2", "Sydney"],
|
|
|
|
|
["sa-east-1", "São Paulo"],
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
interface ProviderCredentialsFieldsProps {
|
|
|
|
|
provider: ProviderName
|
|
|
|
|
// Plain (non-secret) field values — secrets are owned by renderSecret
|
|
|
|
|
name?: string
|
|
|
|
|
baseUrl?: string
|
|
|
|
|
awsRegion?: string
|
2026-10-08 16:20:15 +09:00
|
|
|
// Bedrock: also offer an API key (the user dialog; the admin panel
|
|
|
|
|
// stores access keys only)
|
|
|
|
|
bedrockApiKey?: boolean
|
2026-06-15 00:40:35 +09:00
|
|
|
disabled?: boolean
|
|
|
|
|
// Update a plain text field
|
|
|
|
|
onChange: (field: "name" | "baseUrl" | "awsRegion", value: string) => void
|
|
|
|
|
// Render the control for a secret field. The caller may include trailing
|
|
|
|
|
// UI (e.g. the user dialog's inline Test button + validation error); the
|
|
|
|
|
// shared component only supplies the label above it.
|
|
|
|
|
renderSecret: (opts: { field: SecretField; id: string }) => ReactNode
|
|
|
|
|
// Extra content after the fields — used for the Bedrock test row and the
|
|
|
|
|
// EdgeOne test button, which aren't beside a credential input.
|
|
|
|
|
footer?: ReactNode
|
2026-10-08 18:10:07 +09:00
|
|
|
// The user's settings page: the secrets come first, the display name and
|
|
|
|
|
// base URL fold under "More options", and Bedrock shows only the way of
|
|
|
|
|
// signing in that is chosen. Switching it empties the other way's
|
|
|
|
|
// secrets (clearSecrets), so a request carries one kind of credential.
|
|
|
|
|
settingsLayout?: boolean
|
|
|
|
|
clearSecrets?: (fields: SecretField[]) => void
|
|
|
|
|
// Bedrock credentials filled in, to choose the way of signing in shown
|
|
|
|
|
bedrockFilled?: { apiKey: boolean; accessKey: boolean }
|
|
|
|
|
// Bedrock's optional session token input, for temporary access keys
|
|
|
|
|
// (the admin panel has none)
|
|
|
|
|
sessionTokenInput?: ReactNode
|
2026-06-15 00:40:35 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Display name + per-provider credential inputs, shared by the user
|
|
|
|
|
// ModelConfigDialog and the admin Models panel.
|
|
|
|
|
export function ProviderCredentialsFields({
|
|
|
|
|
provider,
|
|
|
|
|
name,
|
|
|
|
|
baseUrl,
|
|
|
|
|
awsRegion,
|
2026-10-08 16:20:15 +09:00
|
|
|
bedrockApiKey,
|
2026-06-15 00:40:35 +09:00
|
|
|
disabled,
|
|
|
|
|
onChange,
|
|
|
|
|
renderSecret,
|
|
|
|
|
footer,
|
2026-10-08 18:10:07 +09:00
|
|
|
settingsLayout,
|
|
|
|
|
clearSecrets,
|
|
|
|
|
bedrockFilled,
|
|
|
|
|
sessionTokenInput,
|
2026-06-15 00:40:35 +09:00
|
|
|
}: ProviderCredentialsFieldsProps) {
|
|
|
|
|
const dict = useDictionary()
|
|
|
|
|
const info = PROVIDER_INFO[provider]
|
|
|
|
|
const baseUrlLabel = formatMessage(dict.modelConfig.baseUrlWithExample, {
|
|
|
|
|
example: info.defaultBaseUrl || "https://api.example.com/v1",
|
|
|
|
|
})
|
2026-10-05 22:39:32 +09:00
|
|
|
const requestUrl = baseUrl ? chatRequestUrl(provider, baseUrl) : null
|
2026-10-08 18:10:07 +09:00
|
|
|
// Open when it holds something the user set, or for a provider whose
|
|
|
|
|
// base URL usually needs changing or comes with a hint
|
|
|
|
|
const [optionsOpen, setOptionsOpen] = useState(
|
|
|
|
|
() =>
|
|
|
|
|
!!name ||
|
|
|
|
|
(!!baseUrl && baseUrl !== info.defaultBaseUrl) ||
|
|
|
|
|
OPTIONS_OPEN.includes(provider),
|
|
|
|
|
)
|
|
|
|
|
// Bedrock: the way that is filled in (an API key is used first when both
|
|
|
|
|
// are, as the server does), else the one shown last, so emptying the
|
|
|
|
|
// fields to type new keys keeps them on screen
|
|
|
|
|
const filledAuth: BedrockAuth | null = bedrockFilled?.apiKey
|
|
|
|
|
? "apiKey"
|
|
|
|
|
: bedrockFilled?.accessKey
|
|
|
|
|
? "accessKey"
|
|
|
|
|
: null
|
|
|
|
|
const [chosenAuth, setChosenAuth] = useState<BedrockAuth>(
|
|
|
|
|
filledAuth ?? "apiKey",
|
|
|
|
|
)
|
|
|
|
|
if (filledAuth && filledAuth !== chosenAuth) setChosenAuth(filledAuth)
|
|
|
|
|
const bedrockAuth: BedrockAuth = filledAuth ?? chosenAuth
|
|
|
|
|
|
|
|
|
|
const nameField = (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor="provider-name"
|
|
|
|
|
className="text-xs font-medium flex items-center gap-1.5"
|
|
|
|
|
>
|
|
|
|
|
<Tag className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
{dict.modelConfig.displayName}
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="provider-name"
|
|
|
|
|
value={name ?? ""}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onChange={(e) => onChange("name", e.target.value)}
|
|
|
|
|
placeholder={info.label}
|
|
|
|
|
className="h-9"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
const regionField = (
|
|
|
|
|
<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" />
|
|
|
|
|
{dict.modelConfig.awsRegion}
|
|
|
|
|
</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={awsRegion || ""}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onValueChange={(v) => onChange("awsRegion", v)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger
|
|
|
|
|
id="aws-region"
|
|
|
|
|
className="h-9 font-mono text-xs hover:bg-accent"
|
|
|
|
|
>
|
|
|
|
|
<SelectValue placeholder={dict.modelConfig.selectRegion} />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent className="max-h-64">
|
|
|
|
|
{AWS_REGIONS.map(([region, label]) => (
|
|
|
|
|
<SelectItem key={region} value={region}>
|
|
|
|
|
{region} ({label})
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
const vertexBaseUrlField = (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor="vertex-base-url"
|
|
|
|
|
className="text-xs font-medium flex items-center gap-1.5"
|
|
|
|
|
>
|
|
|
|
|
<Link2 className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
{baseUrlLabel}
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="vertex-base-url"
|
|
|
|
|
value={baseUrl ?? ""}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onChange={(e) => onChange("baseUrl", e.target.value)}
|
|
|
|
|
placeholder={dict.modelConfig.customEndpoint}
|
|
|
|
|
className="h-9 font-mono text-xs"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
// API key, with a link to where the provider issues keys
|
|
|
|
|
const apiKeyField = (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<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" />
|
|
|
|
|
{dict.modelConfig.apiKey}
|
|
|
|
|
{provider === "ollama" && ` ${dict.modelConfig.optional}`}
|
|
|
|
|
</Label>
|
|
|
|
|
{info.apiKeyUrl && (
|
|
|
|
|
<a
|
|
|
|
|
href={info.apiKeyUrl}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="text-xs text-primary hover:underline"
|
|
|
|
|
>
|
|
|
|
|
{dict.modelConfig.getApiKey}
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{renderSecret({ field: "apiKey", id: "api-key" })}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
const baseUrlField = (
|
|
|
|
|
<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" />
|
|
|
|
|
{baseUrlLabel}
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="base-url"
|
|
|
|
|
value={baseUrl ?? ""}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onChange={(e) => onChange("baseUrl", e.target.value)}
|
|
|
|
|
// Drop a pasted endpoint path such as /chat/completions
|
|
|
|
|
onBlur={(e) => {
|
|
|
|
|
const normalized = normalizeBaseUrl(e.target.value)
|
|
|
|
|
if (normalized !== e.target.value) {
|
|
|
|
|
onChange("baseUrl", normalized)
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
placeholder={
|
|
|
|
|
info.defaultBaseUrl || dict.modelConfig.customEndpoint
|
|
|
|
|
}
|
|
|
|
|
className="h-9 rounded-xl font-mono text-xs"
|
|
|
|
|
/>
|
|
|
|
|
{requestUrl && (
|
|
|
|
|
<p className="text-xs text-muted-foreground font-mono break-all">
|
|
|
|
|
{formatMessage(dict.modelConfig.requestUrl, {
|
|
|
|
|
url: requestUrl,
|
|
|
|
|
})}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
{provider === "minimax" && (
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{dict.modelConfig.minimaxBaseUrlHint}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
{provider === "mimo" && (
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{dict.modelConfig.mimoBaseUrlHint}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
2026-06-15 00:40:35 +09:00
|
|
|
|
|
|
|
|
// EdgeOne needs no credentials — the caller supplies just a test button
|
|
|
|
|
if (provider === "edgeone") {
|
|
|
|
|
return <div className="space-y-5">{footer}</div>
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-08 18:10:07 +09:00
|
|
|
if (settingsLayout) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{provider === "bedrock" ? (
|
|
|
|
|
<>
|
|
|
|
|
{/* Buttons, not radios: arrow keys would empty
|
|
|
|
|
secrets without a deliberate press */}
|
|
|
|
|
<fieldset
|
|
|
|
|
aria-label={dict.modelConfig.awsCredentials}
|
|
|
|
|
className="inline-flex rounded-lg bg-muted p-0.5"
|
|
|
|
|
>
|
|
|
|
|
{(["apiKey", "accessKey"] as const).map((auth) => (
|
|
|
|
|
<button
|
|
|
|
|
key={auth}
|
|
|
|
|
type="button"
|
|
|
|
|
aria-pressed={bedrockAuth === auth}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (bedrockAuth === auth) return
|
|
|
|
|
setChosenAuth(auth)
|
|
|
|
|
clearSecrets?.(
|
|
|
|
|
auth === "apiKey"
|
|
|
|
|
? [
|
|
|
|
|
"awsAccessKeyId",
|
|
|
|
|
"awsSecretAccessKey",
|
|
|
|
|
]
|
|
|
|
|
: ["apiKey"],
|
|
|
|
|
)
|
|
|
|
|
}}
|
|
|
|
|
className={cn(
|
|
|
|
|
"inline-flex h-7 items-center rounded-md px-2.5 text-xs text-muted-foreground transition-colors",
|
|
|
|
|
bedrockAuth === auth &&
|
|
|
|
|
"bg-card font-medium text-foreground shadow-float",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{auth === "apiKey"
|
|
|
|
|
? dict.modelConfig.bedrockAuthApiKey
|
|
|
|
|
: dict.modelConfig.bedrockAuthAccessKey}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</fieldset>
|
|
|
|
|
{/* Both kinds kept (saved before the choice): the
|
|
|
|
|
access keys sit unused behind the API key */}
|
|
|
|
|
{bedrockFilled?.apiKey && bedrockFilled.accessKey && (
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{dict.modelConfig.bedrockBothSaved}{" "}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
clearSecrets?.([
|
|
|
|
|
"awsAccessKeyId",
|
|
|
|
|
"awsSecretAccessKey",
|
|
|
|
|
])
|
|
|
|
|
// This note goes away with the keys
|
|
|
|
|
document
|
|
|
|
|
.getElementById("bedrock-api-key")
|
|
|
|
|
?.focus()
|
|
|
|
|
}}
|
|
|
|
|
className="font-medium text-foreground underline underline-offset-2"
|
|
|
|
|
>
|
|
|
|
|
{dict.modelConfig.removeAccessKeys}
|
|
|
|
|
</button>
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
{bedrockAuth === "apiKey" ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor="bedrock-api-key"
|
|
|
|
|
className="text-xs font-medium flex items-center gap-1.5"
|
|
|
|
|
>
|
|
|
|
|
<Key className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
{dict.modelConfig.bedrockApiKey}
|
|
|
|
|
</Label>
|
|
|
|
|
{renderSecret({
|
|
|
|
|
field: "apiKey",
|
|
|
|
|
id: "bedrock-api-key",
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<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" />
|
|
|
|
|
{dict.modelConfig.awsAccessKeyId}
|
|
|
|
|
</Label>
|
|
|
|
|
{renderSecret({
|
|
|
|
|
field: "awsAccessKeyId",
|
|
|
|
|
id: "aws-access-key-id",
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
<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" />
|
|
|
|
|
{dict.modelConfig.awsSecretAccessKey}
|
|
|
|
|
</Label>
|
|
|
|
|
{renderSecret({
|
|
|
|
|
field: "awsSecretAccessKey",
|
|
|
|
|
id: "aws-secret-access-key",
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
{sessionTokenInput && (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor="aws-session-token"
|
|
|
|
|
className="text-xs font-medium flex items-center gap-1.5"
|
|
|
|
|
>
|
|
|
|
|
<Key className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
{dict.modelConfig.awsSessionToken}
|
|
|
|
|
</Label>
|
|
|
|
|
{sessionTokenInput}
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{
|
|
|
|
|
dict.modelConfig
|
|
|
|
|
.awsSessionTokenHint
|
|
|
|
|
}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{regionField}
|
|
|
|
|
</>
|
|
|
|
|
) : provider === "vertexai" ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor="vertex-api-key"
|
|
|
|
|
className="text-xs font-medium flex items-center gap-1.5"
|
|
|
|
|
>
|
|
|
|
|
<Key className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
{dict.modelConfig.apiKey}
|
|
|
|
|
</Label>
|
|
|
|
|
{renderSecret({
|
|
|
|
|
field: "vertexApiKey",
|
|
|
|
|
id: "vertex-api-key",
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
apiKeyField
|
|
|
|
|
)}
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setOptionsOpen(!optionsOpen)}
|
|
|
|
|
aria-expanded={optionsOpen}
|
|
|
|
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
|
|
|
|
>
|
|
|
|
|
<ChevronRight
|
|
|
|
|
className={cn(
|
|
|
|
|
"size-3.5 transition-transform",
|
|
|
|
|
optionsOpen && "rotate-90",
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
{provider === "bedrock"
|
|
|
|
|
? dict.modelConfig.moreOptionsName
|
|
|
|
|
: dict.modelConfig.moreOptions}
|
|
|
|
|
</button>
|
|
|
|
|
{optionsOpen && (
|
|
|
|
|
<>
|
|
|
|
|
{provider !== "bedrock" &&
|
|
|
|
|
(provider === "vertexai"
|
|
|
|
|
? vertexBaseUrlField
|
|
|
|
|
: baseUrlField)}
|
|
|
|
|
{nameField}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{footer}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 00:40:35 +09:00
|
|
|
return (
|
|
|
|
|
<div className="space-y-5">
|
2026-10-08 18:10:07 +09:00
|
|
|
{nameField}
|
2026-06-15 00:40:35 +09:00
|
|
|
|
|
|
|
|
{provider === "bedrock" ? (
|
|
|
|
|
<>
|
2026-10-08 16:20:15 +09:00
|
|
|
{/* Bedrock API key: used instead of the access keys */}
|
|
|
|
|
{bedrockApiKey && (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor="bedrock-api-key"
|
|
|
|
|
className="text-xs font-medium flex items-center gap-1.5"
|
|
|
|
|
>
|
|
|
|
|
<Key className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
{dict.modelConfig.bedrockApiKey}
|
|
|
|
|
</Label>
|
|
|
|
|
{renderSecret({
|
|
|
|
|
field: "apiKey",
|
|
|
|
|
id: "bedrock-api-key",
|
|
|
|
|
})}
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{dict.modelConfig.bedrockApiKeyHint}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-06-15 00:40:35 +09:00
|
|
|
{/* 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" />
|
|
|
|
|
{dict.modelConfig.awsAccessKeyId}
|
|
|
|
|
</Label>
|
|
|
|
|
{renderSecret({
|
|
|
|
|
field: "awsAccessKeyId",
|
|
|
|
|
id: "aws-access-key-id",
|
|
|
|
|
})}
|
|
|
|
|
</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" />
|
|
|
|
|
{dict.modelConfig.awsSecretAccessKey}
|
|
|
|
|
</Label>
|
|
|
|
|
{renderSecret({
|
|
|
|
|
field: "awsSecretAccessKey",
|
|
|
|
|
id: "aws-secret-access-key",
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-10-08 18:10:07 +09:00
|
|
|
{regionField}
|
2026-06-15 00:40:35 +09:00
|
|
|
</>
|
|
|
|
|
) : provider === "vertexai" ? (
|
|
|
|
|
<>
|
|
|
|
|
{/* Vertex AI API Key (Express Mode) */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor="vertex-api-key"
|
|
|
|
|
className="text-xs font-medium flex items-center gap-1.5"
|
|
|
|
|
>
|
|
|
|
|
<Key className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
{dict.modelConfig.apiKey}
|
|
|
|
|
</Label>
|
|
|
|
|
{renderSecret({
|
|
|
|
|
field: "vertexApiKey",
|
|
|
|
|
id: "vertex-api-key",
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-10-08 18:10:07 +09:00
|
|
|
{vertexBaseUrlField}
|
2026-06-15 00:40:35 +09:00
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
2026-10-08 18:10:07 +09:00
|
|
|
{apiKeyField}
|
|
|
|
|
{baseUrlField}
|
2026-06-15 00:40:35 +09:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{footer}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|