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:
8
backend/migrations/064_ai_settings.sql
Normal file
8
backend/migrations/064_ai_settings.sql
Normal 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;
|
||||
@@ -203,15 +203,12 @@ Focus on the business value of cannabis market intelligence.`;
|
||||
/**
|
||||
* 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;
|
||||
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<GeneratedSeoPayload> {
|
||||
/**
|
||||
* 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;
|
||||
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<GeneratedSeoPayload> {
|
||||
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<GeneratedSeoPayload> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user