ソースを参照

feat: enhance Azure model handling by supporting multiple deployment names and updating model selection logic

Burak Sormageç 4 ヶ月 前
コミット
8c73b62d14

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

@@ -45,8 +45,15 @@ function createOpenAIChatModel(
     args.configuration!.baseURL = providerConfig.baseUrl;
   }
 
-  // Merge extra headers if provided
-  if (extraFetchOptions?.headers) {
+  // Always add custom headers for OpenRouter to identify Nanobrowser
+  if (providerConfig.type === ProviderTypeEnum.OpenRouter) {
+    args.configuration!.defaultHeaders = {
+      ...(args.configuration!.defaultHeaders || {}),
+      'HTTP-Referer': 'https://nanobrowser.ai',
+      'X-Title': 'Nanobrowser',
+      ...(extraFetchOptions?.headers || {}),
+    };
+  } else if (extraFetchOptions?.headers) {
     args.configuration!.defaultHeaders = {
       ...(args.configuration!.defaultHeaders || {}),
       ...extraFetchOptions.headers,
@@ -168,7 +175,8 @@ export function createChatModel(providerConfig: ProviderConfig, modelConfig: Mod
       // Validate necessary fields first
       if (
         !providerConfig.baseUrl ||
-        !providerConfig.azureDeploymentName ||
+        !providerConfig.azureDeploymentNames ||
+        providerConfig.azureDeploymentNames.length === 0 ||
         !providerConfig.azureApiVersion ||
         !providerConfig.apiKey
       ) {
@@ -177,6 +185,18 @@ export function createChatModel(providerConfig: ProviderConfig, modelConfig: Mod
         );
       }
 
+      // Instead of always using the first deployment name, use the model name from modelConfig
+      // which contains the actual model selected in the UI
+      const deploymentName = modelConfig.modelName;
+
+      // Validate that the selected model exists in the configured deployments
+      if (!providerConfig.azureDeploymentNames.includes(deploymentName)) {
+        console.warn(
+          `[createChatModel] Selected deployment "${deploymentName}" not found in available deployments. ` +
+            `Available: ${JSON.stringify(providerConfig.azureDeploymentNames)}. Using the model anyway.`,
+        );
+      }
+
       // Extract instance name from the endpoint URL
       const instanceName = extractInstanceNameFromUrl(providerConfig.baseUrl);
       if (!instanceName) {
@@ -185,15 +205,25 @@ export function createChatModel(providerConfig: ProviderConfig, modelConfig: Mod
         );
       }
 
+      // Check if the Azure deployment is using an "o" series model (GPT-4o, etc.)
+      const isOSeriesModel = isOpenAIOModel(deploymentName);
+
       // Use AzureChatOpenAI with specific parameters
       const args = {
         azureOpenAIApiInstanceName: instanceName, // Derived from endpoint
-        azureOpenAIApiDeploymentName: providerConfig.azureDeploymentName,
+        azureOpenAIApiDeploymentName: deploymentName,
         azureOpenAIApiKey: providerConfig.apiKey,
         azureOpenAIApiVersion: providerConfig.azureApiVersion,
-        temperature,
-        topP,
-        maxTokens,
+        // For Azure, the model name should be the deployment name itself
+        model: deploymentName, // Set model = deployment name to fix Azure requests
+        // For O series models, use modelKwargs instead of temperature/topP
+        ...(isOSeriesModel
+          ? { modelKwargs: { max_completion_tokens: maxTokens } }
+          : {
+              temperature,
+              topP,
+              maxTokens,
+            }),
         // DO NOT pass baseUrl or configuration here
       };
       console.log('[createChatModel] Azure args passed to AzureChatOpenAI:', args);

+ 0 - 67
chrome-extension/src/background/index.ts

@@ -76,32 +76,7 @@ logger.info('background loaded');
 
 // Listen for simple messages (e.g., from options page)
 chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
-  if (message.type === 'fetch_openrouter_models') {
-    if (!message.apiKey) {
-      sendResponse({ type: 'error', error: 'API Key is required to fetch OpenRouter models.' });
-      return true; // Indicates async response
-    }
-
-    fetchOpenRouterModels(message.apiKey)
-      .then(models => {
-        sendResponse({ type: 'openrouter_models_fetched', models });
-      })
-      .catch(fetchError => {
-        logger.error('Error fetching OpenRouter models:', fetchError);
-        sendResponse({
-          type: 'error',
-          error:
-            fetchError instanceof Error
-              ? `Failed to fetch OpenRouter models: ${fetchError.message}`
-              : 'Failed to fetch OpenRouter models: Unknown error',
-        });
-      });
-
-    return true; // Indicates that the response is sent asynchronously
-  }
-
   // Handle other message types if needed in the future
-
   // Return false if response is not sent asynchronously
   // return false;
 });
@@ -252,48 +227,6 @@ async function setupExecutor(taskId: string, task: string, browserContext: Brows
   return executor;
 }
 
-// Function to fetch models from OpenRouter API
-async function fetchOpenRouterModels(apiKey: string): Promise<string[]> {
-  const url = 'https://openrouter.ai/api/v1/models';
-  try {
-    const response = await fetch(url, {
-      method: 'GET',
-      headers: {
-        Authorization: `Bearer ${apiKey}`,
-        'Content-Type': 'application/json',
-      },
-    });
-
-    if (!response.ok) {
-      // Try to get more specific error from response body
-      let errorDetails = `HTTP error! status: ${response.status}`;
-      try {
-        const errorData = await response.json();
-        errorDetails += ` - ${errorData.error?.message || JSON.stringify(errorData)}`;
-      } catch (jsonError) {
-        // Ignore if response body is not JSON or empty
-      }
-      throw new Error(errorDetails);
-    }
-
-    const data = await response.json();
-
-    // Extract model IDs and check pricing
-    if (data && Array.isArray(data.data)) {
-      return data.data
-        .map((model: { id: string; pricing: Record<string, string> }) => ({
-          id: model.id,
-          isFree: Object.values(model.pricing || {}).every(price => price === '0'),
-        }))
-        .filter(Boolean);
-    }
-    throw new Error('Invalid response format from OpenRouter API');
-  } catch (error) {
-    logger.error('fetchOpenRouterModels failed:', error);
-    throw error; // Re-throw the error to be caught by the caller
-  }
-}
-
 // Update subscribeToExecutorEvents to use port
 async function subscribeToExecutorEvents(executor: Executor) {
   // Clear previous event listeners to prevent multiple subscriptions

+ 13 - 11
packages/storage/lib/settings/llmProviders.ts

@@ -9,10 +9,10 @@ export interface ProviderConfig {
   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 // For Azure: Endpoint
-  modelNames?: string[]; // Chosen model names (NOT used for Azure OpenAI anymore)
+  modelNames?: string[]; // Chosen model names (NOT used for Azure OpenAI)
   createdAt?: number; // Timestamp in milliseconds when the provider was created
   // Azure Specific Fields:
-  azureDeploymentName?: string;
+  azureDeploymentNames?: string[]; // Azure deployment names array
   azureApiVersion?: string;
 }
 
@@ -117,8 +117,8 @@ export function getDefaultProviderConfig(providerId: string): ProviderConfig {
         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
+        // modelNames: [], // Not used for Azure configuration
+        azureDeploymentNames: [], // Azure deployment names
         azureApiVersion: '2024-02-15-preview', // Provide a common default API version
         createdAt: Date.now(),
       };
@@ -161,14 +161,16 @@ function ensureBackwardCompatibility(providerId: string, config: ProviderConfig)
   // 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';
     }
+
+    // Initialize azureDeploymentNames array if it doesn't exist yet
+    if (!updatedConfig.azureDeploymentNames) {
+      updatedConfig.azureDeploymentNames = [];
+    }
+
     // 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}`);
@@ -209,8 +211,8 @@ export const llmProviderStore: LLMProviderStorage = {
       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.azureDeploymentNames || config.azureDeploymentNames.length === 0) {
+        throw new Error('At least one Azure Deployment Name is required');
       }
       if (!config.azureApiVersion?.trim()) {
         throw new Error('Azure API Version is required');
@@ -238,7 +240,7 @@ export const llmProviderStore: LLMProviderStorage = {
       createdAt: config.createdAt || Date.now(),
       ...(providerType === ProviderTypeEnum.AzureOpenAI
         ? {
-            azureDeploymentName: config.azureDeploymentName,
+            azureDeploymentNames: config.azureDeploymentNames || [],
             azureApiVersion: config.azureApiVersion,
           }
         : {

+ 3 - 1
packages/storage/lib/settings/types.ts

@@ -35,7 +35,9 @@ export const llmProviderModelNames = {
   [ProviderTypeEnum.Ollama]: [],
   [ProviderTypeEnum.AzureOpenAI]: ['gpt-4o', 'gpt-4o-mini', 'o3-mini'],
   [ProviderTypeEnum.OpenRouter]: [
-    'openai/gpt-4o',
+    'openai/gpt-4o-2024-05-13',
+    'openai/gpt-4o-2024-11-20',
+    'openai/gpt-3.5-turbo',
     'anthropic/claude-3.5-sonnet',
     'google/gemini-pro-1.5',
     'mistralai/mistral-large',

+ 204 - 271
pages/options/src/components/ModelSettings.tsx

@@ -51,24 +51,7 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
   const [availableModels, setAvailableModels] = useState<
     Array<{ provider: string; providerName: string; model: string }>
   >([]);
-  // State for OpenRouter model fetching
-  interface OpenRouterModel {
-    id: string;
-    isFree: boolean;
-  }
-
-  const [openRouterFetchState, setOpenRouterFetchState] = useState<
-    Record<
-      string,
-      {
-        allModels: OpenRouterModel[] | null; // Store all fetched models here
-        displayMode: 'all' | 'free' | null; // Track which list to show
-        isFetching: boolean;
-        error: string | null;
-      }
-    >
-  >({});
-  const [searchInputs, setSearchInputs] = useState<Record<string, string>>({});
+  // State for model input handling
 
   useEffect(() => {
     const loadProviders = async () => {
@@ -179,15 +162,29 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
 
       // Only use providers that are actually in storage
       for (const [provider, config] of Object.entries(storedProviders)) {
-        const providerModels =
-          config.modelNames || llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [];
-        models.push(
-          ...providerModels.map(model => ({
-            provider,
-            providerName: config.name || provider,
-            model,
-          })),
-        );
+        if (config.type === ProviderTypeEnum.AzureOpenAI) {
+          // Handle Azure providers specially - use deployment names as models
+          const deploymentNames = config.azureDeploymentNames || [];
+
+          models.push(
+            ...deploymentNames.map(deployment => ({
+              provider,
+              providerName: config.name || provider,
+              model: deployment,
+            })),
+          );
+        } else {
+          // Standard handling for non-Azure providers
+          const providerModels =
+            config.modelNames || llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [];
+          models.push(
+            ...providerModels.map(model => ({
+              provider,
+              providerName: config.name || provider,
+              model,
+            })),
+          );
+        }
       }
     } catch (error) {
       console.error('Error loading providers for model selection:', error);
@@ -343,11 +340,11 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
     } 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
+      // Azure needs API Key, Endpoint, Deployment Names, and API Version
       hasInput =
         Boolean(config?.apiKey?.trim()) &&
         Boolean(config?.baseUrl?.trim()) &&
-        Boolean(config?.azureDeploymentName?.trim()) &&
+        Boolean(config?.azureDeploymentNames?.length) &&
         Boolean(config?.azureApiVersion?.trim());
     } else if (providerType === ProviderTypeEnum.OpenRouter) {
       // OpenRouter needs API Key and optionally Base URL (has default)
@@ -499,6 +496,8 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
     // modelValue will be in format "provider>model"
     const [provider, model] = modelValue.split('>');
 
+    console.log(`[handleModelChange] Setting ${agentName} model: provider=${provider}, model=${model}`);
+
     // Set parameters based on provider type
     const newParameters = getDefaultAgentModelParams(provider, agentName);
 
@@ -514,6 +513,14 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
 
     try {
       if (model) {
+        const providerConfig = providers[provider];
+
+        // For Azure, verify the model is in the deployment names list
+        if (providerConfig && providerConfig.type === ProviderTypeEnum.AzureOpenAI) {
+          console.log(`[handleModelChange] Azure model selected: ${model}`);
+          console.log(`[handleModelChange] Available deployments:`, providerConfig.azureDeploymentNames || []);
+        }
+
         await agentModelStore.setAgentModel(agentName, {
           provider,
           modelName: model,
@@ -545,11 +552,23 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
         // Find provider
         let provider: string | undefined;
         for (const [providerKey, providerConfig] of Object.entries(providers)) {
-          const modelNames =
-            providerConfig.modelNames || llmProviderModelNames[providerKey as keyof typeof llmProviderModelNames] || [];
-          if (modelNames.includes(selectedModels[agentName])) {
-            provider = providerKey;
-            break;
+          if (providerConfig.type === ProviderTypeEnum.AzureOpenAI) {
+            // Check Azure deployment names
+            const deploymentNames = providerConfig.azureDeploymentNames || [];
+            if (deploymentNames.includes(selectedModels[agentName])) {
+              provider = providerKey;
+              break;
+            }
+          } else {
+            // Check standard model names for non-Azure providers
+            const modelNames =
+              providerConfig.modelNames ||
+              llmProviderModelNames[providerKey as keyof typeof llmProviderModelNames] ||
+              [];
+            if (modelNames.includes(selectedModels[agentName])) {
+              provider = providerKey;
+              break;
+            }
           }
         }
 
@@ -854,27 +873,76 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
 
   const getProviderForModel = (modelName: string): string => {
     for (const [provider, config] of Object.entries(providers)) {
-      const modelNames =
-        config.modelNames || llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [];
-      if (modelNames.includes(modelName)) {
-        return provider;
+      // Check Azure deployment names
+      if (config.type === ProviderTypeEnum.AzureOpenAI) {
+        const deploymentNames = config.azureDeploymentNames || [];
+        if (deploymentNames.includes(modelName)) {
+          return provider;
+        }
+      } else {
+        // Check regular model names for non-Azure providers
+        const modelNames =
+          config.modelNames || llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [];
+        if (modelNames.includes(modelName)) {
+          return provider;
+        }
       }
     }
     return '';
   };
 
-  // Add state handlers for new Azure fields
-  const handleAzureDeploymentNameChange = (provider: string, deploymentName: string) => {
+  // Add and remove Azure deployments
+  const addAzureDeployment = (provider: string, deploymentName: string) => {
+    if (!deploymentName.trim()) return;
+
     setModifiedProviders(prev => new Set(prev).add(provider));
-    setProviders(prev => ({
+    setProviders(prev => {
+      const providerData = prev[provider] || {};
+
+      // Initialize or use existing deploymentNames array
+      const deploymentNames = providerData.azureDeploymentNames || [];
+
+      // Don't add duplicates
+      if (deploymentNames.includes(deploymentName.trim())) return prev;
+
+      return {
+        ...prev,
+        [provider]: {
+          ...providerData,
+          azureDeploymentNames: [...deploymentNames, deploymentName.trim()],
+        },
+      };
+    });
+
+    // Clear the input
+    setNewModelInputs(prev => ({
       ...prev,
-      [provider]: {
-        ...prev[provider],
-        azureDeploymentName: deploymentName.trim(),
-      },
+      [provider]: '',
     }));
   };
 
+  const removeAzureDeployment = (provider: string, deploymentToRemove: string) => {
+    setModifiedProviders(prev => new Set(prev).add(provider));
+
+    setProviders(prev => {
+      const providerData = prev[provider] || {};
+
+      // Get current deployments
+      const deploymentNames = providerData.azureDeploymentNames || [];
+
+      // Filter out the deployment to remove
+      const filteredDeployments = deploymentNames.filter(name => name !== deploymentToRemove);
+
+      return {
+        ...prev,
+        [provider]: {
+          ...providerData,
+          azureDeploymentNames: filteredDeployments,
+        },
+      };
+    });
+  };
+
   const handleAzureApiVersionChange = (provider: string, apiVersion: string) => {
     setModifiedProviders(prev => new Set(prev).add(provider));
     setProviders(prev => ({
@@ -886,95 +954,6 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
     }));
   };
 
-  // Fix the handleFetchOpenRouterModels function's return block
-  const handleFetchOpenRouterModels = async (providerId: string, mode: 'all' | 'free') => {
-    const apiKey = providers[providerId]?.apiKey;
-    if (!apiKey) {
-      setOpenRouterFetchState(prev => ({
-        ...prev,
-        [providerId]: {
-          ...prev[providerId],
-          isFetching: false,
-          error: 'API Key is required to fetch models.',
-          displayMode: null,
-        }, // Reset displayMode on error
-      }));
-      return;
-    }
-
-    // Update state to show fetching and set display mode
-    setOpenRouterFetchState(prev => ({
-      ...prev,
-      [providerId]: {
-        allModels: prev[providerId]?.allModels ?? null, // Preserve existing models if any
-        isFetching: true,
-        error: null,
-        displayMode: mode, // Set the display mode based on the button clicked
-      },
-    }));
-
-    if (IS_CHROME) {
-      chrome.runtime.sendMessage({ type: 'fetch_openrouter_models', apiKey }, response => {
-        // Check for runtime error before accessing message
-        if (chrome.runtime.lastError) {
-          console.error('Error sending message to background:', chrome.runtime.lastError);
-          setOpenRouterFetchState(prev => ({
-            ...prev,
-            [providerId]: {
-              allModels: null, // Clear models on error
-              isFetching: false,
-              error: `Connection error: ${chrome.runtime.lastError?.message || 'Unknown connection error'}`,
-              displayMode: null, // Reset displayMode
-            },
-          }));
-          return;
-        }
-
-        if (response?.type === 'openrouter_models_fetched') {
-          // Convert returned models to the correct type and sort them
-          const fetchedModels = (response.models as OpenRouterModel[]).sort((a, b) => a.id.localeCompare(b.id));
-          setOpenRouterFetchState(prev => ({
-            ...prev,
-            [providerId]: {
-              ...prev[providerId], // Keep the current displayMode
-              allModels: fetchedModels,
-              isFetching: false,
-              error: null,
-            },
-          }));
-        } else if (response?.type === 'error') {
-          setOpenRouterFetchState(prev => ({
-            ...prev,
-            [providerId]: {
-              allModels: null, // Clear models on error
-              isFetching: false,
-              error: response.error,
-              displayMode: null, // Reset displayMode
-            },
-          }));
-        } else {
-          // Handle unexpected response
-          setOpenRouterFetchState(prev => ({
-            ...prev,
-            [providerId]: {
-              allModels: null,
-              isFetching: false,
-              error: 'Unexpected response from background script.',
-              displayMode: null, // Reset displayMode
-            },
-          }));
-        }
-      });
-    } else {
-      // Handle non-chrome environment (e.g., development, testing)
-      console.warn('Cannot send message: Chrome runtime not available.');
-      setOpenRouterFetchState(prev => ({
-        ...prev,
-        [providerId]: { allModels: null, isFetching: false, error: 'Chrome runtime not available.', displayMode: null },
-      }));
-    }
-  };
-
   return (
     <section className="space-y-6">
       {/* LLM Providers Section */}
@@ -1192,22 +1171,65 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
                       </div>
                     )}
 
-                    {/* NEW: Azure Deployment Name input */}
+                    {/* Azure Deployment Name input as tags/chips like OpenRouter models */}
                     {(providerConfig.type as ProviderTypeEnum) === ProviderTypeEnum.AzureOpenAI && (
-                      <div className="flex items-center">
+                      <div className="flex items-start">
                         <label
                           htmlFor={`${providerId}-azure-deployment`}
-                          className={`w-32 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
-                          Deployment Name*
+                          className={`w-20 pt-2 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
+                          Deployment*
                         </label>
-                        <input
-                          id={`${providerId}-azure-deployment`}
-                          type="text"
-                          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 className="flex-1 space-y-2">
+                          <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`}>
+                            {/* Show azure deployments or empty message */}
+                            {(providerConfig.azureDeploymentNames || []).length > 0 ? (
+                              (providerConfig.azureDeploymentNames || []).map((deploymentName: string) => (
+                                <div
+                                  key={deploymentName}
+                                  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>{deploymentName}</span>
+                                  <button
+                                    type="button"
+                                    onClick={() => removeAzureDeployment(providerId, deploymentName)}
+                                    className={`ml-1 font-bold ${isDarkMode ? 'text-blue-300 hover:text-blue-100' : 'text-blue-600 hover:text-blue-800'}`}
+                                    aria-label={`Remove ${deploymentName}`}>
+                                    ×
+                                  </button>
+                                </div>
+                              ))
+                            ) : (
+                              <span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
+                                No deployment names set. Add model names below.
+                              </span>
+                            )}
+                            <input
+                              id={`${providerId}-azure-deployment-input`}
+                              type="text"
+                              placeholder="Enter Azure model name (e.g. gpt-4o, gpt-4o-mini)"
+                              value={newModelInputs[providerId] || ''}
+                              onChange={e => handleModelsChange(providerId, e.target.value)}
+                              onKeyDown={e => {
+                                if (e.key === 'Enter' || e.key === ' ') {
+                                  e.preventDefault();
+                                  const value = newModelInputs[providerId] || '';
+                                  if (value.trim()) {
+                                    addAzureDeployment(providerId, value.trim());
+                                    // Clear the input
+                                    setNewModelInputs(prev => ({
+                                      ...prev,
+                                      [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>
+                          <p className={`mt-1 text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
+                            Type model name and press Enter or Space to set.
+                          </p>
+                        </div>
                       </div>
                     )}
 
@@ -1216,7 +1238,7 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
                       <div className="flex items-center">
                         <label
                           htmlFor={`${providerId}-azure-version`}
-                          className={`w-32 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
+                          className={`w-20 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
                           API Version*
                         </label>
                         <input
@@ -1230,7 +1252,7 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
                       </div>
                     )}
 
-                    {/* Models/Deployments input section */}
+                    {/* Models input section (for non-Azure providers) */}
                     {(providerConfig.type as ProviderTypeEnum) !== ProviderTypeEnum.AzureOpenAI && (
                       <div className="flex items-start">
                         <label
@@ -1239,136 +1261,47 @@ export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
                           Models
                         </label>
                         <div className="flex-1 space-y-2">
-                          {/* === START: Conditional UI for OpenRouter === */}
+                          {/* Conditional UI for OpenRouter */}
                           {(providerConfig.type as ProviderTypeEnum) === ProviderTypeEnum.OpenRouter ? (
                             <>
-                              {/* Fetch Buttons Container */}
-                              <div className="mb-2 flex space-x-2">
-                                <Button
-                                  variant="secondary"
-                                  onClick={() => handleFetchOpenRouterModels(providerId, 'all')}
-                                  disabled={
-                                    !providers[providerId]?.apiKey || openRouterFetchState[providerId]?.isFetching
-                                  }
-                                  title={!providers[providerId]?.apiKey ? 'Enter API Key first' : ''}>
-                                  {openRouterFetchState[providerId]?.isFetching &&
-                                  openRouterFetchState[providerId]?.displayMode === 'all'
-                                    ? 'Fetching All...'
-                                    : 'Fetch All Models'}
-                                </Button>
-                                <Button
-                                  variant="secondary"
-                                  onClick={() => handleFetchOpenRouterModels(providerId, 'free')}
-                                  disabled={
-                                    !providers[providerId]?.apiKey || openRouterFetchState[providerId]?.isFetching
-                                  }
-                                  title={!providers[providerId]?.apiKey ? 'Enter API Key first' : ''}>
-                                  {openRouterFetchState[providerId]?.isFetching &&
-                                  openRouterFetchState[providerId]?.displayMode === 'free'
-                                    ? 'Fetching Free...'
-                                    : 'Fetch Free Models'}
-                                </Button>
-                              </div>
-
-                              {/* Error Display */}
-                              {openRouterFetchState[providerId]?.error && (
-                                <p
-                                  className={`text-xs ${isDarkMode ? 'text-red-400' : 'text-red-600'}`}>{`Error: ${openRouterFetchState[providerId]?.error}`}</p>
-                              )}
-
-                              {/* Search input for filtering models */}
-                              {openRouterFetchState[providerId]?.allModels && (
+                              <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`}>
+                                {providerConfig.modelNames && providerConfig.modelNames.length > 0 ? (
+                                  providerConfig.modelNames.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>
+                                  ))
+                                ) : (
+                                  <span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
+                                    No models selected. Add model names manually if needed.
+                                  </span>
+                                )}
                                 <input
+                                  id={`${providerId}-models-input`}
                                   type="text"
-                                  placeholder="Search models..."
-                                  className={`mb-2 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`}
-                                  value={searchInputs[providerId] || ''}
-                                  onChange={e =>
-                                    setSearchInputs(prev => ({
-                                      ...prev,
-                                      [providerId]: e.target.value,
-                                    }))
-                                  }
+                                  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`}
                                 />
-                              )}
-
-                              {/* Model Select Dropdown (if models fetched) */}
-                              {openRouterFetchState[providerId]?.allModels && (
-                                <select
-                                  id={`${providerId}-models-select`}
-                                  multiple
-                                  value={providerConfig.modelNames || []} // Keep selection based on providerConfig
-                                  onChange={e => {
-                                    const selectedOptions = Array.from(
-                                      e.target.selectedOptions,
-                                      option => option.value,
-                                    );
-                                    setModifiedProviders(prev => new Set(prev).add(providerId));
-                                    setProviders(prev => ({
-                                      ...prev,
-                                      [providerId]: {
-                                        ...prev[providerId],
-                                        modelNames: selectedOptions.map(option => option), // Just store the IDs
-                                      },
-                                    }));
-                                  }}
-                                  className={`h-32 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`}>
-                                  {/* Filter models based on displayMode and search text */}
-                                  {(openRouterFetchState[providerId]?.allModels || [])
-                                    .filter(modelId => {
-                                      // Filter by display mode (free/all)
-                                      const passesDisplayMode =
-                                        openRouterFetchState[providerId]?.displayMode === 'free'
-                                          ? modelId.isFree
-                                          : true;
-
-                                      // Filter by search text
-                                      const searchText = (searchInputs[providerId] || '').toLowerCase().trim();
-                                      // If search is empty, show all models that pass displayMode
-                                      if (!searchText) return passesDisplayMode;
-                                      // Otherwise filter by search text in model ID
-                                      const passesSearch = modelId.id.toLowerCase().includes(searchText);
-
-                                      return passesDisplayMode && passesSearch;
-                                    })
-                                    .sort((a, b) => a.id.localeCompare(b.id)) // Sort by model ID
-                                    .map(model => (
-                                      <option key={model.id} value={model.id}>
-                                        {model.id}
-                                      </option>
-                                    ))}
-                                </select>
-                              )}
-
-                              {/* Prompt/Selected Models Display (if not fetched yet) */}
-                              {!openRouterFetchState[providerId]?.allModels &&
-                                !openRouterFetchState[providerId]?.isFetching && (
-                                  <p className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
-                                    {providerConfig.modelNames && providerConfig.modelNames.length > 0
-                                      ? 'Currently selected models (fetch to see available options):'
-                                      : 'Click fetch buttons to load model list.'}
-                                  </p>
-                                )}
-                              {!openRouterFetchState[providerId]?.allModels &&
-                                providerConfig.modelNames &&
-                                providerConfig.modelNames.length > 0 && (
-                                  <div className="mt-1 flex flex-wrap gap-1">
-                                    {providerConfig.modelNames.map(model => (
-                                      <span
-                                        key={model}
-                                        className={`rounded-full px-2 py-0.5 text-xs ${isDarkMode ? 'bg-blue-900 text-blue-100' : 'bg-blue-100 text-blue-800'}`}>
-                                        {model}
-                                      </span>
-                                    ))}
-                                  </div>
-                                )}
+                              </div>
+                              <p className={`mt-1 text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
+                                Type and Press Enter or Space to add.
+                              </p>
                             </>
                           ) : (
-                            /* === ELSE: Default Tag Input for other providers === */
+                            /* Default Tag Input for other providers */
                             <>
                               <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`}>