diff --git a/frontend/src/components/Config/CreateTargetDialog.styles.ts b/frontend/src/components/Config/CreateTargetDialog.styles.ts
index 34f023786..67fdb2aa2 100644
--- a/frontend/src/components/Config/CreateTargetDialog.styles.ts
+++ b/frontend/src/components/Config/CreateTargetDialog.styles.ts
@@ -6,4 +6,12 @@ export const useCreateTargetDialogStyles = makeStyles({
flexDirection: 'column',
gap: tokens.spacingVerticalL,
},
+ warningMessage: {
+ width: '100%',
+ },
+ warningMessageBody: {
+ whiteSpace: 'normal',
+ overflowWrap: 'anywhere',
+ wordBreak: 'break-word',
+ },
})
diff --git a/frontend/src/components/Config/CreateTargetDialog.test.tsx b/frontend/src/components/Config/CreateTargetDialog.test.tsx
index b8a9dcfc0..cd096a192 100644
--- a/frontend/src/components/Config/CreateTargetDialog.test.tsx
+++ b/frontend/src/components/Config/CreateTargetDialog.test.tsx
@@ -497,4 +497,180 @@ describe("CreateTargetDialog", () => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
+
+ it("should hide the API Key field and omit api_key/include auth_mode when Entra is selected", async () => {
+ const onCreated = jest.fn();
+ const user = userEvent.setup();
+ mockedTargetsApi.createTarget.mockResolvedValue({
+ target_registry_name: "openai_chat_entra",
+ target_type: "OpenAIChatTarget",
+ });
+
+ render(
+
+
+
+ );
+
+ await selectTargetType(user, "OpenAIChatTarget");
+
+ const endpointInput = screen.getByPlaceholderText(
+ "https://your-resource.openai.azure.com/"
+ );
+ fireEvent.change(endpointInput, {
+ target: { value: "https://my-resource.openai.azure.com/" },
+ });
+
+ // check that API Key field is visible by default.
+ expect(
+ screen.getByPlaceholderText("API key (stored in memory only)")
+ ).toBeInTheDocument();
+
+ // Select Entra option.
+ await user.click(
+ screen.getByRole("radio", {
+ name: /Microsoft Entra Authentication/,
+ })
+ );
+
+ // check that API Key field is hidden when Entra mode is selected.
+ expect(
+ screen.queryByPlaceholderText("API key (stored in memory only)")
+ ).not.toBeInTheDocument();
+
+ await user.click(screen.getByText("Create Target"));
+
+ await waitFor(() => {
+ expect(mockedTargetsApi.createTarget).toHaveBeenCalledWith({
+ type: "OpenAIChatTarget",
+ params: {
+ endpoint: "https://my-resource.openai.azure.com/",
+ },
+ auth_mode: "entra",
+ });
+ expect(onCreated).toHaveBeenCalled();
+ });
+ });
+
+ it("should clear a previously-typed API key when switching to Entra", async () => {
+ const onCreated = jest.fn();
+ const user = userEvent.setup();
+ mockedTargetsApi.createTarget.mockResolvedValue({
+ target_registry_name: "openai_chat_entra",
+ target_type: "OpenAIChatTarget",
+ });
+
+ render(
+
+
+
+ );
+
+ await selectTargetType(user, "OpenAIChatTarget");
+
+ const endpointInput = screen.getByPlaceholderText(
+ "https://your-resource.openai.azure.com/"
+ );
+ fireEvent.change(endpointInput, {
+ target: { value: "https://my-resource.openai.azure.com/" },
+ });
+
+ // Type a key, then switch to Entra option.
+ fireEvent.change(
+ screen.getByPlaceholderText("API key (stored in memory only)"),
+ { target: { value: "sk-typed-before-switch" } }
+ );
+
+ await user.click(
+ screen.getByRole("radio", { name: /Microsoft Entra Authentication/ })
+ );
+
+ await user.click(screen.getByText("Create Target"));
+
+ await waitFor(() => {
+ const call = mockedTargetsApi.createTarget.mock.calls[0][0];
+ expect(call.auth_mode).toBe("entra");
+ expect(call.params).not.toHaveProperty("api_key");
+ });
+ });
+
+ it("should warn the user when Entra is selected for a non-Azure OpenAI endpoint", async () => {
+ const user = userEvent.setup();
+
+ render(
+
+
+
+ );
+
+ await selectTargetType(user, "OpenAIChatTarget");
+
+ const endpointInput = screen.getByPlaceholderText(
+ "https://your-resource.openai.azure.com/"
+ );
+ fireEvent.change(endpointInput, { target: { value: "https://api.openai.com/" } });
+
+ await user.click(
+ screen.getByRole("radio", { name: /Microsoft Entra Authentication/ })
+ );
+
+ expect(
+ screen.getByText(/Entra auth only works with Azure OpenAI/)
+ ).toBeInTheDocument();
+ });
+
+ it("should NOT warn when Entra is selected for a recognized Azure endpoint", async () => {
+ const user = userEvent.setup();
+
+ render(
+
+
+
+ );
+
+ await selectTargetType(user, "OpenAIChatTarget");
+
+ const endpointInput = screen.getByPlaceholderText(
+ "https://your-resource.openai.azure.com/"
+ );
+ fireEvent.change(endpointInput, {
+ target: { value: "https://my-resource.openai.azure.com/" },
+ });
+
+ await user.click(
+ screen.getByRole("radio", { name: /Microsoft Entra Authentication/ })
+ );
+
+ expect(
+ screen.queryByText(/Entra auth only works with Azure OpenAI/)
+ ).not.toBeInTheDocument();
+ });
+
+ it("should disable Create Target and skip API call for Entra + non-Azure OpenAI endpoint", async () => {
+ const user = userEvent.setup();
+
+ render(
+
+
+
+ );
+
+ await selectTargetType(user, "OpenAIChatTarget");
+
+ const endpointInput = screen.getByPlaceholderText(
+ "https://your-resource.openai.azure.com/"
+ );
+ fireEvent.change(endpointInput, { target: { value: "https://api.test.com/" } });
+
+ await user.click(
+ screen.getByRole("radio", { name: /Microsoft Entra Authentication/ })
+ );
+
+ const createButton = screen.getByText("Create Target").closest("button");
+ expect(createButton).toBeDisabled();
+
+ await user.click(screen.getByText("Create Target"));
+
+ expect(mockedTargetsApi.createTarget).not.toHaveBeenCalled();
+ });
});
diff --git a/frontend/src/components/Config/CreateTargetDialog.tsx b/frontend/src/components/Config/CreateTargetDialog.tsx
index 4bce08f56..94f433c17 100644
--- a/frontend/src/components/Config/CreateTargetDialog.tsx
+++ b/frontend/src/components/Config/CreateTargetDialog.tsx
@@ -9,6 +9,8 @@ import {
Button,
Input,
Label,
+ Radio,
+ RadioGroup,
Select,
Switch,
Text,
@@ -32,6 +34,26 @@ const TARGET_TYPE_CONFIG: Record = {
const SUPPORTED_TARGET_TYPES = Object.keys(TARGET_TYPE_CONFIG)
+type AuthMode = 'api_key' | 'entra'
+
+// Mirrors backend's hostname-suffix check (list in target_service.py).
+// The backend still does the check and will reject unsupported endpoints, but this allows us to show a warning in the UI if the user selects Microsoft Entra authentication with a non-Azure OpenAI endpoint.
+const AZURE_OPENAI_HOSTNAME_SUFFIXES = [
+ '.openai.azure.com',
+ '.ai.azure.com',
+ '.services.ai.azure.com',
+ '.cognitiveservices.azure.com',
+]
+
+function isAzureOpenAiEndpoint(endpoint: string): boolean {
+ try {
+ const host = new URL(endpoint).hostname.toLowerCase()
+ return AZURE_OPENAI_HOSTNAME_SUFFIXES.some((s) => host.endsWith(s))
+ } catch {
+ return false
+ }
+}
+
interface CreateTargetDialogProps {
open: boolean
onClose: () => void
@@ -45,6 +67,7 @@ export default function CreateTargetDialog({ open, onClose, onCreated }: CreateT
const [modelName, setModelName] = useState('')
const [hasDifferentUnderlying, setHasDifferentUnderlying] = useState(false)
const [underlyingModel, setUnderlyingModel] = useState('')
+ const [authMode, setAuthMode] = useState('api_key')
const [apiKey, setApiKey] = useState('')
const [maxNewTokens, setMaxNewTokens] = useState('400')
const [temperature, setTemperature] = useState('1.0')
@@ -54,7 +77,11 @@ export default function CreateTargetDialog({ open, onClose, onCreated }: CreateT
const [error, setError] = useState(null)
const [fieldErrors, setFieldErrors] = useState<{ targetType?: string; endpoint?: string }>({})
- const isAzureML = TARGET_TYPE_CONFIG[targetType] === 'azureml'
+ const targetKind = TARGET_TYPE_CONFIG[targetType]
+ const isAzureML = targetKind === 'azureml'
+ const isOpenAi = targetKind === 'openai'
+ const isEntra = authMode === 'entra'
+ const showNonAzureEntraWarning = isEntra && isOpenAi && endpoint !== '' && !isAzureOpenAiEndpoint(endpoint)
const resetForm = () => {
setTargetType('')
@@ -62,6 +89,7 @@ export default function CreateTargetDialog({ open, onClose, onCreated }: CreateT
setModelName('')
setHasDifferentUnderlying(false)
setUnderlyingModel('')
+ setAuthMode('api_key')
setApiKey('')
setMaxNewTokens('400')
setTemperature('1.0')
@@ -94,7 +122,7 @@ export default function CreateTargetDialog({ open, onClose, onCreated }: CreateT
endpoint,
}
if (modelName) params.model_name = modelName
- if (apiKey) params.api_key = apiKey
+ if (!isEntra && apiKey) params.api_key = apiKey
if (hasDifferentUnderlying && underlyingModel) params.underlying_model = underlyingModel
@@ -112,6 +140,7 @@ export default function CreateTargetDialog({ open, onClose, onCreated }: CreateT
await targetsApi.createTarget({
type: targetType,
params,
+ ...(isEntra ? { auth_mode: 'entra' as const } : {}),
})
resetForm()
onCreated()
@@ -243,15 +272,40 @@ export default function CreateTargetDialog({ open, onClose, onCreated }: CreateT
>
)}
-
- setApiKey(data.value)}
- />
+
+ {
+ const next = data.value as AuthMode
+ setAuthMode(next)
+ if (next === 'entra') setApiKey('')
+ }}
+ >
+
+
+
+ {showNonAzureEntraWarning && (
+
+
+ Error: Entra auth only works with Azure OpenAI / AI Foundry endpoints (for example,
+ *.openai.azure.com or *.ai.azure.com).
+
+
+ )}
+
+ {!isEntra && (
+
+ setApiKey(data.value)}
+ />
+
+ )}
+