Unverified 提交 29d8421c authored 作者: Md Rakibul Islam Rocky's avatar Md Rakibul Islam Rocky 提交者: GitHub

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
上级 39266416
...@@ -101,3 +101,4 @@ out/ ...@@ -101,3 +101,4 @@ out/
sqlite.db sqlite.db
userData/ userData/
.env.local .env.local
.idea/
\ No newline at end of file
...@@ -17,27 +17,45 @@ testWithPo("Azure provider settings UI", async ({ po }) => { ...@@ -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(
await expect(po.page.getByText("AZURE_API_KEY")).toBeVisible(); po.page.getByText("Azure OpenAI Configuration Required"),
await expect(po.page.getByText("AZURE_RESOURCE_NAME")).toBeVisible(); ).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();
// Check environment variable status indicators exist // Environment variable helper section should still be available
await expect( await expect(
po.page.getByText("Environment Variables Configuration"), po.page.getByText("Environment Variables (optional)"),
).toBeVisible(); ).toBeVisible();
// Check setup instructions are present // FIX: disambiguate text matches to avoid strict mode violation
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("AZURE_API_KEY", { exact: true }),
).toBeVisible(); ).toBeVisible();
await expect(po.page.getByText("Find your resource name")).toBeVisible();
await expect( await expect(
po.page.getByText("Set these environment variables before starting Dyad"), po.page.getByText("AZURE_RESOURCE_NAME", { exact: true }),
).toBeVisible(); ).toBeVisible();
// Check that status indicators show "Not Set" (since no env vars are configured in test) // Since no env vars are configured in the test run, both should read "Not Set"
const statusElements = po.page.locator(".bg-red-100, .bg-red-800\\/20"); await expect(
await expect(statusElements.first()).toBeVisible(); 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 { ...@@ -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({ ...@@ -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") {
......
...@@ -11,7 +11,11 @@ import {} from "@/components/ui/accordion"; ...@@ -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) { ...@@ -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) { ...@@ -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"; ...@@ -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() { ...@@ -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"; ...@@ -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 { ...@@ -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( ...@@ -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({ ...@@ -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({ ...@@ -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>; ...@@ -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"]);
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论