feat: enhanced AI usage logging (model, provider, response) and fixed build blockers

This commit is contained in:
2025-12-27 00:10:55 +01:00
parent 20659567fd
commit c51cd23d5e
10 changed files with 127 additions and 34 deletions

17
logging_enhancements.sql Normal file
View File

@@ -0,0 +1,17 @@
-- AI Logging Enhancements
-- Add model, provider and response_text to api_usage table
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'api_usage' AND COLUMN_NAME = 'model') THEN
ALTER TABLE api_usage ADD COLUMN model TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'api_usage' AND COLUMN_NAME = 'provider') THEN
ALTER TABLE api_usage ADD COLUMN provider TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'api_usage' AND COLUMN_NAME = 'response_text') THEN
ALTER TABLE api_usage ADD COLUMN response_text TEXT;
END IF;
END $$;

View File

@@ -46,7 +46,7 @@ function sleep(ms: number): Promise<void> {
/**
* Enrich with OpenRouter
*/
async function enrichWithOpenRouter(instruction: string): Promise<{ data: any; apiTime: number }> {
async function enrichWithOpenRouter(instruction: string): Promise<{ data: any; apiTime: number; responseText: string }> {
const client = getOpenRouterClient();
const startApi = performance.now();
const maxRetries = 3;
@@ -86,6 +86,7 @@ async function enrichWithOpenRouter(instruction: string): Promise<{ data: any; a
return {
data: JSON.parse(jsonStr),
apiTime: endApi - startApi,
responseText: content
};
} catch (error: any) {
@@ -109,7 +110,7 @@ async function enrichWithOpenRouter(instruction: string): Promise<{ data: any; a
/**
* Enrich with Gemini
*/
async function enrichWithGemini(instruction: string): Promise<{ data: any; apiTime: number }> {
async function enrichWithGemini(instruction: string): Promise<{ data: any; apiTime: number; responseText: string }> {
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
const model = genAI.getGenerativeModel({
model: 'gemini-2.5-flash',
@@ -130,9 +131,11 @@ async function enrichWithGemini(instruction: string): Promise<{ data: any; apiTi
const result = await model.generateContent(instruction);
const endApi = performance.now();
const responseText = result.response.text();
return {
data: JSON.parse(result.response.text()),
data: JSON.parse(responseText),
apiTime: endApi - startApi,
responseText: responseText
};
}
@@ -200,7 +203,7 @@ Instructions:
3. Provide a clean "search_string" for Whiskybase (e.g., "Distillery Name Age").`;
console.log(`[EnrichData] Using provider: ${provider}`);
let result: { data: any; apiTime: number };
let result: { data: any; apiTime: number; responseText: string };
if (provider === 'openrouter') {
result = await enrichWithOpenRouter(instruction);
@@ -224,7 +227,10 @@ Instructions:
userId: userId,
apiType: 'gemini_ai',
endpoint: `enrichData_${provider}`,
success: true
success: true,
provider: provider,
model: provider === 'openrouter' ? ENRICHMENT_MODEL : 'gemini-2.5-flash',
responseText: result.responseText
});
await deductCredits(userId, 'gemini_ai', `Data enrichment (${provider})`);

View File

@@ -83,7 +83,7 @@ function sleep(ms: number): Promise<void> {
* Analyze whisky label with OpenRouter (Gemma 3 27B)
* Includes retry logic for 429 rate limit errors
*/
async function analyzeWithOpenRouter(base64Data: string, mimeType: string): Promise<{ data: any; apiTime: number }> {
async function analyzeWithOpenRouter(base64Data: string, mimeType: string): Promise<{ data: any; apiTime: number; responseText: string }> {
const client = getOpenRouterClient();
const startApi = performance.now();
const maxRetries = 3;
@@ -129,6 +129,7 @@ async function analyzeWithOpenRouter(base64Data: string, mimeType: string): Prom
return {
data: JSON.parse(jsonStr),
apiTime: endApi - startApi,
responseText: content
};
} catch (error: any) {
@@ -155,7 +156,7 @@ async function analyzeWithOpenRouter(base64Data: string, mimeType: string): Prom
/**
* Analyze whisky label with Gemini
*/
async function analyzeWithGemini(base64Data: string, mimeType: string): Promise<{ data: any; apiTime: number }> {
async function analyzeWithGemini(base64Data: string, mimeType: string): Promise<{ data: any; apiTime: number; responseText: string }> {
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
const model = genAI.getGenerativeModel({
model: 'gemini-2.5-flash',
@@ -179,9 +180,11 @@ async function analyzeWithGemini(base64Data: string, mimeType: string): Promise<
]);
const endApi = performance.now();
const responseText = result.response.text();
return {
data: JSON.parse(result.response.text()),
data: JSON.parse(responseText),
apiTime: endApi - startApi,
responseText: responseText
};
}
@@ -243,7 +246,7 @@ export async function analyzeLabelWithGemini(imageBase64: string): Promise<Gemin
// Call appropriate provider
console.log(`[Vision] Using provider: ${provider}`);
let result: { data: any; apiTime: number };
let result: { data: any; apiTime: number; responseText: string };
if (provider === 'openrouter') {
result = await analyzeWithOpenRouter(base64Data, mimeType);
@@ -278,7 +281,10 @@ export async function analyzeLabelWithGemini(imageBase64: string): Promise<Gemin
userId: user.id,
apiType: 'gemini_ai', // Keep same type for tracking
endpoint: `analyzeLabelWith${provider === 'openrouter' ? 'OpenRouter' : 'Gemini'}`,
success: true
success: true,
provider: provider,
model: provider === 'openrouter' ? OPENROUTER_VISION_MODEL : 'gemini-2.5-flash',
responseText: result.responseText
});
await deductCredits(user.id, 'gemini_ai', `Vision label analysis (${provider})`);

View File

@@ -186,7 +186,10 @@ export async function scanLabel(input: any): Promise<AnalysisResponse> {
userId: userId,
apiType: 'gemini_ai',
endpoint: 'scanLabel_openrouter',
success: true
success: true,
provider: 'openrouter',
model: 'google/gemma-3-27b-it',
responseText: content
});
await deductCredits(userId, 'gemini_ai', 'Bottle OCR scan (OpenRouter)');
@@ -248,7 +251,10 @@ export async function scanLabel(input: any): Promise<AnalysisResponse> {
userId: userId,
apiType: 'gemini_ai',
endpoint: 'scanLabel_gemini',
success: true
success: true,
provider: 'google',
model: 'gemini-2.5-flash',
responseText: result.response.text()
});
await deductCredits(userId, 'gemini_ai', 'Bottle OCR scan (Gemini)');
@@ -279,7 +285,9 @@ export async function scanLabel(input: any): Promise<AnalysisResponse> {
apiType: 'gemini_ai',
endpoint: `scanLabel_${provider}`,
success: false,
errorMessage: aiError.message
errorMessage: aiError.message,
provider: provider,
model: provider === 'openrouter' ? 'google/gemma-3-27b-it' : 'gemini-2.5-flash'
});
return {

View File

@@ -219,36 +219,66 @@ export default async function AdminPage() {
<tr className="border-b border-zinc-200 dark:border-zinc-800">
<th className="text-left py-3 px-4 text-xs font-black uppercase text-zinc-400">Time</th>
<th className="text-left py-3 px-4 text-xs font-black uppercase text-zinc-400">User</th>
<th className="text-left py-3 px-4 text-xs font-black uppercase text-zinc-400">API Type</th>
<th className="text-left py-3 px-4 text-xs font-black uppercase text-zinc-400">API/Provider</th>
<th className="text-left py-3 px-4 text-xs font-black uppercase text-zinc-400">Model</th>
<th className="text-left py-3 px-4 text-xs font-black uppercase text-zinc-400">Endpoint</th>
<th className="text-left py-3 px-4 text-xs font-black uppercase text-zinc-400">Status</th>
</tr>
</thead>
<tbody>
{recentUsage.map((call: any) => (
<tr key={call.id} className="border-b border-zinc-100 dark:border-zinc-800/50">
<td className="py-3 px-4 text-sm text-zinc-600 dark:text-zinc-400">
{new Date(call.created_at).toLocaleString('de-DE')}
<tr key={call.id} className="border-b border-zinc-100 dark:border-zinc-800/50 hover:bg-zinc-50 dark:hover:bg-zinc-900/50 transition-colors">
<td className="py-3 px-4 text-[10px] text-zinc-500 font-mono">
{new Date(call.created_at).toLocaleString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit', day: '2-digit', month: '2-digit' })}
</td>
<td className="py-3 px-4 text-sm text-zinc-900 dark:text-white">
<td className="py-3 px-4 text-sm font-bold text-zinc-900 dark:text-white">
{call.profiles?.username || 'Unknown'}
</td>
<td className="py-3 px-4">
<span className={`px-2 py-1 rounded-full text-xs font-bold ${call.api_type === 'google_search'
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400'
: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400'
}`}>
{call.api_type === 'google_search' ? 'Google Search' : 'Gemini AI'}
<div className="flex flex-col gap-1">
<span className={`px-2 py-0.5 rounded-full text-[10px] font-black uppercase w-fit ${call.api_type === 'google_search'
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400'
: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400'
}`}>
{call.api_type === 'google_search' ? 'Google' : 'AI'}
</span>
{call.provider && (
<span className="text-[10px] font-bold text-zinc-500 uppercase tracking-tighter">
via {call.provider}
</span>
)}
</div>
</td>
<td className="py-3 px-4">
<span className="text-[10px] font-mono text-zinc-600 dark:text-zinc-400 block max-w-[120px] truncate" title={call.model}>
{call.model || '-'}
</span>
</td>
<td className="py-3 px-4 text-sm text-zinc-600 dark:text-zinc-400">
{call.endpoint}
<td className="py-3 px-4">
<div className="space-y-1">
<div className="text-[10px] font-bold text-zinc-500 uppercase">{call.endpoint}</div>
{call.response_text && (
<details className="text-[10px]">
<summary className="cursor-pointer text-orange-600 hover:text-orange-700 font-bold uppercase transition-colors">Response</summary>
<pre className="mt-2 p-2 bg-zinc-100 dark:bg-zinc-950 rounded border border-zinc-200 dark:border-zinc-800 overflow-x-auto max-w-sm whitespace-pre-wrap font-mono text-[9px] text-zinc-400">
{call.response_text}
</pre>
</details>
)}
</div>
</td>
<td className="py-3 px-4">
{call.success ? (
<span className="text-green-600 dark:text-green-400 font-bold"></span>
<span className="text-green-600 dark:text-green-400 font-black text-xs">OK</span>
) : (
<span className="text-red-600 dark:text-red-400 font-bold"></span>
<div className="group relative">
<span className="text-red-600 dark:text-red-400 font-black text-xs cursor-help">ERR</span>
{call.error_message && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-48 p-2 bg-red-600 text-white text-[9px] rounded shadow-xl opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50">
{call.error_message}
</div>
)}
</div>
)}
</td>
</tr>

View File

@@ -132,6 +132,10 @@ export async function updateUserCredits(
currentCredits = newCredits;
}
if (!currentCredits) {
return { success: false, error: 'Konto konnte nicht erstellt werden' };
}
const difference = validated.newBalance - currentCredits.balance;
// Use addCredits which handles the transaction logging

View File

@@ -135,7 +135,10 @@ export async function analyzeBottleMistral(input: any): Promise<AnalysisResponse
userId: userId,
apiType: 'gemini_ai',
endpoint: 'mistral/mistral-large',
success: true
success: true,
provider: 'mistral',
model: 'mistral-large-latest',
responseText: rawContent as string
});
await deductCredits(userId, 'gemini_ai', 'Mistral AI analysis');
@@ -164,7 +167,9 @@ export async function analyzeBottleMistral(input: any): Promise<AnalysisResponse
apiType: 'gemini_ai',
endpoint: 'mistral/mistral-large',
success: false,
errorMessage: aiError.message
errorMessage: aiError.message,
provider: 'mistral',
model: 'mistral-large-latest'
});
return {

View File

@@ -128,7 +128,10 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
userId: userId,
apiType: 'gemini_ai',
endpoint: 'analyzeBottle_openrouter',
success: true
success: true,
provider: 'openrouter',
model: 'google/gemma-3-27b-it',
responseText: content
});
await deductCredits(userId, 'gemini_ai', 'Bottle analysis (OpenRouter)');
@@ -196,7 +199,10 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
userId: userId,
apiType: 'gemini_ai',
endpoint: 'analyzeBottle_gemini',
success: true
success: true,
provider: 'google',
model: 'gemini-2.5-flash',
responseText: responseText
});
await deductCredits(userId, 'gemini_ai', 'Bottle analysis (Gemini)');
@@ -225,7 +231,9 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
apiType: 'gemini_ai',
endpoint: `analyzeBottle_${provider}`,
success: false,
errorMessage: aiError.message
errorMessage: aiError.message,
provider: provider,
model: provider === 'openrouter' ? 'google/gemma-3-27b-it' : 'gemini-2.5-flash'
});
return {

View File

@@ -8,6 +8,9 @@ interface TrackApiUsageParams {
endpoint?: string;
success: boolean;
errorMessage?: string;
model?: string;
provider?: string;
responseText?: string;
}
interface ApiStats {
@@ -49,6 +52,9 @@ export async function trackApiUsage(params: TrackApiUsageParams): Promise<{ succ
endpoint: params.endpoint,
success: params.success,
error_message: params.errorMessage,
model: params.model,
provider: params.provider,
response_text: params.responseText
});
if (error) {

View File

@@ -238,6 +238,9 @@ CREATE TABLE IF NOT EXISTS api_usage (
endpoint TEXT,
success BOOLEAN DEFAULT true,
error_message TEXT,
model TEXT,
provider TEXT,
response_text TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now())
);