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
|
* 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user