mirror of
https://github.com/clucraft/PriceGhost.git
synced 2026-06-08 15:05:16 +02:00
Add AI model selector to settings
- Add anthropic_model and openai_model columns to database - Allow users to select their preferred AI model in settings - Update defaults to current models (Claude Haiku 4.5, GPT-4.1 Nano) - Include model options: Claude 4.5 series, GPT-4.1/5.1 series - Pass user-selected model to all AI extraction/verification functions - Log which model is being used for debugging Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7f6f108243
commit
0a66d55d79
5 changed files with 172 additions and 58 deletions
|
|
@ -122,6 +122,12 @@ async function runMigrations() {
|
|||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'ai_verification_enabled') THEN
|
||||
ALTER TABLE users ADD COLUMN ai_verification_enabled BOOLEAN DEFAULT false;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'anthropic_model') THEN
|
||||
ALTER TABLE users ADD COLUMN anthropic_model TEXT;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'openai_model') THEN
|
||||
ALTER TABLE users ADD COLUMN openai_model TEXT;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'notifications_cleared_at') THEN
|
||||
ALTER TABLE users ADD COLUMN notifications_cleared_at TIMESTAMP;
|
||||
END IF;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@ export interface AISettings {
|
|||
ai_verification_enabled: boolean;
|
||||
ai_provider: 'anthropic' | 'openai' | 'ollama' | null;
|
||||
anthropic_api_key: string | null;
|
||||
anthropic_model: string | null;
|
||||
openai_api_key: string | null;
|
||||
openai_model: string | null;
|
||||
ollama_base_url: string | null;
|
||||
ollama_model: string | null;
|
||||
}
|
||||
|
|
@ -211,7 +213,8 @@ export const userQueries = {
|
|||
getAISettings: async (id: number): Promise<AISettings | null> => {
|
||||
const result = await pool.query(
|
||||
`SELECT ai_enabled, COALESCE(ai_verification_enabled, false) as ai_verification_enabled,
|
||||
ai_provider, anthropic_api_key, openai_api_key, ollama_base_url, ollama_model
|
||||
ai_provider, anthropic_api_key, anthropic_model, openai_api_key, openai_model,
|
||||
ollama_base_url, ollama_model
|
||||
FROM users WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
|
@ -242,10 +245,18 @@ export const userQueries = {
|
|||
fields.push(`anthropic_api_key = $${paramIndex++}`);
|
||||
values.push(settings.anthropic_api_key);
|
||||
}
|
||||
if (settings.anthropic_model !== undefined) {
|
||||
fields.push(`anthropic_model = $${paramIndex++}`);
|
||||
values.push(settings.anthropic_model);
|
||||
}
|
||||
if (settings.openai_api_key !== undefined) {
|
||||
fields.push(`openai_api_key = $${paramIndex++}`);
|
||||
values.push(settings.openai_api_key);
|
||||
}
|
||||
if (settings.openai_model !== undefined) {
|
||||
fields.push(`openai_model = $${paramIndex++}`);
|
||||
values.push(settings.openai_model);
|
||||
}
|
||||
if (settings.ollama_base_url !== undefined) {
|
||||
fields.push(`ollama_base_url = $${paramIndex++}`);
|
||||
values.push(settings.ollama_base_url);
|
||||
|
|
@ -261,7 +272,8 @@ export const userQueries = {
|
|||
const result = await pool.query(
|
||||
`UPDATE users SET ${fields.join(', ')} WHERE id = $${paramIndex}
|
||||
RETURNING ai_enabled, COALESCE(ai_verification_enabled, false) as ai_verification_enabled,
|
||||
ai_provider, anthropic_api_key, openai_api_key, ollama_base_url, ollama_model`,
|
||||
ai_provider, anthropic_api_key, anthropic_model, openai_api_key, openai_model,
|
||||
ollama_base_url, ollama_model`,
|
||||
values
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
|
|
|
|||
|
|
@ -164,16 +164,22 @@ function prepareHtmlForAI(html: string): string {
|
|||
return finalContent;
|
||||
}
|
||||
|
||||
// Default models to use if user hasn't selected one
|
||||
const DEFAULT_ANTHROPIC_MODEL = 'claude-haiku-4-5-20251001';
|
||||
const DEFAULT_OPENAI_MODEL = 'gpt-4.1-nano-2025-04-14';
|
||||
|
||||
async function extractWithAnthropic(
|
||||
html: string,
|
||||
apiKey: string
|
||||
apiKey: string,
|
||||
model?: string | null
|
||||
): Promise<AIExtractionResult> {
|
||||
const anthropic = new Anthropic({ apiKey });
|
||||
|
||||
const preparedHtml = prepareHtmlForAI(html);
|
||||
const modelToUse = model || DEFAULT_ANTHROPIC_MODEL;
|
||||
|
||||
const response = await anthropic.messages.create({
|
||||
model: 'claude-3-5-haiku-20241022',
|
||||
model: modelToUse,
|
||||
max_tokens: 1024,
|
||||
messages: [
|
||||
{
|
||||
|
|
@ -193,14 +199,16 @@ async function extractWithAnthropic(
|
|||
|
||||
async function extractWithOpenAI(
|
||||
html: string,
|
||||
apiKey: string
|
||||
apiKey: string,
|
||||
model?: string | null
|
||||
): Promise<AIExtractionResult> {
|
||||
const openai = new OpenAI({ apiKey });
|
||||
|
||||
const preparedHtml = prepareHtmlForAI(html);
|
||||
const modelToUse = model || DEFAULT_OPENAI_MODEL;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-4o-mini',
|
||||
model: modelToUse,
|
||||
max_tokens: 1024,
|
||||
messages: [
|
||||
{
|
||||
|
|
@ -259,7 +267,8 @@ async function verifyWithAnthropic(
|
|||
html: string,
|
||||
scrapedPrice: number,
|
||||
currency: string,
|
||||
apiKey: string
|
||||
apiKey: string,
|
||||
model?: string | null
|
||||
): Promise<AIVerificationResult> {
|
||||
const anthropic = new Anthropic({ apiKey });
|
||||
|
||||
|
|
@ -267,9 +276,10 @@ async function verifyWithAnthropic(
|
|||
const prompt = VERIFICATION_PROMPT
|
||||
.replace('$SCRAPED_PRICE$', scrapedPrice.toString())
|
||||
.replace('$CURRENCY$', currency) + preparedHtml;
|
||||
const modelToUse = model || DEFAULT_ANTHROPIC_MODEL;
|
||||
|
||||
const response = await anthropic.messages.create({
|
||||
model: 'claude-3-5-haiku-20241022',
|
||||
model: modelToUse,
|
||||
max_tokens: 512,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
|
|
@ -286,7 +296,8 @@ async function verifyWithOpenAI(
|
|||
html: string,
|
||||
scrapedPrice: number,
|
||||
currency: string,
|
||||
apiKey: string
|
||||
apiKey: string,
|
||||
model?: string | null
|
||||
): Promise<AIVerificationResult> {
|
||||
const openai = new OpenAI({ apiKey });
|
||||
|
||||
|
|
@ -294,9 +305,10 @@ async function verifyWithOpenAI(
|
|||
const prompt = VERIFICATION_PROMPT
|
||||
.replace('$SCRAPED_PRICE$', scrapedPrice.toString())
|
||||
.replace('$CURRENCY$', currency) + preparedHtml;
|
||||
const modelToUse = model || DEFAULT_OPENAI_MODEL;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-4o-mini',
|
||||
model: modelToUse,
|
||||
max_tokens: 512,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
|
|
@ -498,9 +510,9 @@ export async function extractWithAI(
|
|||
|
||||
// Use the configured provider
|
||||
if (settings.ai_provider === 'anthropic' && settings.anthropic_api_key) {
|
||||
return extractWithAnthropic(html, settings.anthropic_api_key);
|
||||
return extractWithAnthropic(html, settings.anthropic_api_key, settings.anthropic_model);
|
||||
} else if (settings.ai_provider === 'openai' && settings.openai_api_key) {
|
||||
return extractWithOpenAI(html, settings.openai_api_key);
|
||||
return extractWithOpenAI(html, settings.openai_api_key, settings.openai_model);
|
||||
} else if (settings.ai_provider === 'ollama' && settings.ollama_base_url && settings.ollama_model) {
|
||||
return extractWithOllama(html, settings.ollama_base_url, settings.ollama_model);
|
||||
}
|
||||
|
|
@ -525,11 +537,13 @@ export async function tryAIExtraction(
|
|||
|
||||
// Use the configured provider
|
||||
if (settings.ai_provider === 'anthropic' && settings.anthropic_api_key) {
|
||||
console.log(`[AI] Using Anthropic for ${url}`);
|
||||
return await extractWithAnthropic(html, settings.anthropic_api_key);
|
||||
const modelToUse = settings.anthropic_model || DEFAULT_ANTHROPIC_MODEL;
|
||||
console.log(`[AI] Using Anthropic (${modelToUse}) for ${url}`);
|
||||
return await extractWithAnthropic(html, settings.anthropic_api_key, settings.anthropic_model);
|
||||
} else if (settings.ai_provider === 'openai' && settings.openai_api_key) {
|
||||
console.log(`[AI] Using OpenAI for ${url}`);
|
||||
return await extractWithOpenAI(html, settings.openai_api_key);
|
||||
const modelToUse = settings.openai_model || DEFAULT_OPENAI_MODEL;
|
||||
console.log(`[AI] Using OpenAI (${modelToUse}) for ${url}`);
|
||||
return await extractWithOpenAI(html, settings.openai_api_key, settings.openai_model);
|
||||
} else if (settings.ai_provider === 'ollama' && settings.ollama_base_url && settings.ollama_model) {
|
||||
console.log(`[AI] Using Ollama (${settings.ollama_model}) for ${url}`);
|
||||
return await extractWithOllama(html, settings.ollama_base_url, settings.ollama_model);
|
||||
|
|
@ -561,13 +575,15 @@ export async function tryAIVerification(
|
|||
|
||||
// Need a configured provider
|
||||
if (settings.ai_provider === 'anthropic' && settings.anthropic_api_key) {
|
||||
console.log(`[AI Verify] Using Anthropic to verify $${scrapedPrice} for ${url}`);
|
||||
return await verifyWithAnthropic(html, scrapedPrice, currency, settings.anthropic_api_key);
|
||||
const modelToUse = settings.anthropic_model || DEFAULT_ANTHROPIC_MODEL;
|
||||
console.log(`[AI Verify] Using Anthropic (${modelToUse}) to verify $${scrapedPrice} for ${url}`);
|
||||
return await verifyWithAnthropic(html, scrapedPrice, currency, settings.anthropic_api_key, settings.anthropic_model);
|
||||
} else if (settings.ai_provider === 'openai' && settings.openai_api_key) {
|
||||
console.log(`[AI Verify] Using OpenAI to verify $${scrapedPrice} for ${url}`);
|
||||
return await verifyWithOpenAI(html, scrapedPrice, currency, settings.openai_api_key);
|
||||
const modelToUse = settings.openai_model || DEFAULT_OPENAI_MODEL;
|
||||
console.log(`[AI Verify] Using OpenAI (${modelToUse}) to verify $${scrapedPrice} for ${url}`);
|
||||
return await verifyWithOpenAI(html, scrapedPrice, currency, settings.openai_api_key, settings.openai_model);
|
||||
} else if (settings.ai_provider === 'ollama' && settings.ollama_base_url && settings.ollama_model) {
|
||||
console.log(`[AI Verify] Using Ollama to verify $${scrapedPrice} for ${url}`);
|
||||
console.log(`[AI Verify] Using Ollama (${settings.ollama_model}) to verify $${scrapedPrice} for ${url}`);
|
||||
return await verifyWithOllama(html, scrapedPrice, currency, settings.ollama_base_url, settings.ollama_model);
|
||||
}
|
||||
|
||||
|
|
@ -613,7 +629,8 @@ export interface AIArbitrationResult {
|
|||
async function arbitrateWithAnthropic(
|
||||
html: string,
|
||||
candidates: PriceCandidate[],
|
||||
apiKey: string
|
||||
apiKey: string,
|
||||
model?: string | null
|
||||
): Promise<AIArbitrationResult> {
|
||||
const anthropic = new Anthropic({ apiKey });
|
||||
|
||||
|
|
@ -623,9 +640,10 @@ async function arbitrateWithAnthropic(
|
|||
|
||||
const preparedHtml = prepareHtmlForAI(html);
|
||||
const prompt = ARBITRATION_PROMPT.replace('$CANDIDATES$', candidatesList) + preparedHtml;
|
||||
const modelToUse = model || DEFAULT_ANTHROPIC_MODEL;
|
||||
|
||||
const response = await anthropic.messages.create({
|
||||
model: 'claude-3-5-haiku-20241022',
|
||||
model: modelToUse,
|
||||
max_tokens: 512,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
|
|
@ -641,7 +659,8 @@ async function arbitrateWithAnthropic(
|
|||
async function arbitrateWithOpenAI(
|
||||
html: string,
|
||||
candidates: PriceCandidate[],
|
||||
apiKey: string
|
||||
apiKey: string,
|
||||
model?: string | null
|
||||
): Promise<AIArbitrationResult> {
|
||||
const openai = new OpenAI({ apiKey });
|
||||
|
||||
|
|
@ -651,9 +670,10 @@ async function arbitrateWithOpenAI(
|
|||
|
||||
const preparedHtml = prepareHtmlForAI(html);
|
||||
const prompt = ARBITRATION_PROMPT.replace('$CANDIDATES$', candidatesList) + preparedHtml;
|
||||
const modelToUse = model || DEFAULT_OPENAI_MODEL;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-4o-mini',
|
||||
model: modelToUse,
|
||||
max_tokens: 512,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
|
|
@ -769,13 +789,15 @@ export async function tryAIArbitration(
|
|||
|
||||
// Use the configured provider
|
||||
if (settings.ai_provider === 'anthropic' && settings.anthropic_api_key) {
|
||||
console.log(`[AI Arbitrate] Using Anthropic to arbitrate ${candidates.length} prices for ${url}`);
|
||||
return await arbitrateWithAnthropic(html, candidates, settings.anthropic_api_key);
|
||||
const modelToUse = settings.anthropic_model || DEFAULT_ANTHROPIC_MODEL;
|
||||
console.log(`[AI Arbitrate] Using Anthropic (${modelToUse}) to arbitrate ${candidates.length} prices for ${url}`);
|
||||
return await arbitrateWithAnthropic(html, candidates, settings.anthropic_api_key, settings.anthropic_model);
|
||||
} else if (settings.ai_provider === 'openai' && settings.openai_api_key) {
|
||||
console.log(`[AI Arbitrate] Using OpenAI to arbitrate ${candidates.length} prices for ${url}`);
|
||||
return await arbitrateWithOpenAI(html, candidates, settings.openai_api_key);
|
||||
const modelToUse = settings.openai_model || DEFAULT_OPENAI_MODEL;
|
||||
console.log(`[AI Arbitrate] Using OpenAI (${modelToUse}) to arbitrate ${candidates.length} prices for ${url}`);
|
||||
return await arbitrateWithOpenAI(html, candidates, settings.openai_api_key, settings.openai_model);
|
||||
} else if (settings.ai_provider === 'ollama' && settings.ollama_base_url && settings.ollama_model) {
|
||||
console.log(`[AI Arbitrate] Using Ollama to arbitrate ${candidates.length} prices for ${url}`);
|
||||
console.log(`[AI Arbitrate] Using Ollama (${settings.ollama_model}) to arbitrate ${candidates.length} prices for ${url}`);
|
||||
return await arbitrateWithOllama(html, candidates, settings.ollama_base_url, settings.ollama_model);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -229,7 +229,9 @@ export const settingsApi = {
|
|||
ai_verification_enabled?: boolean;
|
||||
ai_provider?: 'anthropic' | 'openai' | 'ollama' | null;
|
||||
anthropic_api_key?: string | null;
|
||||
anthropic_model?: string | null;
|
||||
openai_api_key?: string | null;
|
||||
openai_model?: string | null;
|
||||
ollama_base_url?: string | null;
|
||||
ollama_model?: string | null;
|
||||
}) => api.put<AISettings & { message: string }>('/settings/ai', data),
|
||||
|
|
@ -247,7 +249,9 @@ export interface AISettings {
|
|||
ai_verification_enabled: boolean;
|
||||
ai_provider: 'anthropic' | 'openai' | 'ollama' | null;
|
||||
anthropic_api_key: string | null;
|
||||
anthropic_model: string | null;
|
||||
openai_api_key: string | null;
|
||||
openai_model: string | null;
|
||||
ollama_base_url: string | null;
|
||||
ollama_model: string | null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,9 @@ export default function Settings() {
|
|||
const [aiVerificationEnabled, setAIVerificationEnabled] = useState(false);
|
||||
const [aiProvider, setAIProvider] = useState<'anthropic' | 'openai' | 'ollama'>('anthropic');
|
||||
const [anthropicApiKey, setAnthropicApiKey] = useState('');
|
||||
const [anthropicModel, setAnthropicModel] = useState('');
|
||||
const [openaiApiKey, setOpenaiApiKey] = useState('');
|
||||
const [openaiModel, setOpenaiModel] = useState('');
|
||||
const [ollamaBaseUrl, setOllamaBaseUrl] = useState('');
|
||||
const [ollamaModel, setOllamaModel] = useState('');
|
||||
const [availableOllamaModels, setAvailableOllamaModels] = useState<string[]>([]);
|
||||
|
|
@ -114,7 +116,9 @@ export default function Settings() {
|
|||
setAIProvider(aiRes.data.ai_provider);
|
||||
}
|
||||
setAnthropicApiKey(aiRes.data.anthropic_api_key || '');
|
||||
setAnthropicModel(aiRes.data.anthropic_model || '');
|
||||
setOpenaiApiKey(aiRes.data.openai_api_key || '');
|
||||
setOpenaiModel(aiRes.data.openai_model || '');
|
||||
setOllamaBaseUrl(aiRes.data.ollama_base_url || '');
|
||||
setOllamaModel(aiRes.data.ollama_model || '');
|
||||
} catch {
|
||||
|
|
@ -369,12 +373,16 @@ export default function Settings() {
|
|||
ai_verification_enabled: aiVerificationEnabled,
|
||||
ai_provider: aiProvider,
|
||||
anthropic_api_key: anthropicApiKey || undefined,
|
||||
anthropic_model: aiProvider === 'anthropic' ? anthropicModel || null : undefined,
|
||||
openai_api_key: openaiApiKey || undefined,
|
||||
openai_model: aiProvider === 'openai' ? openaiModel || null : undefined,
|
||||
ollama_base_url: aiProvider === 'ollama' ? ollamaBaseUrl || null : undefined,
|
||||
ollama_model: aiProvider === 'ollama' ? ollamaModel || null : undefined,
|
||||
});
|
||||
setAISettings(response.data);
|
||||
setAIVerificationEnabled(response.data.ai_verification_enabled ?? false);
|
||||
setAnthropicModel(response.data.anthropic_model || '');
|
||||
setOpenaiModel(response.data.openai_model || '');
|
||||
setAnthropicApiKey('');
|
||||
setOpenaiApiKey('');
|
||||
setSuccess('AI settings saved successfully');
|
||||
|
|
@ -1430,37 +1438,99 @@ export default function Settings() {
|
|||
</div>
|
||||
|
||||
{aiProvider === 'anthropic' && (
|
||||
<div className="settings-form-group">
|
||||
<label>Anthropic API Key</label>
|
||||
<PasswordInput
|
||||
value={anthropicApiKey}
|
||||
onChange={(e) => setAnthropicApiKey(e.target.value)}
|
||||
placeholder="sk-ant-..."
|
||||
/>
|
||||
<p className="hint">
|
||||
Get your API key from{' '}
|
||||
<a href="https://console.anthropic.com/" target="_blank" rel="noopener noreferrer">
|
||||
console.anthropic.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<>
|
||||
<div className="settings-form-group">
|
||||
<label>Anthropic API Key</label>
|
||||
<PasswordInput
|
||||
value={anthropicApiKey}
|
||||
onChange={(e) => setAnthropicApiKey(e.target.value)}
|
||||
placeholder="sk-ant-..."
|
||||
/>
|
||||
<p className="hint">
|
||||
Get your API key from{' '}
|
||||
<a href="https://console.anthropic.com/" target="_blank" rel="noopener noreferrer">
|
||||
console.anthropic.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="settings-form-group">
|
||||
<label>Model</label>
|
||||
<select
|
||||
value={anthropicModel}
|
||||
onChange={(e) => setAnthropicModel(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.625rem 0.75rem',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '0.375rem',
|
||||
background: 'var(--background)',
|
||||
color: 'var(--text)',
|
||||
fontSize: '0.875rem'
|
||||
}}
|
||||
>
|
||||
<option value="">Default (Claude Haiku 4.5)</option>
|
||||
<option value="claude-haiku-4-5-20251001">Claude Haiku 4.5 (Fast, cheap)</option>
|
||||
<option value="claude-sonnet-4-5-20250929">Claude Sonnet 4.5 (Recommended)</option>
|
||||
<option value="claude-opus-4-5-20251101">Claude Opus 4.5 (Most capable)</option>
|
||||
<option value="claude-sonnet-4-20250514">Claude Sonnet 4</option>
|
||||
<option value="claude-3-7-sonnet-20250219">Claude 3.7 Sonnet (Legacy)</option>
|
||||
<option value="claude-3-5-haiku-20241022">Claude 3.5 Haiku (Legacy)</option>
|
||||
</select>
|
||||
<p className="hint">
|
||||
Choose a model based on your cost/accuracy needs. Haiku is fastest and cheapest, Opus is most accurate but expensive.
|
||||
{aiSettings?.anthropic_model && ` (currently: ${aiSettings.anthropic_model})`}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{aiProvider === 'openai' && (
|
||||
<div className="settings-form-group">
|
||||
<label>OpenAI API Key</label>
|
||||
<PasswordInput
|
||||
value={openaiApiKey}
|
||||
onChange={(e) => setOpenaiApiKey(e.target.value)}
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
<p className="hint">
|
||||
Get your API key from{' '}
|
||||
<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer">
|
||||
platform.openai.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<>
|
||||
<div className="settings-form-group">
|
||||
<label>OpenAI API Key</label>
|
||||
<PasswordInput
|
||||
value={openaiApiKey}
|
||||
onChange={(e) => setOpenaiApiKey(e.target.value)}
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
<p className="hint">
|
||||
Get your API key from{' '}
|
||||
<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer">
|
||||
platform.openai.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="settings-form-group">
|
||||
<label>Model</label>
|
||||
<select
|
||||
value={openaiModel}
|
||||
onChange={(e) => setOpenaiModel(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.625rem 0.75rem',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '0.375rem',
|
||||
background: 'var(--background)',
|
||||
color: 'var(--text)',
|
||||
fontSize: '0.875rem'
|
||||
}}
|
||||
>
|
||||
<option value="">Default (GPT-4.1 Nano)</option>
|
||||
<option value="gpt-4.1-nano-2025-04-14">GPT-4.1 Nano (Fast, cheap)</option>
|
||||
<option value="gpt-4.1-mini-2025-04-14">GPT-4.1 Mini (Balanced)</option>
|
||||
<option value="gpt-4.1-2025-04-14">GPT-4.1 (High accuracy)</option>
|
||||
<option value="gpt-5.1-chat-latest">GPT-5.1 Chat (Latest)</option>
|
||||
<option value="gpt-4o-mini">GPT-4o Mini (Legacy)</option>
|
||||
<option value="gpt-4o">GPT-4o (Legacy, retiring Feb 2026)</option>
|
||||
</select>
|
||||
<p className="hint">
|
||||
Choose a model based on your cost/accuracy needs. GPT-4.1 Nano is fastest and cheapest.
|
||||
{aiSettings?.openai_model && ` (currently: ${aiSettings.openai_model})`}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{aiProvider === 'ollama' && (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue