'use client'; import React, { useState, useEffect } from 'react'; import { configManager } from '@/lib/config/storage'; import { validateApiKey as checkApiKey } from '@/lib/llm/llm-client'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Eye, EyeOff, Check, X, ExternalLink, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { ModelSelector } from '@/components/model-selector'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { ProviderId } from '@/lib/llm/providers/types'; import { getOfferableProviders, getProvider } from '@/lib/llm/providers/registry'; import { CodexAuthPanel } from '@/components/settings/codex-auth-panel'; import { HFAuthPanel } from '@/components/settings/hf-auth-panel'; import { ConnectionBadge } from '@/components/settings/connection-badge'; import { checkHFCapabilities } from '@/lib/auth/hf-auth'; import { track } from '@/lib/telemetry'; interface ModelSettingsPanelProps { onClose?: () => void; onModelChange?: (modelId: string) => void; showJudgeModel?: boolean; onJudgeModelChange?: (modelId: string) => void; } export function ModelSettingsPanel({ onClose, onModelChange, showJudgeModel, onJudgeModelChange }: ModelSettingsPanelProps) { const [selectedProvider, setSelectedProvider] = useState(() => configManager.getSelectedProvider() ); const [showApiKey, setShowApiKey] = useState(false); const [validatingKey, setValidatingKey] = useState(false); const [keyValid, setKeyValid] = useState(null); const [currentApiKey, setCurrentApiKey] = useState(''); const [apiKeyStored, setApiKeyStored] = useState(() => { const p = configManager.getSelectedProvider(); return getProvider(p).apiKeyRequired ? !!configManager.getProviderApiKey(p) : false; }); const [codexAvailable, setCodexAvailable] = useState(true); const [useSeparateChatModel, setUseSeparateChatModel] = useState(() => { if (typeof window !== 'undefined') { const stored = localStorage.getItem(`osw-studio-use-separate-chat-model-${configManager.getSelectedProvider()}`); return stored === 'true'; } return false; }); const [compactionEnabled, setCompactionEnabled] = useState(() => { return configManager.isCompactionEnabled(configManager.getSelectedProvider()); }); const [compactionLimit, setCompactionLimit] = useState(() => { const savedLimit = configManager.getCompactionLimit(configManager.getSelectedProvider()); return savedLimit ? String(savedLimit) : ''; }); // Check if Codex is available (blocked on HF Spaces — HttpOnly cookies don't work) useEffect(() => { checkHFCapabilities().then(caps => { setCodexAvailable(caps.codexAvailable); }).catch(() => {}); }, []); useEffect(() => { // Update API key when provider changes const key = configManager.getProviderApiKey(selectedProvider) || ''; setCurrentApiKey(key); setKeyValid(null); // Reset validation const providerCfg = getProvider(selectedProvider); setApiKeyStored(providerCfg.apiKeyRequired ? !!key : false); // Load separate chat model setting for this provider if (typeof window !== 'undefined') { const stored = localStorage.getItem(`osw-studio-use-separate-chat-model-${selectedProvider}`); setUseSeparateChatModel(stored === 'true'); } // Load compaction settings for this provider setCompactionEnabled(configManager.isCompactionEnabled(selectedProvider)); const savedLimit = configManager.getCompactionLimit(selectedProvider); setCompactionLimit(savedLimit ? String(savedLimit) : ''); }, [selectedProvider]); // Persist separate chat model setting useEffect(() => { if (typeof window !== 'undefined') { localStorage.setItem(`osw-studio-use-separate-chat-model-${selectedProvider}`, String(useSeparateChatModel)); } }, [useSeparateChatModel, selectedProvider]); const handleProviderChange = (provider: ProviderId) => { setSelectedProvider(provider); configManager.setSelectedProvider(provider); track('provider_selected', { provider, has_api_key: !!configManager.getProviderApiKey(provider) }); }; const handleApiKeyChange = (key: string) => { setCurrentApiKey(key); configManager.setProviderApiKey(selectedProvider, key); setKeyValid(null); configManager.clearModelCache(selectedProvider); window.dispatchEvent(new CustomEvent('apiKeyUpdated', { detail: { provider: selectedProvider, hasKey: !!key } })); }; const validateApiKey = async () => { if (!currentApiKey) { toast.error('Please enter an API key'); return; } setValidatingKey(true); try { const isValid = await checkApiKey(currentApiKey, selectedProvider); setKeyValid(isValid); if (isValid) { toast.success('API key is valid!'); } else { toast.error('Invalid API key'); } } catch { setKeyValid(false); toast.error('Failed to validate API key'); } finally { setValidatingKey(false); } }; const handleConnect = async () => { const key = currentApiKey.trim(); if (!key) { toast.error('Please enter an API key'); return; } setValidatingKey(true); try { const isValid = await checkApiKey(key, selectedProvider); if (isValid) { configManager.setProviderApiKey(selectedProvider, key); configManager.clearModelCache(selectedProvider); setApiKeyStored(true); setCurrentApiKey(''); setKeyValid(null); toast.success('API key connected!'); window.dispatchEvent(new CustomEvent('apiKeyUpdated', { detail: { provider: selectedProvider, hasKey: true } })); } else { toast.error('Invalid API key. Please check and try again.'); } } catch { toast.error('Failed to validate API key'); } finally { setValidatingKey(false); } }; const handleApiKeyDisconnect = () => { configManager.setProviderApiKey(selectedProvider, ''); configManager.clearModelCache(selectedProvider); setApiKeyStored(false); setCurrentApiKey(''); setKeyValid(null); toast.success(`Disconnected from ${getProvider(selectedProvider).name}`); window.dispatchEvent(new CustomEvent('apiKeyUpdated', { detail: { provider: selectedProvider, hasKey: false } })); }; const providerConfig = getProvider(selectedProvider); return (
{/* Header */}

Model Settings

Configure your AI model and API connection

{/* Scrollable content */}
{/* Provider Selection */}
{/* Auth: OAuth panel for Codex/HF, API key for others */} {providerConfig.usesOAuth ? ( selectedProvider === 'huggingface' ? ( { window.dispatchEvent(new CustomEvent('apiKeyUpdated', { detail: { provider: selectedProvider, hasKey: !!configManager.getProviderApiKey(selectedProvider) } })); }} /> ) : ( { window.dispatchEvent(new CustomEvent('apiKeyUpdated', { detail: { provider: selectedProvider, hasKey: !!configManager.getProviderApiKey(selectedProvider) } })); }} /> ) ) : providerConfig.apiKeyRequired ? ( apiKeyStored ? ( { const k = configManager.getProviderApiKey(selectedProvider); return k ? `···${k.slice(-4)}` : undefined; })()} info={providerConfig.apiKeyHelpUrl && ( Manage on {providerConfig.name} )} onDisconnect={handleApiKeyDisconnect} /> ) : (
{ setCurrentApiKey(e.target.value); setKeyValid(null); }} onKeyDown={(e) => { if (e.key === 'Enter' && currentApiKey.trim()) handleConnect(); }} placeholder={providerConfig.apiKeyPlaceholder || 'API Key'} className="pr-10" data-tour-id="provider-key-input" disabled={validatingKey} />
{providerConfig.apiKeyHelpUrl && (

Get your API key from{' '} {providerConfig.name}

)}
) ) : providerConfig.isLocal ? (
handleApiKeyChange(e.target.value)} placeholder={providerConfig.apiKeyPlaceholder || 'API Key'} className="pr-10" data-tour-id="provider-key-input" />
{keyValid !== null && (
{keyValid ? ( ) : ( )}
)}

