feat: Add AI settings to database (provider/model configurable via UI)

- ai_provider and ai_model stored in settings table
- Editable via /settings page in admin UI
- API keys remain in env vars for security
- Falls back to env vars if settings not in DB

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-07 23:18:52 -07:00
parent 4a5af1bd5f
commit f7081838cf
2 changed files with 45 additions and 12 deletions

View File

@@ -0,0 +1,8 @@
-- Add AI provider settings to settings table
-- API keys remain in environment variables for security
INSERT INTO settings (key, value, description, updated_at)
VALUES
('ai_provider', 'claude', 'AI provider for content generation (claude or openai)', NOW()),
('ai_model', '', 'AI model to use (leave blank for default: gpt-4o for openai, claude-sonnet-4-20250514 for claude)', NOW())
ON CONFLICT (key) DO NOTHING;

View File

@@ -203,15 +203,12 @@ Focus on the business value of cannabis market intelligence.`;
/** /**
* Call Claude API * Call Claude API
*/ */
async function callClaudeAPI(prompt: string): Promise<GeneratedSeoPayload> { async function callClaudeAPI(prompt: string, model: string): Promise<GeneratedSeoPayload> {
const apiKey = process.env.ANTHROPIC_API_KEY; const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) { if (!apiKey) {
throw new Error('ANTHROPIC_API_KEY not configured'); throw new Error('ANTHROPIC_API_KEY not configured');
} }
const model = process.env.AI_MODEL || 'claude-sonnet-4-20250514';
console.log(`[SEO] Using Claude model: ${model}`);
const response = await fetch('https://api.anthropic.com/v1/messages', { const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -250,15 +247,12 @@ async function callClaudeAPI(prompt: string): Promise<GeneratedSeoPayload> {
/** /**
* Call OpenAI API * Call OpenAI API
*/ */
async function callOpenAIAPI(prompt: string): Promise<GeneratedSeoPayload> { async function callOpenAIAPI(prompt: string, model: string): Promise<GeneratedSeoPayload> {
const apiKey = process.env.OPENAI_API_KEY; const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) { if (!apiKey) {
throw new Error('OPENAI_API_KEY not configured'); throw new Error('OPENAI_API_KEY not configured');
} }
const model = process.env.AI_MODEL || 'gpt-4o';
console.log(`[SEO] Using OpenAI model: ${model}`);
const response = await fetch('https://api.openai.com/v1/chat/completions', { const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -296,19 +290,50 @@ async function callOpenAIAPI(prompt: string): Promise<GeneratedSeoPayload> {
return JSON.parse(jsonMatch[0]); return JSON.parse(jsonMatch[0]);
} }
/**
* Get AI settings from database (with env var fallback)
*/
async function getAISettings(): Promise<{ provider: string; model: string }> {
const pool = getPool();
let provider = process.env.AI_PROVIDER || 'claude';
let model = process.env.AI_MODEL || '';
try {
const result = await pool.query(`
SELECT key, value FROM settings
WHERE key IN ('ai_provider', 'ai_model')
`);
for (const row of result.rows) {
if (row.key === 'ai_provider' && row.value) provider = row.value;
if (row.key === 'ai_model' && row.value) model = row.value;
}
} catch (e) {
// Settings table may not exist, use env vars
}
// Default models if not specified
if (!model) {
model = provider === 'openai' ? 'gpt-4o' : 'claude-sonnet-4-20250514';
}
return { provider, model };
}
/** /**
* Generate SEO content using configured AI provider * Generate SEO content using configured AI provider
*/ */
async function generateWithAI(spec: GenerationSpec): Promise<GeneratedSeoPayload> { async function generateWithAI(spec: GenerationSpec): Promise<GeneratedSeoPayload> {
const provider = process.env.AI_PROVIDER || 'claude'; const { provider, model } = await getAISettings();
const prompt = buildSeoPrompt(spec); const prompt = buildSeoPrompt(spec);
console.log(`[SEO] Generating content with ${provider} for ${spec.type}: ${spec.slug}`); console.log(`[SEO] Generating content with ${provider} (${model}) for ${spec.type}: ${spec.slug}`);
if (provider === 'openai') { if (provider === 'openai') {
return callOpenAIAPI(prompt); return callOpenAIAPI(prompt, model);
} else { } else {
return callClaudeAPI(prompt); return callClaudeAPI(prompt, model);
} }
} }