feat: Switch enrichment to use OpenRouter provider

- enrichData now uses same AI provider switch as vision
- Both scan and enrichment use OpenRouter by default
- Added retry logic for rate limits
- Uses Nebius/FP8 provider preferences
- Logs which provider is being used
This commit is contained in:
2025-12-26 00:16:48 +01:00
parent ce49c9e347
commit e978499b54
3 changed files with 126 additions and 30 deletions

View File

@@ -5,6 +5,7 @@ import { createClient } from '@/lib/supabase/server';
import { trackApiUsage } from '@/services/track-api-usage'; import { trackApiUsage } from '@/services/track-api-usage';
import { deductCredits } from '@/services/credit-service'; import { deductCredits } from '@/services/credit-service';
import { getAllSystemTags } from '@/services/tags'; import { getAllSystemTags } from '@/services/tags';
import { getAIProvider, getOpenRouterClient, OPENROUTER_PROVIDER_PREFERENCES } from '@/lib/openrouter';
// Native Schema Definition for Enrichment Data // Native Schema Definition for Enrichment Data
const enrichmentSchema = { const enrichmentSchema = {
@@ -32,10 +33,118 @@ const enrichmentSchema = {
required: [], required: [],
}; };
const ENRICHMENT_MODEL = 'google/gemma-3-27b-it';
/**
* Sleep helper for retry delays
*/
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Enrich with OpenRouter
*/
async function enrichWithOpenRouter(instruction: string): Promise<{ data: any; apiTime: number }> {
const client = getOpenRouterClient();
const startApi = performance.now();
const maxRetries = 3;
let lastError: any = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await client.chat.completions.create({
model: ENRICHMENT_MODEL,
messages: [
{
role: 'user',
content: instruction + `\n\nRespond ONLY with valid JSON in this format:
{
"suggested_tags": ["tag1", "tag2"],
"suggested_custom_tags": ["custom1"],
"search_string": "Distillery Name Age"
}`
},
],
temperature: 0.3,
max_tokens: 512,
// @ts-ignore - OpenRouter-specific field
provider: OPENROUTER_PROVIDER_PREFERENCES,
});
const endApi = performance.now();
const content = response.choices[0]?.message?.content || '{}';
// Extract JSON from response
let jsonStr = content;
const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonMatch) {
jsonStr = jsonMatch[1].trim();
}
return {
data: JSON.parse(jsonStr),
apiTime: endApi - startApi,
};
} catch (error: any) {
lastError = error;
const status = error?.status || error?.response?.status;
if (status === 429 || status === 503) {
const delay = Math.pow(2, attempt) * 1000;
console.log(`[Enrichment] Rate limited (${status}), retry ${attempt}/${maxRetries} in ${delay}ms...`);
await sleep(delay);
continue;
}
throw error;
}
}
throw lastError || new Error('OpenRouter enrichment failed after retries');
}
/**
* Enrich with Gemini
*/
async function enrichWithGemini(instruction: string): Promise<{ data: any; apiTime: number }> {
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
const model = genAI.getGenerativeModel({
model: 'gemini-2.5-flash',
generationConfig: {
responseMimeType: "application/json",
responseSchema: enrichmentSchema as any,
temperature: 0.3,
},
safetySettings: [
{ category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
{ category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE },
{ category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_NONE },
{ category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_NONE },
] as any,
});
const startApi = performance.now();
const result = await model.generateContent(instruction);
const endApi = performance.now();
return {
data: JSON.parse(result.response.text()),
apiTime: endApi - startApi,
};
}
export async function enrichData(name: string, distillery: string, availableTags?: string, language: string = 'de') { export async function enrichData(name: string, distillery: string, availableTags?: string, language: string = 'de') {
if (!process.env.GEMINI_API_KEY) { const provider = getAIProvider();
// Check API key based on provider
if (provider === 'gemini' && !process.env.GEMINI_API_KEY) {
return { success: false, error: 'GEMINI_API_KEY is not configured.' }; return { success: false, error: 'GEMINI_API_KEY is not configured.' };
} }
if (provider === 'openrouter' && !process.env.OPENROUTER_API_KEY) {
return { success: false, error: 'OPENROUTER_API_KEY is not configured.' };
}
let supabase; let supabase;
try { try {
@@ -54,23 +163,6 @@ export async function enrichData(name: string, distillery: string, availableTags
const userId = user.id; const userId = user.id;
// Initialize Gemini with native schema validation
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
const model = genAI.getGenerativeModel({
model: 'gemini-2.5-flash',
generationConfig: {
responseMimeType: "application/json",
responseSchema: enrichmentSchema as any,
temperature: 0.3,
},
safetySettings: [
{ category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
{ category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE },
{ category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_NONE },
{ category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_NONE },
] as any,
});
const instruction = `Identify the sensory profile for the following whisky. const instruction = `Identify the sensory profile for the following whisky.
Whisky: ${name} (${distillery}) Whisky: ${name} (${distillery})
Language: ${language} Language: ${language}
@@ -81,29 +173,33 @@ Instructions:
2. If there are dominant notes that are NOT in the system list, add them to "suggested_custom_tags". 2. If there are dominant notes that are NOT in the system list, add them to "suggested_custom_tags".
3. Provide a clean "search_string" for Whiskybase (e.g., "Distillery Name Age").`; 3. Provide a clean "search_string" for Whiskybase (e.g., "Distillery Name Age").`;
const startApi = performance.now(); console.log(`[EnrichData] Using provider: ${provider}`);
const result = await model.generateContent(instruction); let result: { data: any; apiTime: number };
const endApi = performance.now();
const responseText = result.response.text(); if (provider === 'openrouter') {
console.log('[EnrichData] Raw Response:', responseText); result = await enrichWithOpenRouter(instruction);
const jsonData = JSON.parse(responseText); } else {
result = await enrichWithGemini(instruction);
}
console.log('[EnrichData] Response:', result.data);
// Track usage // Track usage
await trackApiUsage({ await trackApiUsage({
userId: userId, userId: userId,
apiType: 'gemini_ai', apiType: 'gemini_ai',
endpoint: 'enrichData', endpoint: `enrichData_${provider}`,
success: true success: true
}); });
await deductCredits(userId, 'gemini_ai', 'Data enrichment'); await deductCredits(userId, 'gemini_ai', `Data enrichment (${provider})`);
return { return {
success: true, success: true,
data: jsonData, data: result.data,
provider,
perf: { perf: {
apiDuration: endApi - startApi apiDuration: result.apiTime
} }
}; };

View File

@@ -37,7 +37,7 @@ export function getOpenRouterClient(): OpenAI {
} }
// Default OpenRouter model for vision tasks // Default OpenRouter model for vision tasks
export const OPENROUTER_VISION_MODEL = 'google/gemma-3-27b-it:free'; export const OPENROUTER_VISION_MODEL = 'google/gemma-3-27b-it';
/** /**
* OpenRouter provider preferences * OpenRouter provider preferences

File diff suppressed because one or more lines are too long