Jelajahi Sumber

refactor: update LLM provider configurations and enhance Azure support

- Updated `ProviderConfig` interface to include Azure-specific fields: `azureDeploymentName` and `azureApiVersion`.
- Enhanced `createOpenAIChatModel` function to accept optional fetch options.
- Added logging for provider configurations in the background script.
- Updated `ModelSettings` component to handle new Azure fields and validation.
- Refactored model input handling to accommodate Azure deployments.
- Consolidated imports in various files for better organization.
- Updated `pnpm-lock.yaml` to reflect dependency changes.
Burak Sormageç 4 bulan lalu
induk
melakukan
651aadf7bd

+ 94 - 7
chrome-extension/src/background/agent/helper.ts

@@ -1,5 +1,5 @@
 import { type ProviderConfig, type ModelConfig, ProviderTypeEnum } from '@extension/storage';
-import { ChatOpenAI } from '@langchain/openai';
+import { ChatOpenAI, AzureChatOpenAI } from '@langchain/openai';
 import { ChatAnthropic } from '@langchain/anthropic';
 import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
 import { ChatXAI } from '@langchain/xai';
@@ -13,11 +13,22 @@ function isOpenAIOModel(modelName: string): boolean {
   return modelName.startsWith('openai/o') || modelName.startsWith('o');
 }
 
