Optimize AI system with batching, token tracking, and GDPR compliance

- Add AIUsageLog model for persistent token/cost tracking
- Implement batched processing for all AI services:
  - Assignment: 15 projects/batch
  - Filtering: 20 projects/batch
  - Award eligibility: 20 projects/batch
  - Mentor matching: 15 projects/batch
- Create unified error classification (ai-errors.ts)
- Enhance anonymization with comprehensive project data
- Add AI usage dashboard to Settings page
- Add usage stats endpoints to settings router
- Create AI system documentation (5 files)
- Create GDPR compliance documentation (2 files)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 11:58:12 +01:00
parent a72e815d3a
commit 928b1c65dc
19 changed files with 4103 additions and 601 deletions

View File

@@ -1,4 +1,5 @@
import OpenAI from 'openai'
import type { ChatCompletionCreateParamsNonStreaming } from 'openai/resources/chat/completions'
import { prisma } from './prisma'
// OpenAI client singleton with lazy initialization
@@ -7,6 +8,103 @@ const globalForOpenAI = globalThis as unknown as {
openaiInitialized: boolean
}
// ─── Model Type Detection ────────────────────────────────────────────────────
/**
* Reasoning models that require different API parameters:
* - Use max_completion_tokens instead of max_tokens
* - Don't support response_format: json_object (must instruct JSON in prompt)
* - Don't support temperature parameter
* - Don't support system messages (use developer or user role instead)
*/
const REASONING_MODEL_PREFIXES = ['o1', 'o3', 'o4']
/**
* Check if a model is a reasoning model (o1, o3, o4 series)
*/
export function isReasoningModel(model: string): boolean {
const modelLower = model.toLowerCase()
return REASONING_MODEL_PREFIXES.some(prefix =>
modelLower.startsWith(prefix) ||
modelLower.includes(`/${prefix}`) ||
modelLower.includes(`-${prefix}`)
)
}
// ─── Chat Completion Parameter Builder ───────────────────────────────────────
type MessageRole = 'system' | 'user' | 'assistant' | 'developer'
export interface ChatCompletionOptions {
messages: Array<{ role: MessageRole; content: string }>
maxTokens?: number
temperature?: number
jsonMode?: boolean
}
/**
* Build chat completion parameters with correct settings for the model type.
* Handles differences between standard models and reasoning models.
*/
export function buildCompletionParams(
model: string,
options: ChatCompletionOptions
): ChatCompletionCreateParamsNonStreaming {
const isReasoning = isReasoningModel(model)
// Convert messages for reasoning models (system -> developer)
const messages = options.messages.map(msg => {
if (isReasoning && msg.role === 'system') {
return { role: 'developer' as const, content: msg.content }
}
return msg as { role: 'system' | 'user' | 'assistant' | 'developer'; content: string }
})
// For reasoning models requesting JSON, append JSON instruction to last user message
if (isReasoning && options.jsonMode) {
// Find last user message index (polyfill for findLastIndex)
let lastUserIdx = -1
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === 'user') {
lastUserIdx = i
break
}
}
if (lastUserIdx !== -1) {
messages[lastUserIdx] = {
...messages[lastUserIdx],
content: messages[lastUserIdx].content + '\n\nIMPORTANT: Respond with valid JSON only, no other text.',
}
}
}
const params: ChatCompletionCreateParamsNonStreaming = {
model,
messages: messages as ChatCompletionCreateParamsNonStreaming['messages'],
}
// Token limit parameter differs between model types
if (options.maxTokens) {
if (isReasoning) {
params.max_completion_tokens = options.maxTokens
} else {
params.max_tokens = options.maxTokens
}
}
// Reasoning models don't support temperature
if (!isReasoning && options.temperature !== undefined) {
params.temperature = options.temperature
}
// Reasoning models don't support response_format: json_object
if (!isReasoning && options.jsonMode) {
params.response_format = { type: 'json_object' }
}
return params
}
/**
* Get OpenAI API key from SystemSettings
*/
@@ -118,13 +216,14 @@ export async function validateModel(modelId: string): Promise<{
}
}
// Try a minimal completion with the model
await client.chat.completions.create({
model: modelId,
// Try a minimal completion with the model using correct parameters
const params = buildCompletionParams(modelId, {
messages: [{ role: 'user', content: 'test' }],
max_tokens: 1,
maxTokens: 1,
})
await client.chat.completions.create(params)
return { valid: true }
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
@@ -164,13 +263,14 @@ export async function testOpenAIConnection(): Promise<{
// Get the configured model
const configuredModel = await getConfiguredModel()
// Test with the configured model
const response = await client.chat.completions.create({
model: configuredModel,
// Test with the configured model using correct parameters
const params = buildCompletionParams(configuredModel, {
messages: [{ role: 'user', content: 'Hello' }],
max_tokens: 5,
maxTokens: 5,
})
const response = await client.chat.completions.create(params)
return {
success: true,
model: response.model,