From f7081838cf84b1850927c29766bfc7e94d6530cd Mon Sep 17 00:00:00 2001 From: Kelly Date: Sun, 7 Dec 2025 23:18:52 -0700 Subject: [PATCH] feat: Add AI settings to database (provider/model configurable via UI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/migrations/064_ai_settings.sql | 8 +++++ backend/src/services/seoGenerator.ts | 49 +++++++++++++++++++------- 2 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 backend/migrations/064_ai_settings.sql diff --git a/backend/migrations/064_ai_settings.sql b/backend/migrations/064_ai_settings.sql new file mode 100644 index 00000000..f94d80aa --- /dev/null +++ b/backend/migrations/064_ai_settings.sql @@ -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; diff --git a/backend/src/services/seoGenerator.ts b/backend/src/services/seoGenerator.ts index 9faaa9a3..f624c4dd 100644 --- a/backend/src/services/seoGenerator.ts +++ b/backend/src/services/seoGenerator.ts @@ -203,15 +203,12 @@ Focus on the business value of cannabis market intelligence.`; /** * Call Claude API */ -async function callClaudeAPI(prompt: string): Promise { +async function callClaudeAPI(prompt: string, model: string): Promise { const apiKey = process.env.ANTHROPIC_API_KEY; if (!apiKey) { 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', { method: 'POST', headers: { @@ -250,15 +247,12 @@ async function callClaudeAPI(prompt: string): Promise { /** * Call OpenAI API */ -async function callOpenAIAPI(prompt: string): Promise { +async function callOpenAIAPI(prompt: string, model: string): Promise { const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { 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', { method: 'POST', headers: { @@ -296,19 +290,50 @@ async function callOpenAIAPI(prompt: string): Promise { 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 */ async function generateWithAI(spec: GenerationSpec): Promise { - const provider = process.env.AI_PROVIDER || 'claude'; + const { provider, model } = await getAISettings(); 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') { - return callOpenAIAPI(prompt); + return callOpenAIAPI(prompt, model); } else { - return callClaudeAPI(prompt); + return callClaudeAPI(prompt, model); } }