-function createOpenAIChatModel(providerConfig: ProviderConfig, modelConfig: ModelConfig): BaseChatModel {
+function createOpenAIChatModel(
+  providerConfig: ProviderConfig,
+  modelConfig: ModelConfig,
+  // Add optional extra fetch options for headers etc.
+  extraFetchOptions?: { headers?: Record<string, string> },
+): BaseChatModel {
   const args: {
     model: string;
     apiKey?: string;
-    configuration?: Record<string, unknown>;
+    // Configuration should align with ClientOptions from @langchain/openai
+    configuration?: {
+      baseURL?: string;
+      defaultHeaders?: Record<string, string>;
+      // Add other ClientOptions if needed, e.g.?
+      // dangerouslyAllowBrowser?: boolean;
+    };
     modelKwargs?: { max_completion_tokens: number };
     topP?: number;
     temperature?: number;
@@ -25,13 +36,23 @@ function createOpenAIChatModel(providerConfig: ProviderConfig, modelConfig: Mode
   } = {
     model: modelConfig.modelName,
     apiKey: providerConfig.apiKey,
+    // Initialize configuration object
+    configuration: {},
   };
 
   if (providerConfig.baseUrl) {
-    args.configuration = {
-      baseURL: providerConfig.baseUrl,
+    // Set baseURL inside configuration
+    args.configuration!.baseURL = providerConfig.baseUrl;
+  }
+
+  // Merge extra headers if provided
+  if (extraFetchOptions?.headers) {
+    args.configuration!.defaultHeaders = {
+      ...(args.configuration!.defaultHeaders || {}),
+      ...extraFetchOptions.headers,
     };
   }
+
   // custom provider may have no api key
   if (providerConfig.apiKey) {
     args.apiKey = providerConfig.apiKey;
@@ -47,9 +68,26 @@ function createOpenAIChatModel(providerConfig: ProviderConfig, modelConfig: Mode
     args.temperature = (modelConfig.parameters?.temperature ?? 0.1) as number;
     args.maxTokens = maxTokens;
   }
+  // Log args being passed to ChatOpenAI constructor inside the helper
+  console.log('[createOpenAIChatModel] Args passed to new ChatOpenAI:', args);
   return new ChatOpenAI(args);
 }
 
+// Function to extract instance name from Azure endpoint URL
+function extractInstanceNameFromUrl(url: string): string | null {
+  try {
+    const parsedUrl = new URL(url);
+    const hostnameParts = parsedUrl.hostname.split('.');
+    // Expecting format like instance-name.openai.azure.com
+    if (hostnameParts.length >= 4 && hostnameParts[1] === 'openai' && hostnameParts[2] === 'azure') {
+      return hostnameParts[0];
+    }
+  } catch (e) {
+    console.error('Error parsing Azure endpoint URL:', e);
+  }
+  return null;
+}
+
 // create a chat model based on the agent name, the model name and provider
 export function createChatModel(providerConfig: ProviderConfig, modelConfig: ModelConfig): BaseChatModel {
   const temperature = (modelConfig.parameters?.temperature ?? 0.1) as number;
@@ -57,7 +95,8 @@ export function createChatModel(providerConfig: ProviderConfig, modelConfig: Mod
 
   switch (modelConfig.provider) {
     case ProviderTypeEnum.OpenAI: {
-      return createOpenAIChatModel(providerConfig, modelConfig);
+      // Call helper without extra options
+      return createOpenAIChatModel(providerConfig, modelConfig, undefined);
     }
     case ProviderTypeEnum.Anthropic: {
       const args = {
@@ -125,9 +164,57 @@ export function createChatModel(providerConfig: ProviderConfig, modelConfig: Mod
       };
       return new ChatOllama(args);
     }
+    case ProviderTypeEnum.AzureOpenAI: {
+      // Validate necessary fields first
+      if (
+        !providerConfig.baseUrl ||
+        !providerConfig.azureDeploymentName ||
+        !providerConfig.azureApiVersion ||
+        !providerConfig.apiKey
+      ) {
+        throw new Error(
+          'Azure configuration is incomplete. Endpoint, Deployment Name, API Version, and API Key are required. Please check settings.',
+        );
+      }
+
+      // Extract instance name from the endpoint URL
+      const instanceName = extractInstanceNameFromUrl(providerConfig.baseUrl);
+      if (!instanceName) {
+        throw new Error(
+          `Could not extract Instance Name from Azure Endpoint URL: ${providerConfig.baseUrl}. Expected format like https://<your-instance-name>.openai.azure.com/`,
+        );
+      }
+
+      // Use AzureChatOpenAI with specific parameters
+      const args = {
+        azureOpenAIApiInstanceName: instanceName, // Derived from endpoint
+        azureOpenAIApiDeploymentName: providerConfig.azureDeploymentName,
+        azureOpenAIApiKey: providerConfig.apiKey,
+        azureOpenAIApiVersion: providerConfig.azureApiVersion,
+        temperature,
+        topP,
+        maxTokens,
+        // DO NOT pass baseUrl or configuration here
+      };
+      console.log('[createChatModel] Azure args passed to AzureChatOpenAI:', args);
+      return new AzureChatOpenAI(args);
+    }
+    case ProviderTypeEnum.OpenRouter: {
+      // Call the helper function, passing OpenRouter headers via the third argument
+      console.log('[createChatModel] Calling createOpenAIChatModel for OpenRouter');
+      return createOpenAIChatModel(providerConfig, modelConfig, {
+        headers: {
+          'HTTP-Referer': 'nanobrowser-extension',
+          'X-Title': 'NanoBrowser Extension',
+        },
+      });
+    }
     default: {
+      // Handles CustomOpenAI
       // by default, we think it's a openai-compatible provider
-      return createOpenAIChatModel(providerConfig, modelConfig);
+      // Pass undefined for extraFetchOptions for default/custom cases
+      console.log('[createChatModel] Calling createOpenAIChatModel for default/custom provider');
+      return createOpenAIChatModel(providerConfig, modelConfig, undefined);
     }
   }
 }

+ 1 - 2
chrome-extension/src/background/dom/views.ts

@@ -1,5 +1,4 @@
-import type { ViewportInfo, CoordinateSet } from './history/view';
-import type { HashedDomElement } from './history/view';
+import type { ViewportInfo, CoordinateSet, HashedDomElement } from './history/view';
 import { HistoryTreeProcessor } from './history/service';
 
 export abstract class DOMBaseNode {

+ 13 - 4
chrome-extension/src/background/index.ts

@@ -5,7 +5,7 @@ import { Executor } from './agent/executor';
 import { createLogger } from './log';
 import { ExecutionState } from './agent/event/types';
 import { createChatModel } from './agent/helper';
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
+import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
 
 const logger = createLogger('background');
 
@@ -182,18 +182,27 @@ async function setupExecutor(taskId: string, task: string, browserContext: Brows
   if (!navigatorModel) {
     throw new Error('Please choose a model for the navigator in the settings first');
   }
-  const navigatorLLM = createChatModel(providers[navigatorModel.provider], navigatorModel);
+  // Log the provider config being used for the navigator
+  const navigatorProviderConfig = providers[navigatorModel.provider];
+  logger.info('Navigator Provider Config:', JSON.stringify(navigatorProviderConfig, null, 2));
+  const navigatorLLM = createChatModel(navigatorProviderConfig, navigatorModel);
 
   let plannerLLM: BaseChatModel | null = null;
   const plannerModel = agentModels[AgentNameEnum.Planner];
   if (plannerModel) {
-    plannerLLM = createChatModel(providers[plannerModel.provider], plannerModel);
+    // Log the provider config being used for the planner
+    const plannerProviderConfig = providers[plannerModel.provider];
+    logger.info('Planner Provider Config:', JSON.stringify(plannerProviderConfig, null, 2));
+    plannerLLM = createChatModel(plannerProviderConfig, plannerModel);
   }
 
   let validatorLLM: BaseChatModel | null = null;
   const validatorModel = agentModels[AgentNameEnum.Validator];
   if (validatorModel) {
-    validatorLLM = createChatModel(providers[validatorModel.provider], validatorModel);
+    // Log the provider config being used for the validator
+    const validatorProviderConfig = providers[validatorModel.provider];
+    logger.info('Validator Provider Config:', JSON.stringify(validatorProviderConfig, null, 2));
+    validatorLLM = createChatModel(validatorProviderConfig, validatorModel);
   }
 
   const generalSettings = await generalSettingsStore.getSettings();

+ 99 - 15
packages/storage/lib/settings/llmProviders.ts

@@ -8,9 +8,12 @@ export interface ProviderConfig {
   name?: string; // Display name in the options
   type?: ProviderTypeEnum; // Help to decide which LangChain ChatModel package to use
   apiKey: string; // Must be provided, but may be empty for local models
-  baseUrl?: string; // Optional base URL if provided
-  modelNames?: string[]; // Chosen model names, if not provided use hardcoded names from llmProviderModelNames
+  baseUrl?: string; // Optional base URL if provided // For Azure: Endpoint
+  modelNames?: string[]; // Chosen model names (NOT used for Azure OpenAI anymore)
   createdAt?: number; // Timestamp in milliseconds when the provider was created
+  // Azure Specific Fields:
+  azureDeploymentName?: string;
+  azureApiVersion?: string;
 }
 
 // Interface for storing multiple LLM provider configurations
@@ -48,6 +51,8 @@ export function getProviderTypeByProviderId(providerId: string): ProviderTypeEnu
     case ProviderTypeEnum.Gemini:
     case ProviderTypeEnum.Grok:
     case ProviderTypeEnum.Ollama:
+    case ProviderTypeEnum.AzureOpenAI:
+    case ProviderTypeEnum.OpenRouter:
       return providerId;
     default:
       return ProviderTypeEnum.CustomOpenAI;
@@ -70,13 +75,16 @@ export function getDefaultDisplayNameFromProviderId(providerId: string): string
       return 'Grok';
     case ProviderTypeEnum.Ollama:
       return 'Ollama';
+    case ProviderTypeEnum.AzureOpenAI:
+      return 'Azure OpenAI';
+    case ProviderTypeEnum.OpenRouter:
+      return 'OpenRouter';
     default:
       return providerId; // Use the provider id as display name for custom providers by default
   }
 }
 
 // Get default configuration for built-in providers
-// Make sure to update this function if you add a new provider type
 export function getDefaultProviderConfig(providerId: string): ProviderConfig {
   switch (providerId) {
     case ProviderTypeEnum.OpenAI:
@@ -84,10 +92,12 @@ export function getDefaultProviderConfig(providerId: string): ProviderConfig {
     case ProviderTypeEnum.DeepSeek:
     case ProviderTypeEnum.Gemini:
     case ProviderTypeEnum.Grok:
+    case ProviderTypeEnum.OpenRouter: // OpenRouter uses modelNames
       return {
         apiKey: '',
         name: getDefaultDisplayNameFromProviderId(providerId),
         type: providerId,
+        baseUrl: providerId === ProviderTypeEnum.OpenRouter ? 'https://openrouter.ai/api/v1' : undefined,
         modelNames: [...(llmProviderModelNames[providerId] || [])],
         createdAt: Date.now(),
       };
@@ -97,17 +107,28 @@ export function getDefaultProviderConfig(providerId: string): ProviderConfig {
         apiKey: 'ollama', // Set default API key for Ollama
         name: getDefaultDisplayNameFromProviderId(ProviderTypeEnum.Ollama),
         type: ProviderTypeEnum.Ollama,
-        modelNames: [],
+        modelNames: [], // Ollama uses modelNames (user adds them)
         baseUrl: 'http://localhost:11434',
         createdAt: Date.now(),
       };
-    default:
+    case ProviderTypeEnum.AzureOpenAI:
+      return {
+        apiKey: '', // User needs to provide API Key
+        name: getDefaultDisplayNameFromProviderId(ProviderTypeEnum.AzureOpenAI),
+        type: ProviderTypeEnum.AzureOpenAI,
+        baseUrl: '', // User needs to provide Azure endpoint
+        // modelNames: [], // Not used for Azure configuration anymore
+        azureDeploymentName: '', // User needs to provide Deployment Name
+        azureApiVersion: '2024-02-15-preview', // Provide a common default API version
+        createdAt: Date.now(),
+      };
+    default: // Handles CustomOpenAI
       return {
         apiKey: '',
         name: getDefaultDisplayNameFromProviderId(providerId),
         type: ProviderTypeEnum.CustomOpenAI,
         baseUrl: '',
-        modelNames: [],
+        modelNames: [], // Custom providers use modelNames
         createdAt: Date.now(),
       };
   }
@@ -123,20 +144,51 @@ export function getDefaultAgentModelParams(providerId: string, agentName: AgentN
 
 // Helper function to ensure backward compatibility for provider configs
 function ensureBackwardCompatibility(providerId: string, config: ProviderConfig): ProviderConfig {
+  // Log input config
+  // console.log(`[ensureBackwardCompatibility] Input for ${providerId}:`, JSON.stringify(config));
+
   const updatedConfig = { ...config };
+
+  // Ensure name exists
   if (!updatedConfig.name) {
     updatedConfig.name = getDefaultDisplayNameFromProviderId(providerId);
   }
+  // Ensure type exists
   if (!updatedConfig.type) {
     updatedConfig.type = getProviderTypeByProviderId(providerId);
   }
-  if (!updatedConfig.modelNames) {
-    updatedConfig.modelNames = llmProviderModelNames[providerId as keyof typeof llmProviderModelNames] || [];
+
+  // Handle Azure specifics
+  if (updatedConfig.type === ProviderTypeEnum.AzureOpenAI) {
+    // Ensure Azure fields exist, provide defaults if missing
+    if (updatedConfig.azureDeploymentName === undefined) {
+      // console.log(`[ensureBackwardCompatibility] Adding default azureDeploymentName for ${providerId}`);
+      updatedConfig.azureDeploymentName = '';
+    }
+    if (updatedConfig.azureApiVersion === undefined) {
+      // console.log(`[ensureBackwardCompatibility] Adding default azureApiVersion for ${providerId}`);
+      updatedConfig.azureApiVersion = '2024-02-15-preview';
+    }
+    // CRITICAL: Delete modelNames if it exists for Azure type to clean up old configs
+    if (Object.prototype.hasOwnProperty.call(updatedConfig, 'modelNames')) {
+      // console.log(`[ensureBackwardCompatibility] Deleting modelNames for Azure config ${providerId}`);
+      delete updatedConfig.modelNames;
+    }
+  } else {
+    // Ensure modelNames exists ONLY for non-Azure types
+    if (!updatedConfig.modelNames) {
+      // console.log(`[ensureBackwardCompatibility] Adding default modelNames for non-Azure ${providerId}`);
+      updatedConfig.modelNames = llmProviderModelNames[providerId as keyof typeof llmProviderModelNames] || [];
+    }
   }
+
+  // Ensure createdAt exists
   if (!updatedConfig.createdAt) {
-    // if createdAt is not set, set it to "03/04/2025" for backward compatibility
     updatedConfig.createdAt = new Date('03/04/2025').getTime();
   }
+
+  // Log output config
+  // console.log(`[ensureBackwardCompatibility] Output for ${providerId}:`, JSON.stringify(updatedConfig));
   return updatedConfig;
 }
 
@@ -151,19 +203,51 @@ export const llmProviderStore: LLMProviderStorage = {
       throw new Error('API key must be provided (can be empty for local models)');
     }
 
-    if (!config.modelNames) {
-      throw new Error('Model names must be provided');
+    const providerType = config.type || getProviderTypeByProviderId(providerId);
+
+    if (providerType === ProviderTypeEnum.AzureOpenAI) {
+      if (!config.baseUrl?.trim()) {
+        throw new Error('Azure Endpoint (baseUrl) is required');
+      }
+      if (!config.azureDeploymentName?.trim()) {
+        throw new Error('Azure Deployment Name is required');
+      }
+      if (!config.azureApiVersion?.trim()) {
+        throw new Error('Azure API Version is required');
+      }
+      if (!config.apiKey?.trim()) {
+        throw new Error('API Key is required for Azure OpenAI');
+      }
+    } else if (providerType !== ProviderTypeEnum.CustomOpenAI && providerType !== ProviderTypeEnum.Ollama) {
+      if (!config.apiKey?.trim()) {
+        throw new Error(`API Key is required for ${getDefaultDisplayNameFromProviderId(providerId)}`);
+      }
+    }
+
+    if (providerType !== ProviderTypeEnum.AzureOpenAI) {
+      if (!config.modelNames || config.modelNames.length === 0) {
+        console.warn(`Provider ${providerId} of type ${providerType} is being saved without model names.`);
+      }
     }
 
-    // Ensure backward compatibility by filling in missing fields
     const completeConfig: ProviderConfig = {
-      ...config,
+      apiKey: config.apiKey || '',
+      baseUrl: config.baseUrl,
       name: config.name || getDefaultDisplayNameFromProviderId(providerId),
-      type: config.type || getProviderTypeByProviderId(providerId),
-      modelNames: config.modelNames,
+      type: providerType,
       createdAt: config.createdAt || Date.now(),
+      ...(providerType === ProviderTypeEnum.AzureOpenAI
+        ? {
+            azureDeploymentName: config.azureDeploymentName,
+            azureApiVersion: config.azureApiVersion,
+          }
+        : {
+            modelNames: config.modelNames || [],
+          }),
     };
 
+    console.log(`[llmProviderStore.setProvider] Saving config for ${providerId}:`, JSON.stringify(completeConfig));
+
     const current = (await storage.get()) || { providers: {} };
     await storage.set({
       providers: {

+ 37 - 0
packages/storage/lib/settings/types.ts

@@ -15,6 +15,8 @@ export enum ProviderTypeEnum {
   Gemini = 'gemini',
   Grok = 'grok',
   Ollama = 'ollama',
+  AzureOpenAI = 'azure_openai',
+  OpenRouter = 'openrouter',
   CustomOpenAI = 'custom_openai',
 }
 
@@ -31,6 +33,13 @@ export const llmProviderModelNames = {
   ],
   [ProviderTypeEnum.Grok]: ['grok-2', 'grok-2-vision'],
   [ProviderTypeEnum.Ollama]: [],
+  [ProviderTypeEnum.AzureOpenAI]: ['gpt-4o', 'gpt-4o-mini', 'o3-mini'],
+  [ProviderTypeEnum.OpenRouter]: [
+    'openai/gpt-4o',
+    'anthropic/claude-3.5-sonnet',
+    'google/gemini-pro-1.5',
+    'mistralai/mistral-large',
+  ],
   // Custom OpenAI providers don't have predefined models as they are user-defined
 };
 
@@ -106,4 +115,32 @@ export const llmProviderParameters = {
       topP: 0.001,
     },
   },
+  [ProviderTypeEnum.AzureOpenAI]: {
+    [AgentNameEnum.Planner]: {
+      temperature: 0.01,
+      topP: 0.001,
+    },
+    [AgentNameEnum.Navigator]: {
+      temperature: 0,
+      topP: 0.001,
+    },
+    [AgentNameEnum.Validator]: {
+      temperature: 0,
+      topP: 0.001,
+    },
+  },
+  [ProviderTypeEnum.OpenRouter]: {
+    [AgentNameEnum.Planner]: {
+      temperature: 0.01,
+      topP: 0.001,
+    },
+    [AgentNameEnum.Navigator]: {
+      temperature: 0,
+      topP: 0.001,
+    },
+    [AgentNameEnum.Validator]: {
+      temperature: 0,
+      topP: 0.001,
+    },
+  },
 };

+ 387 - 265
pages/options/src/components/ModelSettings.tsx

@@ -10,6 +10,7 @@ import {
   getDefaultDisplayNameFromProviderId,
   getDefaultProviderConfig,
   getDefaultAgentModelParams,
+  type ProviderConfig,
 } from '@extension/storage';
 
 interface ModelSettingsProps {
@@ -17,19 +18,7 @@ interface ModelSettingsProps {
 }
 
 export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
-  const [providers, setProviders] = useState<
-    Record<
-      string,
-      {
-        apiKey: string;
-        baseUrl?: string;
-        name?: string;
-        modelNames?: string[];
-        type?: ProviderTypeEnum;
-        createdAt?: number;
-      }
-    >
-  >({});
+  const [providers, setProviders] = useState<Record<string, ProviderConfig>>({});
   const [modifiedProviders, setModifiedProviders] = useState<Set<string>>(new Set());
   const [providersFromStorage, setProvidersFromStorage] = useState<Set<string>>(new Set());
   const [selectedModels, setSelectedModels] = useState<Record<AgentNameEnum, string>>({
@@ -305,7 +294,6 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
   const getButtonProps = (provider: string) => {
     const isInStorage = providersFromStorage.has(provider);
     const isModified = modifiedProviders.has(provider);
-    const isCustom = providers[provider]?.type === ProviderTypeEnum.CustomOpenAI;
 
     // For deletion, we only care if it's in storage and not modified
     if (isInStorage && !isModified) {
@@ -318,8 +306,28 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
     }
 
     // For saving, we need to check if it has the required inputs
-    // Only custom providers can be saved without an API key
-    const hasInput = isCustom || Boolean(providers[provider]?.apiKey?.trim());
+    let hasInput = false;
+    const providerType = providers[provider]?.type;
+    const config = providers[provider];
+
+    if (providerType === ProviderTypeEnum.CustomOpenAI) {
+      hasInput = Boolean(config?.baseUrl?.trim()); // Custom needs Base URL, name checked elsewhere
+    } else if (providerType === ProviderTypeEnum.Ollama) {
+      hasInput = Boolean(config?.baseUrl?.trim()); // Ollama needs Base URL
+    } else if (providerType === ProviderTypeEnum.AzureOpenAI) {
+      // Azure needs API Key, Endpoint, Deployment Name, and API Version
+      hasInput =
+        Boolean(config?.apiKey?.trim()) &&
+        Boolean(config?.baseUrl?.trim()) &&
+        Boolean(config?.azureDeploymentName?.trim()) &&
+        Boolean(config?.azureApiVersion?.trim());
+    } else if (providerType === ProviderTypeEnum.OpenRouter) {
+      // OpenRouter needs API Key and optionally Base URL (has default)
+      hasInput = Boolean(config?.apiKey?.trim()) && Boolean(config?.baseUrl?.trim());
+    } else {
+      // Other built-in providers just need API Key
+      hasInput = Boolean(config?.apiKey?.trim());
+    }
 
     return {
       theme: isDarkMode ? 'dark' : 'light',
@@ -340,20 +348,15 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
         return;
       }
 
-      // Check if base URL is required but missing for custom_openai
+      // Check if base URL is required but missing for custom_openai, ollama, azure_openai or openrouter
       if (
-        providers[provider].type === ProviderTypeEnum.CustomOpenAI &&
+        (providers[provider].type === ProviderTypeEnum.CustomOpenAI ||
+          providers[provider].type === ProviderTypeEnum.Ollama ||
+          providers[provider].type === ProviderTypeEnum.AzureOpenAI ||
+          providers[provider].type === ProviderTypeEnum.OpenRouter) &&
         (!providers[provider].baseUrl || !providers[provider].baseUrl.trim())
       ) {
-        alert('Base URL is required for custom OpenAI-compatible API providers');
-        return;
-      }
-
-      // Check if API key is required but empty for built-in providers (except custom)
-      const isCustom = providers[provider].type === ProviderTypeEnum.CustomOpenAI;
-
-      if (!isCustom && (!providers[provider].apiKey || !providers[provider].apiKey.trim())) {
-        alert('API key is required for this provider');
+        alert(`Base URL is required for ${getDefaultDisplayNameFromProviderId(provider)}. Please enter it.`);
         return;
       }
 
@@ -364,15 +367,31 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
         modelNames = [...(llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [])];
       }
 
-      // The provider store will handle filling in the missing fields
-      await llmProviderStore.setProvider(provider, {
-        apiKey: providers[provider].apiKey || '',
-        baseUrl: providers[provider].baseUrl,
-        name: providers[provider].name,
-        modelNames: modelNames,
-        type: providers[provider].type,
-        createdAt: providers[provider].createdAt,
-      });
+      // Prepare data for saving using the correctly typed config from state
+      // We can directly pass the relevant parts of the state config
+      // Create a copy to avoid modifying state directly if needed, though setProvider likely handles it
+      const configToSave: Partial<ProviderConfig> = { ...providers[provider] }; // Use Partial to allow deleting modelNames
+
+      // Explicitly set required fields that might be missing in partial state updates (though unlikely now)
+      configToSave.apiKey = providers[provider].apiKey || '';
+      configToSave.name = providers[provider].name || getDefaultDisplayNameFromProviderId(provider);
+      configToSave.type = providers[provider].type;
+      configToSave.createdAt = providers[provider].createdAt || Date.now();
+      // baseUrl, azureDeploymentName, azureApiVersion should be correctly set by handlers
+
+      if (providers[provider].type === ProviderTypeEnum.AzureOpenAI) {
+        // Ensure modelNames is NOT included for Azure
+        delete configToSave.modelNames;
+      } else {
+        // Ensure modelNames IS included for non-Azure
+        // Use existing modelNames from state, or default if somehow missing
+        configToSave.modelNames =
+          providers[provider].modelNames || llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [];
+      }
+
+      // Pass the cleaned config to setProvider
+      // Cast to ProviderConfig as we've ensured necessary fields based on type
+      await llmProviderStore.setProvider(provider, configToSave as ProviderConfig);
 
       // Clear any name errors on successful save
       setNameErrors(prev => {
@@ -734,7 +753,13 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
   // Sort providers to ensure newly added providers appear at the bottom
   const getSortedProviders = () => {
     // Filter providers to only include those from storage and newly added providers
-    const filteredProviders = Object.entries(providers).filter(([providerId]) => {
+    const filteredProviders = Object.entries(providers).filter(([providerId, config]) => {
+      // ALSO filter out any provider missing a config or type, to satisfy TS
+      if (!config || !config.type) {
+        console.warn(`Filtering out provider ${providerId} with missing config or type.`);
+        return false;
+      }
+
       // Include if it's from storage
       if (providersFromStorage.has(providerId)) {
         return true;
@@ -810,6 +835,29 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
     return '';
   };
 
+  // Add state handlers for new Azure fields
+  const handleAzureDeploymentNameChange = (provider: string, deploymentName: string) => {
+    setModifiedProviders(prev => new Set(prev).add(provider));
+    setProviders(prev => ({
+      ...prev,
+      [provider]: {
+        ...prev[provider],
+        azureDeploymentName: deploymentName.trim(),
+      },
+    }));
+  };
+
+  const handleAzureApiVersionChange = (provider: string, apiVersion: string) => {
+    setModifiedProviders(prev => new Set(prev).add(provider));
+    setProviders(prev => ({
+      ...prev,
+      [provider]: {
+        ...prev[provider],
+        azureApiVersion: apiVersion.trim(),
+      },
+    }));
+  };
+
   return (
     <section className="space-y-6">
       {/* LLM Providers Section */}
@@ -824,265 +872,339 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
               <p className="mb-4">No providers configured yet. Add a provider to get started.</p>
             </div>
           ) : (
-            getSortedProviders().map(([providerId, providerConfig]) => (
-              <div
-                key={providerId}
-                id={`provider-${providerId}`}
-                className={`space-y-4 ${modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) ? `rounded-lg border p-4 ${isDarkMode ? 'border-blue-700 bg-slate-700' : 'border-blue-200 bg-blue-50/70'}` : ''}`}>
-                <div className="flex items-center justify-between">
-                  <h3 className={`text-lg font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
-                    {providerConfig.name || providerId}
-                  </h3>
-                  <div className="flex space-x-2">
-                    {/* Show Cancel button for newly added providers */}
-                    {modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) && (
-                      <Button variant="secondary" onClick={() => handleCancelProvider(providerId)}>
-                        Cancel
+            getSortedProviders().map(([providerId, providerConfig]) => {
+              // Add type guard to satisfy TypeScript
+              if (!providerConfig || !providerConfig.type) {
+                console.warn(`Skipping rendering for providerId ${providerId} due to missing config or type`);
+                return null; // Skip rendering this item if config/type is somehow missing
+              }
+
+              return (
+                <div
+                  key={providerId}
+                  id={`provider-${providerId}`}
+                  className={`space-y-4 ${modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) ? `rounded-lg border p-4 ${isDarkMode ? 'border-blue-700 bg-slate-700' : 'border-blue-200 bg-blue-50/70'}` : ''}`}>
+                  <div className="flex items-center justify-between">
+                    <h3 className={`text-lg font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
+                      {providerConfig.name || providerId}
+                    </h3>
+                    <div className="flex space-x-2">
+                      {/* Show Cancel button for newly added providers */}
+                      {modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) && (
+                        <Button variant="secondary" onClick={() => handleCancelProvider(providerId)}>
+                          Cancel
+                        </Button>
+                      )}
+                      <Button
+                        variant={getButtonProps(providerId).variant}
+                        disabled={getButtonProps(providerId).disabled}
+                        onClick={() =>
+                          providersFromStorage.has(providerId) && !modifiedProviders.has(providerId)
+                            ? handleDelete(providerId)
+                            : handleSave(providerId)
+                        }>
+                        {getButtonProps(providerId).children}
                       </Button>
-                    )}
-                    <Button
-                      variant={getButtonProps(providerId).variant}
-                      disabled={getButtonProps(providerId).disabled}
-                      onClick={() =>
-                        providersFromStorage.has(providerId) && !modifiedProviders.has(providerId)
-                          ? handleDelete(providerId)
-                          : handleSave(providerId)
-                      }>
-                      {getButtonProps(providerId).children}
-                    </Button>
+                    </div>
                   </div>
-                </div>
 
-                {/* Show message for newly added providers */}
-                {modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) && (
-                  <div className={`mb-2 text-sm ${isDarkMode ? 'text-teal-300' : 'text-teal-700'}`}>
-                    <p>This provider is newly added. Enter your API key and click Save to configure it.</p>
-                  </div>
-                )}
+                  {/* Show message for newly added providers */}
+                  {modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) && (
+                    <div className={`mb-2 text-sm ${isDarkMode ? 'text-teal-300' : 'text-teal-700'}`}>
+                      <p>This provider is newly added. Enter your API key and click Save to configure it.</p>
+                    </div>
+                  )}
 
-                <div className="space-y-3">
-                  {/* Name input (only for custom_openai) - moved to top for prominence */}
-                  {providerConfig.type === ProviderTypeEnum.CustomOpenAI && (
-                    <div className="flex flex-col">
-                      <div className="flex items-center">
-                        <label
-                          htmlFor={`${providerId}-name`}
-                          className={`w-20 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
-                          Name
-                        </label>
+                  <div className="space-y-3">
+                    {/* Name input (only for custom_openai) - moved to top for prominence */}
+                    {providerConfig.type === ProviderTypeEnum.CustomOpenAI && (
+                      <div className="flex flex-col">
+                        <div className="flex items-center">
+                          <label
+                            htmlFor={`${providerId}-name`}
+                            className={`w-20 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
+                            Name
+                          </label>
+                          <input
+                            id={`${providerId}-name`}
+                            type="text"
+                            placeholder="Provider name"
+                            value={providerConfig.name || ''}
+                            onChange={e => {
+                              console.log('Name input changed:', e.target.value);
+                              handleNameChange(providerId, e.target.value);
+                            }}
+                            className={`flex-1 rounded-md border p-2 text-sm ${
+                              nameErrors[providerId]
+                                ? isDarkMode
+                                  ? 'border-red-700 bg-slate-700 text-gray-200 focus:border-red-600 focus:ring-2 focus:ring-red-900'
+                                  : 'border-red-300 bg-gray-50 focus:border-red-400 focus:ring-2 focus:ring-red-200'
+                                : isDarkMode
+                                  ? 'border-blue-700 bg-slate-700 text-gray-200 focus:border-blue-600 focus:ring-2 focus:ring-blue-900'
+                                  : 'border-blue-300 bg-gray-50 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'
+                            } outline-none`}
+                          />
+                        </div>
+                        {nameErrors[providerId] ? (
+                          <p className={`ml-20 mt-1 text-xs ${isDarkMode ? 'text-red-400' : 'text-red-500'}`}>
+                            {nameErrors[providerId]}
+                          </p>
+                        ) : (
+                          <p className={`ml-20 mt-1 text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
+                            Provider name (spaces are not allowed when saving)
+                          </p>
+                        )}
+                      </div>
+                    )}
+
+                    {/* API Key input with label */}
+                    <div className="flex items-center">
+                      <label
+                        htmlFor={`${providerId}-api-key`}
+                        className={`w-20 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
+                        API Key
+                        {/* Show asterisk only if required */}
+                        {providerConfig.type !== ProviderTypeEnum.CustomOpenAI &&
+                        providerConfig.type !== ProviderTypeEnum.Ollama
+                          ? '*'
+                          : ''}
+                      </label>
+                      <div className="relative flex-1">
                         <input
-                          id={`${providerId}-name`}
-                          type="text"
-                          placeholder="Provider name"
-                          value={providerConfig.name || ''}
-                          onChange={e => {
-                            console.log('Name input changed:', e.target.value);
-                            handleNameChange(providerId, e.target.value);
-                          }}
-                          className={`flex-1 rounded-md border p-2 text-sm ${
-                            nameErrors[providerId]
-                              ? isDarkMode
-                                ? 'border-red-700 bg-slate-700 text-gray-200 focus:border-red-600 focus:ring-2 focus:ring-red-900'
-                                : 'border-red-300 bg-gray-50 focus:border-red-400 focus:ring-2 focus:ring-red-200'
-                              : isDarkMode
-                                ? 'border-blue-700 bg-slate-700 text-gray-200 focus:border-blue-600 focus:ring-2 focus:ring-blue-900'
-                                : 'border-blue-300 bg-gray-50 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'
-                          } outline-none`}
+                          id={`${providerId}-api-key`}
+                          type="password"
+                          placeholder={
+                            providerConfig.type === ProviderTypeEnum.CustomOpenAI
+                              ? `${providerConfig.name || providerId} API key (optional)`
+                              : providerConfig.type === ProviderTypeEnum.Ollama
+                                ? 'API Key (leave empty for Ollama)'
+                                : `${providerConfig.name || providerId} API key (required)`
+                          }
+                          value={providerConfig.apiKey || ''}
+                          onChange={e => handleApiKeyChange(providerId, e.target.value, providerConfig.baseUrl)}
+                          className={`w-full rounded-md border text-sm ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-800' : 'border-gray-300 bg-white text-gray-700 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'} p-2 outline-none`}
                         />
+                        {/* Show eye button only for newly added providers */}
+                        {modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) && (
+                          <button
+                            type="button"
+                            className={`absolute right-2 top-1/2 -translate-y-1/2 ${
+                              isDarkMode ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'
+                            }`}
+                            onClick={() => toggleApiKeyVisibility(providerId)}
+                            aria-label={visibleApiKeys[providerId] ? 'Hide API key' : 'Show API key'}>
+                            <svg
+                              xmlns="http://www.w3.org/2000/svg"
+                              viewBox="0 0 24 24"
+                              fill="none"
+                              stroke="currentColor"
+                              strokeWidth="2"
+                              strokeLinecap="round"
+                              strokeLinejoin="round"
+                              className="size-5"
+                              aria-hidden="true">
+                              <title>{visibleApiKeys[providerId] ? 'Hide API key' : 'Show API key'}</title>
+                              {visibleApiKeys[providerId] ? (
+                                <>
+                                  <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
+                                  <circle cx="12" cy="12" r="3" />
+                                  <line x1="2" y1="22" x2="22" y2="2" />
+                                </>
+                              ) : (
+                                <>
+                                  <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
+                                  <circle cx="12" cy="12" r="3" />
+                                </>
+                              )}
+                            </svg>
+                          </button>
+                        )}
                       </div>
-                      {nameErrors[providerId] ? (
-                        <p className={`ml-20 mt-1 text-xs ${isDarkMode ? 'text-red-400' : 'text-red-500'}`}>
-                          {nameErrors[providerId]}
-                        </p>
-                      ) : (
-                        <p className={`ml-20 mt-1 text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
-                          Provider name (spaces are not allowed when saving)
-                        </p>
-                      )}
                     </div>
-                  )}
 
-                  {/* API Key input with label */}
-                  <div className="flex items-center">
-                    <label
-                      htmlFor={`${providerId}-api-key`}
-                      className={`w-20 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
-                      Key{providerConfig.type !== ProviderTypeEnum.CustomOpenAI ? '*' : ''}
-                    </label>
-                    <div className="relative flex-1">
-                      <input
-                        id={`${providerId}-api-key`}
-                        type="password"
-                        placeholder={
-                          providerConfig.type === ProviderTypeEnum.CustomOpenAI
-                            ? `${providerConfig.name || providerId} API key (optional)`
-                            : `${providerConfig.name || providerId} API key (required)`
-                        }
-                        value={providerConfig.apiKey || ''}
-                        onChange={e => handleApiKeyChange(providerId, e.target.value, providerConfig.baseUrl)}
-                        className={`w-full rounded-md border text-sm ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-800' : 'border-gray-300 bg-white text-gray-700 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'} p-2 outline-none`}
-                      />
-                      {/* Show eye button only for newly added providers */}
-                      {modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) && (
-                        <button
-                          type="button"
-                          className={`absolute right-2 top-1/2 -translate-y-1/2 ${
-                            isDarkMode ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'
-                          }`}
-                          onClick={() => toggleApiKeyVisibility(providerId)}
-                          aria-label={visibleApiKeys[providerId] ? 'Hide API key' : 'Show API key'}>
-                          <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            viewBox="0 0 24 24"
-                            fill="none"
-                            stroke="currentColor"
-                            strokeWidth="2"
-                            strokeLinecap="round"
-                            strokeLinejoin="round"
-                            className="h-5 w-5"
-                            aria-hidden="true">
-                            <title>{visibleApiKeys[providerId] ? 'Hide API key' : 'Show API key'}</title>
-                            {visibleApiKeys[providerId] ? (
-                              <>
-                                <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
-                                <circle cx="12" cy="12" r="3" />
-                                <line x1="2" y1="22" x2="22" y2="2" />
-                              </>
-                            ) : (
-                              <>
-                                <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
-                                <circle cx="12" cy="12" r="3" />
-                              </>
-                            )}
-                          </svg>
-                        </button>
+                    {/* Display API key for newly added providers only when visible */}
+                    {modifiedProviders.has(providerId) &&
+                      !providersFromStorage.has(providerId) &&
+                      visibleApiKeys[providerId] &&
+                      providerConfig.apiKey && (
+                        <div className="ml-20 mt-1">
+                          <p
+                            className={`break-words font-mono text-sm ${isDarkMode ? 'text-emerald-400' : 'text-emerald-600'}`}>
+                            {providerConfig.apiKey}
+                          </p>
+                        </div>
                       )}
-                    </div>
-                  </div>
 
-                  {/* Display API key for newly added providers only when visible */}
-                  {modifiedProviders.has(providerId) &&
-                    !providersFromStorage.has(providerId) &&
-                    visibleApiKeys[providerId] &&
-                    providerConfig.apiKey && (
-                      <div className="ml-20 mt-1">
-                        <p
-                          className={`font-mono text-sm break-words ${isDarkMode ? 'text-emerald-400' : 'text-emerald-600'}`}>
-                          {providerConfig.apiKey}
-                        </p>
+                    {/* Base URL input (for custom_openai, ollama, azure_openai, and openrouter) */}
+                    {(providerConfig.type === ProviderTypeEnum.CustomOpenAI ||
+                      providerConfig.type === ProviderTypeEnum.Ollama ||
+                      providerConfig.type === ProviderTypeEnum.AzureOpenAI ||
+                      providerConfig.type === ProviderTypeEnum.OpenRouter) && (
+                      <div className="flex flex-col">
+                        <div className="flex items-center">
+                          <label
+                            htmlFor={`${providerId}-base-url`}
+                            className={`w-20 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
+                            {/* Adjust Label based on provider */}
+                            {providerConfig.type === ProviderTypeEnum.AzureOpenAI ? 'Endpoint*' : 'Base URL'}
+                            {/* Show asterisk only if required */}
+                            {/* OpenRouter has a default, so not strictly required, but needed for save button */}
+                            {providerConfig.type === ProviderTypeEnum.CustomOpenAI ||
+                            providerConfig.type === ProviderTypeEnum.AzureOpenAI
+                              ? '*'
+                              : ''}
+                          </label>
+                          <input
+                            id={`${providerId}-base-url`}
+                            type="text"
+                            placeholder={
+                              providerConfig.type === ProviderTypeEnum.CustomOpenAI
+                                ? 'Required OpenAI-compatible API endpoint'
+                                : providerConfig.type === ProviderTypeEnum.AzureOpenAI
+                                  ? // Updated Azure placeholder
+                                    'https://YOUR_RESOURCE_NAME.openai.azure.com/'
+                                  : providerConfig.type === ProviderTypeEnum.OpenRouter
+                                    ? 'OpenRouter Base URL (optional, defaults to https://openrouter.ai/api/v1)'
+                                    : 'Ollama base URL'
+                            }
+                            value={providerConfig.baseUrl || ''}
+                            onChange={e => handleApiKeyChange(providerId, providerConfig.apiKey || '', e.target.value)}
+                            className={`flex-1 rounded-md border text-sm ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-800' : 'border-gray-300 bg-white text-gray-700 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'} p-2 outline-none`}
+                          />
+                        </div>
                       </div>
                     )}
 
-                  {/* Base URL input (for custom_openai and ollama) */}
-                  {(providerConfig.type === ProviderTypeEnum.CustomOpenAI ||
-                    providerConfig.type === ProviderTypeEnum.Ollama) && (
-                    <div className="flex flex-col">
+                    {/* NEW: Azure Deployment Name input */}
+                    {(providerConfig.type as ProviderTypeEnum) === ProviderTypeEnum.AzureOpenAI && (
                       <div className="flex items-center">
                         <label
-                          htmlFor={`${providerId}-base-url`}
-                          className={`w-20 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
-                          Base URL
-                          {providerConfig.type === ProviderTypeEnum.CustomOpenAI ||
-                          providerConfig.type === ProviderTypeEnum.Ollama
-                            ? '*'
-                            : ''}
+                          htmlFor={`${providerId}-azure-deployment`}
+                          className={`w-32 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
+                          Deployment Name*
                         </label>
                         <input
-                          id={`${providerId}-base-url`}
+                          id={`${providerId}-azure-deployment`}
                           type="text"
-                          placeholder={
-                            providerConfig.type === ProviderTypeEnum.CustomOpenAI
-                              ? 'Required for custom OpenAI-compatible API providers'
-                              : 'Ollama base URL'
-                          }
-                          value={providerConfig.baseUrl || ''}
-                          onChange={e => handleApiKeyChange(providerId, providerConfig.apiKey || '', e.target.value)}
+                          placeholder="Your Azure deployment name" // e.g., my-gpt4o
+                          value={providerConfig.azureDeploymentName || ''}
+                          onChange={e => handleAzureDeploymentNameChange(providerId, e.target.value)}
                           className={`flex-1 rounded-md border text-sm ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-800' : 'border-gray-300 bg-white text-gray-700 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'} p-2 outline-none`}
                         />
                       </div>
-                    </div>
-                  )}
+                    )}
 
-                  {/* Models input field with tags */}
-                  <div className="flex items-start">
-                    <label
-                      htmlFor={`${providerId}-models`}
-                      className={`w-20 pt-2 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
-                      Models
-                    </label>
-                    <div className="flex-1">
-                      <div
-                        className={`flex min-h-[42px] flex-wrap items-center gap-2 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} p-2`}>
-                        {/* Display existing models as tags */}
-                        {(() => {
-                          // Get models from provider config or default models
-                          const models =
-                            providerConfig.modelNames !== undefined
-                              ? providerConfig.modelNames
-                              : llmProviderModelNames[providerId as keyof typeof llmProviderModelNames] || [];
-
-                          return models.map(model => (
-                            <div
-                              key={model}
-                              className={`flex items-center rounded-full ${isDarkMode ? 'bg-blue-900 text-blue-100' : 'bg-blue-100 text-blue-800'} px-2 py-1 text-sm`}>
-                              <span>{model}</span>
-                              <button
-                                type="button"
-                                onClick={() => removeModel(providerId, model)}
-                                className={`ml-1 font-bold ${isDarkMode ? 'text-blue-300 hover:text-blue-100' : 'text-blue-600 hover:text-blue-800'}`}
-                                aria-label={`Remove ${model}`}>
-                                ×
-                              </button>
-                            </div>
-                          ));
-                        })()}
-
-                        {/* Input for new models */}
+                    {/* NEW: Azure API Version input */}
+                    {(providerConfig.type as ProviderTypeEnum) === ProviderTypeEnum.AzureOpenAI && (
+                      <div className="flex items-center">
+                        <label
+                          htmlFor={`${providerId}-azure-version`}
+                          className={`w-32 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
+                          API Version*
+                        </label>
                         <input
-                          id={`${providerId}-models`}
+                          id={`${providerId}-azure-version`}
                           type="text"
-                          placeholder=""
-                          value={newModelInputs[providerId] || ''}
-                          onChange={e => handleModelsChange(providerId, e.target.value)}
-                          onKeyDown={e => handleKeyDown(e, providerId)}
-                          className={`min-w-[150px] flex-1 border-none text-sm ${isDarkMode ? 'bg-transparent text-gray-200' : 'bg-transparent text-gray-700'} p-1 outline-none`}
+                          placeholder="e.g., 2024-02-15-preview" // Common example
+                          value={providerConfig.azureApiVersion || ''}
+                          onChange={e => handleAzureApiVersionChange(providerId, e.target.value)}
+                          className={`flex-1 rounded-md border text-sm ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-800' : 'border-gray-300 bg-white text-gray-700 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'} p-2 outline-none`}
                         />
                       </div>
-                      <p className={`mt-1 text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
-                        Type and Press Enter or Space to add a model
-                      </p>
-                    </div>
+                    )}
+
+                    {/* Models/Deployments input field with tags (HIDE for Azure) */}
+                    {(providerConfig.type as ProviderTypeEnum) !== ProviderTypeEnum.AzureOpenAI && (
+                      <div className="flex items-start">
+                        <label
+                          htmlFor={`${providerId}-models`}
+                          className={`w-20 pt-2 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
+                          {/* Updated label for Azure */}
+                          {providerConfig.type === ProviderTypeEnum.AzureOpenAI ? 'Deployments' : 'Models'}
+                        </label>
+                        <div className="flex-1">
+                          <div
+                            className={`flex min-h-[42px] flex-wrap items-center gap-2 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} p-2`}>
+                            {/* Display existing models as tags */}
+                            {(() => {
+                              // Get models from provider config or default models
+                              const models =
+                                providerConfig.modelNames !== undefined
+                                  ? providerConfig.modelNames
+                                  : llmProviderModelNames[providerId as keyof typeof llmProviderModelNames] || [];
+
+                              return models.map(model => (
+                                <div
+                                  key={model}
+                                  className={`flex items-center rounded-full ${isDarkMode ? 'bg-blue-900 text-blue-100' : 'bg-blue-100 text-blue-800'} px-2 py-1 text-sm`}>
+                                  <span>{model}</span>
+                                  <button
+                                    type="button"
+                                    onClick={() => removeModel(providerId, model)}
+                                    className={`ml-1 font-bold ${isDarkMode ? 'text-blue-300 hover:text-blue-100' : 'text-blue-600 hover:text-blue-800'}`}
+                                    aria-label={`Remove ${model}`}>
+                                    ×
+                                  </button>
+                                </div>
+                              ));
+                            })()}
+
+                            {/* Input for new models */}
+                            <input
+                              id={`${providerId}-models`}
+                              type="text"
+                              placeholder="" // Placeholder kept empty for tags input style
+                              value={newModelInputs[providerId] || ''}
+                              onChange={e => handleModelsChange(providerId, e.target.value)}
+                              onKeyDown={e => handleKeyDown(e, providerId)}
+                              className={`min-w-[150px] flex-1 border-none text-sm ${isDarkMode ? 'bg-transparent text-gray-200' : 'bg-transparent text-gray-700'} p-1 outline-none`}
+                            />
+                          </div>
+                          {/* Updated description for model input */}
+                          <p className={`mt-1 text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
+                            Type and Press Enter or Space to add.
+                            {/* Added clarification for Azure */}
+                            {providerConfig.type === ProviderTypeEnum.AzureOpenAI && (
+                              <span className="block">
+                                Enter your exact Azure Deployment Names here (e.g., 'my-gpt4o-deployment'). This name is
+                                used to call the specific model you deployed in Azure.
+                              </span>
+                            )}
+                          </p>
+                        </div>
+                      </div>
+                    )}
+
+                    {/* Ollama reminder at the bottom of the section */}
+                    {providerConfig.type === ProviderTypeEnum.Ollama && (
+                      <div
+                        className={`mt-4 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700' : 'border-blue-100 bg-blue-50'} p-3`}>
+                        <p className={`text-sm ${isDarkMode ? 'text-gray-200' : 'text-gray-700'}`}>
+                          <strong>Remember:</strong> Add{' '}
+                          <code
+                            className={`rounded italic ${isDarkMode ? 'bg-slate-600 px-1 py-0.5' : 'bg-blue-100 px-1 py-0.5'}`}>
+                            OLLAMA_ORIGINS=chrome-extension://*
+                          </code>{' '}
+                          environment variable for the Ollama server.
+                          <a
+                            href="https://github.com/ollama/ollama/issues/6489"
+                            target="_blank"
+                            rel="noopener noreferrer"
+                            className={`ml-1 ${isDarkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-800'}`}>
+                            Learn more
+                          </a>
+                        </p>
+                      </div>
+                    )}
                   </div>
 
-                  {/* Ollama reminder at the bottom of the section */}
-                  {providerConfig.type === ProviderTypeEnum.Ollama && (
-                    <div
-                      className={`mt-4 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700' : 'border-blue-100 bg-blue-50'} p-3`}>
-                      <p className={`text-sm ${isDarkMode ? 'text-gray-200' : 'text-gray-700'}`}>
-                        <strong>Remember:</strong> Add{' '}
-                        <code
-                          className={`rounded italic ${isDarkMode ? 'bg-slate-600 px-1 py-0.5' : 'bg-blue-100 px-1 py-0.5'}`}>
-                          OLLAMA_ORIGINS=chrome-extension://*
-                        </code>{' '}
-                        environment variable for the Ollama server.
-                        <a
-                          href="https://github.com/ollama/ollama/issues/6489"
-                          target="_blank"
-                          rel="noopener noreferrer"
-                          className={`ml-1 ${isDarkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-800'}`}>
-                          Learn more
-                        </a>
-                      </p>
-                    </div>
+                  {/* Add divider except for the last item */}
+                  {Object.keys(providers).indexOf(providerId) < Object.keys(providers).length - 1 && (
+                    <div className={`mt-4 border-t ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`} />
                   )}
                 </div>
-
-                {/* Add divider except for the last item */}
-                {Object.keys(providers).indexOf(providerId) < Object.keys(providers).length - 1 && (
-                  <div className={`mt-4 border-t ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`} />
-                )}
-              </div>
-            ))
+              );
+            })
           )}
 
           {/* Add Provider button and dropdown */}

+ 1 - 1
pages/side-panel/src/components/ChatHistoryList.tsx

@@ -44,7 +44,7 @@ const ChatHistoryList: React.FC<ChatHistoryListProps> = ({
               key={session.id}
               className={`group relative rounded-lg ${
                 isDarkMode ? 'bg-slate-800 hover:bg-slate-700' : 'bg-white/50 hover:bg-white/70'
-              } p-3 transition-all backdrop-blur-sm`}>
+              } p-3 backdrop-blur-sm transition-all`}>
               <button onClick={() => onSessionSelect(session.id)} className="w-full text-left" type="button">
                 <h3 className={`text-sm font-medium ${isDarkMode ? 'text-gray-200' : 'text-gray-900'}`}>
                   {session.title}

+ 1 - 1
pages/side-panel/src/components/MessageList.tsx

@@ -63,7 +63,7 @@ function MessageBlock({ message, isSameActor, isDarkMode = false }: MessageBlock
           <div className={`whitespace-pre-wrap break-words text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
             {isProgress ? (
               <div className={`h-1 overflow-hidden rounded ${isDarkMode ? 'bg-gray-700' : 'bg-gray-200'}`}>
-                <div className="animate-progress h-full bg-blue-500" />
+                <div className="h-full animate-progress bg-blue-500" />
               </div>
             ) : (
               message.content

+ 29 - 29
pnpm-lock.yaml

@@ -118,25 +118,25 @@ importers:
         version: link:../packages/storage
       '@langchain/anthropic':
         specifier: ^0.3.12
-        version: 0.3.12(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))
+        version: 0.3.12(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))
       '@langchain/core':
         specifier: ^0.3.37
-        version: 0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
+        version: 0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
       '@langchain/deepseek':
         specifier: ^0.0.1
-        version: 0.0.1(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)
+        version: 0.0.1(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)
       '@langchain/google-genai':
         specifier: 0.1.11
-        version: 0.1.11(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(zod@3.24.1)
+        version: 0.1.11(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(zod@3.24.1)
       '@langchain/ollama':
         specifier: ^0.2.0
-        version: 0.2.0(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))
+        version: 0.2.0(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))
       '@langchain/openai':
         specifier: ^0.4.2
-        version: 0.4.2(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)
+        version: 0.4.2(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)
       '@langchain/xai':
         specifier: ^0.0.2
-        version: 0.0.2(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)
+        version: 0.0.2(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)
       puppeteer-core:
         specifier: 24.1.1
         version: 24.1.1
@@ -619,8 +619,8 @@ packages:
     peerDependencies:
       '@langchain/core': '>=0.2.21 <0.4.0'
 
-  '@langchain/core@0.3.37':
-    resolution: {integrity: sha512-LFk9GqHxcyCFx0oXvCBP7vDZIOUHYzzNU7JR+2ofIMnfkBLzcCKzBLySQDfPtd13PrpGHkaeOeLq8H1Tqi9lSw==}
+  '@langchain/core@0.3.44':
+    resolution: {integrity: sha512-3BsSFf7STvPPZyl2kMANgtVnCUvDdyP4k+koP+nY2Tczd5V+RFkuazIn/JOj/xxy/neZjr4PxFU4BFyF1aKXOA==}
     engines: {node: '>=18'}
 
   '@langchain/deepseek@0.0.1':
@@ -3465,17 +3465,17 @@ snapshots:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.0
 
-  '@langchain/anthropic@0.3.12(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))':
+  '@langchain/anthropic@0.3.12(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))':
     dependencies:
       '@anthropic-ai/sdk': 0.32.1
-      '@langchain/core': 0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
+      '@langchain/core': 0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
       fast-xml-parser: 4.5.0
       zod: 3.24.1
       zod-to-json-schema: 3.24.4(zod@3.24.1)
     transitivePeerDependencies:
       - encoding
 
-  '@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1))':
+  '@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1))':
     dependencies:
       '@cfworker/json-schema': 4.1.1
       ansi-styles: 5.2.0
@@ -3487,39 +3487,39 @@ snapshots:
       p-queue: 6.6.2
       p-retry: 4.6.2
       uuid: 10.0.0
-      zod: 3.24.1
-      zod-to-json-schema: 3.24.4(zod@3.24.1)
+      zod: 3.24.2
+      zod-to-json-schema: 3.24.4(zod@3.24.2)
     transitivePeerDependencies:
       - openai
 
-  '@langchain/deepseek@0.0.1(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)':
+  '@langchain/deepseek@0.0.1(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)':
     dependencies:
-      '@langchain/core': 0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
-      '@langchain/openai': 0.4.4(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)
+      '@langchain/core': 0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
+      '@langchain/openai': 0.4.4(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)
       zod: 3.24.2
     transitivePeerDependencies:
       - encoding
       - ws
 
-  '@langchain/google-genai@0.1.11(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(zod@3.24.1)':
+  '@langchain/google-genai@0.1.11(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(zod@3.24.1)':
     dependencies:
       '@google/generative-ai': 0.21.0
-      '@langchain/core': 0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
+      '@langchain/core': 0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
       zod-to-json-schema: 3.24.4(zod@3.24.1)
     transitivePeerDependencies:
       - zod
 
-  '@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))':
+  '@langchain/ollama@0.2.0(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))':
     dependencies:
-      '@langchain/core': 0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
+      '@langchain/core': 0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
       ollama: 0.5.14
       uuid: 10.0.0
       zod: 3.24.1
       zod-to-json-schema: 3.24.4(zod@3.24.1)
 
-  '@langchain/openai@0.4.2(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)':
+  '@langchain/openai@0.4.2(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)':
     dependencies:
-      '@langchain/core': 0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
+      '@langchain/core': 0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
       js-tiktoken: 1.0.17
       openai: 4.82.0(ws@8.18.0)(zod@3.24.1)
       zod: 3.24.1
@@ -3528,9 +3528,9 @@ snapshots:
       - encoding
       - ws
 
-  '@langchain/openai@0.4.4(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)':
+  '@langchain/openai@0.4.4(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)':
     dependencies:
-      '@langchain/core': 0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
+      '@langchain/core': 0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
       js-tiktoken: 1.0.17
       openai: 4.82.0(ws@8.18.0)(zod@3.24.2)
       zod: 3.24.2
@@ -3539,10 +3539,10 @@ snapshots:
       - encoding
       - ws
 
-  '@langchain/xai@0.0.2(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)':
+  '@langchain/xai@0.0.2(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)':
     dependencies:
-      '@langchain/core': 0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
-      '@langchain/openai': 0.4.4(@langchain/core@0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)
+      '@langchain/core': 0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
+      '@langchain/openai': 0.4.4(@langchain/core@0.3.44(openai@4.82.0(ws@8.18.0)(zod@3.24.1)))(ws@8.18.0)
       zod: 3.24.2
     transitivePeerDependencies:
       - encoding
@@ -4086,7 +4086,7 @@ snapshots:
 
   ast-types@0.13.4:
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   asynckit@0.4.0: {}