Enhance Azure configuration handling and UI updates (#1289)
# Changes
- Update Azure configuration components to manage API key and resource
name settings.
- Improve visibility of conThis pull request introduces a redesigned
Azure provider settings UI and refactors the logic for configuring Azure
OpenAI credentials, both in the frontend and supporting hooks. The main
changes focus on making the Azure configuration experience clearer and
more robust by supporting both environment variable and saved settings,
improving status indicators, and updating the logic for determining
provider readiness.
**Azure Provider UI and Logic Improvements**
* Added a new `AzureConfiguration` component that provides a dedicated
form for entering and saving Azure resource name and API key, with clear
status indicators and error handling. The UI now explains precedence
between saved settings and environment variables, and guides users
through configuration.
(`src/components/settings/AzureConfiguration.tsx`)
* Updated the main provider settings page and API key configuration
logic to pass Azure-specific settings and update functions to the new
component, ensuring seamless integration and correct state management.
(`src/components/settings/ApiKeyConfiguration.tsx`,
`src/components/settings/ProviderSettingsPage.tsx`)
[[1]](diffhunk://#diff-2104fb487cda3768cc5777889100e882f51e7fb3e13abe3cc89cf8ed1444300aR35)
[[2]](diffhunk://#diff-2104fb487cda3768cc5777889100e882f51e7fb3e13abe3cc89cf8ed1444300aR51-R61)
[[3]](diffhunk://#diff-9140e707ebb56ffed3272b4661ea1e6d8388ee604a8535c58e8a1564d280057cR297)
* Refactored the logic for determining whether Azure is configured to
check both saved settings and environment variables, ensuring accurate
status display and enabling fallback to environment variables if no
settings are saved. (`src/components/settings/ProviderSettingsPage.tsx`,
`src/hooks/useLanguageModelProviders.ts`)
[[1]](diffhunk://#diff-9140e707ebb56ffed3272b4661ea1e6d8388ee604a8535c58e8a1564d280057cL72-R99)
[[2]](diffhunk://#diff-9ac9e279a0cda34a0bc519348d5474b2e355b0828a678495be3af1e8984b5be5R35-R48)
* Updated the Azure provider E2E test to verify the new UI elements,
status indicators, and guidance, ensuring the test matches the new
configuration flow and messaging.
(`e2e-tests/azure_provider_settings.spec.ts`)
**Supporting Type and Import Updates**
* Added and updated type imports for `AzureProviderSetting` and
`VertexProviderSetting` where needed to support the new logic and UI.
(`src/components/settings/ProviderSettingsPage.tsx`,
`src/hooks/useLanguageModelProviders.ts`,
`src/ipc/utils/get_model_client.ts`)
[[1]](diffhunk://#diff-9140e707ebb56ffed3272b4661ea1e6d8388ee604a8535c58e8a1564d280057cL14-R14)
[[2]](diffhunk://#diff-9ac9e279a0cda34a0bc519348d5474b2e355b0828a678495be3af1e8984b5be5L5-R5)
[[3]](diffhunk://#diff-3cd526c6c10413c1387bfef450e48b880ba6f54865e96046044586ff4192bcceR15)
* Changed Azure model client import to use `createAzure` for consistency
and future extensibility. (`src/ipc/utils/get_model_client.ts`)
[Copilot is generating a summary...]figuration status and error handling
in the UI.
- Refactor environment variable checks to prioritize saved settings.
- Add support for Azure provider settings in the schema.
- Modify tests to reflect changes in Azure configuration requirements.
# Changes in short
- **Azure settings panel**
- Replaced with a full form that:
- Persists API key and resource name
- Surfaces save state
- Keeps the environment-variable helper
- *(src/components/settings/AzureConfiguration.tsx:23-214)*
- **Settings stack workflow**
- Threaded the new Azure workflow:
- Config shim now passes `updateSettings`
- Provider status checks prefer saved Azure values before env vars
- *(src/components/settings/ApiKeyConfiguration.tsx:40-55,
src/components/settings/ProviderSettingsPage.tsx:60-105)*
- **Provider detection**
- Azure treated like other saved credentials by:
- Looking for both stored fields, or
- The pair of env vars
- *(src/hooks/useLanguageModelProviders.ts:5-57)*
- **Back-end model creation**
- Reads saved Azure credentials (falling back to env vars)
- Builds the client via `createAzure`
- *(src/ipc/utils/get_model_client.ts:316-369)*
- **Provider schema support**
- Extended so Azure can store its resource name alongside the secret
- *(src/lib/schemas.ts:82-109)*
- **E2E tests**
- Updated Azure Playwright spec to cover the new UI
- *(e2e-tests/azure_provider_settings.spec.ts:4-50)*
Issues resolved: #1275
This commit is contained in:
committed by
GitHub
parent
39266416c7
commit
29d8421ce9
3
.gitignore
vendored
3
.gitignore
vendored
@@ -100,4 +100,5 @@ out/
|
|||||||
|
|
||||||
sqlite.db
|
sqlite.db
|
||||||
userData/
|
userData/
|
||||||
.env.local
|
.env.local
|
||||||
|
.idea/
|
||||||
@@ -17,27 +17,45 @@ testWithPo("Azure provider settings UI", async ({ po }) => {
|
|||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check that Azure-specific UI is displayed
|
// Confirm the new configuration form is rendered
|
||||||
await expect(po.page.getByText("Azure OpenAI Configuration")).toBeVisible();
|
|
||||||
await expect(po.page.getByText("AZURE_API_KEY")).toBeVisible();
|
|
||||||
await expect(po.page.getByText("AZURE_RESOURCE_NAME")).toBeVisible();
|
|
||||||
|
|
||||||
// Check environment variable status indicators exist
|
|
||||||
await expect(
|
await expect(
|
||||||
po.page.getByText("Environment Variables Configuration"),
|
po.page.getByText("Azure OpenAI Configuration Required"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(po.page.getByLabel("Resource Name")).toBeVisible();
|
||||||
|
await expect(po.page.getByLabel("API Key")).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
po.page.getByRole("button", { name: "Save Settings" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// Check setup instructions are present
|
// Environment variable helper section should still be available
|
||||||
await expect(po.page.getByText("How to configure:")).toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
po.page.getByText("Get your API key from the Azure portal"),
|
po.page.getByText("Environment Variables (optional)"),
|
||||||
).toBeVisible();
|
|
||||||
await expect(po.page.getByText("Find your resource name")).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
po.page.getByText("Set these environment variables before starting Dyad"),
|
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// Check that status indicators show "Not Set" (since no env vars are configured in test)
|
// FIX: disambiguate text matches to avoid strict mode violation
|
||||||
const statusElements = po.page.locator(".bg-red-100, .bg-red-800\\/20");
|
await expect(
|
||||||
await expect(statusElements.first()).toBeVisible();
|
po.page.getByText("AZURE_API_KEY", { exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
po.page.getByText("AZURE_RESOURCE_NAME", { exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Since no env vars are configured in the test run, both should read "Not Set"
|
||||||
|
await expect(
|
||||||
|
po.page
|
||||||
|
.getByTestId("azure-api-key-status")
|
||||||
|
.getByText("Not Set", { exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
po.page
|
||||||
|
.getByTestId("azure-resource-name-status")
|
||||||
|
.getByText("Not Set", { exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// The guidance text should explain precedence between saved settings and environment variables
|
||||||
|
await expect(
|
||||||
|
po.page.getByText(
|
||||||
|
"Values saved in Settings take precedence over environment variables.",
|
||||||
|
),
|
||||||
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ interface ApiKeyConfigurationProps {
|
|||||||
onSaveKey: () => Promise<void>;
|
onSaveKey: () => Promise<void>;
|
||||||
onDeleteKey: () => Promise<void>;
|
onDeleteKey: () => Promise<void>;
|
||||||
isDyad: boolean;
|
isDyad: boolean;
|
||||||
|
updateSettings: (settings: Partial<UserSettings>) => Promise<UserSettings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ApiKeyConfiguration({
|
export function ApiKeyConfiguration({
|
||||||
@@ -47,10 +48,17 @@ export function ApiKeyConfiguration({
|
|||||||
onSaveKey,
|
onSaveKey,
|
||||||
onDeleteKey,
|
onDeleteKey,
|
||||||
isDyad,
|
isDyad,
|
||||||
|
updateSettings,
|
||||||
}: ApiKeyConfigurationProps) {
|
}: ApiKeyConfigurationProps) {
|
||||||
// Special handling for Azure OpenAI which requires environment variables
|
// Special handling for Azure OpenAI which requires environment variables
|
||||||
if (provider === "azure") {
|
if (provider === "azure") {
|
||||||
return <AzureConfiguration envVars={envVars} />;
|
return (
|
||||||
|
<AzureConfiguration
|
||||||
|
settings={settings}
|
||||||
|
envVars={envVars}
|
||||||
|
updateSettings={updateSettings}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Special handling for Google Vertex AI which uses service account credentials
|
// Special handling for Google Vertex AI which uses service account credentials
|
||||||
if (provider === "vertex") {
|
if (provider === "vertex") {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
@@ -5,111 +8,264 @@ import {
|
|||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { Info, KeyRound } from "lucide-react";
|
import { CheckCircle2, Info, KeyRound } from "lucide-react";
|
||||||
|
import type { AzureProviderSetting, UserSettings } from "@/lib/schemas";
|
||||||
|
|
||||||
interface AzureConfigurationProps {
|
interface AzureConfigurationProps {
|
||||||
|
settings: UserSettings | null | undefined;
|
||||||
envVars: Record<string, string | undefined>;
|
envVars: Record<string, string | undefined>;
|
||||||
|
updateSettings: (settings: Partial<UserSettings>) => Promise<UserSettings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AzureConfiguration({ envVars }: AzureConfigurationProps) {
|
const AZURE_API_KEY_VAR = "AZURE_API_KEY";
|
||||||
const azureApiKey = envVars["AZURE_API_KEY"];
|
const AZURE_RESOURCE_NAME_VAR = "AZURE_RESOURCE_NAME";
|
||||||
const azureResourceName = envVars["AZURE_RESOURCE_NAME"];
|
|
||||||
|
|
||||||
const isAzureConfigured = !!(azureApiKey && azureResourceName);
|
export function AzureConfiguration({
|
||||||
|
settings,
|
||||||
|
envVars,
|
||||||
|
updateSettings,
|
||||||
|
}: AzureConfigurationProps) {
|
||||||
|
const existing =
|
||||||
|
(settings?.providerSettings?.azure as AzureProviderSetting | undefined) ??
|
||||||
|
{};
|
||||||
|
const existingApiKey = existing.apiKey?.value ?? "";
|
||||||
|
const existingResourceName = existing.resourceName ?? "";
|
||||||
|
|
||||||
|
const [apiKey, setApiKey] = useState(existingApiKey);
|
||||||
|
const [resourceName, setResourceName] = useState(existingResourceName);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setApiKey(existingApiKey);
|
||||||
|
setResourceName(existingResourceName);
|
||||||
|
}, [existingApiKey, existingResourceName]);
|
||||||
|
|
||||||
|
const envApiKey = envVars[AZURE_API_KEY_VAR];
|
||||||
|
const envResourceName = envVars[AZURE_RESOURCE_NAME_VAR];
|
||||||
|
|
||||||
|
const hasSavedSettings = Boolean(existingApiKey && existingResourceName);
|
||||||
|
const hasEnvConfiguration = Boolean(envApiKey && envResourceName);
|
||||||
|
const isConfigured = hasSavedSettings || hasEnvConfiguration;
|
||||||
|
const usingEnvironmentOnly = hasEnvConfiguration && !hasSavedSettings;
|
||||||
|
|
||||||
|
const hasUnsavedChanges = useMemo(() => {
|
||||||
|
return apiKey !== existingApiKey || resourceName !== existingResourceName;
|
||||||
|
}, [apiKey, existingApiKey, resourceName, existingResourceName]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
setSaved(false);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const trimmedApiKey = apiKey.trim();
|
||||||
|
const trimmedResourceName = resourceName.trim();
|
||||||
|
|
||||||
|
const azureSettings: AzureProviderSetting = {
|
||||||
|
...existing,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (trimmedResourceName) {
|
||||||
|
azureSettings.resourceName = trimmedResourceName;
|
||||||
|
} else {
|
||||||
|
delete azureSettings.resourceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmedApiKey) {
|
||||||
|
azureSettings.apiKey = { value: trimmedApiKey };
|
||||||
|
} else {
|
||||||
|
delete azureSettings.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerSettings = {
|
||||||
|
...settings?.providerSettings,
|
||||||
|
azure: azureSettings,
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateSettings({
|
||||||
|
providerSettings,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSaved(true);
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e?.message || "Failed to save Azure settings");
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const status = useMemo(() => {
|
||||||
|
if (hasSavedSettings) {
|
||||||
|
return {
|
||||||
|
variant: "default" as const,
|
||||||
|
title: "Azure OpenAI Configured",
|
||||||
|
description:
|
||||||
|
"Dyad will use the credentials saved in Settings for Azure OpenAI models.",
|
||||||
|
icon: KeyRound,
|
||||||
|
titleClassName: "",
|
||||||
|
descriptionClassName: "",
|
||||||
|
alertClassName: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (usingEnvironmentOnly) {
|
||||||
|
return {
|
||||||
|
variant: "default" as const,
|
||||||
|
title: "Using Environment Variables",
|
||||||
|
description:
|
||||||
|
"AZURE_API_KEY and AZURE_RESOURCE_NAME are set. Values saved below will override them.",
|
||||||
|
icon: Info,
|
||||||
|
titleClassName: "",
|
||||||
|
descriptionClassName: "",
|
||||||
|
alertClassName: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
variant: "destructive" as const,
|
||||||
|
title: "Azure OpenAI Configuration Required",
|
||||||
|
description:
|
||||||
|
"Provide your Azure resource name and API key below, or configure the AZURE_API_KEY and AZURE_RESOURCE_NAME environment variables.",
|
||||||
|
icon: Info,
|
||||||
|
titleClassName: "text-red-800 dark:text-red-400",
|
||||||
|
descriptionClassName: "text-red-800 dark:text-red-400",
|
||||||
|
alertClassName:
|
||||||
|
"border-red-200 bg-red-100 dark:border-red-800/50 dark:bg-red-800/20",
|
||||||
|
};
|
||||||
|
}, [hasSavedSettings, usingEnvironmentOnly]);
|
||||||
|
|
||||||
|
const StatusIcon = status.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Alert
|
<Alert variant={status.variant} className={status.alertClassName}>
|
||||||
variant={isAzureConfigured ? "default" : "destructive"}
|
<StatusIcon className="h-4 w-4" />
|
||||||
className={
|
<AlertTitle className={status.titleClassName}>
|
||||||
isAzureConfigured
|
{status.title}
|
||||||
? ""
|
|
||||||
: "border-red-200 bg-red-100 dark:border-red-800/50 dark:bg-red-800/20"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Info
|
|
||||||
className={`h-4 w-4 ${isAzureConfigured ? "" : "text-red-800 dark:text-red-400"}`}
|
|
||||||
/>
|
|
||||||
<AlertTitle
|
|
||||||
className={isAzureConfigured ? "" : "text-red-800 dark:text-red-400"}
|
|
||||||
>
|
|
||||||
Azure OpenAI Configuration
|
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<AlertDescription
|
<AlertDescription className={status.descriptionClassName}>
|
||||||
className={isAzureConfigured ? "" : "text-red-800 dark:text-red-400"}
|
{status.description}
|
||||||
>
|
|
||||||
Azure OpenAI requires both an API key and resource name to be
|
|
||||||
configured via environment variables.
|
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="azure-resource-name"
|
||||||
|
className="block text-sm font-medium mb-1"
|
||||||
|
>
|
||||||
|
Resource Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="azure-resource-name"
|
||||||
|
value={resourceName}
|
||||||
|
onChange={(e) => {
|
||||||
|
setResourceName(e.target.value);
|
||||||
|
setSaved(false);
|
||||||
|
setError(null);
|
||||||
|
}}
|
||||||
|
placeholder="your-azure-openai-resource"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="azure-api-key"
|
||||||
|
className="block text-sm font-medium mb-1"
|
||||||
|
>
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="azure-api-key"
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => {
|
||||||
|
setApiKey(e.target.value);
|
||||||
|
setSaved(false);
|
||||||
|
setError(null);
|
||||||
|
}}
|
||||||
|
placeholder="Enter your Azure OpenAI API key"
|
||||||
|
autoComplete="off"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button onClick={handleSave} disabled={saving || !hasUnsavedChanges}>
|
||||||
|
{saving ? "Saving..." : "Save Settings"}
|
||||||
|
</Button>
|
||||||
|
{saved && !error && (
|
||||||
|
<span className="flex items-center text-green-600 text-sm">
|
||||||
|
<CheckCircle2 className="h-4 w-4 mr-1" /> Saved
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isConfigured && !error && (
|
||||||
|
<Alert variant="default">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertTitle>Configuration Needed</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Azure OpenAI requests require both a resource name and API key.
|
||||||
|
Enter them above or supply the environment variables instead.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTitle>Save Error</AlertTitle>
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<Accordion
|
<Accordion
|
||||||
type="multiple"
|
type="single"
|
||||||
|
collapsible
|
||||||
|
defaultValue="azure-env"
|
||||||
className="w-full space-y-4"
|
className="w-full space-y-4"
|
||||||
defaultValue={["azure-config"]}
|
|
||||||
>
|
>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="azure-config"
|
value="azure-env"
|
||||||
className="border rounded-lg px-4 bg-background"
|
className="border rounded-lg px-4 bg-background"
|
||||||
>
|
>
|
||||||
<AccordionTrigger className="text-lg font-medium hover:no-underline cursor-pointer">
|
<AccordionTrigger className="text-lg font-medium hover:no-underline cursor-pointer">
|
||||||
Environment Variables Configuration
|
Environment Variables (optional)
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="pt-4">
|
<AccordionContent className="pt-4 space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-3 text-sm">
|
||||||
<div>
|
<div className="flex justify-between items-center p-3 bg-muted rounded border">
|
||||||
<h4 className="font-medium mb-2">
|
<code className="font-mono text-foreground">
|
||||||
Required Environment Variables:
|
{AZURE_API_KEY_VAR}
|
||||||
</h4>
|
</code>
|
||||||
<div className="space-y-3 text-sm">
|
<span
|
||||||
<div className="flex justify-between items-center p-3 bg-muted rounded border">
|
data-testid="azure-api-key-status"
|
||||||
<code className="font-mono text-foreground">
|
className={`px-2 py-1 rounded text-xs font-medium ${envApiKey ? "bg-green-100 text-green-800 dark:bg-green-800/20 dark:text-green-400" : "bg-red-100 text-red-800 dark:bg-red-800/20 dark:text-red-400"}`}
|
||||||
AZURE_API_KEY
|
>
|
||||||
</code>
|
{envApiKey ? "Set" : "Not Set"}
|
||||||
<span
|
</span>
|
||||||
className={`px-2 py-1 rounded text-xs font-medium ${azureApiKey ? "bg-green-100 text-green-800 dark:bg-green-800/20 dark:text-green-400" : "bg-red-100 text-red-800 dark:bg-red-800/20 dark:text-red-400"}`}
|
|
||||||
>
|
|
||||||
{azureApiKey ? "Set" : "Not Set"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center p-3 bg-muted rounded border">
|
|
||||||
<code className="font-mono text-foreground">
|
|
||||||
AZURE_RESOURCE_NAME
|
|
||||||
</code>
|
|
||||||
<span
|
|
||||||
className={`px-2 py-1 rounded text-xs font-medium ${azureResourceName ? "bg-green-100 text-green-800 dark:bg-green-800/20 dark:text-green-400" : "bg-red-100 text-red-800 dark:bg-red-800/20 dark:text-red-400"}`}
|
|
||||||
>
|
|
||||||
{azureResourceName ? "Set" : "Not Set"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between items-center p-3 bg-muted rounded border">
|
||||||
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-950/30 rounded border border-blue-200 dark:border-blue-700">
|
<code className="font-mono text-foreground">
|
||||||
<h5 className="font-medium mb-2 text-blue-900 dark:text-blue-200">
|
{AZURE_RESOURCE_NAME_VAR}
|
||||||
How to configure:
|
</code>
|
||||||
</h5>
|
<span
|
||||||
<ol className="list-decimal list-inside space-y-1 text-sm text-blue-800 dark:text-blue-300">
|
data-testid="azure-resource-name-status"
|
||||||
<li>Get your API key from the Azure portal</li>
|
className={`px-2 py-1 rounded text-xs font-medium ${envResourceName ? "bg-green-100 text-green-800 dark:bg-green-800/20 dark:text-green-400" : "bg-red-100 text-red-800 dark:bg-red-800/20 dark:text-red-400"}`}
|
||||||
<li>
|
>
|
||||||
Find your resource name (the name you gave your Azure OpenAI
|
{envResourceName ? "Set" : "Not Set"}
|
||||||
resource)
|
</span>
|
||||||
</li>
|
|
||||||
<li>Set these environment variables before starting Dyad</li>
|
|
||||||
<li>Restart Dyad after setting the environment variables</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{isAzureConfigured && (
|
<div className="text-sm text-muted-foreground space-y-2">
|
||||||
<Alert>
|
<p>
|
||||||
<KeyRound className="h-4 w-4" />
|
You can continue to configure Azure via environment variables.
|
||||||
<AlertTitle>Azure OpenAI Configured</AlertTitle>
|
If both variables are present and no settings are saved, Dyad
|
||||||
<AlertDescription>
|
will use them automatically.
|
||||||
Both required environment variables are set. You can now use
|
</p>
|
||||||
Azure OpenAI models.
|
<p>
|
||||||
</AlertDescription>
|
Values saved in Settings take precedence over environment
|
||||||
</Alert>
|
variables. Restart Dyad after changing environment variables.
|
||||||
)}
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import {} from "@/components/ui/accordion";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { showError } from "@/lib/toast";
|
import { showError } from "@/lib/toast";
|
||||||
import { UserSettings } from "@/lib/schemas";
|
import {
|
||||||
|
UserSettings,
|
||||||
|
AzureProviderSetting,
|
||||||
|
VertexProviderSetting,
|
||||||
|
} from "@/lib/schemas";
|
||||||
|
|
||||||
import { ProviderSettingsHeader } from "./ProviderSettingsHeader";
|
import { ProviderSettingsHeader } from "./ProviderSettingsHeader";
|
||||||
import { ApiKeyConfiguration } from "./ApiKeyConfiguration";
|
import { ApiKeyConfiguration } from "./ApiKeyConfiguration";
|
||||||
@@ -69,20 +73,34 @@ export function ProviderSettingsPage({ provider }: ProviderSettingsPageProps) {
|
|||||||
userApiKey !== "Not Set";
|
userApiKey !== "Not Set";
|
||||||
const hasEnvKey = !!(envVarName && envVars[envVarName]);
|
const hasEnvKey = !!(envVarName && envVars[envVarName]);
|
||||||
|
|
||||||
// Special handling for Azure OpenAI configuration
|
const azureSettings = settings?.providerSettings?.azure as
|
||||||
const isAzureConfigured =
|
| AzureProviderSetting
|
||||||
provider === "azure"
|
| undefined;
|
||||||
? !!(envVars["AZURE_API_KEY"] && envVars["AZURE_RESOURCE_NAME"])
|
const azureApiKeyFromSettings = (azureSettings?.apiKey?.value ?? "").trim();
|
||||||
: false;
|
const azureResourceNameFromSettings = (
|
||||||
|
azureSettings?.resourceName ?? ""
|
||||||
|
).trim();
|
||||||
|
const azureHasSavedSettings = Boolean(
|
||||||
|
azureApiKeyFromSettings && azureResourceNameFromSettings,
|
||||||
|
);
|
||||||
|
const azureHasEnvConfiguration = Boolean(
|
||||||
|
envVars["AZURE_API_KEY"] && envVars["AZURE_RESOURCE_NAME"],
|
||||||
|
);
|
||||||
|
|
||||||
// Special handling for Vertex configuration status
|
const vertexSettings = settings?.providerSettings?.vertex as
|
||||||
const vertexSettings = settings?.providerSettings?.vertex as any;
|
| VertexProviderSetting
|
||||||
|
| undefined;
|
||||||
const isVertexConfigured = Boolean(
|
const isVertexConfigured = Boolean(
|
||||||
vertexSettings?.projectId &&
|
vertexSettings?.projectId &&
|
||||||
vertexSettings?.location &&
|
vertexSettings?.location &&
|
||||||
vertexSettings?.serviceAccountKey?.value,
|
vertexSettings?.serviceAccountKey?.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isAzureConfigured =
|
||||||
|
provider === "azure"
|
||||||
|
? azureHasSavedSettings || azureHasEnvConfiguration
|
||||||
|
: false;
|
||||||
|
|
||||||
const isConfigured =
|
const isConfigured =
|
||||||
provider === "azure"
|
provider === "azure"
|
||||||
? isAzureConfigured
|
? isAzureConfigured
|
||||||
@@ -280,6 +298,7 @@ export function ProviderSettingsPage({ provider }: ProviderSettingsPageProps) {
|
|||||||
onSaveKey={handleSaveKey}
|
onSaveKey={handleSaveKey}
|
||||||
onDeleteKey={handleDeleteKey}
|
onDeleteKey={handleDeleteKey}
|
||||||
isDyad={isDyad}
|
isDyad={isDyad}
|
||||||
|
updateSettings={updateSettings}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
import type { LanguageModelProvider } from "@/ipc/ipc_types";
|
import type { LanguageModelProvider } from "@/ipc/ipc_types";
|
||||||
import { useSettings } from "./useSettings";
|
import { useSettings } from "./useSettings";
|
||||||
import { cloudProviders, VertexProviderSetting } from "@/lib/schemas";
|
import {
|
||||||
|
cloudProviders,
|
||||||
|
VertexProviderSetting,
|
||||||
|
AzureProviderSetting,
|
||||||
|
} from "@/lib/schemas";
|
||||||
|
|
||||||
export function useLanguageModelProviders() {
|
export function useLanguageModelProviders() {
|
||||||
const ipcClient = IpcClient.getInstance();
|
const ipcClient = IpcClient.getInstance();
|
||||||
@@ -32,6 +36,20 @@ export function useLanguageModelProviders() {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (provider === "azure") {
|
||||||
|
const azureSettings = providerSettings as AzureProviderSetting;
|
||||||
|
const hasSavedSettings = Boolean(
|
||||||
|
(azureSettings?.apiKey?.value ?? "").trim() &&
|
||||||
|
(azureSettings?.resourceName ?? "").trim(),
|
||||||
|
);
|
||||||
|
if (hasSavedSettings) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (envVars["AZURE_API_KEY"] && envVars["AZURE_RESOURCE_NAME"]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (providerSettings?.apiKey?.value) {
|
if (providerSettings?.apiKey?.value) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createGoogleGenerativeAI as createGoogle } from "@ai-sdk/google";
|
|||||||
import { createAnthropic } from "@ai-sdk/anthropic";
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
||||||
import { createXai } from "@ai-sdk/xai";
|
import { createXai } from "@ai-sdk/xai";
|
||||||
import { createVertex as createGoogleVertex } from "@ai-sdk/google-vertex";
|
import { createVertex as createGoogleVertex } from "@ai-sdk/google-vertex";
|
||||||
import { azure } from "@ai-sdk/azure";
|
import { createAzure } from "@ai-sdk/azure";
|
||||||
import { LanguageModelV2 } from "@ai-sdk/provider";
|
import { LanguageModelV2 } from "@ai-sdk/provider";
|
||||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
||||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
LargeLanguageModel,
|
LargeLanguageModel,
|
||||||
UserSettings,
|
UserSettings,
|
||||||
VertexProviderSetting,
|
VertexProviderSetting,
|
||||||
|
AzureProviderSetting,
|
||||||
} from "../../lib/schemas";
|
} from "../../lib/schemas";
|
||||||
import { getEnvVar } from "./read_env";
|
import { getEnvVar } from "./read_env";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
@@ -335,28 +336,41 @@ function getRegularModelClient(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Azure OpenAI requires both API key and resource name as env vars
|
const azureSettings = settings.providerSettings?.azure as
|
||||||
// We use environment variables for Azure configuration
|
| AzureProviderSetting
|
||||||
const resourceName = getEnvVar("AZURE_RESOURCE_NAME");
|
| undefined;
|
||||||
const azureApiKey = getEnvVar("AZURE_API_KEY");
|
const azureApiKeyFromSettings = (
|
||||||
|
azureSettings?.apiKey?.value ?? ""
|
||||||
|
).trim();
|
||||||
|
const azureResourceNameFromSettings = (
|
||||||
|
azureSettings?.resourceName ?? ""
|
||||||
|
).trim();
|
||||||
|
const envResourceName = (getEnvVar("AZURE_RESOURCE_NAME") ?? "").trim();
|
||||||
|
const envAzureApiKey = (getEnvVar("AZURE_API_KEY") ?? "").trim();
|
||||||
|
|
||||||
|
const resourceName = azureResourceNameFromSettings || envResourceName;
|
||||||
|
const azureApiKey = azureApiKeyFromSettings || envAzureApiKey;
|
||||||
|
|
||||||
if (!resourceName) {
|
if (!resourceName) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Azure OpenAI resource name is required. Please set the AZURE_RESOURCE_NAME environment variable.",
|
"Azure OpenAI resource name is required. Provide it in Settings or set the AZURE_RESOURCE_NAME environment variable.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!azureApiKey) {
|
if (!azureApiKey) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Azure OpenAI API key is required. Please set the AZURE_API_KEY environment variable.",
|
"Azure OpenAI API key is required. Provide it in Settings or set the AZURE_API_KEY environment variable.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the default Azure provider with environment variables
|
const provider = createAzure({
|
||||||
// The azure provider automatically picks up AZURE_RESOURCE_NAME and AZURE_API_KEY
|
resourceName,
|
||||||
|
apiKey: azureApiKey,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
modelClient: {
|
modelClient: {
|
||||||
model: azure(model.name),
|
model: provider(model.name),
|
||||||
builtinProviderId: providerId,
|
builtinProviderId: providerId,
|
||||||
},
|
},
|
||||||
backupModelClients: [],
|
backupModelClients: [],
|
||||||
|
|||||||
@@ -98,6 +98,11 @@ export const RegularProviderSettingSchema = z.object({
|
|||||||
apiKey: SecretSchema.optional(),
|
apiKey: SecretSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AzureProviderSettingSchema = z.object({
|
||||||
|
apiKey: SecretSchema.optional(),
|
||||||
|
resourceName: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const VertexProviderSettingSchema = z.object({
|
export const VertexProviderSettingSchema = z.object({
|
||||||
// We make this undefined so that it makes existing callsites easier.
|
// We make this undefined so that it makes existing callsites easier.
|
||||||
apiKey: z.undefined(),
|
apiKey: z.undefined(),
|
||||||
@@ -109,6 +114,7 @@ export const VertexProviderSettingSchema = z.object({
|
|||||||
export const ProviderSettingSchema = z.union([
|
export const ProviderSettingSchema = z.union([
|
||||||
// Must use more specific type first!
|
// Must use more specific type first!
|
||||||
// Zod uses the first type that matches.
|
// Zod uses the first type that matches.
|
||||||
|
AzureProviderSettingSchema,
|
||||||
VertexProviderSettingSchema,
|
VertexProviderSettingSchema,
|
||||||
RegularProviderSettingSchema,
|
RegularProviderSettingSchema,
|
||||||
]);
|
]);
|
||||||
@@ -120,6 +126,7 @@ export type ProviderSetting = z.infer<typeof ProviderSettingSchema>;
|
|||||||
export type RegularProviderSetting = z.infer<
|
export type RegularProviderSetting = z.infer<
|
||||||
typeof RegularProviderSettingSchema
|
typeof RegularProviderSettingSchema
|
||||||
>;
|
>;
|
||||||
|
export type AzureProviderSetting = z.infer<typeof AzureProviderSettingSchema>;
|
||||||
export type VertexProviderSetting = z.infer<typeof VertexProviderSettingSchema>;
|
export type VertexProviderSetting = z.infer<typeof VertexProviderSettingSchema>;
|
||||||
|
|
||||||
export const RuntimeModeSchema = z.enum(["web-sandbox", "local-node", "unset"]);
|
export const RuntimeModeSchema = z.enum(["web-sandbox", "local-node", "unset"]);
|
||||||
|
|||||||
Reference in New Issue
Block a user