API key is optional for {providerConfig.name}. Only needed if you've configured authentication on your local server.

) : null} {!providerConfig.apiKeyRequired && providerConfig.isLocal && (

Local Provider

Make sure {providerConfig.name} is running on your machine.

Default endpoint: {providerConfig.baseUrl}

{selectedProvider === 'lmstudio' && (

For tool use support:

• Load a model like qwen/qwen3-4b-thinking-2507

• Start the local server in LM Studio

• Models will be automatically discovered

)}
)} {/* Divider */}
{/* Code Model */}
{ if (typeof window !== 'undefined') { localStorage.setItem(`osw-studio-code-model-${selectedProvider}`, modelId); } if (!useSeparateChatModel) { onModelChange?.(modelId); } }} className="space-y-2" />
{/* Separate Chat Model Toggle — hidden in judge mode */} {!showJudgeModel && (
Use different model for chat

Select a separate (usually cheaper) model for chat/planning

setUseSeparateChatModel(checked)} />
)} {/* Chat Model (conditional) — hidden in judge mode */} {!showJudgeModel && useSeparateChatModel && (
{ if (typeof window !== 'undefined') { localStorage.setItem(`osw-studio-chat-model-${selectedProvider}`, modelId); } onModelChange?.(modelId); }} className="space-y-2" />
)} {/* Judge Model — shown only in benchmark mode */} {showJudgeModel && ( <>

Separate model for evaluating subjective test criteria

onJudgeModelChange?.(modelId)} className="space-y-2" />
)} {/* Auto-Compaction */}
Auto-compact

Summarize conversation when reaching 60% of the model's context limit

{ setCompactionEnabled(checked); configManager.setCompactionEnabled(selectedProvider, checked); }} />
{compactionEnabled && (

Leave empty to auto-detect (60% of model context). Manual values are used as-is.

{ const val = e.target.value; setCompactionLimit(val); const num = parseInt(val, 10); configManager.setCompactionLimit( selectedProvider, isNaN(num) || num <= 0 ? undefined : num ); }} placeholder="e.g. 128000" className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" min={1} />
)}
{/* end scrollable content */} {/* Footer */} {onClose && (
)}
); }