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