"use client" import { ChevronRight, Key, Link2, Tag } from "lucide-react" import { type ReactNode, useState } from "react" 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" import { OPTIONS_OPEN } from "@/lib/provider-setup" import { chatRequestUrl, normalizeBaseUrl, PROVIDER_INFO, type ProviderName, } from "@/lib/types/model-config" import { cn } from "@/lib/utils" // 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" /** Bedrock signs in with an API key or with an access key pair */ export type BedrockAuth = "apiKey" | "accessKey" // AWS regions offered for Bedrock (shared by both screens) export const AWS_REGIONS: Array<[string, string]> = [ ["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 // Bedrock: also offer an API key (the user dialog; the admin panel // stores access keys only) bedrockApiKey?: boolean 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 // 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 } // Display name + per-provider credential inputs, shared by the user // ModelConfigDialog and the admin Models panel. export function ProviderCredentialsFields({ provider, name, baseUrl, awsRegion, bedrockApiKey, disabled, onChange, renderSecret, footer, settingsLayout, clearSecrets, bedrockFilled, sessionTokenInput, }: ProviderCredentialsFieldsProps) { const dict = useDictionary() const info = PROVIDER_INFO[provider] const baseUrlLabel = formatMessage(dict.modelConfig.baseUrlWithExample, { example: info.defaultBaseUrl || "https://api.example.com/v1", }) const requestUrl = baseUrl ? chatRequestUrl(provider, baseUrl) : null // 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( filledAuth ?? "apiKey", ) if (filledAuth && filledAuth !== chosenAuth) setChosenAuth(filledAuth) const bedrockAuth: BedrockAuth = filledAuth ?? chosenAuth const nameField = (
onChange("name", e.target.value)} placeholder={info.label} className="h-9" />
) const regionField = (
) const vertexBaseUrlField = (
onChange("baseUrl", e.target.value)} placeholder={dict.modelConfig.customEndpoint} className="h-9 font-mono text-xs" />
) // API key, with a link to where the provider issues keys const apiKeyField = (
{info.apiKeyUrl && ( {dict.modelConfig.getApiKey} )}
{renderSecret({ field: "apiKey", id: "api-key" })}
) const baseUrlField = (
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 && (

{formatMessage(dict.modelConfig.requestUrl, { url: requestUrl, })}

)} {provider === "minimax" && (

{dict.modelConfig.minimaxBaseUrlHint}

)} {provider === "mimo" && (

{dict.modelConfig.mimoBaseUrlHint}

)}
) // EdgeOne needs no credentials — the caller supplies just a test button if (provider === "edgeone") { return
{footer}
} if (settingsLayout) { return (
{provider === "bedrock" ? ( <> {/* Buttons, not radios: arrow keys would empty secrets without a deliberate press */}
{(["apiKey", "accessKey"] as const).map((auth) => ( ))}
{/* Both kinds kept (saved before the choice): the access keys sit unused behind the API key */} {bedrockFilled?.apiKey && bedrockFilled.accessKey && (

{dict.modelConfig.bedrockBothSaved}{" "}

)} {bedrockAuth === "apiKey" ? (
{renderSecret({ field: "apiKey", id: "bedrock-api-key", })}
) : ( <>
{renderSecret({ field: "awsAccessKeyId", id: "aws-access-key-id", })}
{renderSecret({ field: "awsSecretAccessKey", id: "aws-secret-access-key", })}
{sessionTokenInput && (
{sessionTokenInput}

{ dict.modelConfig .awsSessionTokenHint }

)} )} {regionField} ) : provider === "vertexai" ? (
{renderSecret({ field: "vertexApiKey", id: "vertex-api-key", })}
) : ( apiKeyField )}
{optionsOpen && ( <> {provider !== "bedrock" && (provider === "vertexai" ? vertexBaseUrlField : baseUrlField)} {nameField} )}
{footer}
) } return (
{nameField} {provider === "bedrock" ? ( <> {/* Bedrock API key: used instead of the access keys */} {bedrockApiKey && (
{renderSecret({ field: "apiKey", id: "bedrock-api-key", })}

{dict.modelConfig.bedrockApiKeyHint}

)} {/* AWS Access Key ID */}
{renderSecret({ field: "awsAccessKeyId", id: "aws-access-key-id", })}
{/* AWS Secret Access Key */}
{renderSecret({ field: "awsSecretAccessKey", id: "aws-secret-access-key", })}
{regionField} ) : provider === "vertexai" ? ( <> {/* Vertex AI API Key (Express Mode) */}
{renderSecret({ field: "vertexApiKey", id: "vertex-api-key", })}
{vertexBaseUrlField} ) : ( <> {apiKeyField} {baseUrlField} )} {footer}
) }