Add Azure OpenAI Custom Model Integration (#1001)
Fixes #710 This PR implements comprehensive Azure OpenAI integration for Dyad, enabling users to leverage Azure OpenAI models through proper environment variable configuration. The implementation adds Azure as a supported provider with full integration into the existing language model architecture, including support for GPT-5 models. Key features include environment-based configuration using `AZURE_API_KEY` and `AZURE_RESOURCE_NAME`, specialized UI components that provide clear setup instructions and status indicators, and seamless integration with Dyad's existing provider system. The Azure provider leverages the @ai-sdk/azure package (v1.3.25) for compatibility with the current TypeScript language model interfaces. The implementation includes robust error handling for missing configuration, comprehensive test coverage with 9 new unit tests covering critical functionality like model client creation and error scenarios, and an E2E test for the Azure-specific settings UI. <img width="1510" height="908" alt="Screenshot 2025-08-18 at 9 14 32 PM" src="https://github.com/user-attachments/assets/04aa99e1-1590-4bb0-86c9-a67b97bc7500" /> --------- Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> Co-authored-by: Will Chen <willchen90@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { AzureConfiguration } from "./AzureConfiguration";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { UserSettings } from "@/lib/schemas";
|
||||
@@ -46,6 +47,11 @@ export function ApiKeyConfiguration({
|
||||
onDeleteKey,
|
||||
isDyad,
|
||||
}: ApiKeyConfigurationProps) {
|
||||
// Special handling for Azure OpenAI which requires environment variables
|
||||
if (provider === "azure") {
|
||||
return <AzureConfiguration envVars={envVars} />;
|
||||
}
|
||||
|
||||
const envApiKey = envVarName ? envVars[envVarName] : undefined;
|
||||
const userApiKey = settings?.providerSettings?.[provider]?.apiKey?.value;
|
||||
|
||||
|
||||
119
src/components/settings/AzureConfiguration.tsx
Normal file
119
src/components/settings/AzureConfiguration.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Info, KeyRound } from "lucide-react";
|
||||
|
||||
interface AzureConfigurationProps {
|
||||
envVars: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
export function AzureConfiguration({ envVars }: AzureConfigurationProps) {
|
||||
const azureApiKey = envVars["AZURE_API_KEY"];
|
||||
const azureResourceName = envVars["AZURE_RESOURCE_NAME"];
|
||||
|
||||
const isAzureConfigured = !!(azureApiKey && azureResourceName);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Alert
|
||||
variant={isAzureConfigured ? "default" : "destructive"}
|
||||
className={
|
||||
isAzureConfigured
|
||||
? ""
|
||||
: "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>
|
||||
<AlertDescription
|
||||
className={isAzureConfigured ? "" : "text-red-800 dark:text-red-400"}
|
||||
>
|
||||
Azure OpenAI requires both an API key and resource name to be
|
||||
configured via environment variables.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="w-full space-y-4"
|
||||
defaultValue={["azure-config"]}
|
||||
>
|
||||
<AccordionItem
|
||||
value="azure-config"
|
||||
className="border rounded-lg px-4 bg-background"
|
||||
>
|
||||
<AccordionTrigger className="text-lg font-medium hover:no-underline cursor-pointer">
|
||||
Environment Variables Configuration
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pt-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">
|
||||
Required Environment Variables:
|
||||
</h4>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between items-center p-3 bg-muted rounded border">
|
||||
<code className="font-mono text-foreground">
|
||||
AZURE_API_KEY
|
||||
</code>
|
||||
<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 className="mt-4 p-4 bg-blue-50 dark:bg-blue-950/30 rounded border border-blue-200 dark:border-blue-700">
|
||||
<h5 className="font-medium mb-2 text-blue-900 dark:text-blue-200">
|
||||
How to configure:
|
||||
</h5>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm text-blue-800 dark:text-blue-300">
|
||||
<li>Get your API key from the Azure portal</li>
|
||||
<li>
|
||||
Find your resource name (the name you gave your Azure OpenAI
|
||||
resource)
|
||||
</li>
|
||||
<li>Set these environment variables before starting Dyad</li>
|
||||
<li>Restart Dyad after setting the environment variables</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{isAzureConfigured && (
|
||||
<Alert>
|
||||
<KeyRound className="h-4 w-4" />
|
||||
<AlertTitle>Azure OpenAI Configured</AlertTitle>
|
||||
<AlertDescription>
|
||||
Both required environment variables are set. You can now use
|
||||
Azure OpenAI models.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -69,7 +69,14 @@ export function ProviderSettingsPage({ provider }: ProviderSettingsPageProps) {
|
||||
userApiKey !== "Not Set";
|
||||
const hasEnvKey = !!(envVarName && envVars[envVarName]);
|
||||
|
||||
const isConfigured = isValidUserKey || hasEnvKey; // Configured if either is set
|
||||
// Special handling for Azure OpenAI configuration
|
||||
const isAzureConfigured =
|
||||
provider === "azure"
|
||||
? !!(envVars["AZURE_API_KEY"] && envVars["AZURE_RESOURCE_NAME"])
|
||||
: false;
|
||||
|
||||
const isConfigured =
|
||||
provider === "azure" ? isAzureConfigured : isValidUserKey || hasEnvKey; // Configured if either is set
|
||||
|
||||
// --- Save Handler ---
|
||||
const handleSaveKey = async () => {
|
||||
|
||||
@@ -211,6 +211,40 @@ export const MODEL_OPTIONS: Record<string, ModelOption[]> = {
|
||||
temperature: 0,
|
||||
},
|
||||
],
|
||||
azure: [
|
||||
{
|
||||
name: "gpt-5",
|
||||
displayName: "GPT-5",
|
||||
description: "Azure OpenAI GPT-5 model with reasoning capabilities",
|
||||
maxOutputTokens: 128_000,
|
||||
contextWindow: 400_000,
|
||||
temperature: 0,
|
||||
},
|
||||
{
|
||||
name: "gpt-5-mini",
|
||||
displayName: "GPT-5 Mini",
|
||||
description: "Azure OpenAI GPT-5 Mini model",
|
||||
maxOutputTokens: 128_000,
|
||||
contextWindow: 400_000,
|
||||
temperature: 0,
|
||||
},
|
||||
{
|
||||
name: "gpt-5-nano",
|
||||
displayName: "GPT-5 Nano",
|
||||
description: "Azure OpenAI GPT-5 Nano model",
|
||||
maxOutputTokens: 128_000,
|
||||
contextWindow: 400_000,
|
||||
temperature: 0,
|
||||
},
|
||||
{
|
||||
name: "gpt-5-chat",
|
||||
displayName: "GPT-5 Chat",
|
||||
description: "Azure OpenAI GPT-5 Chat model",
|
||||
maxOutputTokens: 16_384,
|
||||
contextWindow: 128_000,
|
||||
temperature: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const PROVIDER_TO_ENV_VAR: Record<string, string> = {
|
||||
@@ -218,6 +252,7 @@ export const PROVIDER_TO_ENV_VAR: Record<string, string> = {
|
||||
anthropic: "ANTHROPIC_API_KEY",
|
||||
google: "GEMINI_API_KEY",
|
||||
openrouter: "OPENROUTER_API_KEY",
|
||||
azure: "AZURE_API_KEY",
|
||||
};
|
||||
|
||||
export const CLOUD_PROVIDERS: Record<
|
||||
@@ -258,6 +293,12 @@ export const CLOUD_PROVIDERS: Record<
|
||||
websiteUrl: "https://academy.dyad.sh/settings",
|
||||
gatewayPrefix: "dyad/",
|
||||
},
|
||||
azure: {
|
||||
displayName: "Azure OpenAI",
|
||||
hasFreeTier: false,
|
||||
websiteUrl: "https://portal.azure.com/",
|
||||
gatewayPrefix: "",
|
||||
},
|
||||
};
|
||||
|
||||
const LOCAL_PROVIDERS: Record<
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createOpenAI } from "@ai-sdk/openai";
|
||||
import { createGoogleGenerativeAI as createGoogle } from "@ai-sdk/google";
|
||||
import { createAnthropic } from "@ai-sdk/anthropic";
|
||||
import { azure } from "@ai-sdk/azure";
|
||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
||||
import type { LargeLanguageModel, UserSettings } from "../../lib/schemas";
|
||||
@@ -224,6 +225,54 @@ function getRegularModelClient(
|
||||
backupModelClients: [],
|
||||
};
|
||||
}
|
||||
case "azure": {
|
||||
// Check if we're in e2e testing mode
|
||||
const testAzureBaseUrl = getEnvVar("TEST_AZURE_BASE_URL");
|
||||
|
||||
if (testAzureBaseUrl) {
|
||||
// Use fake server for e2e testing
|
||||
logger.info(`Using test Azure base URL: ${testAzureBaseUrl}`);
|
||||
const provider = createOpenAICompatible({
|
||||
name: "azure-test",
|
||||
baseURL: testAzureBaseUrl,
|
||||
apiKey: "fake-api-key-for-testing",
|
||||
});
|
||||
return {
|
||||
modelClient: {
|
||||
model: provider(model.name),
|
||||
builtinProviderId: providerId,
|
||||
},
|
||||
backupModelClients: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Azure OpenAI requires both API key and resource name as env vars
|
||||
// We use environment variables for Azure configuration
|
||||
const resourceName = getEnvVar("AZURE_RESOURCE_NAME");
|
||||
const azureApiKey = getEnvVar("AZURE_API_KEY");
|
||||
|
||||
if (!resourceName) {
|
||||
throw new Error(
|
||||
"Azure OpenAI resource name is required. Please set the AZURE_RESOURCE_NAME environment variable.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!azureApiKey) {
|
||||
throw new Error(
|
||||
"Azure OpenAI API key is required. Please set the AZURE_API_KEY environment variable.",
|
||||
);
|
||||
}
|
||||
|
||||
// Use the default Azure provider with environment variables
|
||||
// The azure provider automatically picks up AZURE_RESOURCE_NAME and AZURE_API_KEY
|
||||
return {
|
||||
modelClient: {
|
||||
model: azure(model.name),
|
||||
builtinProviderId: providerId,
|
||||
},
|
||||
backupModelClients: [],
|
||||
};
|
||||
}
|
||||
case "ollama": {
|
||||
const provider = createOllamaProvider({ baseURL: getOllamaApiUrl() });
|
||||
return {
|
||||
|
||||
@@ -34,6 +34,7 @@ const providers = [
|
||||
"openrouter",
|
||||
"ollama",
|
||||
"lmstudio",
|
||||
"azure",
|
||||
] as const;
|
||||
|
||||
export const cloudProviders = providers.filter(
|
||||
|
||||
Reference in New Issue
Block a user