From 2ec4363970be7d69ccf747f450e7ca2c6a5e014b Mon Sep 17 00:00:00 2001 From: hemantch01 Date: Sat, 7 Mar 2026 09:48:13 +0000 Subject: [PATCH 1/6] feat:add provider models constant with curated model lists Signed-off-by: hemantch01 --- src/ai-assistant/providerModels.ts | 67 ++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/ai-assistant/providerModels.ts diff --git a/src/ai-assistant/providerModels.ts b/src/ai-assistant/providerModels.ts new file mode 100644 index 00000000..1d5efae2 --- /dev/null +++ b/src/ai-assistant/providerModels.ts @@ -0,0 +1,67 @@ +/** + * Curated list of popular models for each AI provider. + * Used to populate the model selection dropdown in AIConfigPopup. + */ + +export interface ModelOption { + label: string; + value: string; +} + +export const PROVIDER_MODELS: Record = { + openai: [ + { label: 'GPT-5.2', value: 'gpt-5.2' }, + { label: 'GPT-5.2 Pro', value: 'gpt-5.2-pro' }, + { label: 'GPT-5 Mini', value: 'gpt-5-mini' }, + { label: 'GPT-5 Nano', value: 'gpt-5-nano' }, + { label: 'o3', value: 'o3' }, + { label: 'o3 Pro', value: 'o3-pro' }, + { label: 'o4 Mini', value: 'o4-mini' }, + { label: 'GPT-4.1', value: 'gpt-4.1' }, + { label: 'GPT-4.1 Mini', value: 'gpt-4.1-mini' }, + { label: 'GPT-4.1 Nano', value: 'gpt-4.1-nano' }, + ], + anthropic: [ + { label: 'Claude Opus 4.6', value: 'claude-opus-4.6' }, + { label: 'Claude Sonnet 4.5', value: 'claude-sonnet-4.5' }, + { label: 'Claude Haiku 4.5', value: 'claude-haiku-4.5' }, + ], + google: [ + { label: 'Gemini 2.5 Pro', value: 'gemini-2.5-pro' }, + { label: 'Gemini 2.5 Flash', value: 'gemini-2.5-flash' }, + { label: 'Gemini 2.0 Flash', value: 'gemini-2.0-flash' }, + { label: 'Gemini 2.0 Flash Lite', value: 'gemini-2.0-flash-lite' }, + ], + mistral: [ + { label: 'Mistral Large', value: 'mistral-large-latest' }, + { label: 'Mistral Medium', value: 'mistral-medium-latest' }, + { label: 'Mistral Small', value: 'mistral-small-latest' }, + { label: 'Codestral', value: 'codestral-latest' }, + ], + openrouter: [ + { label: 'OpenAI GPT-5.2', value: 'openai/gpt-5.2' }, + { label: 'Anthropic Claude Opus 4.6', value: 'anthropic/claude-opus-4.6' }, + { label: 'Google Gemini 2.5 Pro', value: 'google/gemini-2.5-pro' }, + { label: 'Google Gemini 2.5 Flash', value: 'google/gemini-2.5-flash' }, + ], + ollama: [ + { label: 'LLaMA 3', value: 'llama3' }, + { label: 'LLaMA 3.1', value: 'llama3.1' }, + { label: 'LLaMA 3.2', value: 'llama3.2' }, + { label: 'Mistral', value: 'mistral' }, + { label: 'Mixtral', value: 'mixtral' }, + { label: 'Code LLaMA', value: 'codellama' }, + { label: 'Qwen 2.5', value: 'qwen2.5' }, + { label: 'Qwen 2.5 (0.5B)', value: 'qwen2.5:0.5b' }, + { label: 'TinyLLaMA', value: 'tinyllama' }, + { label: 'Phi 3', value: 'phi3' }, + ], +}; + +/** + * Returns the list of suggested models for a given provider. + * Returns an empty array for unknown or custom providers. + */ +export function getModelsForProvider(provider: string): ModelOption[] { + return PROVIDER_MODELS[provider] ?? []; +} From bee2684b0dc3235972a737b13e215697a8f56b23 Mon Sep 17 00:00:00 2001 From: hemantch01 Date: Sat, 7 Mar 2026 10:07:53 +0000 Subject: [PATCH 2/6] replace model input with provider-aware select dropdown Signed-off-by: hemantch01 --- src/components/AIConfigPopup.tsx | 383 ++++++++++++++++++------------- 1 file changed, 222 insertions(+), 161 deletions(-) diff --git a/src/components/AIConfigPopup.tsx b/src/components/AIConfigPopup.tsx index 6a4eae61..34c15f30 100644 --- a/src/components/AIConfigPopup.tsx +++ b/src/components/AIConfigPopup.tsx @@ -9,6 +9,7 @@ import { clearStoredKey, } from '../utils/secureKeyStorage'; import { AiOutlineEye, AiOutlineEyeInvisible } from "react-icons/ai"; +import { getModelsForProvider } from '../ai-assistant/providerModels'; const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => { const { backgroundColor, keyProtectionLevel, setKeyProtectionLevel } = useAppStore((state) => ({ @@ -62,6 +63,7 @@ const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => { const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); const [maxTokens, setMaxTokens] = useState(''); + const [isCustomModel, setIsCustomModel] = useState(false); const [showFullPrompt, setShowFullPrompt] = useState(false); const [enableCodeSelectionMenu, setEnableCodeSelectionMenu] = useState(true); const [enableInlineSuggestions, setEnableInlineSuggestions] = useState(true); @@ -69,6 +71,20 @@ const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => { const [isEncrypting, setIsEncrypting] = useState(false); const [securityMessage, setSecurityMessage] = useState(''); + const [fetchedModels, setFetchedModels] = useState([]); + const staticModels = useMemo(() => getModelsForProvider(provider), [provider]); + + const combinedModels = useMemo(() => { + const models = [...staticModels]; + const staticValues = new Set(staticModels.map(m => m.value)); + for (const m of fetchedModels) { + if (!staticValues.has(m)) { + models.push({ label: m, value: m }); + } + } + return models; + }, [staticModels, fetchedModels]); + // Check WebAuthn PRF support on mount useEffect(() => { isWebAuthnPRFSupported().then(setWebauthnAvailable).catch(() => setWebauthnAvailable(false)); @@ -86,7 +102,14 @@ const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => { const savedEnableInlineSuggestions = localStorage.getItem('aiEnableInlineSuggestions') !== 'false'; if (savedProvider) setProvider(savedProvider); - if (savedModel) setModel(savedModel); + if (savedModel) { + setModel(savedModel); + // initially check if it's a known model (static list) + const models = getModelsForProvider(savedProvider ?? ''); + const isKnown = models.some(m => m.value === savedModel); + setIsCustomModel(!isKnown && savedModel !== ''); + } + if (savedCustomEndpoint) setCustomEndpoint(savedCustomEndpoint); if (savedMaxTokens) setMaxTokens(savedMaxTokens); @@ -117,7 +140,6 @@ const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => { } }, [setKeyProtectionLevel]); - const [availableModels, setAvailableModels] = useState([]); const [showApiKey, setShowApiKey] = useState(false); const [debouncedApiKey] = useDebounce(apiKey, 1000); @@ -129,7 +151,7 @@ const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => { }, [isOpen, loadConfig]); useEffect(() => { - setAvailableModels([]); + setFetchedModels([]); if (!provider || !debouncedApiKey) return; const controller = new AbortController(); @@ -139,55 +161,55 @@ const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => { try { switch (provider) { case 'openai': - case 'openai-compatible': - if (!apiKey) return; - - let endpoint = provider === 'openai-compatible' ? customEndpoint : 'https://api.openai.com/v1'; - if (!endpoint) return; - endpoint = endpoint.replace(/\/$/, ''); - const url = `${endpoint}/models`; - - const res = await fetch(url, { - headers: { Authorization: `Bearer ${apiKey}` }, - signal, - }); - - if (!res.ok) { - console.error(`Fetch error (${res.status}): ${res.statusText}`); - return; - } - - const data = await res.json(); - if (!signal.aborted) { - let models = data.data?.map((m: any) => m.id) || [] - setAvailableModels(models); - setModel(prev => models.includes(prev) ? prev : ''); - } - break; + case 'openai-compatible': + if (!apiKey) return; + + let endpoint = provider === 'openai-compatible' ? customEndpoint : 'https://api.openai.com/v1'; + if (!endpoint) return; + endpoint = endpoint.replace(/\/$/, ''); + const url = `${endpoint}/models`; + + const res = await fetch(url, { + headers: { Authorization: `Bearer ${apiKey}` }, + signal, + }); + + if (!res.ok) { + console.error(`Fetch error (${res.status}): ${res.statusText}`); + return; + } + + const data = await res.json(); + if (!signal.aborted) { + let models = data.data?.map((m: any) => m.id) || [] + setFetchedModels(models); + setModel(prev => models.includes(prev) ? prev : ''); + } + break; case 'anthropic': - if (!apiKey) return; - { - const res = await fetch('https://api.anthropic.com/v1/models', { - headers: { - 'x-api-key': apiKey, - 'anthropic-version': '2023-06-01', - 'anthropic-dangerous-direct-browser-access': 'true', - }, - signal, - }); - if (!res.ok) { - console.error(`Fetch error (${res.status}): ${res.statusText}`); - return; - } - const data = await res.json(); - if (!signal.aborted) { - let models = data.data?.map((m: any) => m.id) || []; - setAvailableModels(models); - setModel(prev => models.includes(prev) ? prev : ''); - } - } - break; + if (!apiKey) return; + { + const res = await fetch('https://api.anthropic.com/v1/models', { + headers: { + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + 'anthropic-dangerous-direct-browser-access': 'true', + }, + signal, + }); + if (!res.ok) { + console.error(`Fetch error (${res.status}): ${res.statusText}`); + return; + } + const data = await res.json(); + if (!signal.aborted) { + let models = data.data?.map((m: any) => m.id) || []; + setFetchedModels(models); + setModel(prev => models.includes(prev) ? prev : ''); + } + } + break; case 'google': if (!apiKey) return; @@ -196,16 +218,16 @@ const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => { headers: { 'x-goog-api-key': apiKey }, signal, }); - if (!res.ok) { - console.error(`Fetch error (${res.status}): ${res.statusText}`); - return; - } + if (!res.ok) { + console.error(`Fetch error (${res.status}): ${res.statusText}`); + return; + } const data = await res.json(); if (!signal.aborted) { - let models = data.models?.map((m: any) => m.name) || []; - setAvailableModels(models); - setModel(prev => models.includes(prev) ? prev : ''); - } + let models = data.models?.map((m: any) => m.name) || []; + setFetchedModels(models); + setModel(prev => models.includes(prev) ? prev : ''); + } } break; @@ -216,34 +238,34 @@ const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => { headers: { Authorization: `Bearer ${apiKey}` }, signal, }); - if (!res.ok) { - console.error(`Fetch error (${res.status}): ${res.statusText}`); - return; - } + if (!res.ok) { + console.error(`Fetch error (${res.status}): ${res.statusText}`); + return; + } const data = await res.json(); if (!signal.aborted) { - let models = data.models?.map((m: any) => m.id) || []; - setAvailableModels(models); - setModel(prev => models.includes(prev) ? prev : ''); - } + let models = data.models?.map((m: any) => m.id) || []; + setFetchedModels(models); + setModel(prev => models.includes(prev) ? prev : ''); + } } break; case 'ollama': - { - const res = await fetch('http://localhost:11434/api/tags', { signal }); - if (!res.ok) { - console.error(`Ollama fetch failed: ${res.statusText}`); - return; - } - const data = await res.json(); - if (!signal.aborted) { - let models = data.models?.map((m: any) => m.name || m.model) || []; - setAvailableModels(models); - setModel(prev => models.includes(prev) ? prev : ''); - } - } - break; + { + const res = await fetch('http://localhost:11434/api/tags', { signal }); + if (!res.ok) { + console.error(`Ollama fetch failed: ${res.statusText}`); + return; + } + const data = await res.json(); + if (!signal.aborted) { + let models = data.models?.map((m: any) => m.name || m.model) || []; + setFetchedModels(models); + setModel(prev => models.includes(prev) ? prev : ''); + } + } + break; case 'openrouter': if (!apiKey) return; @@ -252,26 +274,26 @@ const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => { headers: { Authorization: `Bearer ${apiKey}` }, signal, }); - if (!res.ok) { - console.error(`Fetch error (${res.status}): ${res.statusText}`); - return; - } + if (!res.ok) { + console.error(`Fetch error (${res.status}): ${res.statusText}`); + return; + } const data = await res.json(); if (!signal.aborted) { - let models = data.data?.map((m: any) => m.id) || []; - setAvailableModels(models); - setModel(prev => models.includes(prev) ? prev : ''); - } + let models = data.data?.map((m: any) => m.id) || []; + setFetchedModels(models); + setModel(prev => models.includes(prev) ? prev : ''); + } } break; default: - setAvailableModels([]); + setFetchedModels([]); } } catch (err: any) { if (err.name !== 'AbortError') { console.error('Failed to fetch models:', err); - setAvailableModels([]); + setFetchedModels([]); } } }; @@ -284,10 +306,15 @@ const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => { }, [provider, debouncedApiKey, customEndpoint]); useEffect(() => { - if (availableModels.length > 0 && model && !availableModels.includes(model)) { - setModel(''); + if (combinedModels.length > 0 && model && model !== '') { + const isKnown = combinedModels.some(m => m.value === model); + if (!isKnown && !isCustomModel) { + setIsCustomModel(true); + } else if (isKnown && isCustomModel) { + setIsCustomModel(false); + } } - }, [model, availableModels]); + }, [model, combinedModels, isCustomModel]); const handleSave = async () => { localStorage.setItem('aiProvider', provider); @@ -461,88 +488,122 @@ const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => { )} -
- -
- setApiKey(e.target.value)} - placeholder="Enter API key" - className={`flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 ${theme.input}`} - /> - -
- - {/* Security status indicators */} - {keyProtectionLevel === 'webauthn' && ( -
- - - - Protected with Passkey (WebAuthn) -
- )} - {keyProtectionLevel === 'memory-only' && ( -
- - - - Stored in memory only (cleared on refresh) -
- )} - {!webauthnAvailable && apiKey && provider !== 'ollama' && !keyProtectionLevel && ( -
- ⚠️ WebAuthn not available. Key will be stored in memory only. -
- )} - {securityMessage && ( -
- {securityMessage} -
- )} -
+
+ +
+ setApiKey(e.target.value)} + placeholder="Enter API key" + className={`flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 ${theme.input}`} + /> + +
+ + {/* Security status indicators */} + {keyProtectionLevel === 'webauthn' && ( +
+ + + + Protected with Passkey (WebAuthn) +
+ )} + {keyProtectionLevel === 'memory-only' && ( +
+ + + + Stored in memory only (cleared on refresh) +
+ )} + {!webauthnAvailable && apiKey && provider !== 'ollama' && !keyProtectionLevel && ( +
+ ⚠️ WebAuthn not available. Key will be stored in memory only. +
+ )} + {securityMessage && ( +
+ {securityMessage} +
+ )} +
- + + {provider === 'openai-compatible' || combinedModels.length === 0 ? ( + { + setModel(e.target.value); + setIsCustomModel(true); + }} + placeholder="Enter model name" + className={`w-full p-2 border rounded-lg focus:outline-none focus:ring-2 ${theme.input}`} + /> + ) : ( + <> + + {isCustomModel && ( + setModel(e.target.value)} + placeholder="Enter custom model name" + className={`w-full p-2 mt-2 border rounded-lg focus:outline-none focus:ring-2 ${theme.input}`} + /> + )} + + )} {provider && (
- {provider === 'openai' && 'Example: gpt-5, gpt-5-mini'} - {provider === 'anthropic' && 'Example: claude-opus-4-1-20250805, claude-sonnet-4-5-20250929'} - {provider === 'google' && 'Example: gemini-3-pro, gemini-2.5-flash'} + {provider === 'openai' && 'Example: gpt-4, gpt-4-mini'} + {provider === 'anthropic' && 'Example: claude-3-5-sonnet-241022'} + {provider === 'google' && 'Example: gemini-2.5-pro, gemini-2.5-flash'} {provider === 'mistral' && 'Example: mistral-large-latest, mistral-medium-latest'} - {provider === 'openrouter' && 'Example: openai/gpt-5, meta-llama/llama-3.3-70b-instruct'} + {provider === 'openrouter' && 'Example: openai/gpt-4o, meta-llama/llama-3.3-70b-instruct'} {provider === 'ollama' && ( ⚠️ Must run: OLLAMA_ORIGINS="*" ollama serve -
Example models: tinyllama, qwen2.5:0.5b, llama3 +
Example models: tinyllama, qwen2.5:0.5b, llama3
)} -
)} +
@@ -643,7 +704,7 @@ const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => {