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