From 70f3ab7e48e2df19d459dfa07515319f1284e719 Mon Sep 17 00:00:00 2001 From: Krtin Shet Date: Tue, 12 Aug 2025 12:48:04 +0530 Subject: [PATCH 1/4] feat: enhance OpenAI and Azure OpenAI authentication support - Updated documentation in README.md and authentication.md to include Azure OpenAI configuration options. - Modified CLI to accept Azure OpenAI parameters and handle them appropriately. - Enhanced OpenAIKeyPrompt component to support Azure OpenAI input fields. - Updated OpenAIContentGenerator to differentiate between Standard OpenAI and Azure OpenAI configurations. - Added environment variable checks for Azure OpenAI in the sandbox and configuration files. --- README.md | 23 +++ docs/cli/authentication.md | 25 ++- docs/cli/openai-auth.md | 28 ++- packages/cli/src/config/auth.ts | 30 ++- packages/cli/src/config/config.ts | 34 ++++ .../cli/src/ui/components/OpenAIKeyPrompt.tsx | 179 +++++++++++++++++- packages/cli/src/utils/sandbox.ts | 26 +++ .../core/src/core/openaiContentGenerator.ts | 69 +++++-- 8 files changed, 388 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 5790f693c23..437eeacfce6 100644 --- a/README.md +++ b/README.md @@ -141,20 +141,43 @@ Use API keys for OpenAI or other compatible providers: 1. **Environment Variables** + **For Standard OpenAI:** + ```bash export OPENAI_API_KEY="your_api_key_here" export OPENAI_BASE_URL="your_api_endpoint" export OPENAI_MODEL="your_model_choice" ``` + **For Azure OpenAI:** + + ```bash + export AZURE_OPENAI_ENDPOINT="https://your-resource-name.openai.azure.com" + export AZURE_OPENAI_DEPLOYMENT="your-deployment-name" + export AZURE_OPENAI_API_KEY="your-azure-api-key" + export AZURE_OPENAI_API_VERSION="2024-05-01-preview" + ``` + 2. **Project `.env` File** Create a `.env` file in your project root: + + **For Standard OpenAI:** + ```env OPENAI_API_KEY=your_api_key_here OPENAI_BASE_URL=your_api_endpoint OPENAI_MODEL=your_model_choice ``` + **For Azure OpenAI:** + + ```env + AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com + AZURE_OPENAI_DEPLOYMENT=your-deployment-name + AZURE_OPENAI_API_KEY=your-azure-api-key + AZURE_OPENAI_API_VERSION=2024-05-01-preview + ``` + **API Provider Options** > ⚠️ **Regional Notice:** diff --git a/docs/cli/authentication.md b/docs/cli/authentication.md index 1ad751c9622..4dabcdc9cf1 100644 --- a/docs/cli/authentication.md +++ b/docs/cli/authentication.md @@ -41,26 +41,45 @@ Qwen Code supports two main authentication methods to access AI models. Choose t a) **Environment Variables:** + **For Standard OpenAI:** + ```bash export OPENAI_API_KEY="your_api_key_here" export OPENAI_BASE_URL="your_api_endpoint" # Optional export OPENAI_MODEL="your_model_choice" # Optional ``` + **For Azure OpenAI:** + + ```bash + export AZURE_OPENAI_ENDPOINT="https://your-resource-name.openai.azure.com" + export AZURE_OPENAI_DEPLOYMENT="your-deployment-name" + export AZURE_OPENAI_API_KEY="your-azure-api-key" + export AZURE_OPENAI_API_VERSION="2024-05-01-preview" # Optional + ``` + b) **Project `.env` File:** Create a `.env` file in your project root: + **For Standard OpenAI:** + ```env OPENAI_API_KEY=your_api_key_here OPENAI_BASE_URL=your_api_endpoint OPENAI_MODEL=your_model_choice ``` + **For Azure OpenAI:** + + ```env + AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com + AZURE_OPENAI_DEPLOYMENT=your-deployment-name + AZURE_OPENAI_API_KEY=your-azure-api-key + AZURE_OPENAI_API_VERSION=2024-05-01-preview + ``` + **Supported Providers:** - OpenAI (https://platform.openai.com/api-keys) - - Alibaba Cloud Bailian - - ModelScope - - OpenRouter - Azure OpenAI - Any OpenAI-compatible API diff --git a/docs/cli/openai-auth.md b/docs/cli/openai-auth.md index 9dd8c0caa2c..3a34c7e3e5d 100644 --- a/docs/cli/openai-auth.md +++ b/docs/cli/openai-auth.md @@ -6,12 +6,21 @@ Qwen Code CLI supports OpenAI authentication for users who want to use OpenAI mo ### 1. Interactive Authentication (Recommended) -When you first run the CLI and select OpenAI as your authentication method, you'll be prompted to enter: +When you first run the CLI and select OpenAI as your authentication method, you'll be prompted to enter either: + +**Standard OpenAI:** - **API Key**: Your OpenAI API key from [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys) - **Base URL**: The base URL for OpenAI API (defaults to `https://api.openai.com/v1`) - **Model**: The OpenAI model to use (defaults to `gpt-4o`) +**OR Azure OpenAI:** + +- **Endpoint**: Your Azure OpenAI endpoint (e.g., `https://your-resource-name.openai.azure.com`) +- **Deployment**: Your Azure OpenAI deployment name +- **API Key**: Your Azure OpenAI API key +- **API Version**: The Azure OpenAI API version (defaults to `2024-05-01-preview`) + The CLI will guide you through each field: 1. Enter your API key and press Enter @@ -33,18 +42,35 @@ qwen-code --openai-api-key "your-api-key-here" --openai-base-url "https://your-c # With custom model qwen-code --openai-api-key "your-api-key-here" --model "gpt-4-turbo" + +# Azure OpenAI usage +qwen-code --azure-openai-endpoint "https://your-resource-name.openai.azure.com" \ + --azure-openai-deployment "your-deployment-name" \ + --azure-openai-api-key "your-azure-api-key" \ + --azure-openai-api-version "2024-05-01-preview" ``` ### 3. Environment Variables Set the following environment variables in your shell or `.env` file: +**For Standard OpenAI:** + ```bash export OPENAI_API_KEY="your-api-key-here" export OPENAI_BASE_URL="https://api.openai.com/v1" # Optional, defaults to this value export OPENAI_MODEL="gpt-4o" # Optional, defaults to gpt-4o ``` +**For Azure OpenAI:** + +```bash +export AZURE_OPENAI_ENDPOINT="https://your-resource-name.openai.azure.com" +export AZURE_OPENAI_DEPLOYMENT="your-deployment-name" +export AZURE_OPENAI_API_KEY="your-azure-api-key" +export AZURE_OPENAI_API_VERSION="2024-05-01-preview" # Optional, defaults to 2024-05-01-preview +``` + ## Supported Models The CLI supports all OpenAI models that are available through the OpenAI API, including: diff --git a/packages/cli/src/config/auth.ts b/packages/cli/src/config/auth.ts index 801c983cc2a..6696ca0c976 100644 --- a/packages/cli/src/config/auth.ts +++ b/packages/cli/src/config/auth.ts @@ -39,8 +39,20 @@ export const validateAuthMethod = (authMethod: string): string | null => { } if (authMethod === AuthType.USE_OPENAI) { - if (!process.env.OPENAI_API_KEY) { - return 'OPENAI_API_KEY environment variable not found. You can enter it interactively or add it to your .env file.'; + const isOpenAIKeySet = !!process.env.OPENAI_API_KEY; + const isAzureConfigSet = !!( + process.env.AZURE_OPENAI_ENDPOINT && + process.env.AZURE_OPENAI_DEPLOYMENT && + process.env.AZURE_OPENAI_API_KEY + ); + + if (!isOpenAIKeySet && !isAzureConfigSet) { + return ( + 'OpenAI configuration not found. You must set either:\n' + + '• OPENAI_API_KEY environment variable for standard OpenAI\n' + + '• AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT, and AZURE_OPENAI_API_KEY for Azure OpenAI\n' + + 'You can enter these interactively or add them to your .env file.' + ); } return null; } @@ -58,6 +70,20 @@ export const setOpenAIApiKey = (apiKey: string): void => { process.env.OPENAI_API_KEY = apiKey; }; +export const setAzureOpenAIConfig = ( + endpoint: string, + deployment: string, + apiKey: string, + apiVersion?: string, +): void => { + process.env.AZURE_OPENAI_ENDPOINT = endpoint; + process.env.AZURE_OPENAI_DEPLOYMENT = deployment; + process.env.AZURE_OPENAI_API_KEY = apiKey; + if (apiVersion) { + process.env.AZURE_OPENAI_API_VERSION = apiVersion; + } +}; + export const setOpenAIBaseUrl = (baseUrl: string): void => { process.env.OPENAI_BASE_URL = baseUrl; }; diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index af879a995a7..4401ae1a436 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -66,6 +66,10 @@ export interface CliArgs { openaiLogging: boolean | undefined; openaiApiKey: string | undefined; openaiBaseUrl: string | undefined; + azureOpenAIEndpoint?: string | undefined; + azureOpenAIDeployment?: string | undefined; + azureOpenAIApiKey?: string | undefined; + azureOpenAIApiVersion?: string | undefined; proxy: string | undefined; includeDirectories: string[] | undefined; loadMemoryFromIncludeDirectories: boolean | undefined; @@ -220,6 +224,22 @@ export async function parseArguments(): Promise { type: 'string', description: 'Tavily API key for web search functionality', }) + .option('azure-openai-endpoint', { + type: 'string', + description: 'Azure OpenAI endpoint (for Azure OpenAI deployments)', + }) + .option('azure-openai-deployment', { + type: 'string', + description: 'Azure OpenAI deployment name', + }) + .option('azure-openai-api-key', { + type: 'string', + description: 'Azure OpenAI API key', + }) + .option('azure-openai-api-version', { + type: 'string', + description: 'Azure OpenAI API version (default: 2024-05-01-preview)', + }) .option('proxy', { type: 'string', description: @@ -339,6 +359,20 @@ export async function loadCliConfig( process.env.OPENAI_BASE_URL = argv.openaiBaseUrl; } + // Handle Azure OpenAI configuration from command line + if (argv.azureOpenAIEndpoint) { + process.env.AZURE_OPENAI_ENDPOINT = argv.azureOpenAIEndpoint; + } + if (argv.azureOpenAIDeployment) { + process.env.AZURE_OPENAI_DEPLOYMENT = argv.azureOpenAIDeployment; + } + if (argv.azureOpenAIApiKey) { + process.env.AZURE_OPENAI_API_KEY = argv.azureOpenAIApiKey; + } + if (argv.azureOpenAIApiVersion) { + process.env.AZURE_OPENAI_API_VERSION = argv.azureOpenAIApiVersion; + } + // Handle Tavily API key from command line if (argv.tavilyApiKey) { process.env.TAVILY_API_KEY = argv.tavilyApiKey; diff --git a/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx index bf9f4bff807..0faf0e79781 100644 --- a/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx +++ b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx @@ -20,8 +20,18 @@ export function OpenAIKeyPrompt({ const [apiKey, setApiKey] = useState(''); const [baseUrl, setBaseUrl] = useState(''); const [model, setModel] = useState(''); + const [azureEndpoint, setAzureEndpoint] = useState(''); + const [azureDeployment, setAzureDeployment] = useState(''); + const [azureApiKey, setAzureApiKey] = useState(''); + const [azureApiVersion, setAzureApiVersion] = useState(''); const [currentField, setCurrentField] = useState< - 'apiKey' | 'baseUrl' | 'model' + | 'apiKey' + | 'baseUrl' + | 'model' + | 'azureEndpoint' + | 'azureDeployment' + | 'azureApiKey' + | 'azureApiVersion' >('apiKey'); useInput((input, key) => { @@ -49,6 +59,14 @@ export function OpenAIKeyPrompt({ setBaseUrl((prev) => prev + cleanInput); } else if (currentField === 'model') { setModel((prev) => prev + cleanInput); + } else if (currentField === 'azureEndpoint') { + setAzureEndpoint((prev) => prev + cleanInput); + } else if (currentField === 'azureDeployment') { + setAzureDeployment((prev) => prev + cleanInput); + } else if (currentField === 'azureApiKey') { + setAzureApiKey((prev) => prev + cleanInput); + } else if (currentField === 'azureApiVersion') { + setAzureApiVersion((prev) => prev + cleanInput); } return; } @@ -63,13 +81,48 @@ export function OpenAIKeyPrompt({ setCurrentField('model'); return; } else if (currentField === 'model') { - // 只有在提交时才检查 API key 是否为空 - if (apiKey.trim()) { + // Check if we should use Azure configuration + if (azureEndpoint && azureDeployment && azureApiKey) { + // Set Azure environment variables and submit + process.env.AZURE_OPENAI_ENDPOINT = azureEndpoint; + process.env.AZURE_OPENAI_DEPLOYMENT = azureDeployment; + process.env.AZURE_OPENAI_API_KEY = azureApiKey; + if (azureApiVersion) { + process.env.AZURE_OPENAI_API_VERSION = azureApiVersion; + } + // Use a placeholder API key since Azure uses the API key in a different way + onSubmit('azure-api-key', '', azureDeployment); + } else if (apiKey.trim()) { onSubmit(apiKey.trim(), baseUrl.trim(), model.trim()); } else { - // 如果 API key 为空,回到 API key 字段 + // If API key is empty, go back to API key field setCurrentField('apiKey'); } + } else if (currentField === 'azureEndpoint') { + setCurrentField('azureDeployment'); + return; + } else if (currentField === 'azureDeployment') { + setCurrentField('azureApiKey'); + return; + } else if (currentField === 'azureApiKey') { + setCurrentField('azureApiVersion'); + return; + } else if (currentField === 'azureApiVersion') { + // After filling all Azure fields, submit + if (azureEndpoint && azureDeployment && azureApiKey) { + // Set Azure environment variables and submit + process.env.AZURE_OPENAI_ENDPOINT = azureEndpoint; + process.env.AZURE_OPENAI_DEPLOYMENT = azureDeployment; + process.env.AZURE_OPENAI_API_KEY = azureApiKey; + if (azureApiVersion) { + process.env.AZURE_OPENAI_API_VERSION = azureApiVersion; + } + // Use a placeholder API key since Azure uses the API key in a different way + onSubmit('azure-api-key', '', azureDeployment); + } else { + // If Azure fields are not complete, go back to Azure endpoint field + setCurrentField('azureEndpoint'); + } } return; } @@ -86,6 +139,14 @@ export function OpenAIKeyPrompt({ } else if (currentField === 'baseUrl') { setCurrentField('model'); } else if (currentField === 'model') { + setCurrentField('azureEndpoint'); + } else if (currentField === 'azureEndpoint') { + setCurrentField('azureDeployment'); + } else if (currentField === 'azureDeployment') { + setCurrentField('azureApiKey'); + } else if (currentField === 'azureApiKey') { + setCurrentField('azureApiVersion'); + } else if (currentField === 'azureApiVersion') { setCurrentField('apiKey'); } return; @@ -97,6 +158,14 @@ export function OpenAIKeyPrompt({ setCurrentField('apiKey'); } else if (currentField === 'model') { setCurrentField('baseUrl'); + } else if (currentField === 'azureEndpoint') { + setCurrentField('model'); + } else if (currentField === 'azureDeployment') { + setCurrentField('azureEndpoint'); + } else if (currentField === 'azureApiKey') { + setCurrentField('azureDeployment'); + } else if (currentField === 'azureApiVersion') { + setCurrentField('azureApiKey'); } return; } @@ -106,6 +175,14 @@ export function OpenAIKeyPrompt({ setCurrentField('baseUrl'); } else if (currentField === 'baseUrl') { setCurrentField('model'); + } else if (currentField === 'model') { + setCurrentField('azureEndpoint'); + } else if (currentField === 'azureEndpoint') { + setCurrentField('azureDeployment'); + } else if (currentField === 'azureDeployment') { + setCurrentField('azureApiKey'); + } else if (currentField === 'azureApiKey') { + setCurrentField('azureApiVersion'); } return; } @@ -118,6 +195,14 @@ export function OpenAIKeyPrompt({ setBaseUrl((prev) => prev.slice(0, -1)); } else if (currentField === 'model') { setModel((prev) => prev.slice(0, -1)); + } else if (currentField === 'azureEndpoint') { + setAzureEndpoint((prev) => prev.slice(0, -1)); + } else if (currentField === 'azureDeployment') { + setAzureDeployment((prev) => prev.slice(0, -1)); + } else if (currentField === 'azureApiKey') { + setAzureApiKey((prev) => prev.slice(0, -1)); + } else if (currentField === 'azureApiVersion') { + setAzureApiVersion((prev) => prev.slice(0, -1)); } return; } @@ -143,7 +228,7 @@ export function OpenAIKeyPrompt({ - + @@ -158,7 +243,7 @@ export function OpenAIKeyPrompt({ - + @@ -173,7 +258,7 @@ export function OpenAIKeyPrompt({ - + @@ -187,6 +272,86 @@ export function OpenAIKeyPrompt({ + + {/* Azure OpenAI Configuration */} + + + OR Azure OpenAI Configuration + + + + + + Endpoint: + + + + + {currentField === 'azureEndpoint' ? '> ' : ' '} + {azureEndpoint} + + + + + + + Deployment: + + + + + {currentField === 'azureDeployment' ? '> ' : ' '} + {azureDeployment} + + + + + + + API Key: + + + + + {currentField === 'azureApiKey' ? '> ' : ' '} + {azureApiKey} + + + + + + + API Version: + + + + + {currentField === 'azureApiVersion' ? '> ' : ' '} + {azureApiVersion || '2024-05-01-preview'} + + + + Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index 6f111221c70..7fb122ef8d4 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -573,6 +573,32 @@ export async function start_sandbox( args.push('--env', `OPENAI_MODEL=${process.env.OPENAI_MODEL}`); } + // Copy Azure OpenAI environment variables + if (process.env.AZURE_OPENAI_ENDPOINT) { + args.push( + '--env', + `AZURE_OPENAI_ENDPOINT=${process.env.AZURE_OPENAI_ENDPOINT}`, + ); + } + if (process.env.AZURE_OPENAI_DEPLOYMENT) { + args.push( + '--env', + `AZURE_OPENAI_DEPLOYMENT=${process.env.AZURE_OPENAI_DEPLOYMENT}`, + ); + } + if (process.env.AZURE_OPENAI_API_KEY) { + args.push( + '--env', + `AZURE_OPENAI_API_KEY=${process.env.AZURE_OPENAI_API_KEY}`, + ); + } + if (process.env.AZURE_OPENAI_API_VERSION) { + args.push( + '--env', + `AZURE_OPENAI_API_VERSION=${process.env.AZURE_OPENAI_API_VERSION}`, + ); + } + // copy GOOGLE_GENAI_USE_VERTEXAI if (process.env.GOOGLE_GENAI_USE_VERTEXAI) { args.push( diff --git a/packages/core/src/core/openaiContentGenerator.ts b/packages/core/src/core/openaiContentGenerator.ts index eeba5db7f81..99b52d2cda5 100644 --- a/packages/core/src/core/openaiContentGenerator.ts +++ b/packages/core/src/core/openaiContentGenerator.ts @@ -96,6 +96,14 @@ export class OpenAIContentGenerator implements ContentGenerator { this.config = config; const baseURL = process.env.OPENAI_BASE_URL || ''; + // Check if using Azure OpenAI + const azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT; + const azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT; + const azureApiKey = process.env.AZURE_OPENAI_API_KEY; + const azureApiVersion = + process.env.AZURE_OPENAI_API_VERSION || '2024-05-01-preview'; + const isAzureOpenAI = !!(azureEndpoint && azureDeployment && azureApiKey); + // Configure timeout settings - using progressive timeouts const timeoutConfig = { // Base timeout for most requests (2 minutes) @@ -130,13 +138,28 @@ export class OpenAIContentGenerator implements ContentGenerator { : {}), }; - this.client = new OpenAI({ - apiKey, - baseURL, - timeout: timeoutConfig.timeout, - maxRetries: timeoutConfig.maxRetries, - defaultHeaders, - }); + if (isAzureOpenAI) { + // Azure OpenAI configuration + this.client = new OpenAI({ + apiKey: azureApiKey, + baseURL: `${azureEndpoint}/openai/deployments/${azureDeployment}`, + defaultQuery: { 'api-version': azureApiVersion }, + defaultHeaders, + timeout: timeoutConfig.timeout, + maxRetries: timeoutConfig.maxRetries, + }); + // Use the deployment name as the model for Azure + this.model = azureDeployment; + } else { + // Standard OpenAI configuration + this.client = new OpenAI({ + apiKey, + baseURL, + timeout: timeoutConfig.timeout, + maxRetries: timeoutConfig.maxRetries, + defaultHeaders, + }); + } } /** @@ -1373,20 +1396,40 @@ export class OpenAIContentGenerator implements ContentGenerator { * 2. Request-level parameters (medium priority) * 3. Default values (lowest priority) */ + private isAzureOpenAI(): boolean { + // Check if using Azure OpenAI by checking for Azure-specific environment variables + const azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT; + const azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT; + const azureApiKey = process.env.AZURE_OPENAI_API_KEY; + const isAzureEnvSet = !!(azureEndpoint && azureDeployment && azureApiKey); + + // Also check if the baseURL contains Azure-specific patterns + const baseURL = this.client?.baseURL || ''; + const isAzureURL = + baseURL.includes('azure') || baseURL.includes('openai/deployments'); + + return isAzureEnvSet || isAzureURL; + } + private buildSamplingParameters( request: GenerateContentParameters, ): Record { const configSamplingParams = this.config.getContentGeneratorConfig()?.samplingParams; + // For Azure OpenAI, temperature must be 1.0 (the default) + const isAzure = this.isAzureOpenAI(); + const temperature = isAzure + ? 1.0 // Hardcode to 1.0 for Azure OpenAI + : configSamplingParams?.temperature !== undefined + ? configSamplingParams.temperature + : request.config?.temperature !== undefined + ? request.config.temperature + : 0.0; + const params = { // Temperature: config > request > default - temperature: - configSamplingParams?.temperature !== undefined - ? configSamplingParams.temperature - : request.config?.temperature !== undefined - ? request.config.temperature - : 0.0, + temperature, // Max tokens: config > request > undefined ...(configSamplingParams?.max_tokens !== undefined From 691fd84144e97ea7949dedde3c15ace18fca21b6 Mon Sep 17 00:00:00 2001 From: krtinshet8 Date: Sat, 16 Aug 2025 21:16:40 +0530 Subject: [PATCH 2/4] fix: address PR feedback for Azure OpenAI support - Implemented Azure OpenAI authentication in validateNonInterActiveAuth.ts and auth.ts. - Updated AuthDialog and OpenAIKeyPrompt components to handle Azure OpenAI input fields and prompts. - Introduced AzureOpenAIContentGenerator for Azure-specific content generation. - Enhanced content generator configuration to support Azure OpenAI parameters. - Updated logging to exclude Azure OpenAI responses and errors for better security. --- packages/cli/src/config/auth.ts | 18 + packages/cli/src/ui/components/AuthDialog.tsx | 46 +- .../cli/src/ui/components/OpenAIKeyPrompt.tsx | 475 ++++++++---------- .../cli/src/validateNonInterActiveAuth.ts | 7 + .../src/core/azureOpenAIContentGenerator.ts | 59 +++ packages/core/src/core/contentGenerator.ts | 30 ++ packages/core/src/core/geminiChat.ts | 16 +- 7 files changed, 384 insertions(+), 267 deletions(-) create mode 100644 packages/core/src/core/azureOpenAIContentGenerator.ts diff --git a/packages/cli/src/config/auth.ts b/packages/cli/src/config/auth.ts index 6696ca0c976..b37338dd0fd 100644 --- a/packages/cli/src/config/auth.ts +++ b/packages/cli/src/config/auth.ts @@ -38,6 +38,24 @@ export const validateAuthMethod = (authMethod: string): string | null => { return null; } + if (authMethod === AuthType.AZURE_OPENAI) { + const hasCore = + !!process.env.AZURE_OPENAI_ENDPOINT && + !!process.env.AZURE_OPENAI_DEPLOYMENT; + const hasCred = + !!process.env.AZURE_OPENAI_API_KEY || !!process.env.AZURE_OPENAI_BEARER_TOKEN; + + if (!hasCore || !hasCred) { + return ( + 'Azure OpenAI configuration not found. You must set:\n' + + '• AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT and\n' + + ' either AZURE_OPENAI_API_KEY or AZURE_OPENAI_BEARER_TOKEN.\n' + + 'You can enter these interactively or add them to your .env file.' + ); + } + return null; + } + if (authMethod === AuthType.USE_OPENAI) { const isOpenAIKeySet = !!process.env.OPENAI_API_KEY; const isAzureConfigSet = !!( diff --git a/packages/cli/src/ui/components/AuthDialog.tsx b/packages/cli/src/ui/components/AuthDialog.tsx index 1b8e6b8a2a3..d7afaf0abc0 100644 --- a/packages/cli/src/ui/components/AuthDialog.tsx +++ b/packages/cli/src/ui/components/AuthDialog.tsx @@ -45,9 +45,13 @@ export function AuthDialog({ initialErrorMessage || null, ); const [showOpenAIKeyPrompt, setShowOpenAIKeyPrompt] = useState(false); + // Track which prompt variant to render to avoid showing both sets of fields at once + const [promptVariant, setPromptVariant] = useState<'openai' | 'azure'>('openai'); + const items = [ { label: 'Qwen OAuth', value: AuthType.QWEN_OAUTH }, { label: 'OpenAI', value: AuthType.USE_OPENAI }, + { label: 'Azure OpenAI', value: AuthType.AZURE_OPENAI }, ]; const initialAuthIndex = Math.max( @@ -79,12 +83,30 @@ export function AuthDialog({ const handleAuthSelect = (authMethod: AuthType) => { const error = validateAuthMethod(authMethod); if (error) { - if (authMethod === AuthType.USE_OPENAI && !process.env.OPENAI_API_KEY) { - setShowOpenAIKeyPrompt(true); - setErrorMessage(null); - } else { - setErrorMessage(error); + // Decide which prompt variant is needed + if (authMethod === AuthType.USE_OPENAI) { + const missingOpenAI = !process.env.OPENAI_API_KEY; + if (missingOpenAI) { + setPromptVariant('openai'); + setShowOpenAIKeyPrompt(true); + setErrorMessage(null); + return; + } + } else if (authMethod === AuthType.AZURE_OPENAI) { + const missingAzure = + !(process.env.AZURE_OPENAI_API_KEY || + process.env.AZURE_OPENAI_BEARER_TOKEN) || + !process.env.AZURE_OPENAI_ENDPOINT || + !process.env.AZURE_OPENAI_DEPLOYMENT; + if (missingAzure) { + setPromptVariant('azure'); + setShowOpenAIKeyPrompt(true); + setErrorMessage(null); + return; + } } + // If we get here, show the validation error + setErrorMessage(error); } else { setErrorMessage(null); onSelect(authMethod, SettingScope.User); @@ -100,7 +122,18 @@ export function AuthDialog({ setOpenAIBaseUrl(baseUrl); setOpenAIModel(model); setShowOpenAIKeyPrompt(false); - onSelect(AuthType.USE_OPENAI, SettingScope.User); + + // Decide which auth type is appropriate based on envs set by prompt + const isAzure = + process.env.AZURE_OPENAI_ENDPOINT && + process.env.AZURE_OPENAI_DEPLOYMENT && + (process.env.AZURE_OPENAI_API_KEY || + process.env.AZURE_OPENAI_BEARER_TOKEN); + + onSelect( + isAzure ? AuthType.AZURE_OPENAI : AuthType.USE_OPENAI, + SettingScope.User, + ); }; const handleOpenAIKeyCancel = () => { @@ -135,6 +168,7 @@ export function AuthDialog({ ); } diff --git a/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx index 0faf0e79781..3f883012f8f 100644 --- a/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx +++ b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx @@ -8,14 +8,18 @@ import React, { useState } from 'react'; import { Box, Text, useInput } from 'ink'; import { Colors } from '../colors.js'; +type PromptVariant = 'openai' | 'azure'; + interface OpenAIKeyPromptProps { onSubmit: (apiKey: string, baseUrl: string, model: string) => void; onCancel: () => void; + variant: PromptVariant; } export function OpenAIKeyPrompt({ onSubmit, onCancel, + variant, }: OpenAIKeyPromptProps): React.JSX.Element { const [apiKey, setApiKey] = useState(''); const [baseUrl, setBaseUrl] = useState(''); @@ -32,96 +36,92 @@ export function OpenAIKeyPrompt({ | 'azureDeployment' | 'azureApiKey' | 'azureApiVersion' - >('apiKey'); + >(variant === 'azure' ? 'azureEndpoint' : 'apiKey'); useInput((input, key) => { - // 过滤粘贴相关的控制序列 + // Robustly strip terminal focus and ANSI control sequences to avoid spurious '0'/'I' + // Remove CSI sequences like ESC[... let cleanInput = (input || '') - // 过滤 ESC 开头的控制序列(如 \u001b[200~、\u001b[201~ 等) - .replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '') // eslint-disable-line no-control-regex - // 过滤粘贴开始标记 [200~ - .replace(/\[200~/g, '') - // 过滤粘贴结束标记 [201~ - .replace(/\[201~/g, '') - // 过滤单独的 [ 和 ~ 字符(可能是粘贴标记的残留) + .replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '') // eslint-disable-line no-control-regex + // Remove common bracketed paste markers + .replace(/\[200~|\[201~|\[0I|\[I|\[O/g, '') // handle possible leftover without ESC + // Remove leading/trailing stray '[' or '~' .replace(/^\[|~$/g, ''); - // 再过滤所有不可见字符(ASCII < 32,除了回车换行) + // Remove any residual "0I" or "I" that sometimes leak from focus events + if (/^(0?I|O)$/.test(cleanInput)) { + cleanInput = ''; + } + + // Filter non-printable ASCII except CR/LF which we process separately cleanInput = cleanInput .split('') .filter((ch) => ch.charCodeAt(0) >= 32) .join(''); if (cleanInput.length > 0) { - if (currentField === 'apiKey') { - setApiKey((prev) => prev + cleanInput); - } else if (currentField === 'baseUrl') { - setBaseUrl((prev) => prev + cleanInput); - } else if (currentField === 'model') { - setModel((prev) => prev + cleanInput); - } else if (currentField === 'azureEndpoint') { - setAzureEndpoint((prev) => prev + cleanInput); - } else if (currentField === 'azureDeployment') { - setAzureDeployment((prev) => prev + cleanInput); - } else if (currentField === 'azureApiKey') { - setAzureApiKey((prev) => prev + cleanInput); - } else if (currentField === 'azureApiVersion') { - setAzureApiVersion((prev) => prev + cleanInput); + if (variant === 'openai') { + if (currentField === 'apiKey') { + setApiKey((prev) => prev + cleanInput); + } else if (currentField === 'baseUrl') { + setBaseUrl((prev) => prev + cleanInput); + } else if (currentField === 'model') { + setModel((prev) => prev + cleanInput); + } + } else { + if (currentField === 'azureEndpoint') { + setAzureEndpoint((prev) => prev + cleanInput); + } else if (currentField === 'azureDeployment') { + setAzureDeployment((prev) => prev + cleanInput); + } else if (currentField === 'azureApiKey') { + setAzureApiKey((prev) => prev + cleanInput); + } else if (currentField === 'azureApiVersion') { + setAzureApiVersion((prev) => prev + cleanInput); + } } return; } - // 检查是否是 Enter 键(通过检查输入是否包含换行符) + // Enter handling (treat CR/LF as submission or advance) if (input.includes('\n') || input.includes('\r')) { - if (currentField === 'apiKey') { - // 允许空 API key 跳转到下一个字段,让用户稍后可以返回修改 - setCurrentField('baseUrl'); - return; - } else if (currentField === 'baseUrl') { - setCurrentField('model'); - return; - } else if (currentField === 'model') { - // Check if we should use Azure configuration - if (azureEndpoint && azureDeployment && azureApiKey) { - // Set Azure environment variables and submit - process.env.AZURE_OPENAI_ENDPOINT = azureEndpoint; - process.env.AZURE_OPENAI_DEPLOYMENT = azureDeployment; - process.env.AZURE_OPENAI_API_KEY = azureApiKey; - if (azureApiVersion) { - process.env.AZURE_OPENAI_API_VERSION = azureApiVersion; + if (variant === 'openai') { + if (currentField === 'apiKey') { + setCurrentField('baseUrl'); + return; + } else if (currentField === 'baseUrl') { + setCurrentField('model'); + return; + } else if (currentField === 'model') { + if (apiKey.trim()) { + onSubmit(apiKey.trim(), baseUrl.trim(), model.trim()); + } else { + setCurrentField('apiKey'); } - // Use a placeholder API key since Azure uses the API key in a different way - onSubmit('azure-api-key', '', azureDeployment); - } else if (apiKey.trim()) { - onSubmit(apiKey.trim(), baseUrl.trim(), model.trim()); - } else { - // If API key is empty, go back to API key field - setCurrentField('apiKey'); + return; } - } else if (currentField === 'azureEndpoint') { - setCurrentField('azureDeployment'); - return; - } else if (currentField === 'azureDeployment') { - setCurrentField('azureApiKey'); - return; - } else if (currentField === 'azureApiKey') { - setCurrentField('azureApiVersion'); - return; - } else if (currentField === 'azureApiVersion') { - // After filling all Azure fields, submit - if (azureEndpoint && azureDeployment && azureApiKey) { - // Set Azure environment variables and submit - process.env.AZURE_OPENAI_ENDPOINT = azureEndpoint; - process.env.AZURE_OPENAI_DEPLOYMENT = azureDeployment; - process.env.AZURE_OPENAI_API_KEY = azureApiKey; - if (azureApiVersion) { - process.env.AZURE_OPENAI_API_VERSION = azureApiVersion; + } else { + if (currentField === 'azureEndpoint') { + setCurrentField('azureDeployment'); + return; + } else if (currentField === 'azureDeployment') { + setCurrentField('azureApiKey'); + return; + } else if (currentField === 'azureApiKey') { + setCurrentField('azureApiVersion'); + return; + } else if (currentField === 'azureApiVersion') { + if (azureEndpoint && azureDeployment && azureApiKey) { + process.env.AZURE_OPENAI_ENDPOINT = azureEndpoint; + process.env.AZURE_OPENAI_DEPLOYMENT = azureDeployment; + process.env.AZURE_OPENAI_API_KEY = azureApiKey; + if (azureApiVersion) { + process.env.AZURE_OPENAI_API_VERSION = azureApiVersion; + } + onSubmit('azure-api-key', '', azureDeployment); + } else { + setCurrentField('azureEndpoint'); } - // Use a placeholder API key since Azure uses the API key in a different way - onSubmit('azure-api-key', '', azureDeployment); - } else { - // If Azure fields are not complete, go back to Azure endpoint field - setCurrentField('azureEndpoint'); + return; } } return; @@ -132,77 +132,57 @@ export function OpenAIKeyPrompt({ return; } - // Handle Tab key for field navigation + // Handle Tab key for field navigation (cycle within current variant only) if (key.tab) { - if (currentField === 'apiKey') { - setCurrentField('baseUrl'); - } else if (currentField === 'baseUrl') { - setCurrentField('model'); - } else if (currentField === 'model') { - setCurrentField('azureEndpoint'); - } else if (currentField === 'azureEndpoint') { - setCurrentField('azureDeployment'); - } else if (currentField === 'azureDeployment') { - setCurrentField('azureApiKey'); - } else if (currentField === 'azureApiKey') { - setCurrentField('azureApiVersion'); - } else if (currentField === 'azureApiVersion') { - setCurrentField('apiKey'); + if (variant === 'openai') { + if (currentField === 'apiKey') setCurrentField('baseUrl'); + else if (currentField === 'baseUrl') setCurrentField('model'); + else if (currentField === 'model') setCurrentField('apiKey'); + } else { + if (currentField === 'azureEndpoint') setCurrentField('azureDeployment'); + else if (currentField === 'azureDeployment') setCurrentField('azureApiKey'); + else if (currentField === 'azureApiKey') setCurrentField('azureApiVersion'); + else if (currentField === 'azureApiVersion') setCurrentField('azureEndpoint'); } return; } - // Handle arrow keys for field navigation + // Handle arrow keys for field navigation (within variant) if (key.upArrow) { - if (currentField === 'baseUrl') { - setCurrentField('apiKey'); - } else if (currentField === 'model') { - setCurrentField('baseUrl'); - } else if (currentField === 'azureEndpoint') { - setCurrentField('model'); - } else if (currentField === 'azureDeployment') { - setCurrentField('azureEndpoint'); - } else if (currentField === 'azureApiKey') { - setCurrentField('azureDeployment'); - } else if (currentField === 'azureApiVersion') { - setCurrentField('azureApiKey'); + if (variant === 'openai') { + if (currentField === 'baseUrl') setCurrentField('apiKey'); + else if (currentField === 'model') setCurrentField('baseUrl'); + } else { + if (currentField === 'azureDeployment') setCurrentField('azureEndpoint'); + else if (currentField === 'azureApiKey') setCurrentField('azureDeployment'); + else if (currentField === 'azureApiVersion') setCurrentField('azureApiKey'); } return; } if (key.downArrow) { - if (currentField === 'apiKey') { - setCurrentField('baseUrl'); - } else if (currentField === 'baseUrl') { - setCurrentField('model'); - } else if (currentField === 'model') { - setCurrentField('azureEndpoint'); - } else if (currentField === 'azureEndpoint') { - setCurrentField('azureDeployment'); - } else if (currentField === 'azureDeployment') { - setCurrentField('azureApiKey'); - } else if (currentField === 'azureApiKey') { - setCurrentField('azureApiVersion'); + if (variant === 'openai') { + if (currentField === 'apiKey') setCurrentField('baseUrl'); + else if (currentField === 'baseUrl') setCurrentField('model'); + } else { + if (currentField === 'azureEndpoint') setCurrentField('azureDeployment'); + else if (currentField === 'azureDeployment') setCurrentField('azureApiKey'); + else if (currentField === 'azureApiKey') setCurrentField('azureApiVersion'); } return; } // Handle backspace - check both key.backspace and delete key if (key.backspace || key.delete) { - if (currentField === 'apiKey') { - setApiKey((prev) => prev.slice(0, -1)); - } else if (currentField === 'baseUrl') { - setBaseUrl((prev) => prev.slice(0, -1)); - } else if (currentField === 'model') { - setModel((prev) => prev.slice(0, -1)); - } else if (currentField === 'azureEndpoint') { - setAzureEndpoint((prev) => prev.slice(0, -1)); - } else if (currentField === 'azureDeployment') { - setAzureDeployment((prev) => prev.slice(0, -1)); - } else if (currentField === 'azureApiKey') { - setAzureApiKey((prev) => prev.slice(0, -1)); - } else if (currentField === 'azureApiVersion') { - setAzureApiVersion((prev) => prev.slice(0, -1)); + if (variant === 'openai') { + if (currentField === 'apiKey') setApiKey((prev) => prev.slice(0, -1)); + else if (currentField === 'baseUrl') setBaseUrl((prev) => prev.slice(0, -1)); + else if (currentField === 'model') setModel((prev) => prev.slice(0, -1)); + } else { + if (currentField === 'azureEndpoint') setAzureEndpoint((prev) => prev.slice(0, -1)); + else if (currentField === 'azureDeployment') setAzureDeployment((prev) => prev.slice(0, -1)); + else if (currentField === 'azureApiKey') setAzureApiKey((prev) => prev.slice(0, -1)); + else if (currentField === 'azureApiVersion') setAzureApiVersion((prev) => prev.slice(0, -1)); } return; } @@ -217,140 +197,121 @@ export function OpenAIKeyPrompt({ width="100%" > - OpenAI Configuration Required + {variant === 'azure' ? 'Azure OpenAI Configuration Required' : 'OpenAI Configuration Required'} - - - Please enter your OpenAI configuration. You can get an API key from{' '} - - https://platform.openai.com/api-keys - - - - - - - API Key: - - - - - {currentField === 'apiKey' ? '> ' : ' '} - {apiKey || ' '} - - - - - - - Base URL: - - - - - {currentField === 'baseUrl' ? '> ' : ' '} - {baseUrl} - - - - - - - Model: - - - - - {currentField === 'model' ? '> ' : ' '} - {model} - - - - {/* Azure OpenAI Configuration */} - - - OR Azure OpenAI Configuration - - - - - - Endpoint: - - - - - {currentField === 'azureEndpoint' ? '> ' : ' '} - {azureEndpoint} - - - - - - - Deployment: - - - - - {currentField === 'azureDeployment' ? '> ' : ' '} - {azureDeployment} - - - - - - - API Key: - - - - - {currentField === 'azureApiKey' ? '> ' : ' '} - {azureApiKey} - - - - - - - API Version: - - - - - {currentField === 'azureApiVersion' ? '> ' : ' '} - {azureApiVersion || '2024-05-01-preview'} - - - + {variant === 'openai' ? ( + <> + + + Please enter your OpenAI configuration. You can get an API key from{' '} + + https://platform.openai.com/api-keys + + + + + + + + API Key: + + + + + {currentField === 'apiKey' ? '> ' : ' '} + {apiKey || ' '} + + + + + + + + Base URL: + + + + + {currentField === 'baseUrl' ? '> ' : ' '} + {baseUrl} + + + + + + + + Model: + + + + + {currentField === 'model' ? '> ' : ' '} + {model} + + + + + ) : ( + <> + + + + Endpoint: + + + + + {currentField === 'azureEndpoint' ? '> ' : ' '} + {azureEndpoint} + + + + + + + + Deployment: + + + + + {currentField === 'azureDeployment' ? '> ' : ' '} + {azureDeployment} + + + + + + + + API Key: + + + + + {currentField === 'azureApiKey' ? '> ' : ' '} + {azureApiKey} + + + + + + + + API Version: + + + + + {currentField === 'azureApiVersion' ? '> ' : ' '} + {azureApiVersion || '2024-05-01-preview'} + + + + + )} diff --git a/packages/cli/src/validateNonInterActiveAuth.ts b/packages/cli/src/validateNonInterActiveAuth.ts index c1e7c586b43..8c51f78c040 100644 --- a/packages/cli/src/validateNonInterActiveAuth.ts +++ b/packages/cli/src/validateNonInterActiveAuth.ts @@ -18,6 +18,13 @@ function getAuthTypeFromEnv(): AuthType | undefined { if (process.env.GEMINI_API_KEY) { return AuthType.USE_GEMINI; } + if ( + process.env.AZURE_OPENAI_ENDPOINT && + process.env.AZURE_OPENAI_DEPLOYMENT && + (process.env.AZURE_OPENAI_API_KEY || process.env.AZURE_OPENAI_BEARER_TOKEN) + ) { + return AuthType.AZURE_OPENAI; + } if (process.env.OPENAI_API_KEY) { return AuthType.USE_OPENAI; } diff --git a/packages/core/src/core/azureOpenAIContentGenerator.ts b/packages/core/src/core/azureOpenAIContentGenerator.ts new file mode 100644 index 00000000000..fd81001b526 --- /dev/null +++ b/packages/core/src/core/azureOpenAIContentGenerator.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + * + * Azure-specific OpenAI content generator wrapper. + * Delegates to OpenAIContentGenerator but configures the client with + * Azure endpoint / deployment / api-version headers and token or key. + */ + +import { Config } from '../config/config.js'; +import { OpenAIContentGenerator } from './openaiContentGenerator.js'; +import OpenAI from 'openai'; + +/** + * Separate wrapper so core OpenAI implementation stays provider-agnostic. + * Token provider support: if AZURE_OPENAI_BEARER_TOKEN is provided we use it + * via defaultHeaders.Authorization, otherwise we fall back to API key flow. + */ +export class AzureOpenAIContentGenerator extends OpenAIContentGenerator { + constructor(apiKey: string, model: string, config: Config) { + // Resolve Azure envs – validated upstream. + const endpoint = process.env.AZURE_OPENAI_ENDPOINT as string; + const deployment = process.env.AZURE_OPENAI_DEPLOYMENT as string; + const apiVersion = + process.env.AZURE_OPENAI_API_VERSION ?? '2024-05-01-preview'; + + // If bearer token provided we pass empty apiKey to parent and populate + // Authorization header later. Passing empty string keeps parent happy. + const bearer = process.env.AZURE_OPENAI_BEARER_TOKEN; + super(bearer ? '' : apiKey, model || deployment, config); + + // Override the underlying OpenAI client that super() created. + // Re-instantiate with Azure-specific URL / headers. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore access protected + const version = config.getCliVersion() || 'unknown'; + const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore access protected + this.client = new OpenAI({ + apiKey: bearer ? undefined : apiKey, + baseURL: `${endpoint}/openai/deployments/${deployment}`, + defaultQuery: { 'api-version': apiVersion }, + defaultHeaders: { + 'User-Agent': userAgent, + ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}), + }, + timeout: config.getContentGeneratorTimeout() ?? 120_000, + maxRetries: config.getContentGeneratorMaxRetries() ?? 3, + }); + + // Ensure model equals deployment to satisfy OpenAI SDK requirement. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore model is private in parent but we need correct value + this.model = deployment; + } +} \ No newline at end of file diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 76c09ad321a..49b8c7dea21 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -46,6 +46,7 @@ export enum AuthType { USE_VERTEX_AI = 'vertex-ai', CLOUD_SHELL = 'cloud-shell', USE_OPENAI = 'openai', + AZURE_OPENAI = 'azure-openai', QWEN_OAUTH = 'qwen-oauth', } @@ -124,6 +125,21 @@ export function createContentGeneratorConfig( return contentGeneratorConfig; } + if (authType === AuthType.AZURE_OPENAI) { + // Azure OpenAI requires endpoint & deployment + const azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT; + const azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT; + const azureApiKey = process.env.AZURE_OPENAI_API_KEY; + const azureBearer = process.env.AZURE_OPENAI_BEARER_TOKEN; + if (azureEndpoint && azureDeployment && (azureApiKey || azureBearer)) { + contentGeneratorConfig.apiKey = azureApiKey ?? ''; + // Use deployment name as model if not explicitly set + contentGeneratorConfig.model = + process.env.OPENAI_MODEL || azureDeployment; + return contentGeneratorConfig; + } + } + if (authType === AuthType.USE_OPENAI && openaiApiKey) { contentGeneratorConfig.apiKey = openaiApiKey; contentGeneratorConfig.model = @@ -182,6 +198,20 @@ export async function createContentGenerator( return googleGenAI.models; } + if (config.authType === AuthType.AZURE_OPENAI) { + // Import Azure generator dynamically + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TypeScript may not resolve the .js suffix in TS source; runtime path is correct after build. + const { AzureOpenAIContentGenerator } = await import( + './azureOpenAIContentGenerator.js' + ); + return new AzureOpenAIContentGenerator( + config.apiKey ?? '', + config.model, + gcConfig, + ); + } + if (config.authType === AuthType.USE_OPENAI) { if (!config.apiKey) { throw new Error('OpenAI API key is required'); diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index c66237c4f2b..87dc5f5020b 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -162,8 +162,12 @@ export class GeminiChat { ): Promise { const authType = this.config.getContentGeneratorConfig()?.authType; - // Don't log API responses for openaiContentGenerator - if (authType === AuthType.QWEN_OAUTH || authType === AuthType.USE_OPENAI) { + // Don't log API responses for OpenAI-compatible providers + if ( + authType === AuthType.QWEN_OAUTH || + authType === AuthType.USE_OPENAI || + authType === AuthType.AZURE_OPENAI + ) { return; } @@ -192,8 +196,12 @@ export class GeminiChat { const authType = this.config.getContentGeneratorConfig()?.authType; - // Don't log API errors for openaiContentGenerator - if (authType === AuthType.QWEN_OAUTH || authType === AuthType.USE_OPENAI) { + // Don't log API errors for OpenAI-compatible providers + if ( + authType === AuthType.QWEN_OAUTH || + authType === AuthType.USE_OPENAI || + authType === AuthType.AZURE_OPENAI + ) { return; } From d000924c6e0359d0148491a0d046ac9797770749 Mon Sep 17 00:00:00 2001 From: krtinshet8 Date: Sun, 17 Aug 2025 19:28:01 +0530 Subject: [PATCH 3/4] fix(cli): default OpenAIKeyPrompt to 'openai' when variant is omitted to match tests and expected UX --- docs/cli/authentication.md | 4 +- packages/cli/src/config/auth.ts | 3 +- packages/cli/src/ui/components/AuthDialog.tsx | 10 +- .../cli/src/ui/components/OpenAIKeyPrompt.tsx | 109 +++++++++++++----- .../src/core/azureOpenAIContentGenerator.ts | 2 +- .../core/src/core/openaiContentGenerator.ts | 69 +++-------- 6 files changed, 108 insertions(+), 89 deletions(-) diff --git a/docs/cli/authentication.md b/docs/cli/authentication.md index 4dabcdc9cf1..c296c329b2f 100644 --- a/docs/cli/authentication.md +++ b/docs/cli/authentication.md @@ -81,7 +81,9 @@ Qwen Code supports two main authentication methods to access AI models. Choose t **Supported Providers:** - OpenAI (https://platform.openai.com/api-keys) - Azure OpenAI - - Any OpenAI-compatible API + - Alibaba Cloud Bailian + - ModelScope + - OpenRouter ## Switching Authentication Methods diff --git a/packages/cli/src/config/auth.ts b/packages/cli/src/config/auth.ts index b37338dd0fd..1774bcde2a5 100644 --- a/packages/cli/src/config/auth.ts +++ b/packages/cli/src/config/auth.ts @@ -43,7 +43,8 @@ export const validateAuthMethod = (authMethod: string): string | null => { !!process.env.AZURE_OPENAI_ENDPOINT && !!process.env.AZURE_OPENAI_DEPLOYMENT; const hasCred = - !!process.env.AZURE_OPENAI_API_KEY || !!process.env.AZURE_OPENAI_BEARER_TOKEN; + !!process.env.AZURE_OPENAI_API_KEY || + !!process.env.AZURE_OPENAI_BEARER_TOKEN; if (!hasCore || !hasCred) { return ( diff --git a/packages/cli/src/ui/components/AuthDialog.tsx b/packages/cli/src/ui/components/AuthDialog.tsx index d7afaf0abc0..34bb2d33f1a 100644 --- a/packages/cli/src/ui/components/AuthDialog.tsx +++ b/packages/cli/src/ui/components/AuthDialog.tsx @@ -46,7 +46,9 @@ export function AuthDialog({ ); const [showOpenAIKeyPrompt, setShowOpenAIKeyPrompt] = useState(false); // Track which prompt variant to render to avoid showing both sets of fields at once - const [promptVariant, setPromptVariant] = useState<'openai' | 'azure'>('openai'); + const [promptVariant, setPromptVariant] = useState<'openai' | 'azure'>( + 'openai', + ); const items = [ { label: 'Qwen OAuth', value: AuthType.QWEN_OAUTH }, @@ -94,8 +96,10 @@ export function AuthDialog({ } } else if (authMethod === AuthType.AZURE_OPENAI) { const missingAzure = - !(process.env.AZURE_OPENAI_API_KEY || - process.env.AZURE_OPENAI_BEARER_TOKEN) || + !( + process.env.AZURE_OPENAI_API_KEY || + process.env.AZURE_OPENAI_BEARER_TOKEN + ) || !process.env.AZURE_OPENAI_ENDPOINT || !process.env.AZURE_OPENAI_DEPLOYMENT; if (missingAzure) { diff --git a/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx index 3f883012f8f..c23ac80c34e 100644 --- a/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx +++ b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx @@ -13,13 +13,13 @@ type PromptVariant = 'openai' | 'azure'; interface OpenAIKeyPromptProps { onSubmit: (apiKey: string, baseUrl: string, model: string) => void; onCancel: () => void; - variant: PromptVariant; + variant?: PromptVariant; // default to 'openai' when not provided } export function OpenAIKeyPrompt({ onSubmit, onCancel, - variant, + variant = 'openai', }: OpenAIKeyPromptProps): React.JSX.Element { const [apiKey, setApiKey] = useState(''); const [baseUrl, setBaseUrl] = useState(''); @@ -139,10 +139,14 @@ export function OpenAIKeyPrompt({ else if (currentField === 'baseUrl') setCurrentField('model'); else if (currentField === 'model') setCurrentField('apiKey'); } else { - if (currentField === 'azureEndpoint') setCurrentField('azureDeployment'); - else if (currentField === 'azureDeployment') setCurrentField('azureApiKey'); - else if (currentField === 'azureApiKey') setCurrentField('azureApiVersion'); - else if (currentField === 'azureApiVersion') setCurrentField('azureEndpoint'); + if (currentField === 'azureEndpoint') + setCurrentField('azureDeployment'); + else if (currentField === 'azureDeployment') + setCurrentField('azureApiKey'); + else if (currentField === 'azureApiKey') + setCurrentField('azureApiVersion'); + else if (currentField === 'azureApiVersion') + setCurrentField('azureEndpoint'); } return; } @@ -153,9 +157,12 @@ export function OpenAIKeyPrompt({ if (currentField === 'baseUrl') setCurrentField('apiKey'); else if (currentField === 'model') setCurrentField('baseUrl'); } else { - if (currentField === 'azureDeployment') setCurrentField('azureEndpoint'); - else if (currentField === 'azureApiKey') setCurrentField('azureDeployment'); - else if (currentField === 'azureApiVersion') setCurrentField('azureApiKey'); + if (currentField === 'azureDeployment') + setCurrentField('azureEndpoint'); + else if (currentField === 'azureApiKey') + setCurrentField('azureDeployment'); + else if (currentField === 'azureApiVersion') + setCurrentField('azureApiKey'); } return; } @@ -165,9 +172,12 @@ export function OpenAIKeyPrompt({ if (currentField === 'apiKey') setCurrentField('baseUrl'); else if (currentField === 'baseUrl') setCurrentField('model'); } else { - if (currentField === 'azureEndpoint') setCurrentField('azureDeployment'); - else if (currentField === 'azureDeployment') setCurrentField('azureApiKey'); - else if (currentField === 'azureApiKey') setCurrentField('azureApiVersion'); + if (currentField === 'azureEndpoint') + setCurrentField('azureDeployment'); + else if (currentField === 'azureDeployment') + setCurrentField('azureApiKey'); + else if (currentField === 'azureApiKey') + setCurrentField('azureApiVersion'); } return; } @@ -176,13 +186,19 @@ export function OpenAIKeyPrompt({ if (key.backspace || key.delete) { if (variant === 'openai') { if (currentField === 'apiKey') setApiKey((prev) => prev.slice(0, -1)); - else if (currentField === 'baseUrl') setBaseUrl((prev) => prev.slice(0, -1)); - else if (currentField === 'model') setModel((prev) => prev.slice(0, -1)); + else if (currentField === 'baseUrl') + setBaseUrl((prev) => prev.slice(0, -1)); + else if (currentField === 'model') + setModel((prev) => prev.slice(0, -1)); } else { - if (currentField === 'azureEndpoint') setAzureEndpoint((prev) => prev.slice(0, -1)); - else if (currentField === 'azureDeployment') setAzureDeployment((prev) => prev.slice(0, -1)); - else if (currentField === 'azureApiKey') setAzureApiKey((prev) => prev.slice(0, -1)); - else if (currentField === 'azureApiVersion') setAzureApiVersion((prev) => prev.slice(0, -1)); + if (currentField === 'azureEndpoint') + setAzureEndpoint((prev) => prev.slice(0, -1)); + else if (currentField === 'azureDeployment') + setAzureDeployment((prev) => prev.slice(0, -1)); + else if (currentField === 'azureApiKey') + setAzureApiKey((prev) => prev.slice(0, -1)); + else if (currentField === 'azureApiVersion') + setAzureApiVersion((prev) => prev.slice(0, -1)); } return; } @@ -197,14 +213,17 @@ export function OpenAIKeyPrompt({ width="100%" > - {variant === 'azure' ? 'Azure OpenAI Configuration Required' : 'OpenAI Configuration Required'} + {variant === 'azure' + ? 'Azure OpenAI Configuration Required' + : 'OpenAI Configuration Required'} {variant === 'openai' ? ( <> - Please enter your OpenAI configuration. You can get an API key from{' '} + Please enter your OpenAI configuration. You can get an API key + from{' '} https://platform.openai.com/api-keys @@ -213,7 +232,11 @@ export function OpenAIKeyPrompt({ - + API Key: @@ -227,7 +250,11 @@ export function OpenAIKeyPrompt({ - + Base URL: @@ -241,7 +268,11 @@ export function OpenAIKeyPrompt({ - + Model: @@ -257,7 +288,13 @@ export function OpenAIKeyPrompt({ <> - + Endpoint: @@ -271,7 +308,13 @@ export function OpenAIKeyPrompt({ - + Deployment: @@ -285,7 +328,13 @@ export function OpenAIKeyPrompt({ - + API Key: @@ -299,7 +348,13 @@ export function OpenAIKeyPrompt({ - + API Version: diff --git a/packages/core/src/core/azureOpenAIContentGenerator.ts b/packages/core/src/core/azureOpenAIContentGenerator.ts index fd81001b526..b4a843b7e82 100644 --- a/packages/core/src/core/azureOpenAIContentGenerator.ts +++ b/packages/core/src/core/azureOpenAIContentGenerator.ts @@ -56,4 +56,4 @@ export class AzureOpenAIContentGenerator extends OpenAIContentGenerator { // @ts-ignore model is private in parent but we need correct value this.model = deployment; } -} \ No newline at end of file +} diff --git a/packages/core/src/core/openaiContentGenerator.ts b/packages/core/src/core/openaiContentGenerator.ts index 99b52d2cda5..eeba5db7f81 100644 --- a/packages/core/src/core/openaiContentGenerator.ts +++ b/packages/core/src/core/openaiContentGenerator.ts @@ -96,14 +96,6 @@ export class OpenAIContentGenerator implements ContentGenerator { this.config = config; const baseURL = process.env.OPENAI_BASE_URL || ''; - // Check if using Azure OpenAI - const azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT; - const azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT; - const azureApiKey = process.env.AZURE_OPENAI_API_KEY; - const azureApiVersion = - process.env.AZURE_OPENAI_API_VERSION || '2024-05-01-preview'; - const isAzureOpenAI = !!(azureEndpoint && azureDeployment && azureApiKey); - // Configure timeout settings - using progressive timeouts const timeoutConfig = { // Base timeout for most requests (2 minutes) @@ -138,28 +130,13 @@ export class OpenAIContentGenerator implements ContentGenerator { : {}), }; - if (isAzureOpenAI) { - // Azure OpenAI configuration - this.client = new OpenAI({ - apiKey: azureApiKey, - baseURL: `${azureEndpoint}/openai/deployments/${azureDeployment}`, - defaultQuery: { 'api-version': azureApiVersion }, - defaultHeaders, - timeout: timeoutConfig.timeout, - maxRetries: timeoutConfig.maxRetries, - }); - // Use the deployment name as the model for Azure - this.model = azureDeployment; - } else { - // Standard OpenAI configuration - this.client = new OpenAI({ - apiKey, - baseURL, - timeout: timeoutConfig.timeout, - maxRetries: timeoutConfig.maxRetries, - defaultHeaders, - }); - } + this.client = new OpenAI({ + apiKey, + baseURL, + timeout: timeoutConfig.timeout, + maxRetries: timeoutConfig.maxRetries, + defaultHeaders, + }); } /** @@ -1396,40 +1373,20 @@ export class OpenAIContentGenerator implements ContentGenerator { * 2. Request-level parameters (medium priority) * 3. Default values (lowest priority) */ - private isAzureOpenAI(): boolean { - // Check if using Azure OpenAI by checking for Azure-specific environment variables - const azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT; - const azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT; - const azureApiKey = process.env.AZURE_OPENAI_API_KEY; - const isAzureEnvSet = !!(azureEndpoint && azureDeployment && azureApiKey); - - // Also check if the baseURL contains Azure-specific patterns - const baseURL = this.client?.baseURL || ''; - const isAzureURL = - baseURL.includes('azure') || baseURL.includes('openai/deployments'); - - return isAzureEnvSet || isAzureURL; - } - private buildSamplingParameters( request: GenerateContentParameters, ): Record { const configSamplingParams = this.config.getContentGeneratorConfig()?.samplingParams; - // For Azure OpenAI, temperature must be 1.0 (the default) - const isAzure = this.isAzureOpenAI(); - const temperature = isAzure - ? 1.0 // Hardcode to 1.0 for Azure OpenAI - : configSamplingParams?.temperature !== undefined - ? configSamplingParams.temperature - : request.config?.temperature !== undefined - ? request.config.temperature - : 0.0; - const params = { // Temperature: config > request > default - temperature, + temperature: + configSamplingParams?.temperature !== undefined + ? configSamplingParams.temperature + : request.config?.temperature !== undefined + ? request.config.temperature + : 0.0, // Max tokens: config > request > undefined ...(configSamplingParams?.max_tokens !== undefined From 22d54e6c1911736b808d0cb33338caf7f056d985 Mon Sep 17 00:00:00 2001 From: Krtin Shet Date: Fri, 17 Oct 2025 15:54:54 +0530 Subject: [PATCH 4/4] fix: address Azure OpenAI PR review comments and improve implementation --- package-lock.json | 39 +++++++++++------ packages/cli/src/config/auth.ts | 41 +++++++++--------- packages/cli/src/ui/components/AuthDialog.tsx | 42 ++++++++++++------- .../cli/src/ui/components/OpenAIKeyPrompt.tsx | 3 +- packages/core/src/core/contentGenerator.ts | 6 +-- 5 files changed, 79 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3bd6e6d7663..84b7e33340a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -290,6 +290,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -313,6 +314,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1209,6 +1211,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.1.tgz", "integrity": "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", @@ -1270,6 +1273,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2170,8 +2174,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/body-parser": { "version": "1.19.6", @@ -2432,6 +2435,7 @@ "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2442,6 +2446,7 @@ "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2598,6 +2603,7 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -2989,6 +2995,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4354,8 +4361,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-serializer": { "version": "2.0.0", @@ -4814,6 +4820,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -5202,6 +5209,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6324,6 +6332,7 @@ "resolved": "https://registry.npmjs.org/ink/-/ink-6.0.1.tgz", "integrity": "sha512-vhhFrCodTHZAPPSdMYzLEbeI0Ug37R9j6yA0kLKok9kSK53lQtj/RJhEQJUjq6OwT4N33nxqSRd/7yXhEhVPIw==", "license": "MIT", + "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^7.0.0", @@ -7662,7 +7671,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -8984,6 +8992,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8994,6 +9003,7 @@ "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -9027,6 +9037,7 @@ "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -10391,6 +10402,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10575,7 +10587,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/type-check": { "version": "0.4.0", @@ -10700,6 +10713,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10994,6 +11008,7 @@ "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", @@ -11107,6 +11122,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11120,6 +11136,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -11685,6 +11702,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -11784,7 +11802,6 @@ "version": "10.4.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -11805,7 +11822,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -11847,7 +11863,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -11858,7 +11873,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -11870,7 +11884,6 @@ "version": "5.3.0", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -11884,8 +11897,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "packages/cli/node_modules/string-width": { "version": "7.2.0", @@ -12043,6 +12055,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/packages/cli/src/config/auth.ts b/packages/cli/src/config/auth.ts index 1774bcde2a5..4a4d57e66f5 100644 --- a/packages/cli/src/config/auth.ts +++ b/packages/cli/src/config/auth.ts @@ -39,18 +39,24 @@ export const validateAuthMethod = (authMethod: string): string | null => { } if (authMethod === AuthType.AZURE_OPENAI) { - const hasCore = - !!process.env.AZURE_OPENAI_ENDPOINT && - !!process.env.AZURE_OPENAI_DEPLOYMENT; - const hasCred = - !!process.env.AZURE_OPENAI_API_KEY || - !!process.env.AZURE_OPENAI_BEARER_TOKEN; + const hasEndpoint = !!process.env.AZURE_OPENAI_ENDPOINT; + const hasDeployment = !!process.env.AZURE_OPENAI_DEPLOYMENT; + const hasApiKey = !!process.env.AZURE_OPENAI_API_KEY; + const hasBearerToken = !!process.env.AZURE_OPENAI_BEARER_TOKEN; + const hasCred = hasApiKey || hasBearerToken; - if (!hasCore || !hasCred) { + const missingVars = []; + if (!hasEndpoint) missingVars.push('AZURE_OPENAI_ENDPOINT'); + if (!hasDeployment) missingVars.push('AZURE_OPENAI_DEPLOYMENT'); + if (!hasCred) + missingVars.push('AZURE_OPENAI_API_KEY or AZURE_OPENAI_BEARER_TOKEN'); + + if (!hasEndpoint || !hasDeployment || !hasCred) { return ( - 'Azure OpenAI configuration not found. You must set:\n' + - '• AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT and\n' + - ' either AZURE_OPENAI_API_KEY or AZURE_OPENAI_BEARER_TOKEN.\n' + + 'Azure OpenAI configuration incomplete. Missing:\n' + + '• ' + + missingVars.join('\n• ') + + '\n' + 'You can enter these interactively or add them to your .env file.' ); } @@ -59,18 +65,15 @@ export const validateAuthMethod = (authMethod: string): string | null => { if (authMethod === AuthType.USE_OPENAI) { const isOpenAIKeySet = !!process.env.OPENAI_API_KEY; - const isAzureConfigSet = !!( - process.env.AZURE_OPENAI_ENDPOINT && - process.env.AZURE_OPENAI_DEPLOYMENT && - process.env.AZURE_OPENAI_API_KEY - ); - if (!isOpenAIKeySet && !isAzureConfigSet) { + // For USE_OPENAI auth type, only validate OpenAI API key + // Azure configuration should use AZURE_OPENAI auth type instead + if (!isOpenAIKeySet) { return ( - 'OpenAI configuration not found. You must set either:\n' + + 'OpenAI configuration not found. You must set:\n' + '• OPENAI_API_KEY environment variable for standard OpenAI\n' + - '• AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT, and AZURE_OPENAI_API_KEY for Azure OpenAI\n' + - 'You can enter these interactively or add them to your .env file.' + 'You can enter this interactively or add it to your .env file.\n' + + '\nNote: For Azure OpenAI, please select "Azure OpenAI" authentication instead.' ); } return null; diff --git a/packages/cli/src/ui/components/AuthDialog.tsx b/packages/cli/src/ui/components/AuthDialog.tsx index 34bb2d33f1a..4cf5983df72 100644 --- a/packages/cli/src/ui/components/AuthDialog.tsx +++ b/packages/cli/src/ui/components/AuthDialog.tsx @@ -85,16 +85,26 @@ export function AuthDialog({ const handleAuthSelect = (authMethod: AuthType) => { const error = validateAuthMethod(authMethod); if (error) { - // Decide which prompt variant is needed + // Decide which prompt variant is needed based on missing configuration if (authMethod === AuthType.USE_OPENAI) { - const missingOpenAI = !process.env.OPENAI_API_KEY; - if (missingOpenAI) { + // For OpenAI, check if we have a standard OpenAI API key + const hasOpenAIKey = !!process.env.OPENAI_API_KEY; + // Also check if user might be trying to use Azure config with OpenAI auth type + const hasAzureConfig = !!( + process.env.AZURE_OPENAI_ENDPOINT && + process.env.AZURE_OPENAI_DEPLOYMENT && + (process.env.AZURE_OPENAI_API_KEY || + process.env.AZURE_OPENAI_BEARER_TOKEN) + ); + + if (!hasOpenAIKey && !hasAzureConfig) { setPromptVariant('openai'); setShowOpenAIKeyPrompt(true); setErrorMessage(null); return; } } else if (authMethod === AuthType.AZURE_OPENAI) { + // For Azure OpenAI, specifically check for Azure configuration const missingAzure = !( process.env.AZURE_OPENAI_API_KEY || @@ -102,6 +112,7 @@ export function AuthDialog({ ) || !process.env.AZURE_OPENAI_ENDPOINT || !process.env.AZURE_OPENAI_DEPLOYMENT; + if (missingAzure) { setPromptVariant('azure'); setShowOpenAIKeyPrompt(true); @@ -122,22 +133,21 @@ export function AuthDialog({ baseUrl: string, model: string, ) => { - setOpenAIApiKey(apiKey); - setOpenAIBaseUrl(baseUrl); - setOpenAIModel(model); + // Don't set OpenAI environment variables if we're in Azure mode + if (promptVariant === 'openai') { + setOpenAIApiKey(apiKey); + setOpenAIBaseUrl(baseUrl); + setOpenAIModel(model); + } + // Azure environment variables are already set in the prompt component setShowOpenAIKeyPrompt(false); - // Decide which auth type is appropriate based on envs set by prompt - const isAzure = - process.env.AZURE_OPENAI_ENDPOINT && - process.env.AZURE_OPENAI_DEPLOYMENT && - (process.env.AZURE_OPENAI_API_KEY || - process.env.AZURE_OPENAI_BEARER_TOKEN); + // Decide which auth type is appropriate based on the prompt variant + // This ensures we don't accidentally detect Azure config when user intended OpenAI + const selectedAuthType = + promptVariant === 'azure' ? AuthType.AZURE_OPENAI : AuthType.USE_OPENAI; - onSelect( - isAzure ? AuthType.AZURE_OPENAI : AuthType.USE_OPENAI, - SettingScope.User, - ); + onSelect(selectedAuthType, SettingScope.User); }; const handleOpenAIKeyCancel = () => { diff --git a/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx index c23ac80c34e..ff141adc9b5 100644 --- a/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx +++ b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx @@ -117,7 +117,8 @@ export function OpenAIKeyPrompt({ if (azureApiVersion) { process.env.AZURE_OPENAI_API_VERSION = azureApiVersion; } - onSubmit('azure-api-key', '', azureDeployment); + // For Azure, use the deployment name as the model + onSubmit(azureApiKey, '', azureDeployment); } else { setCurrentField('azureEndpoint'); } diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 49b8c7dea21..9a5cd24c94e 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -133,9 +133,9 @@ export function createContentGeneratorConfig( const azureBearer = process.env.AZURE_OPENAI_BEARER_TOKEN; if (azureEndpoint && azureDeployment && (azureApiKey || azureBearer)) { contentGeneratorConfig.apiKey = azureApiKey ?? ''; - // Use deployment name as model if not explicitly set - contentGeneratorConfig.model = - process.env.OPENAI_MODEL || azureDeployment; + // For Azure OpenAI, always use the deployment name as the model + // The Azure OpenAI API requires the deployment name, not a model name + contentGeneratorConfig.model = azureDeployment; return contentGeneratorConfig; } }