Browse Source

Merge pull request #52 from nanobrowser/providers

Providers
Ashu 5 months ago
parent
commit
8ad35b7091

+ 24 - 8
.github/ISSUE_TEMPLATE/bug_report.yml

@@ -32,20 +32,36 @@ body:
       label: Steps to Reproduce
       description: Steps to reproduce the behavior
       placeholder: |
-        1. set '...'
-        2. Click on '....'
+        1. My prompt is '...'
+        2. I click '...'
+        3. I see '...'
     validations:
       required: true
       
   - type: dropdown
-    id: os
+    id: llm_provider
     attributes:
-      label: Operating System
-      description: What operating system are you using?
+      label: LLM Service Provider
+      description: Which LLM service provider are you using?
       options:
-        - macOS
-        - Windows
-        - Linux
+        - OpenAI
+        - Anthropic
+        - Google Gemini
+        - Ollama
+        - OpenRouter
+        - Other (please specify in description)
+    validations:
+      required: true
+      
+  - type: textarea
+    id: models
+    attributes:
+      label: Models Used
+      description: Which models are you using for the 3 agents (Planner, Navigator, Validator)?
+      placeholder: |
+        Planner: gpt-4o
+        Navigator: claude-3.7-sonnet
+        Validator: gemini-2.0-flash
     validations:
       required: true
  

+ 32 - 4
README.md

@@ -35,7 +35,7 @@ Looking for a powerful AI web agent without the $200/month price tag of OpenAI O
 - **Flexible LLM Options** - Connect to your preferred LLM providers with the freedom to choose different models for different agents.
 - **Fully Open Source** - Complete transparency in how your browser is automated. No black boxes or hidden processes.
 
-> **Note:** We currently support OpenAI, Anthropic and Gemini, with more LLM integrations coming soon!
+> **Note:** We currently support OpenAI, Anthropic, Gemini, Ollama and custom OpenAI-Compatible providers, more providers will be supported.
 
 
 ## 📊 Key Features
@@ -66,6 +66,19 @@ Looking for a powerful AI web agent without the $200/month price tag of OpenAI O
     *   Add your LLM API keys.
     *   Choose which model to use for different agents (Navigator, Planner, Validator)
 
+## 🔄 Upgrading
+
+1. **Download**:
+    * Download the latest `nanobrowser.zip` file from the official Github [release page](https://github.com/nanobrowser/nanobrowser/releases).
+
+2. **Replace**:
+    * Unzip `nanobrowser.zip`.
+    * Replace your existing Nanobrowser files with the new ones.
+
+3. **Refresh**:
+    * Go to `chrome://extensions/` in Chrome.
+    * Click the refresh icon on the Nanobrowser card.
+
 ## 🛠️ Build from Source
 
 If you prefer to build Nanobrowser yourself, follow these steps:
@@ -119,6 +132,22 @@ Nanobrowser allows you to configure different LLM models for each agent to balan
   - Lightweight and cost-efficient
   - Suitable for basic navigation tasks
 
+### Local Models
+- **Setup Options**:
+  - Use Ollama or other custom OpenAI-compatible providers to run models locally
+  - Zero API costs and complete privacy with no data leaving your machine
+
+- **Recommended Models**:
+  - **Qwen 2.5 Coder 14B**
+  - **Mistral Small 24B**
+  - We welcome community experience sharing with other local models in our [Discord](https://discord.gg/NN3ABHggMK)
+
+- **Prompt Engineering**:
+  - Local models require more specific and cleaner prompts
+  - Avoid high-level, ambiguous commands
+  - Break complex tasks into clear, detailed steps
+  - Provide explicit context and constraints
+
 > **Note**: The cost-effective configuration may produce less stable outputs and require more iterations for complex tasks.
 
 > **Tip**: Feel free to experiment with your own model configurations! Found a great combination? Share it with the community in our [Discord](https://discord.gg/NN3ABHggMK) to help others optimize their setup.
@@ -157,9 +186,8 @@ We're actively developing Nanobrowser with exciting features on the horizon, wel
 *  **Provide Feedback** 
    * Try Nanobrowser and give us feedback on its performance or suggest improvements in our [Discord server](https://discord.gg/NN3ABHggMK).
 * **Contribute Code**
-   * Create an issue
-   * Fork the repository
-   * Open a pull request
+   * Check out our [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute code to the project.
+   * Submit pull requests for bug fixes, features, or documentation improvements.
 
 
 We believe in the power of open source and community collaboration.  Join us in building the future of web automation!

+ 1 - 1
chrome-extension/package.json

@@ -21,7 +21,7 @@
     "@langchain/anthropic": "^0.3.12",
     "@langchain/core": "^0.3.37",
     "@langchain/google-genai": "0.1.10",
-    "@langchain/groq": "^0.1.3",
+    "@langchain/ollama": "^0.2.0",
     "@langchain/openai": "^0.4.2",
     "@langchain/xai": "^0.0.2",
     "puppeteer-core": "24.1.1",

+ 83 - 80
chrome-extension/src/background/agent/helper.ts

@@ -1,119 +1,122 @@
-import { type ProviderConfig, LLMProviderEnum, AgentNameEnum } from '@extension/storage';
+import { type ProviderConfig, type ModelConfig, ProviderTypeEnum } from '@extension/storage';
 import { ChatOpenAI } from '@langchain/openai';
 import { ChatAnthropic } from '@langchain/anthropic';
 import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
-import { ChatGroq } from '@langchain/groq';
 import { ChatXAI } from '@langchain/xai';
 import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
+import { ChatOllama } from '@langchain/ollama';
+
+const maxTokens = 1024 * 4;
+
+function isOpenAIOModel(modelName: string): boolean {
+  return modelName.startsWith('openai/o') || modelName.startsWith('o');
+}
+
+function createOpenAIChatModel(providerConfig: ProviderConfig, modelConfig: ModelConfig): BaseChatModel {
+  const args: {
+    model: string;
+    apiKey?: string;
+    configuration?: Record<string, unknown>;
+    modelKwargs?: { max_completion_tokens: number };
+    topP?: number;
+    temperature?: number;
+    maxTokens?: number;
+  } = {
+    model: modelConfig.modelName,
+    apiKey: providerConfig.apiKey,
+  };
+
+  if (providerConfig.baseUrl) {
+    args.configuration = {
+      baseURL: providerConfig.baseUrl,
+    };
+  }
+  // custom provider may have no api key
+  if (providerConfig.apiKey) {
+    args.apiKey = providerConfig.apiKey;
+  }
+
+  // O series models have different parameters
+  if (isOpenAIOModel(modelConfig.modelName)) {
+    args.modelKwargs = {
+      max_completion_tokens: maxTokens,
+    };
+  } else {
+    args.topP = (modelConfig.parameters?.topP ?? 0.1) as number;
+    args.temperature = (modelConfig.parameters?.temperature ?? 0.1) as number;
+    args.maxTokens = maxTokens;
+  }
+  return new ChatOpenAI(args);
+}
 
 // create a chat model based on the agent name, the model name and provider
-export function createChatModel(
-  agentName: string,
-  providerName: LLMProviderEnum,
-  providerConfig: ProviderConfig,
-  modelName: string,
-): BaseChatModel {
-  const maxTokens = 2000;
-  const maxCompletionTokens = 5000;
-  let temperature = 0;
-  let topP = 0.001;
-  switch (providerName) {
-    case LLMProviderEnum.OpenAI: {
-      if (agentName === AgentNameEnum.Planner) {
-        temperature = 0.02;
-      }
-      const args: any = {
-        model: modelName,
-        apiKey: providerConfig.apiKey,
-        configuration: {},
-      };
-      if (providerConfig.baseUrl) {
-        args.configuration = {
-          baseURL: providerConfig.baseUrl,
-        };
-      }
+export function createChatModel(providerConfig: ProviderConfig, modelConfig: ModelConfig): BaseChatModel {
+  const temperature = (modelConfig.parameters?.temperature ?? 0.1) as number;
+  const topP = (modelConfig.parameters?.topP ?? 0.1) as number;
 
-      // O series models have different parameters
-      if (modelName.startsWith('o')) {
-        args.modelKwargs = {
-          max_completion_tokens: maxCompletionTokens,
-        };
-      } else {
-        args.topP = topP;
-        args.temperature = temperature;
-        args.maxTokens = maxTokens;
-      }
-      return new ChatOpenAI(args);
+  console.log('providerConfig', providerConfig);
+  console.log('modelConfig', modelConfig);
+
+  switch (modelConfig.provider) {
+    case ProviderTypeEnum.OpenAI: {
+      return createOpenAIChatModel(providerConfig, modelConfig);
     }
-    case LLMProviderEnum.Anthropic: {
-      temperature = 0.1;
-      topP = 0.1;
+    case ProviderTypeEnum.Anthropic: {
       const args = {
-        model: modelName,
+        model: modelConfig.modelName,
         apiKey: providerConfig.apiKey,
         maxTokens,
         temperature,
         topP,
         clientOptions: {},
       };
-      if (providerConfig.baseUrl) {
-        args.clientOptions = {
-          baseURL: providerConfig.baseUrl,
-        };
-      }
       return new ChatAnthropic(args);
     }
-    case LLMProviderEnum.Gemini: {
-      temperature = 0.5;
-      topP = 0.8;
+    case ProviderTypeEnum.Gemini: {
       const args = {
-        model: modelName,
+        model: modelConfig.modelName,
         apiKey: providerConfig.apiKey,
         temperature,
         topP,
       };
       return new ChatGoogleGenerativeAI(args);
     }
-    case LLMProviderEnum.Groq: {
-      temperature = 0.7;
-      const args: any = {
-        model: modelName,
+    case ProviderTypeEnum.Grok: {
+      const args = {
+        model: modelConfig.modelName,
         apiKey: providerConfig.apiKey,
         temperature,
+        topP,
         maxTokens,
         configuration: {},
-        modelKwargs: {
-          stop: [],
-          stream: false,
-        },
       };
-      if (providerConfig.baseUrl) {
-        args.configuration = {
-          baseURL: providerConfig.baseUrl,
-        };
-      }
-      return new ChatGroq(args);
+      return new ChatXAI(args) as BaseChatModel;
     }
-    case LLMProviderEnum.Grok: {
-      temperature = 0.7;
-      topP = 0.9;
-      const args: any = {
-        model: modelName,
-        apiKey: providerConfig.apiKey,
-        temperature,
+    case ProviderTypeEnum.Ollama: {
+      const args: {
+        model: string;
+        apiKey?: string;
+        baseUrl: string;
+        modelKwargs?: { max_completion_tokens: number };
+        topP?: number;
+        temperature?: number;
+        maxTokens?: number;
+        numCtx: number;
+      } = {
+        model: modelConfig.modelName,
+        // required but ignored by ollama
+        apiKey: providerConfig.apiKey === '' ? 'ollama' : providerConfig.apiKey,
+        baseUrl: providerConfig.baseUrl ?? 'http://localhost:11434',
         topP,
+        temperature,
         maxTokens,
-        configuration: {},
+        numCtx: 128000,
       };
-      if (providerConfig.baseUrl) {
-        args.configuration = {
-          baseURL: providerConfig.baseUrl,
-        };
-      }
-      return new ChatXAI(args);
+      return new ChatOllama(args);
     }
     default: {
-      throw new Error(`Provider ${providerName} not supported yet`);
+      // by default, we think it's a openai-compatible provider
+      return createOpenAIChatModel(providerConfig, modelConfig);
     }
   }
 }

+ 3 - 18
chrome-extension/src/background/index.ts

@@ -181,33 +181,18 @@ 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(
-    AgentNameEnum.Navigator,
-    navigatorModel.provider,
-    providers[navigatorModel.provider],
-    navigatorModel.modelName,
-  );
+  const navigatorLLM = createChatModel(providers[navigatorModel.provider], navigatorModel);
 
   let plannerLLM = null;
   const plannerModel = agentModels[AgentNameEnum.Planner];
   if (plannerModel) {
-    plannerLLM = createChatModel(
-      AgentNameEnum.Planner,
-      plannerModel.provider,
-      providers[plannerModel.provider],
-      plannerModel.modelName,
-    );
+    plannerLLM = createChatModel(providers[plannerModel.provider], plannerModel);
   }
 
   let validatorLLM = null;
   const validatorModel = agentModels[AgentNameEnum.Validator];
   if (validatorModel) {
-    validatorLLM = createChatModel(
-      AgentNameEnum.Validator,
-      validatorModel.provider,
-      providers[validatorModel.provider],
-      validatorModel.modelName,
-    );
+    validatorLLM = createChatModel(providers[validatorModel.provider], validatorModel);
   }
 
   const generalSettings = await generalSettingsStore.getSettings();

+ 31 - 8
packages/storage/lib/settings/agentModels.ts

@@ -1,12 +1,15 @@
 import { StorageEnum } from '../base/enums';
 import { createStorage } from '../base/base';
 import type { BaseStorage } from '../base/types';
-import { type AgentNameEnum, type LLMProviderEnum, llmProviderModelNames } from './types';
+import type { AgentNameEnum } from './types';
+import { llmProviderParameters } from './types';
 
 // Interface for a single model configuration
 export interface ModelConfig {
-  provider: LLMProviderEnum;
+  // providerId, the key of the provider in the llmProviderStore, not the provider name
+  provider: string;
   modelName: string;
+  parameters?: Record<string, unknown>;
 }
 
 // Interface for storing multiple agent model configurations
@@ -36,27 +39,47 @@ function validateModelConfig(config: ModelConfig) {
   if (!config.provider || !config.modelName) {
     throw new Error('Provider and model name must be specified');
   }
+}
 
-  const validModels = llmProviderModelNames[config.provider];
-  if (!validModels.includes(config.modelName)) {
-    throw new Error(`Invalid model "${config.modelName}" for provider "${config.provider}"`);
-  }
+function getModelParameters(agent: AgentNameEnum, provider: string): Record<string, unknown> {
+  const providerParams = llmProviderParameters[provider as keyof typeof llmProviderParameters]?.[agent];
+  return providerParams ?? { temperature: 0.1, topP: 0.1 };
 }
 
 export const agentModelStore: AgentModelStorage = {
   ...storage,
   setAgentModel: async (agent: AgentNameEnum, config: ModelConfig) => {
     validateModelConfig(config);
+    // Merge default parameters with provided parameters
+    const defaultParams = getModelParameters(agent, config.provider);
+    const mergedConfig = {
+      ...config,
+      parameters: {
+        ...defaultParams,
+        ...config.parameters,
+      },
+    };
     await storage.set(current => ({
       agents: {
         ...current.agents,
-        [agent]: config,
+        [agent]: mergedConfig,
       },
     }));
   },
   getAgentModel: async (agent: AgentNameEnum) => {
     const data = await storage.get();
-    return data.agents[agent];
+    const config = data.agents[agent];
+    if (!config) return undefined;
+
+    // Merge default parameters with stored parameters
+    const defaultParams = getModelParameters(agent, config.provider);
+    return {
+      ...config,
+      parameters: {
+        ...defaultParams,
+        ...config.parameters,
+      },
+    };
   },
   resetAgentModel: async (agent: AgentNameEnum) => {
     await storage.set(current => {

+ 150 - 35
packages/storage/lib/settings/llmProviders.ts

@@ -1,83 +1,198 @@
 import { StorageEnum } from '../base/enums';
 import { createStorage } from '../base/base';
 import type { BaseStorage } from '../base/types';
-import type { LLMProviderEnum } from './types';
+import { type AgentNameEnum, llmProviderModelNames, llmProviderParameters, ProviderTypeEnum } from './types';
 
 // Interface for a single provider configuration
 export interface ProviderConfig {
-  apiKey: string;
-  baseUrl?: string;
+  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
+  createdAt?: number; // Timestamp in milliseconds when the provider was created
 }
 
 // Interface for storing multiple LLM provider configurations
+// The key is the provider id, which is the same as the provider type for built-in providers, but is custom for custom providers
 export interface LLMKeyRecord {
-  providers: Record<LLMProviderEnum, ProviderConfig>;
+  providers: Record<string, ProviderConfig>;
 }
 
 export type LLMProviderStorage = BaseStorage<LLMKeyRecord> & {
-  setProvider: (provider: LLMProviderEnum, config: ProviderConfig) => Promise<void>;
-  getProvider: (provider: LLMProviderEnum) => Promise<ProviderConfig | undefined>;
-  removeProvider: (provider: LLMProviderEnum) => Promise<void>;
-  hasProvider: (provider: LLMProviderEnum) => Promise<boolean>;
-  getConfiguredProviders: () => Promise<LLMProviderEnum[]>;
-  getAllProviders: () => Promise<Record<LLMProviderEnum, ProviderConfig>>;
+  setProvider: (providerId: string, config: ProviderConfig) => Promise<void>;
+  getProvider: (providerId: string) => Promise<ProviderConfig | undefined>;
+  removeProvider: (providerId: string) => Promise<void>;
+  hasProvider: (providerId: string) => Promise<boolean>;
+  getAllProviders: () => Promise<Record<string, ProviderConfig>>;
 };
 
+// Storage for LLM provider configurations
+// use "llm-api-keys" as the key for the storage, for backward compatibility
 const storage = createStorage<LLMKeyRecord>(
   'llm-api-keys',
-  { providers: {} as Record<LLMProviderEnum, ProviderConfig> },
+  { providers: {} },
   {
     storageEnum: StorageEnum.Local,
     liveUpdate: true,
   },
 );
 
+// Helper function to determine provider type from provider name
+// Make sure to update this function if you add a new provider type
+export function getProviderTypeByProviderId(providerId: string): ProviderTypeEnum {
+  switch (providerId) {
+    case ProviderTypeEnum.OpenAI:
+    case ProviderTypeEnum.Anthropic:
+    case ProviderTypeEnum.Gemini:
+    case ProviderTypeEnum.Grok:
+    case ProviderTypeEnum.Ollama:
+      return providerId;
+    default:
+      return ProviderTypeEnum.CustomOpenAI;
+  }
+}
+
+// Helper function to get display name from provider id
+// Make sure to update this function if you add a new provider type
+export function getDefaultDisplayNameFromProviderId(providerId: string): string {
+  switch (providerId) {
+    case ProviderTypeEnum.OpenAI:
+      return 'OpenAI';
+    case ProviderTypeEnum.Anthropic:
+      return 'Anthropic';
+    case ProviderTypeEnum.Gemini:
+      return 'Gemini';
+    case ProviderTypeEnum.Grok:
+      return 'Grok';
+    case ProviderTypeEnum.Ollama:
+      return 'Ollama';
+    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:
+    case ProviderTypeEnum.Anthropic:
+    case ProviderTypeEnum.Gemini:
+    case ProviderTypeEnum.Grok:
+      return {
+        apiKey: '',
+        name: getDefaultDisplayNameFromProviderId(providerId),
+        type: providerId,
+        modelNames: [...(llmProviderModelNames[providerId] || [])],
+        createdAt: Date.now(),
+      };
+
+    case ProviderTypeEnum.Ollama:
+      return {
+        apiKey: 'ollama', // Set default API key for Ollama
+        name: getDefaultDisplayNameFromProviderId(ProviderTypeEnum.Ollama),
+        type: ProviderTypeEnum.Ollama,
+        modelNames: [],
+        baseUrl: 'http://localhost:11434',
+        createdAt: Date.now(),
+      };
+    default:
+      return {
+        apiKey: '',
+        name: getDefaultDisplayNameFromProviderId(providerId),
+        type: ProviderTypeEnum.CustomOpenAI,
+        baseUrl: '',
+        modelNames: [],
+        createdAt: Date.now(),
+      };
+  }
+}
+
+export function getDefaultAgentModelParams(providerId: string, agentName: AgentNameEnum): Record<string, number> {
+  const newParameters = llmProviderParameters[providerId as keyof typeof llmProviderParameters]?.[agentName] || {
+    temperature: 0.1,
+    topP: 0.1,
+  };
+  return newParameters;
+}
+
+// Helper function to ensure backward compatibility for provider configs
+function ensureBackwardCompatibility(providerId: string, config: ProviderConfig): ProviderConfig {
+  const updatedConfig = { ...config };
+  if (!updatedConfig.name) {
+    updatedConfig.name = getDefaultDisplayNameFromProviderId(providerId);
+  }
+  if (!updatedConfig.type) {
+    updatedConfig.type = getProviderTypeByProviderId(providerId);
+  }
+  if (!updatedConfig.modelNames) {
+    updatedConfig.modelNames = llmProviderModelNames[providerId as keyof typeof llmProviderModelNames] || [];
+  }
+  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();
+  }
+  return updatedConfig;
+}
+
 export const llmProviderStore: LLMProviderStorage = {
   ...storage,
-  async setProvider(provider: LLMProviderEnum, config: ProviderConfig) {
-    if (!provider) {
-      throw new Error('Provider name cannot be empty');
+  async setProvider(providerId: string, config: ProviderConfig) {
+    if (!providerId) {
+      throw new Error('Provider id cannot be empty');
     }
-    if (!config.apiKey) {
-      throw new Error('API key cannot be empty');
+
+    if (config.apiKey === undefined) {
+      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');
     }
+
+    // Ensure backward compatibility by filling in missing fields
+    const completeConfig: ProviderConfig = {
+      ...config,
+      name: config.name || getDefaultDisplayNameFromProviderId(providerId),
+      type: config.type || getProviderTypeByProviderId(providerId),
+      modelNames: config.modelNames,
+      createdAt: config.createdAt || Date.now(),
+    };
+
     const current = (await storage.get()) || { providers: {} };
     await storage.set({
       providers: {
         ...current.providers,
-        [provider]: config,
+        [providerId]: completeConfig,
       },
     });
   },
-  async getProvider(provider: LLMProviderEnum) {
+  async getProvider(providerId: string) {
     const data = (await storage.get()) || { providers: {} };
-    return data.providers[provider];
+    const config = data.providers[providerId];
+    return config ? ensureBackwardCompatibility(providerId, config) : undefined;
   },
-  async removeProvider(provider: LLMProviderEnum) {
+  async removeProvider(providerId: string) {
     const current = (await storage.get()) || { providers: {} };
     const newProviders = { ...current.providers };
-    delete newProviders[provider];
+    delete newProviders[providerId];
     await storage.set({ providers: newProviders });
   },
-  async hasProvider(provider: LLMProviderEnum) {
+  async hasProvider(providerId: string) {
     const data = (await storage.get()) || { providers: {} };
-    return provider in data.providers;
+    return providerId in data.providers;
   },
-  async getConfiguredProviders() {
-    console.log('Getting configured providers');
+
+  async getAllProviders() {
     const data = await storage.get();
-    console.log('Raw storage data:', data); // Debug the entire data object
+    const providers = { ...data.providers };
 
-    if (!data || !data.providers) {
-      console.log('No data found, returning empty array');
-      return [];
+    // Add backward compatibility for all providers
+    for (const [providerId, config] of Object.entries(providers)) {
+      providers[providerId] = ensureBackwardCompatibility(providerId, config);
     }
 
-    console.log('Configured providers:', data.providers);
-    return Object.keys(data.providers) as LLMProviderEnum[];
-  },
-  async getAllProviders() {
-    const data = await storage.get();
-    return data.providers;
+    return providers;
   },
 };

+ 87 - 23
packages/storage/lib/settings/types.ts

@@ -1,43 +1,107 @@
+// Agent name, used to identify the agent in the settings
 export enum AgentNameEnum {
   Planner = 'planner',
   Navigator = 'navigator',
   Validator = 'validator',
 }
 
-// Enum for supported LLM providers
-export enum LLMProviderEnum {
+// Provider type, types before CustomOpenAI are built-in providers, CustomOpenAI is a custom provider
+// For built-in providers, we will create ChatModel instances with its respective LangChain ChatModel classes
+// For custom providers, we will create ChatModel instances with the ChatOpenAI class
+export enum ProviderTypeEnum {
   OpenAI = 'openai',
   Anthropic = 'anthropic',
   Gemini = 'gemini',
-  Groq = 'groq',
   Grok = 'grok',
+  Ollama = 'ollama',
+  CustomOpenAI = 'custom_openai',
 }
 
+// Default supported models for each built-in provider
 export const llmProviderModelNames = {
-  [LLMProviderEnum.OpenAI]: ['gpt-4o', 'gpt-4o-mini', 'o1', 'o1-mini', 'o3-mini', 'deepseek-r1'],
-  [LLMProviderEnum.Anthropic]: ['claude-3-7-sonnet-latest', 'claude-3-5-haiku-latest'],
-  [LLMProviderEnum.Gemini]: [
+  [ProviderTypeEnum.OpenAI]: ['gpt-4o', 'gpt-4o-mini', 'o1', 'o3-mini'],
+  [ProviderTypeEnum.Anthropic]: ['claude-3-7-sonnet-latest', 'claude-3-5-sonnet-latest', 'claude-3-5-haiku-latest'],
+  [ProviderTypeEnum.Gemini]: [
     'gemini-2.0-flash',
     'gemini-2.0-flash-lite',
     'gemini-2.0-pro-exp-02-05',
     // 'gemini-2.0-flash-thinking-exp-01-21', // TODO: not support function calling for now
   ],
-  [LLMProviderEnum.Groq]: [
-    'llama-3.1-8b-instant',
-    'mixtral-8x7b-32768',
-    'llama2-70b-4096',
-    'llama-2-70b-4096',
-    'gemma-7b-it',
-  ],
-  [LLMProviderEnum.Grok]: ['grok-2', 'grok-2-vision'],
+  [ProviderTypeEnum.Grok]: ['grok-2', 'grok-2-vision'],
+  [ProviderTypeEnum.Ollama]: [],
+  // Custom OpenAI providers don't have predefined models as they are user-defined
 };
 
-/**
- * Creates a mapping of LLM model names to their corresponding providers.
- *
- * This function takes the llmProviderModelNames object and converts it into a new object
- * where each model name is mapped to its corresponding provider.
- */
-export const llmModelNamesToProvider = Object.fromEntries(
-  Object.entries(llmProviderModelNames).flatMap(([provider, models]) => models.map(model => [model, provider])),
-);
+// Default parameters for each agent per provider, for providers not specified, use OpenAI parameters
+export const llmProviderParameters = {
+  [ProviderTypeEnum.OpenAI]: {
+    [AgentNameEnum.Planner]: {
+      temperature: 0.01,
+      topP: 0.001,
+    },
+    [AgentNameEnum.Navigator]: {
+      temperature: 0,
+      topP: 0.001,
+    },
+    [AgentNameEnum.Validator]: {
+      temperature: 0,
+      topP: 0.001,
+    },
+  },
+  [ProviderTypeEnum.Anthropic]: {
+    [AgentNameEnum.Planner]: {
+      temperature: 0.1,
+      topP: 0.1,
+    },
+    [AgentNameEnum.Navigator]: {
+      temperature: 0.1,
+      topP: 0.1,
+    },
+    [AgentNameEnum.Validator]: {
+      temperature: 0.05,
+      topP: 0.1,
+    },
+  },
+  [ProviderTypeEnum.Gemini]: {
+    [AgentNameEnum.Planner]: {
+      temperature: 0.01,
+      topP: 0.1,
+    },
+    [AgentNameEnum.Navigator]: {
+      temperature: 0.01,
+      topP: 0.1,
+    },
+    [AgentNameEnum.Validator]: {
+      temperature: 0.1,
+      topP: 0.1,
+    },
+  },
+  [ProviderTypeEnum.Grok]: {
+    [AgentNameEnum.Planner]: {
+      temperature: 0.7,
+      topP: 0.9,
+    },
+    [AgentNameEnum.Navigator]: {
+      temperature: 0.7,
+      topP: 0.9,
+    },
+    [AgentNameEnum.Validator]: {
+      temperature: 0.7,
+      topP: 0.9,
+    },
+  },
+  [ProviderTypeEnum.Ollama]: {
+    [AgentNameEnum.Planner]: {
+      temperature: 0,
+      topP: 0.001,
+    },
+    [AgentNameEnum.Navigator]: {
+      temperature: 0.01,
+      topP: 0.001,
+    },
+    [AgentNameEnum.Validator]: {
+      temperature: 0,
+      topP: 0.001,
+    },
+  },
+};

+ 14 - 6
packages/ui/lib/components/Button.tsx

@@ -14,16 +14,24 @@ export function Button({ theme, variant = 'primary', className, disabled, childr
         'py-1 px-4 rounded shadow transition-all',
         {
           // Primary variant
-          'bg-blue-500 hover:bg-blue-600 text-white hover:scale-105': variant === 'primary' && !disabled,
-          'bg-gray-500 text-gray-700 cursor-not-allowed': variant === 'primary' && disabled,
+          'bg-blue-500 hover:bg-blue-600 text-white hover:scale-105':
+            variant === 'primary' && !disabled && theme !== 'dark',
+          'bg-blue-600 hover:bg-blue-700 text-white hover:scale-105':
+            variant === 'primary' && !disabled && theme === 'dark',
+          'bg-gray-400 text-gray-600 cursor-not-allowed': variant === 'primary' && disabled,
 
           // Secondary variant
-          'bg-gray-100 hover:bg-gray-200 text-gray-800 hover:scale-105': variant === 'secondary' && !disabled,
-          'bg-gray-500 text-gray-700 cursor-not-allowed': variant === 'secondary' && disabled,
+          'bg-gray-300 hover:bg-gray-400 text-gray-800 hover:scale-105': variant === 'secondary' && !disabled,
+          'bg-gray-100 text-gray-400 cursor-not-allowed': variant === 'secondary' && disabled,
 
           // Danger variant
-          'bg-red-500 hover:bg-red-600 text-white hover:scale-105': variant === 'danger' && !disabled,
-          'bg-red-300 text-red-100 cursor-not-allowed': variant === 'danger' && disabled,
+          // Note: bg-red-400 causes the button to appear black (RGB 0,0,0) for unknown reasons
+          // Using bg-red-500 with opacity to achieve a softer look
+          'bg-red-600 bg-opacity-80 hover:bg-red-700 hover:bg-opacity-90 text-white hover:scale-105':
+            variant === 'danger' && !disabled && theme !== 'dark',
+          'bg-red-500 bg-opacity-70 hover:bg-red-700 hover:bg-opacity-90 text-white hover:scale-105':
+            variant === 'danger' && !disabled && theme === 'dark',
+          'bg-red-300 bg-opacity-80 text-red-100 cursor-not-allowed': variant === 'danger' && disabled,
         },
         className,
       )}

+ 1 - 1
pages/options/src/Options.tsx

@@ -49,7 +49,7 @@ const Options = () => {
               <li key={item.id}>
                 <Button
                   onClick={() => setActiveTab(item.id)}
-                  className={`flex w-full items-center space-x-2 rounded-lg px-4 py-2 text-left 
+                  className={`flex w-full items-center space-x-2 rounded-lg px-4 py-2 text-left text-base 
                     ${
                       activeTab !== item.id
                         ? `${isDarkMode ? 'bg-slate-700/70 text-gray-300 hover:text-white' : 'bg-[#0EA5E9]/15 font-medium text-gray-700 hover:text-white'} backdrop-blur-sm`

+ 4 - 4
pages/options/src/components/GeneralSettings.tsx

@@ -46,7 +46,7 @@ export const GeneralSettings = ({ isDarkMode = false }: GeneralSettingsProps) =>
               min={1}
               max={100}
               value={settings.maxSteps}
-              onChange={e => updateSetting('maxSteps', parseInt(e.target.value, 10))}
+              onChange={e => updateSetting('maxSteps', Number.parseInt(e.target.value, 10))}
               className={`w-20 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} px-3 py-2`}
             />
           </div>
@@ -69,7 +69,7 @@ export const GeneralSettings = ({ isDarkMode = false }: GeneralSettingsProps) =>
               min={1}
               max={100}
               value={settings.maxActionsPerStep}
-              onChange={e => updateSetting('maxActionsPerStep', parseInt(e.target.value, 10))}
+              onChange={e => updateSetting('maxActionsPerStep', Number.parseInt(e.target.value, 10))}
               className={`w-20 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} px-3 py-2`}
             />
           </div>
@@ -92,7 +92,7 @@ export const GeneralSettings = ({ isDarkMode = false }: GeneralSettingsProps) =>
               min={1}
               max={10}
               value={settings.maxFailures}
-              onChange={e => updateSetting('maxFailures', parseInt(e.target.value, 10))}
+              onChange={e => updateSetting('maxFailures', Number.parseInt(e.target.value, 10))}
               className={`w-20 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} px-3 py-2`}
             />
           </div>
@@ -163,7 +163,7 @@ export const GeneralSettings = ({ isDarkMode = false }: GeneralSettingsProps) =>
               min={1}
               max={20}
               value={settings.planningInterval}
-              onChange={e => updateSetting('planningInterval', parseInt(e.target.value, 10))}
+              onChange={e => updateSetting('planningInterval', Number.parseInt(e.target.value, 10))}
               className={`w-20 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} px-3 py-2`}
             />
           </div>

+ 999 - 219
pages/options/src/components/ModelSettings.tsx

@@ -1,11 +1,15 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useRef, useCallback } from 'react';
+import type { KeyboardEvent } from 'react';
 import { Button } from '@extension/ui';
 import {
   llmProviderStore,
   agentModelStore,
   AgentNameEnum,
-  LLMProviderEnum,
   llmProviderModelNames,
+  ProviderTypeEnum,
+  getDefaultDisplayNameFromProviderId,
+  getDefaultProviderConfig,
+  getDefaultAgentModelParams,
 } from '@extension/storage';
 
 interface ModelSettingsProps {
@@ -13,323 +17,1099 @@ interface ModelSettingsProps {
 }
 
 export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
-  const [apiKeys, setApiKeys] = useState<Record<LLMProviderEnum, { apiKey: string; baseUrl?: string }>>(
-    {} as Record<LLMProviderEnum, { apiKey: string; baseUrl?: string }>,
-  );
-  const [modifiedProviders, setModifiedProviders] = useState<Set<LLMProviderEnum>>(new Set());
+  const [providers, setProviders] = useState<
+    Record<
+      string,
+      {
+        apiKey: string;
+        baseUrl?: string;
+        name?: string;
+        modelNames?: string[];
+        type?: ProviderTypeEnum;
+        createdAt?: number;
+      }
+    >
+  >({});
+  const [modifiedProviders, setModifiedProviders] = useState<Set<string>>(new Set());
+  const [providersFromStorage, setProvidersFromStorage] = useState<Set<string>>(new Set());
   const [selectedModels, setSelectedModels] = useState<Record<AgentNameEnum, string>>({
     [AgentNameEnum.Navigator]: '',
     [AgentNameEnum.Planner]: '',
     [AgentNameEnum.Validator]: '',
   });
+  const [modelParameters, setModelParameters] = useState<Record<AgentNameEnum, { temperature: number; topP: number }>>({
+    [AgentNameEnum.Navigator]: { temperature: 0, topP: 0 },
+    [AgentNameEnum.Planner]: { temperature: 0, topP: 0 },
+    [AgentNameEnum.Validator]: { temperature: 0, topP: 0 },
+  });
+  const [newModelInputs, setNewModelInputs] = useState<Record<string, string>>({});
+  const [isProviderSelectorOpen, setIsProviderSelectorOpen] = useState(false);
+  const newlyAddedProviderRef = useRef<string | null>(null);
+  const [nameErrors, setNameErrors] = useState<Record<string, string>>({});
+  // Create a non-async wrapper for use in render functions
+  const [availableModels, setAvailableModels] = useState<
+    Array<{ provider: string; providerName: string; model: string }>
+  >([]);
 
   useEffect(() => {
-    const loadApiKeys = async () => {
+    const loadProviders = async () => {
       try {
-        const providers = await llmProviderStore.getConfiguredProviders();
-
-        const keys: Record<LLMProviderEnum, { apiKey: string; baseUrl?: string }> = {} as Record<
-          LLMProviderEnum,
-          { apiKey: string; baseUrl?: string }
-        >;
+        const allProviders = await llmProviderStore.getAllProviders();
+        console.log('allProviders', allProviders);
 
-        for (const provider of providers) {
-          const config = await llmProviderStore.getProvider(provider);
-          if (config) {
-            keys[provider] = {
-              apiKey: config.apiKey || '',
-              baseUrl: config.baseUrl,
-            };
-          }
-        }
+        // Track which providers are from storage
+        const fromStorage = new Set(Object.keys(allProviders));
+        setProvidersFromStorage(fromStorage);
 
-        setApiKeys(keys);
+        // Only use providers from storage, don't add default ones
+        setProviders(allProviders);
       } catch (error) {
-        console.error('Failed to load API keys:', error);
+        console.error('Error loading providers:', error);
+        // Set empty providers on error
+        setProviders({});
+        // No providers from storage on error
+        setProvidersFromStorage(new Set());
       }
     };
 
+    loadProviders();
+  }, []);
+
+  // Load existing agent models and parameters on mount
+  useEffect(() => {
     const loadAgentModels = async () => {
       try {
         const models: Record<AgentNameEnum, string> = {
-          [AgentNameEnum.Navigator]: '',
           [AgentNameEnum.Planner]: '',
+          [AgentNameEnum.Navigator]: '',
           [AgentNameEnum.Validator]: '',
         };
 
         for (const agent of Object.values(AgentNameEnum)) {
-          const model = await agentModelStore.getAgentModel(agent);
-          if (model) {
-            models[agent] = model.modelName;
+          const config = await agentModelStore.getAgentModel(agent);
+          if (config) {
+            models[agent] = config.modelName;
+            if (config.parameters?.temperature !== undefined || config.parameters?.topP !== undefined) {
+              setModelParameters(prev => ({
+                ...prev,
+                [agent]: {
+                  temperature: config.parameters?.temperature ?? prev[agent].temperature,
+                  topP: config.parameters?.topP ?? prev[agent].topP,
+                },
+              }));
+            }
           }
         }
-
         setSelectedModels(models);
       } catch (error) {
-        console.error('Failed to load agent models:', error);
+        console.error('Error loading agent models:', error);
       }
     };
 
-    loadApiKeys();
     loadAgentModels();
   }, []);
 
-  const handleApiKeyChange = (provider: LLMProviderEnum, apiKey: string, baseUrl?: string) => {
-    setApiKeys(prev => ({
+  // Auto-focus the input field when a new provider is added
+  useEffect(() => {
+    // Only focus if we have a newly added provider reference
+    if (newlyAddedProviderRef.current && providers[newlyAddedProviderRef.current]) {
+      const providerId = newlyAddedProviderRef.current;
+      const config = providers[providerId];
+
+      // For custom providers, focus on the name input
+      if (config.type === ProviderTypeEnum.CustomOpenAI) {
+        const nameInput = document.getElementById(`${providerId}-name`);
+        if (nameInput) {
+          nameInput.focus();
+        }
+      } else {
+        // For default providers, focus on the API key input
+        const apiKeyInput = document.getElementById(`${providerId}-api-key`);
+        if (apiKeyInput) {
+          apiKeyInput.focus();
+        }
+      }
+
+      // Clear the ref after focusing
+      newlyAddedProviderRef.current = null;
+    }
+  }, [providers]);
+
+  // Add a click outside handler to close the dropdown
+  useEffect(() => {
+    const handleClickOutside = (event: MouseEvent) => {
+      const target = event.target as HTMLElement;
+      if (isProviderSelectorOpen && !target.closest('.provider-selector-container')) {
+        setIsProviderSelectorOpen(false);
+      }
+    };
+
+    document.addEventListener('mousedown', handleClickOutside);
+    return () => {
+      document.removeEventListener('mousedown', handleClickOutside);
+    };
+  }, [isProviderSelectorOpen]);
+
+  // Create a memoized version of getAvailableModels
+  const getAvailableModelsCallback = useCallback(async () => {
+    const models: Array<{ provider: string; providerName: string; model: string }> = [];
+
+    try {
+      // Load providers directly from storage
+      const storedProviders = await llmProviderStore.getAllProviders();
+
+      // 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,
+          })),
+        );
+      }
+    } catch (error) {
+      console.error('Error loading providers for model selection:', error);
+    }
+
+    return models;
+  }, []);
+
+  // Update available models whenever providers change
+  useEffect(() => {
+    const updateAvailableModels = async () => {
+      const models = await getAvailableModelsCallback();
+      setAvailableModels(models);
+    };
+
+    updateAvailableModels();
+  }, [getAvailableModelsCallback]); // Only depends on the callback
+
+  const handleApiKeyChange = (provider: string, apiKey: string, baseUrl?: string) => {
+    setModifiedProviders(prev => new Set(prev).add(provider));
+    setProviders(prev => ({
       ...prev,
-      [provider]: { apiKey, baseUrl },
+      [provider]: {
+        ...prev[provider],
+        apiKey: apiKey.trim(),
+        baseUrl: baseUrl !== undefined ? baseUrl.trim() : prev[provider]?.baseUrl,
+      },
     }));
+  };
 
-    setModifiedProviders(prev => {
-      const newSet = new Set(prev);
-      newSet.add(provider);
-      return newSet;
+  const handleNameChange = (provider: string, name: string) => {
+    setModifiedProviders(prev => new Set(prev).add(provider));
+    setProviders(prev => {
+      const updated = {
+        ...prev,
+        [provider]: {
+          ...prev[provider],
+          name: name.trim(),
+        },
+      };
+      return updated;
     });
   };
 
-  const handleSave = async (provider: LLMProviderEnum) => {
+  const handleModelsChange = (provider: string, modelsString: string) => {
+    setNewModelInputs(prev => ({
+      ...prev,
+      [provider]: modelsString,
+    }));
+  };
+
+  const addModel = (provider: string, model: string) => {
+    if (!model.trim()) return;
+
+    setModifiedProviders(prev => new Set(prev).add(provider));
+    setProviders(prev => {
+      const providerData = prev[provider] || {};
+
+      // Get current models - either from provider config or default models
+      let currentModels = providerData.modelNames;
+      if (currentModels === undefined) {
+        currentModels = [...(llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [])];
+      }
+
+      // Don't add duplicates
+      if (currentModels.includes(model.trim())) return prev;
+
+      return {
+        ...prev,
+        [provider]: {
+          ...providerData,
+          modelNames: [...currentModels, model.trim()],
+        },
+      };
+    });
+
+    // Clear the input
+    setNewModelInputs(prev => ({
+      ...prev,
+      [provider]: '',
+    }));
+  };
+
+  const removeModel = (provider: string, modelToRemove: string) => {
+    setModifiedProviders(prev => new Set(prev).add(provider));
+
+    setProviders(prev => {
+      const providerData = prev[provider] || {};
+
+      // If modelNames doesn't exist in the provider data yet, we need to initialize it
+      // with the default models from llmProviderModelNames first
+      if (!providerData.modelNames) {
+        const defaultModels = llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [];
+        const filteredModels = defaultModels.filter(model => model !== modelToRemove);
+
+        return {
+          ...prev,
+          [provider]: {
+            ...providerData,
+            modelNames: filteredModels,
+          },
+        };
+      }
+
+      // If modelNames already exists, just filter out the model to remove
+      return {
+        ...prev,
+        [provider]: {
+          ...providerData,
+          modelNames: providerData.modelNames.filter(model => model !== modelToRemove),
+        },
+      };
+    });
+  };
+
+  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>, provider: string) => {
+    if (e.key === 'Enter' || e.key === ' ') {
+      e.preventDefault();
+      const value = newModelInputs[provider] || '';
+      addModel(provider, value);
+    }
+  };
+
+  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) {
+      return {
+        theme: isDarkMode ? 'dark' : 'light',
+        variant: 'danger' as const,
+        children: 'Delete',
+        disabled: false,
+      };
+    }
+
+    // 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());
+
+    return {
+      theme: isDarkMode ? 'dark' : 'light',
+      variant: 'primary' as const,
+      children: 'Save',
+      disabled: !hasInput || !isModified,
+    };
+  };
+
+  const handleSave = async (provider: string) => {
     try {
-      await llmProviderStore.setProvider(provider, apiKeys[provider]);
+      // Check if name contains spaces for custom providers
+      if (providers[provider].type === ProviderTypeEnum.CustomOpenAI && providers[provider].name?.includes(' ')) {
+        setNameErrors(prev => ({
+          ...prev,
+          [provider]: 'Spaces are not allowed in provider names. Please use underscores or other characters instead.',
+        }));
+        return;
+      }
+
+      // Check if base URL is required but missing for custom_openai
+      if (
+        providers[provider].type === ProviderTypeEnum.CustomOpenAI &&
+        (!providers[provider].baseUrl || !providers[provider].baseUrl.trim())
+      ) {
+        alert('Base URL is required for custom OpenAI 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');
+        return;
+      }
+
+      // Ensure modelNames is provided
+      let modelNames = providers[provider].modelNames;
+      if (!modelNames) {
+        // Use default model names if not explicitly set
+        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,
+      });
+
+      // Clear any name errors on successful save
+      setNameErrors(prev => {
+        const newErrors = { ...prev };
+        delete newErrors[provider];
+        return newErrors;
+      });
+
+      // Add to providersFromStorage since it's now saved
+      setProvidersFromStorage(prev => new Set(prev).add(provider));
+
       setModifiedProviders(prev => {
-        const newSet = new Set(prev);
-        newSet.delete(provider);
-        return newSet;
+        const next = new Set(prev);
+        next.delete(provider);
+        return next;
       });
+
+      // Refresh available models
+      const models = await getAvailableModelsCallback();
+      setAvailableModels(models);
     } catch (error) {
-      console.error('Failed to save API key:', error);
+      console.error('Error saving API key:', error);
     }
   };
 
-  const handleDelete = async (provider: LLMProviderEnum) => {
+  const handleDelete = async (provider: string) => {
     try {
+      // Delete the provider from storage regardless of its API key value
       await llmProviderStore.removeProvider(provider);
-      setApiKeys(prev => {
-        const newKeys = { ...prev };
-        delete newKeys[provider];
-        return newKeys;
+
+      // Remove from providersFromStorage
+      setProvidersFromStorage(prev => {
+        const next = new Set(prev);
+        next.delete(provider);
+        return next;
+      });
+
+      // Remove from providers state
+      setProviders(prev => {
+        const next = { ...prev };
+        delete next[provider];
+        return next;
       });
+
+      // Also remove from modifiedProviders if it's there
       setModifiedProviders(prev => {
-        const newSet = new Set(prev);
-        newSet.delete(provider);
-        return newSet;
+        const next = new Set(prev);
+        next.delete(provider);
+        return next;
       });
+
+      // Refresh available models
+      const models = await getAvailableModelsCallback();
+      setAvailableModels(models);
     } catch (error) {
-      console.error('Failed to delete API key:', error);
+      console.error('Error deleting provider:', error);
     }
   };
 
-  const getButtonProps = (provider: LLMProviderEnum) => {
-    const isModified = modifiedProviders.has(provider);
-    const hasApiKey = apiKeys[provider]?.apiKey;
-
-    return {
-      saveButton: {
-        disabled: !isModified || !hasApiKey,
-        className: `rounded-md px-3 py-1 text-sm font-medium ${
-          !isModified || !hasApiKey
-            ? `${isDarkMode ? 'bg-slate-700 text-gray-400' : 'bg-gray-200 text-gray-500'}`
-            : `${isDarkMode ? 'bg-blue-600 text-white hover:bg-blue-700' : 'bg-blue-500 text-white hover:bg-blue-600'}`
-        }`,
-      },
-      deleteButton: {
-        disabled: !hasApiKey,
-        className: `ml-2 rounded-md px-3 py-1 text-sm font-medium ${
-          !hasApiKey
-            ? `${isDarkMode ? 'bg-slate-700 text-gray-400' : 'bg-gray-200 text-gray-500'}`
-            : `${isDarkMode ? 'bg-red-600 text-white hover:bg-red-700' : 'bg-red-500 text-white hover:bg-red-600'}`
-        }`,
-      },
-    };
-  };
+  const handleCancelProvider = (providerId: string) => {
+    // Remove the provider from the state
+    setProviders(prev => {
+      const next = { ...prev };
+      delete next[providerId];
+      return next;
+    });
 
-  const getAvailableModels = () => {
-    const models: string[] = [];
-    Object.values(LLMProviderEnum).forEach(provider => {
-      if (apiKeys[provider]?.apiKey) {
-        models.push(...(llmProviderModelNames[provider] || []));
-      }
+    // Remove from modified providers
+    setModifiedProviders(prev => {
+      const next = new Set(prev);
+      next.delete(providerId);
+      return next;
     });
-    return models;
   };
 
-  const handleModelChange = async (agentName: AgentNameEnum, model: string) => {
-    try {
-      // Determine provider from model name
-      let provider: LLMProviderEnum | undefined;
-      for (const [providerKey, models] of Object.entries(llmProviderModelNames)) {
-        if (models.includes(model)) {
-          provider = providerKey as LLMProviderEnum;
-          break;
-        }
-      }
+  const handleModelChange = async (agentName: AgentNameEnum, modelValue: string) => {
+    // modelValue will be in format "provider>model"
+    const [provider, model] = modelValue.split('>');
+
+    // Set parameters based on provider type
+    const newParameters = getDefaultAgentModelParams(provider, agentName);
 
-      if (provider) {
+    setModelParameters(prev => ({
+      ...prev,
+      [agentName]: newParameters,
+    }));
+
+    setSelectedModels(prev => ({
+      ...prev,
+      [agentName]: model,
+    }));
+
+    try {
+      if (model) {
         await agentModelStore.setAgentModel(agentName, {
           provider,
           modelName: model,
+          parameters: newParameters,
         });
-        setSelectedModels(prev => ({
-          ...prev,
-          [agentName]: model,
-        }));
+      } else {
+        // Reset storage if no model is selected
+        await agentModelStore.resetAgentModel(agentName);
       }
     } catch (error) {
-      console.error(`Failed to set model for ${agentName}:`, error);
+      console.error('Error saving agent model:', error);
     }
   };
 
-  const renderApiKeyInput = (provider: LLMProviderEnum) => {
-    const buttonProps = getButtonProps(provider);
-    const needsBaseUrl =
-      provider === LLMProviderEnum.OpenAI ||
-      provider === LLMProviderEnum.Anthropic ||
-      provider === LLMProviderEnum.Groq ||
-      provider === LLMProviderEnum.Grok;
-
-    return (
-      <div key={provider} className="mb-6">
-        <div className="mb-2 flex items-center justify-between">
-          <h3 className={`text-lg font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
-            {provider === LLMProviderEnum.OpenAI
-              ? 'OpenAI'
-              : provider === LLMProviderEnum.Anthropic
-                ? 'Anthropic'
-                : provider === LLMProviderEnum.Gemini
-                  ? 'Gemini'
-                  : provider === LLMProviderEnum.Groq
-                    ? 'Groq AI'
-                    : provider === LLMProviderEnum.Grok
-                      ? 'Grok AI'
-                      : provider}
-          </h3>
-          <div>
-            <Button
-              onClick={() => handleSave(provider)}
-              disabled={buttonProps.saveButton.disabled}
-              className={buttonProps.saveButton.className}>
-              Save
-            </Button>
-            <Button
-              onClick={() => handleDelete(provider)}
-              disabled={buttonProps.deleteButton.disabled}
-              className={buttonProps.deleteButton.className}>
-              Delete
-            </Button>
-          </div>
+  const handleParameterChange = async (agentName: AgentNameEnum, paramName: 'temperature' | 'topP', value: number) => {
+    const newParameters = {
+      ...modelParameters[agentName],
+      [paramName]: value,
+    };
+
+    setModelParameters(prev => ({
+      ...prev,
+      [agentName]: newParameters,
+    }));
+
+    // Only update if we have a selected model
+    if (selectedModels[agentName]) {
+      try {
+        // 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 (provider) {
+          await agentModelStore.setAgentModel(agentName, {
+            provider,
+            modelName: selectedModels[agentName],
+            parameters: newParameters,
+          });
+        }
+      } catch (error) {
+        console.error('Error saving agent parameters:', error);
+      }
+    }
+  };
+
+  const renderModelSelect = (agentName: AgentNameEnum) => (
+    <div
+      className={`rounded-lg border ${isDarkMode ? 'border-gray-700 bg-slate-800' : 'border-gray-200 bg-gray-50'} p-4`}>
+      <h3 className={`mb-2 text-lg font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
+        {agentName.charAt(0).toUpperCase() + agentName.slice(1)}
+      </h3>
+      <p className={`mb-4 text-sm font-normal ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
+        {getAgentDescription(agentName)}
+      </p>
+
+      <div className="space-y-4">
+        {/* Model Selection */}
+        <div className="flex items-center">
+          <label
+            htmlFor={`${agentName}-model`}
+            className={`w-24 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
+            Model
+          </label>
+          <select
+            id={`${agentName}-model`}
+            className={`flex-1 rounded-md border text-sm ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} px-3 py-2`}
+            disabled={availableModels.length <= 1}
+            value={
+              selectedModels[agentName]
+                ? `${getProviderForModel(selectedModels[agentName])}>${selectedModels[agentName]}`
+                : ''
+            }
+            onChange={e => handleModelChange(agentName, e.target.value)}>
+            <option key="default" value="">
+              Choose model
+            </option>
+            {availableModels.map(({ provider, providerName, model }) => (
+              <option key={`${provider}>${model}`} value={`${provider}>${model}`}>
+                {`${providerName} > ${model}`}
+              </option>
+            ))}
+          </select>
         </div>
 
-        <div className="space-y-2">
-          <div>
-            <label
-              htmlFor={`${provider}-api-key`}
-              className={`mb-1 block text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
-              API Key
-            </label>
+        {/* Temperature Slider */}
+        <div className="flex items-center">
+          <label
+            htmlFor={`${agentName}-temperature`}
+            className={`w-24 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
+            Temperature
+          </label>
+          <div className="flex flex-1 items-center space-x-2">
             <input
-              id={`${provider}-api-key`}
-              type="password"
-              value={apiKeys[provider]?.apiKey || ''}
-              onChange={e => handleApiKeyChange(provider, e.target.value, apiKeys[provider]?.baseUrl)}
-              className={`w-full rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} px-3 py-2`}
-              placeholder={`Enter your ${provider} API key`}
+              id={`${agentName}-temperature`}
+              type="range"
+              min="0"
+              max="2"
+              step="0.01"
+              value={modelParameters[agentName].temperature}
+              onChange={e => handleParameterChange(agentName, 'temperature', Number.parseFloat(e.target.value))}
+              style={{
+                background: `linear-gradient(to right, ${isDarkMode ? '#3b82f6' : '#60a5fa'} 0%, ${isDarkMode ? '#3b82f6' : '#60a5fa'} ${(modelParameters[agentName].temperature / 2) * 100}%, ${isDarkMode ? '#475569' : '#cbd5e1'} ${(modelParameters[agentName].temperature / 2) * 100}%, ${isDarkMode ? '#475569' : '#cbd5e1'} 100%)`,
+              }}
+              className={`flex-1 ${isDarkMode ? 'accent-blue-500' : 'accent-blue-400'} h-1 appearance-none rounded-full`}
             />
+            <div className="flex items-center space-x-2">
+              <span className={`w-12 text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
+                {modelParameters[agentName].temperature.toFixed(2)}
+              </span>
+              <input
+                type="number"
+                min="0"
+                max="2"
+                step="0.01"
+                value={modelParameters[agentName].temperature}
+                onChange={e => {
+                  const value = Number.parseFloat(e.target.value);
+                  if (!Number.isNaN(value) && value >= 0 && value <= 2) {
+                    handleParameterChange(agentName, 'temperature', value);
+                  }
+                }}
+                className={`w-20 rounded-md border ${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'} px-2 py-1 text-sm`}
+                aria-label={`${agentName} temperature number input`}
+              />
+            </div>
           </div>
+        </div>
 
-          {needsBaseUrl && (
-            <div>
-              <label
-                htmlFor={`${provider}-base-url`}
-                className={`mb-1 block text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
-                Base URL (Optional)
-              </label>
+        {/* Top P Slider */}
+        <div className="flex items-center">
+          <label
+            htmlFor={`${agentName}-topP`}
+            className={`w-24 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
+            Top P
+          </label>
+          <div className="flex flex-1 items-center space-x-2">
+            <input
+              id={`${agentName}-topP`}
+              type="range"
+              min="0"
+              max="1"
+              step="0.001"
+              value={modelParameters[agentName].topP}
+              onChange={e => handleParameterChange(agentName, 'topP', Number.parseFloat(e.target.value))}
+              style={{
+                background: `linear-gradient(to right, ${isDarkMode ? '#3b82f6' : '#60a5fa'} 0%, ${isDarkMode ? '#3b82f6' : '#60a5fa'} ${modelParameters[agentName].topP * 100}%, ${isDarkMode ? '#475569' : '#cbd5e1'} ${modelParameters[agentName].topP * 100}%, ${isDarkMode ? '#475569' : '#cbd5e1'} 100%)`,
+              }}
+              className={`flex-1 ${isDarkMode ? 'accent-blue-500' : 'accent-blue-400'} h-1 appearance-none rounded-full`}
+            />
+            <div className="flex items-center space-x-2">
+              <span className={`w-12 text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
+                {modelParameters[agentName].topP.toFixed(3)}
+              </span>
               <input
-                id={`${provider}-base-url`}
-                type="text"
-                value={apiKeys[provider]?.baseUrl || ''}
-                onChange={e => handleApiKeyChange(provider, apiKeys[provider]?.apiKey || '', e.target.value)}
-                className={`w-full rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} px-3 py-2`}
-                placeholder={
-                  provider === LLMProviderEnum.Groq
-                    ? 'https://api.groq.com/v1'
-                    : provider === LLMProviderEnum.Grok
-                      ? 'https://api.grok.x.ai/v1'
-                      : `Enter custom base URL for ${provider} (optional)`
-                }
+                type="number"
+                min="0"
+                max="1"
+                step="0.001"
+                value={modelParameters[agentName].topP}
+                onChange={e => {
+                  const value = Number.parseFloat(e.target.value);
+                  if (!Number.isNaN(value) && value >= 0 && value <= 1) {
+                    handleParameterChange(agentName, 'topP', value);
+                  }
+                }}
+                className={`w-20 rounded-md border ${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'} px-2 py-1 text-sm`}
+                aria-label={`${agentName} top P number input`}
               />
             </div>
-          )}
+          </div>
         </div>
       </div>
-    );
-  };
-
-  const renderModelSelect = (agentName: AgentNameEnum) => (
-    <div key={agentName} className="mb-6">
-      <div className="mb-2">
-        <h3 className={`text-lg font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>{agentName}</h3>
-        <p className={`text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>{getAgentDescription(agentName)}</p>
-      </div>
-
-      <select
-        id={`${agentName}-model`}
-        value={selectedModels[agentName] || ''}
-        onChange={e => handleModelChange(agentName, e.target.value)}
-        className={`w-full rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} px-3 py-2`}
-        disabled={getAvailableModels().length === 0}>
-        <option value="">Select a model</option>
-        {getAvailableModels().map(model => (
-          <option key={model} value={model}>
-            {model}
-          </option>
-        ))}
-      </select>
     </div>
   );
 
   const getAgentDescription = (agentName: AgentNameEnum) => {
     switch (agentName) {
       case AgentNameEnum.Navigator:
-        return 'Handles browsing and interacting with web pages';
+        return 'Navigates websites and performs actions';
       case AgentNameEnum.Planner:
-        return 'Creates and updates the plan for completing tasks';
+        return 'Develops and refines strategies to complete tasks';
       case AgentNameEnum.Validator:
-        return 'Validates the results of actions and task completion';
+        return 'Checks if tasks are completed successfully';
       default:
         return '';
     }
   };
 
+  const getMaxCustomProviderNumber = () => {
+    let maxNumber = 0;
+    for (const providerId of Object.keys(providers)) {
+      if (providerId.startsWith('custom_openai_')) {
+        const match = providerId.match(/custom_openai_(\d+)/);
+        if (match) {
+          const number = Number.parseInt(match[1], 10);
+          maxNumber = Math.max(maxNumber, number);
+        }
+      }
+    }
+    return maxNumber;
+  };
+
+  const addCustomProvider = () => {
+    const nextNumber = getMaxCustomProviderNumber() + 1;
+    const providerId = `custom_openai_${nextNumber}`;
+
+    setProviders(prev => ({
+      ...prev,
+      [providerId]: {
+        apiKey: '',
+        name: `CustomProvider${nextNumber}`,
+        type: ProviderTypeEnum.CustomOpenAI,
+        baseUrl: '',
+        modelNames: [],
+        createdAt: Date.now(),
+      },
+    }));
+
+    setModifiedProviders(prev => new Set(prev).add(providerId));
+
+    // Set the newly added provider ref
+    newlyAddedProviderRef.current = providerId;
+
+    // Scroll to the newly added provider after render
+    setTimeout(() => {
+      const providerElement = document.getElementById(`provider-${providerId}`);
+      if (providerElement) {
+        providerElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
+      }
+    }, 100);
+  };
+
+  const addBuiltInProvider = (provider: string) => {
+    // Get the default provider configuration
+    const config = getDefaultProviderConfig(provider);
+
+    // Add the provider to the state
+    setProviders(prev => ({
+      ...prev,
+      [provider]: config,
+    }));
+
+    // Mark as modified so it shows up in the UI
+    setModifiedProviders(prev => new Set(prev).add(provider));
+
+    // Set the newly added provider ref
+    newlyAddedProviderRef.current = provider;
+
+    // Scroll to the newly added provider after render
+    setTimeout(() => {
+      const providerElement = document.getElementById(`provider-${provider}`);
+      if (providerElement) {
+        providerElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
+      }
+    }, 100);
+  };
+
+  // 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]) => {
+      // Include if it's from storage
+      if (providersFromStorage.has(providerId)) {
+        return true;
+      }
+
+      // Include if it's a newly added provider (has been modified)
+      if (modifiedProviders.has(providerId)) {
+        return true;
+      }
+
+      // Exclude providers that aren't from storage and haven't been modified
+      return false;
+    });
+
+    // Sort the filtered providers
+    return filteredProviders.sort(([keyA, configA], [keyB, configB]) => {
+      // First, separate newly added providers from stored providers
+      const isNewA = !providersFromStorage.has(keyA) && modifiedProviders.has(keyA);
+      const isNewB = !providersFromStorage.has(keyB) && modifiedProviders.has(keyB);
+
+      // If one is new and one is stored, new ones go to the end
+      if (isNewA && !isNewB) return 1;
+      if (!isNewA && isNewB) return -1;
+
+      // If both are new or both are stored, sort by createdAt
+      if (configA.createdAt && configB.createdAt) {
+        return configA.createdAt - configB.createdAt; // Sort in ascending order (oldest first)
+      }
+
+      // If only one has createdAt, put the one without createdAt at the end
+      if (configA.createdAt) return -1;
+      if (configB.createdAt) return 1;
+
+      // If neither has createdAt, sort by type and then name
+      const isCustomA = configA.type === ProviderTypeEnum.CustomOpenAI;
+      const isCustomB = configB.type === ProviderTypeEnum.CustomOpenAI;
+
+      if (isCustomA && !isCustomB) {
+        return 1; // Custom providers come after non-custom
+      }
+
+      if (!isCustomA && isCustomB) {
+        return -1; // Non-custom providers come before custom
+      }
+
+      // Sort alphabetically by name within each group
+      return (configA.name || keyA).localeCompare(configB.name || keyB);
+    });
+  };
+
+  const handleProviderSelection = (providerType: string) => {
+    // Close the dropdown immediately
+    setIsProviderSelectorOpen(false);
+
+    // Handle custom provider
+    if (providerType === ProviderTypeEnum.CustomOpenAI) {
+      addCustomProvider();
+      return;
+    }
+
+    // Handle built-in supported providers
+    addBuiltInProvider(providerType);
+  };
+
+  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;
+      }
+    }
+    return '';
+  };
+
   return (
-    <div className="space-y-8">
+    <section className="space-y-6">
+      {/* LLM Providers Section */}
       <div
-        className={`rounded-lg border ${isDarkMode ? 'border-slate-700 bg-slate-800' : 'border-blue-100 bg-white'} p-6 text-left shadow-sm`}>
+        className={`rounded-lg border ${isDarkMode ? 'border-slate-700 bg-slate-800' : 'border-blue-100 bg-gray-50'} p-6 text-left shadow-sm`}>
         <h2 className={`mb-4 text-xl font-semibold ${isDarkMode ? 'text-gray-200' : 'text-gray-800'}`}>
-          LLM Provider API Keys
+          LLM Providers
         </h2>
-        <p className={`mb-4 text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
-          Configure your API keys for the LLM providers you want to use. Your keys are stored locally and never sent to
-          our servers.
-        </p>
+        <div className="space-y-6">
+          {getSortedProviders().length === 0 ? (
+            <div className="py-8 text-center text-gray-500">
+              <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
+                      </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>
 
-        {Object.values(LLMProviderEnum).map(provider => renderApiKeyInput(provider))}
-      </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={`rounded-lg border ${isDarkMode ? 'border-slate-700 bg-slate-800' : 'border-blue-100 bg-white'} p-6 text-left shadow-sm`}>
-        <h2 className={`mb-4 text-xl font-semibold ${isDarkMode ? 'text-gray-200' : 'text-gray-800'}`}>Agent Models</h2>
-        <p className={`mb-4 text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
-          Select which models to use for each agent. You must configure at least one LLM provider above.
-        </p>
-
-        {getAvailableModels().length === 0 && (
-          <div
-            className={`mb-4 rounded-md ${isDarkMode ? 'bg-slate-700 text-gray-300' : 'bg-yellow-50 text-yellow-800'} p-3`}>
-            <p className="text-sm">Please configure at least one LLM provider API key to select models.</p>
+                <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'}`}>
+                      Key{providerConfig.type !== ProviderTypeEnum.CustomOpenAI ? '*' : ''}
+                    </label>
+                    <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={`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>
+
+                  {/* Display API key for newly added providers */}
+                  {modifiedProviders.has(providerId) &&
+                    !providersFromStorage.has(providerId) &&
+                    providerConfig.apiKey && (
+                      <div className="ml-20 mt-1">
+                        <p className={`font-mono text-sm ${isDarkMode ? 'text-emerald-400' : 'text-emerald-600'}`}>
+                          {providerConfig.apiKey}
+                        </p>
+                      </div>
+                    )}
+
+                  {/* Base URL input (for custom_openai and ollama) */}
+                  {(providerConfig.type === ProviderTypeEnum.CustomOpenAI ||
+                    providerConfig.type === ProviderTypeEnum.Ollama) && (
+                    <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'}`}>
+                          Base URL
+                          {providerConfig.type === ProviderTypeEnum.CustomOpenAI ||
+                          providerConfig.type === ProviderTypeEnum.Ollama
+                            ? '*'
+                            : ''}
+                        </label>
+                        <input
+                          id={`${providerId}-base-url`}
+                          type="text"
+                          placeholder={
+                            providerConfig.type === ProviderTypeEnum.CustomOpenAI
+                              ? 'Required for custom OpenAI providers'
+                              : '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>
+                  )}
+
+                  {/* 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 */}
+                        <input
+                          id={`${providerId}-models`}
+                          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`}
+                        />
+                      </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>
+                  </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>
+
+                {/* 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 */}
+          <div className="provider-selector-container relative pt-4">
+            <Button
+              variant="secondary"
+              onClick={() => setIsProviderSelectorOpen(prev => !prev)}
+              className={`flex w-full items-center justify-center font-medium ${
+                isDarkMode
+                  ? 'border-blue-700 bg-blue-600 text-white hover:bg-blue-500'
+                  : 'border-blue-200 bg-blue-100 text-blue-800 hover:bg-blue-200'
+              }`}>
+              <span className="mr-2 text-sm">+</span> <span className="text-sm">Add New Provider</span>
+            </Button>
+
+            {isProviderSelectorOpen && (
+              <div
+                className={`absolute z-10 mt-2 w-full overflow-hidden rounded-md border ${
+                  isDarkMode
+                    ? 'border-blue-600 bg-slate-700 shadow-lg shadow-slate-900/50'
+                    : 'border-blue-200 bg-white shadow-xl shadow-blue-100/50'
+                }`}>
+                <div className="py-1">
+                  {/* Map through provider types to create buttons */}
+                  {Object.values(ProviderTypeEnum)
+                    // Filter out CustomOpenAI and already added providers
+                    .filter(
+                      type =>
+                        type !== ProviderTypeEnum.CustomOpenAI &&
+                        !providersFromStorage.has(type) &&
+                        !modifiedProviders.has(type),
+                    )
+                    .map(type => (
+                      <button
+                        key={type}
+                        type="button"
+                        className={`flex w-full items-center px-4 py-3 text-left text-sm ${
+                          isDarkMode
+                            ? 'text-blue-200 hover:bg-blue-600/30 hover:text-white'
+                            : 'text-blue-700 hover:bg-blue-100 hover:text-blue-800'
+                        } transition-colors duration-150`}
+                        onClick={() => handleProviderSelection(type)}>
+                        <span className="font-medium">{getDefaultDisplayNameFromProviderId(type)}</span>
+                      </button>
+                    ))}
+
+                  {/* Custom provider button (always shown) */}
+                  <button
+                    type="button"
+                    className={`flex w-full items-center px-4 py-3 text-left text-sm ${
+                      isDarkMode
+                        ? 'text-blue-200 hover:bg-blue-600/30 hover:text-white'
+                        : 'text-blue-700 hover:bg-blue-100 hover:text-blue-800'
+                    } transition-colors duration-150`}
+                    onClick={() => handleProviderSelection(ProviderTypeEnum.CustomOpenAI)}>
+                    <span className="font-medium">Custom OpenAI-compatible</span>
+                  </button>
+                </div>
+              </div>
+            )}
           </div>
-        )}
+        </div>
+      </div>
 
-        {Object.values(AgentNameEnum).map(agentName => renderModelSelect(agentName))}
+      {/* Updated Agent Models Section */}
+      <div
+        className={`rounded-lg border ${isDarkMode ? 'border-slate-700 bg-slate-800' : 'border-blue-100 bg-gray-50'} p-6 text-left shadow-sm`}>
+        <h2 className={`mb-4 text-left text-xl font-semibold ${isDarkMode ? 'text-gray-200' : 'text-gray-800'}`}>
+          Model Selection
+        </h2>
+        <div className="space-y-4">
+          {[AgentNameEnum.Planner, AgentNameEnum.Navigator, AgentNameEnum.Validator].map(agentName => (
+            <div key={agentName}>{renderModelSelect(agentName)}</div>
+          ))}
+        </div>
       </div>
-    </div>
+    </section>
   );
 };

+ 5 - 5
pages/side-panel/src/SidePanel.tsx

@@ -485,7 +485,7 @@ const SidePanel = () => {
   return (
     <div>
       <div
-        className={`flex flex-col h-[100vh] ${isDarkMode ? 'bg-slate-900' : "bg-[url('/bg.jpg')] bg-cover bg-no-repeat"} overflow-hidden border ${isDarkMode ? 'border-sky-800' : 'border-[rgb(186,230,253)]'} rounded-2xl`}>
+        className={`flex h-screen flex-col ${isDarkMode ? 'bg-slate-900' : "bg-[url('/bg.jpg')] bg-cover bg-no-repeat"} overflow-hidden border ${isDarkMode ? 'border-sky-800' : 'border-[rgb(186,230,253)]'} rounded-2xl`}>
         <header className="header relative">
           <div className="header-logo">
             {showHistory ? (
@@ -497,7 +497,7 @@ const SidePanel = () => {
                 ← Back
               </button>
             ) : (
-              <img src="/icon-128.png" alt="Extension Logo" className="h-6 w-6" />
+              <img src="/icon-128.png" alt="Extension Logo" className="size-6" />
             )}
           </div>
           <div className="header-icons">
@@ -556,7 +556,7 @@ const SidePanel = () => {
             {messages.length === 0 && (
               <>
                 <div
-                  className={`border-t ${isDarkMode ? 'border-sky-900' : 'border-sky-100'} backdrop-blur-sm p-2 shadow-sm mb-2`}>
+                  className={`border-t ${isDarkMode ? 'border-sky-900' : 'border-sky-100'} mb-2 p-2 shadow-sm backdrop-blur-sm`}>
                   <ChatInput
                     onSendMessage={handleSendMessage}
                     onStopTask={handleStopTask}
@@ -578,13 +578,13 @@ const SidePanel = () => {
               </>
             )}
             <div
-              className={`flex-1 overflow-y-scroll overflow-x-hidden scrollbar-gutter-stable p-4 scroll-smooth ${isDarkMode ? 'bg-slate-900 bg-opacity-80' : ''}`}>
+              className={`scrollbar-gutter-stable flex-1 overflow-x-hidden overflow-y-scroll scroll-smooth p-2 ${isDarkMode ? 'bg-slate-900/80' : ''}`}>
               <MessageList messages={messages} isDarkMode={isDarkMode} />
               <div ref={messagesEndRef} />
             </div>
             {messages.length > 0 && (
               <div
-                className={`border-t ${isDarkMode ? 'border-sky-900' : 'border-sky-100'} backdrop-blur-sm p-2 shadow-sm`}>
+                className={`border-t ${isDarkMode ? 'border-sky-900' : 'border-sky-100'} p-2 shadow-sm backdrop-blur-sm`}>
                 <ChatInput
                   onSendMessage={handleSendMessage}
                   onStopTask={handleStopTask}

+ 3 - 3
pages/side-panel/src/components/ChatInput.tsx

@@ -82,8 +82,8 @@ export default function ChatInput({
           onChange={handleTextChange}
           onKeyDown={handleKeyDown}
           disabled={disabled}
-          rows={4}
-          className={`w-full resize-none border-none p-3 focus:outline-none ${
+          rows={5}
+          className={`w-full resize-none border-none p-2 focus:outline-none ${
             disabled
               ? isDarkMode
                 ? 'bg-slate-800 text-gray-400'
@@ -97,7 +97,7 @@ export default function ChatInput({
         />
 
         <div
-          className={`flex items-center justify-between px-3 py-1.5 ${
+          className={`flex items-center justify-between px-2 py-1.5 ${
             disabled ? (isDarkMode ? 'bg-slate-800' : 'bg-gray-100') : isDarkMode ? 'bg-slate-800' : 'bg-white'
           }`}>
           <div className="flex gap-2 text-gray-500">{/* Icons can go here */}</div>

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

@@ -38,7 +38,7 @@ function MessageBlock({ message, isSameActor, isDarkMode = false }: MessageBlock
 
   return (
     <div
-      className={`flex max-w-full gap-3 px-4 ${
+      className={`flex max-w-full gap-3 ${
         !isSameActor
           ? `mt-4 border-t ${isDarkMode ? 'border-sky-800/50' : 'border-sky-200/50'} pt-4 first:mt-0 first:border-t-0 first:pt-0`
           : ''

+ 3 - 2
pages/side-panel/src/components/TemplateList.tsx

@@ -13,11 +13,12 @@ interface TemplateListProps {
 
 const TemplateList: React.FC<TemplateListProps> = ({ templates, onTemplateSelect, isDarkMode = false }) => {
   return (
-    <div className="p-4">
-      <h3 className={`mb-3 text-sm font-medium ${isDarkMode ? 'text-gray-200' : 'text-gray-700'}`}>Templates</h3>
+    <div className="p-2">
+      <h3 className={`mb-3 text-sm font-medium ${isDarkMode ? 'text-gray-200' : 'text-gray-700'}`}>Quick Start</h3>
       <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
         {templates.map(template => (
           <button
+            type="button"
             key={template.id}
             onClick={() => onTemplateSelect(template.content)}
             className={`rounded-lg p-3 text-left transition-colors ${

+ 3 - 3
pages/side-panel/src/templates.tsx

@@ -8,13 +8,13 @@ export const defaultTemplates = [
   },
   {
     id: '2',
-    title: 'Follow us on X!',
+    title: '🐦 Follow us on X/Twitter!',
     content: 'Follow us at https://x.com/nanobrowser_ai to stay updated on the latest news and features!',
   },
   {
     id: '3',
-    title: 'Explore AI Papers',
+    title: '📚 Explore AI Papers',
     content:
-      '- Go to https://huggingface.co/papers and click through each of the top 3 upvoted papers.\n- For each paper:\n  - Record the title, URL\n  - Summarise the abstract\n  - Finally, compile together a summary of all 3 papers, ranked by upvotes',
+      '- Go to https://huggingface.co/papers and click through each of the top 3 upvoted papers.\n- For each paper:\n  - Record the title, URL and upvotes\n  - Summarise the abstract section\n  - Finally, compile together a summary of all 3 papers, ranked by upvotes',
   },
 ];

+ 88 - 0
pnpm-lock.yaml

@@ -120,9 +120,15 @@ importers:
       '@langchain/google-genai':
         specifier: 0.1.10
         version: 0.1.10(@langchain/core@0.3.37(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)))
       '@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)
+      '@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)
       puppeteer-core:
         specifier: 24.1.1
         version: 24.1.1
@@ -750,12 +756,30 @@ packages:
     peerDependencies:
       '@langchain/core': '>=0.3.17 <0.4.0'
 
+  '@langchain/ollama@0.2.0':
+    resolution: {integrity: sha512-jLlYFqt+nbhaJKLakk7lRTWHZJ7wHeJLM6yuv4jToQ8zPzpL//372+MjggDoW0mnw8ofysg1T2C6mEJspKJtiA==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@langchain/core': '>=0.2.21 <0.4.0'
+
   '@langchain/openai@0.4.2':
     resolution: {integrity: sha512-Cuj7qbVcycALTP0aqZuPpEc7As8cwiGaU21MhXRyZFs+dnWxKYxZ1Q1z4kcx6cYkq/I+CNwwmk+sP+YruU73Aw==}
     engines: {node: '>=18'}
     peerDependencies:
       '@langchain/core': '>=0.3.29 <0.4.0'
 
+  '@langchain/openai@0.4.4':
+    resolution: {integrity: sha512-UZybJeMd8+UX7Kn47kuFYfqKdBCeBUWNqDtmAr6ZUIMMnlsNIb6MkrEEhGgAEjGCpdT4CU8U/DyyddTz+JayOQ==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@langchain/core': '>=0.3.39 <0.4.0'
+
+  '@langchain/xai@0.0.2':
+    resolution: {integrity: sha512-wVOs7SfJs4VWk/oiHJomaoaZ+r9nQhPqbEXlQ2D8L0d54PxYhb1ILR9rub9LT1RpqazSX8HG4A8+hX4R01qkSg==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@langchain/core': '>=0.2.21 <0.4.0'
+
   '@laynezh/vite-plugin-lib-assets@0.6.1':
     resolution: {integrity: sha512-pdIRW/PiJkuM7/OObjGBGfQmsWetmVObeez6uwT3nhP5cu2zT0L5QELq69caWD/v3QlPY3CPXVN0kZrzQzdvsQ==}
     peerDependencies:
@@ -2490,6 +2514,9 @@ packages:
     resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==}
     engines: {node: '>= 0.4'}
 
+  ollama@0.5.14:
+    resolution: {integrity: sha512-pvOuEYa2WkkAumxzJP0RdEYHkbZ64AYyyUszXVX7ruLvk5L+EiO2G71da2GqEQ4IAk4j6eLoUbGk5arzFT1wJA==}
+
   once@1.4.0:
     resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
 
@@ -3295,6 +3322,9 @@ packages:
       webpack-cli:
         optional: true
 
+  whatwg-fetch@3.6.20:
+    resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
+
   whatwg-url@5.0.0:
     resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
 
@@ -3385,6 +3415,9 @@ packages:
   zod@3.24.1:
     resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
 
+  zod@3.24.2:
+    resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
+
 snapshots:
 
   '@alloc/quick-lru@5.2.0': {}
@@ -3659,6 +3692,14 @@ snapshots:
     transitivePeerDependencies:
       - zod
 
+  '@langchain/ollama@0.2.0(@langchain/core@0.3.37(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))
+      ollama: 0.5.14
+      uuid: 10.0.0
+      zod: 3.24.1
+      zod-to-json-schema: 3.24.1(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)':
     dependencies:
       '@langchain/core': 0.3.37(openai@4.82.0(ws@8.18.0)(zod@3.24.1))
@@ -3670,6 +3711,26 @@ 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)':
+    dependencies:
+      '@langchain/core': 0.3.37(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
+      zod-to-json-schema: 3.24.1(zod@3.24.2)
+    transitivePeerDependencies:
+      - 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)':
+    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)
+      zod: 3.24.2
+    transitivePeerDependencies:
+      - encoding
+      - ws
+
   '@laynezh/vite-plugin-lib-assets@0.6.1(vite@6.0.5(@types/node@22.7.4)(jiti@1.21.6)(sass@1.79.4)(terser@5.34.1)(tsx@4.19.2)(yaml@2.5.1))':
     dependencies:
       escape-string-regexp: 4.0.0
@@ -5601,6 +5662,10 @@ snapshots:
       define-properties: 1.2.1
       es-object-atoms: 1.0.0
 
+  ollama@0.5.14:
+    dependencies:
+      whatwg-fetch: 3.6.20
+
   once@1.4.0:
     dependencies:
       wrappy: 1.0.2
@@ -5628,6 +5693,21 @@ snapshots:
     transitivePeerDependencies:
       - encoding
 
+  openai@4.82.0(ws@8.18.0)(zod@3.24.2):
+    dependencies:
+      '@types/node': 18.19.74
+      '@types/node-fetch': 2.6.12
+      abort-controller: 3.0.0
+      agentkeepalive: 4.6.0
+      form-data-encoder: 1.7.2
+      formdata-node: 4.4.1
+      node-fetch: 2.7.0
+    optionalDependencies:
+      ws: 8.18.0
+      zod: 3.24.2
+    transitivePeerDependencies:
+      - encoding
+
   optionator@0.9.4:
     dependencies:
       deep-is: 0.1.4
@@ -6488,6 +6568,8 @@ snapshots:
       - esbuild
       - uglify-js
 
+  whatwg-fetch@3.6.20: {}
+
   whatwg-url@5.0.0:
     dependencies:
       tr46: 0.0.3
@@ -6588,4 +6670,10 @@ snapshots:
     dependencies:
       zod: 3.24.1
 
+  zod-to-json-schema@3.24.1(zod@3.24.2):
+    dependencies:
+      zod: 3.24.2
+
   zod@3.24.1: {}
+
+  zod@3.24.2: {}