/** * OpenAI API Mock Server * * Provides a drop-in replacement for OpenAI API calls during development * and testing to prevent unexpected costs and enable offline development. */ import { calculateMockCost, generateBatchResponse, generateSessionAnalysisResponse, MOCK_RESPONSE_GENERATORS, type MockBatchResponse, type MockChatCompletion, type MockResponseType, } from "./openai-responses"; interface ChatCompletionParams { model: string; messages: Array<{ role: string; content: string }>; temperature?: number; max_tokens?: number; [key: string]: unknown; } interface BatchCreateParams { input_file_id: string; endpoint: string; completion_window: string; metadata?: Record; } interface FileCreateParams { file: string; // File content as string for mock purposes purpose: string; } interface MockOpenAIConfig { enabled: boolean; baseDelay: number; // Base delay in ms to simulate API latency randomDelay: number; // Additional random delay (0 to this value) errorRate: number; // Probability of simulated errors (0.0 to 1.0) logRequests: boolean; // Whether to log mock requests } class OpenAIMockServer { private config: MockOpenAIConfig; private totalCost = 0; private requestCount = 0; private activeBatches: Map = new Map(); constructor(config: Partial = {}) { this.config = { enabled: process.env.OPENAI_MOCK_MODE === "true", baseDelay: 500, // 500ms base delay randomDelay: 1000, // 0-1000ms additional delay errorRate: 0.02, // 2% error rate logRequests: process.env.NODE_ENV === "development", ...config, }; } /** * Check if mock mode is enabled */ isEnabled(): boolean { return this.config.enabled; } /** * Simulate network delay */ private async simulateDelay(): Promise { const delay = this.config.baseDelay + Math.random() * this.config.randomDelay; await new Promise((resolve) => setTimeout(resolve, delay)); } /** * Simulate random API errors */ private shouldSimulateError(): boolean { return Math.random() < this.config.errorRate; } /** * Log mock requests for debugging */ private logRequest(endpoint: string, data: unknown): void { if (this.config.logRequests) { console.log(`[OpenAI Mock] ${endpoint}:`, JSON.stringify(data, null, 2)); } } /** * Check if this is a session analysis request (comprehensive JSON format) */ private isSessionAnalysisRequest(prompt: string): boolean { const promptLower = prompt.toLowerCase(); return ( promptLower.includes("session_id") && (promptLower.includes("sentiment") || promptLower.includes("category") || promptLower.includes("language")) ); } /** * Extract processing type from prompt */ private extractProcessingType(prompt: string): MockResponseType { const promptLower = prompt.toLowerCase(); if ( promptLower.includes("sentiment") || promptLower.includes("positive") || promptLower.includes("negative") ) { return "sentiment"; } if (promptLower.includes("category") || promptLower.includes("classify")) { return "category"; } if (promptLower.includes("summary") || promptLower.includes("summarize")) { return "summary"; } if (promptLower.includes("question") || promptLower.includes("extract")) { return "questions"; } // Default to sentiment analysis return "sentiment"; } /** * Mock chat completions endpoint */ async mockChatCompletion( request: ChatCompletionParams ): Promise { this.requestCount++; await this.simulateDelay(); if (this.shouldSimulateError()) { throw new Error("Mock OpenAI API error: Rate limit exceeded"); } this.logRequest("/v1/chat/completions", request); // Extract the user content to analyze const userMessage = request.messages.find((msg) => msg.role === "user")?.content || ""; const systemMessage = request.messages.find((msg) => msg.role === "system")?.content || ""; let response: MockChatCompletion; let processingType: string; // Check if this is a comprehensive session analysis request if (this.isSessionAnalysisRequest(systemMessage)) { // Extract session ID from system message for session analysis const sessionIdMatch = systemMessage.match(/"session_id":\s*"([^"]+)"/); const sessionId = sessionIdMatch?.[1] || `mock-session-${Date.now()}`; response = generateSessionAnalysisResponse(userMessage, sessionId); processingType = "session_analysis"; } else { // Use simple response generators for other types const detectedType = this.extractProcessingType( `${systemMessage} ${userMessage}` ); response = MOCK_RESPONSE_GENERATORS[detectedType](userMessage); processingType = detectedType; } // Track costs const cost = calculateMockCost(response.usage); this.totalCost += cost; if (this.config.logRequests) { console.log( `[OpenAI Mock] Generated ${processingType} response. Cost: $${cost.toFixed(6)}, Total: $${this.totalCost.toFixed(6)}` ); } return response; } /** * Mock batch creation endpoint */ async mockCreateBatch( request: BatchCreateParams ): Promise { await this.simulateDelay(); if (this.shouldSimulateError()) { throw new Error("Mock OpenAI API error: Invalid file format"); } this.logRequest("/v1/batches", request); const batch = generateBatchResponse("validating"); this.activeBatches.set(batch.id, batch); // Simulate batch processing progression this.simulateBatchProgression(batch.id); return batch; } /** * Mock batch retrieval endpoint */ async mockGetBatch(batchId: string): Promise { await this.simulateDelay(); const batch = this.activeBatches.get(batchId); if (!batch) { throw new Error(`Mock OpenAI API error: Batch ${batchId} not found`); } this.logRequest(`/v1/batches/${batchId}`, { batchId }); return batch; } /** * Mock file upload endpoint */ async mockUploadFile(request: FileCreateParams): Promise<{ id: string; object: string; purpose: string; filename: string; }> { await this.simulateDelay(); if (this.shouldSimulateError()) { throw new Error("Mock OpenAI API error: File too large"); } const fileId = `file-mock-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; this.logRequest("/v1/files", { purpose: request.purpose, size: request.file.length, }); return { id: fileId, object: "file", purpose: request.purpose, filename: "batch_input.jsonl", }; } /** * Mock file content retrieval */ async mockGetFileContent(fileId: string): Promise { await this.simulateDelay(); // Find the batch that owns this file const batch = Array.from(this.activeBatches.values()).find( (b) => b.output_file_id === fileId ); if (!batch) { throw new Error(`Mock OpenAI API error: File ${fileId} not found`); } // Generate mock batch results const results: Array<{ id: string; custom_id: string; response: { status_code: number; body: unknown; }; }> = []; for (let i = 0; i < batch.request_counts.total; i++) { const response = MOCK_RESPONSE_GENERATORS.sentiment(`Sample text ${i}`); results.push({ id: `batch-req-${i}`, custom_id: `req-${i}`, response: { status_code: 200, body: response, }, }); } return results.map((r) => JSON.stringify(r)).join("\n"); } /** * Simulate batch processing progression over time */ private simulateBatchProgression(batchId: string): void { const batch = this.activeBatches.get(batchId); if (!batch) return; // Validating -> In Progress (after 30 seconds) setTimeout(() => { const currentBatch = this.activeBatches.get(batchId); if (currentBatch && currentBatch.status === "validating") { currentBatch.status = "in_progress"; currentBatch.in_progress_at = Math.floor(Date.now() / 1000); this.activeBatches.set(batchId, currentBatch); } }, 30000); // In Progress -> Finalizing (after 2 minutes) setTimeout(() => { const currentBatch = this.activeBatches.get(batchId); if (currentBatch && currentBatch.status === "in_progress") { currentBatch.status = "finalizing"; currentBatch.finalizing_at = Math.floor(Date.now() / 1000); this.activeBatches.set(batchId, currentBatch); } }, 120000); // Finalizing -> Completed (after 3 minutes) setTimeout(() => { const currentBatch = this.activeBatches.get(batchId); if (currentBatch && currentBatch.status === "finalizing") { currentBatch.status = "completed"; currentBatch.completed_at = Math.floor(Date.now() / 1000); currentBatch.output_file_id = `file-mock-output-${batchId}`; currentBatch.request_counts.completed = currentBatch.request_counts.total; this.activeBatches.set(batchId, currentBatch); } }, 180000); } /** * Get mock statistics */ getStats(): { totalCost: number; requestCount: number; activeBatches: number; isEnabled: boolean; } { return { totalCost: this.totalCost, requestCount: this.requestCount, activeBatches: this.activeBatches.size, isEnabled: this.config.enabled, }; } /** * Reset statistics (useful for tests) */ resetStats(): void { this.totalCost = 0; this.requestCount = 0; this.activeBatches.clear(); } /** * Update configuration */ updateConfig(newConfig: Partial): void { this.config = { ...this.config, ...newConfig }; } } // Global instance export const openAIMock = new OpenAIMockServer(); /** * Drop-in replacement for OpenAI client that uses mocks when enabled */ interface OpenAIClient { chat: { completions: { create: (params: ChatCompletionParams) => Promise; }; }; batches: { create: (params: BatchCreateParams) => Promise; retrieve: (batchId: string) => Promise; }; files: { create: (params: FileCreateParams) => Promise<{ id: string; object: string; purpose: string; filename: string; }>; content: (fileId: string) => Promise; }; } export class MockOpenAIClient { private realClient: OpenAIClient; constructor(realClient: OpenAIClient) { this.realClient = realClient; } get chat() { return { completions: { create: async (params: ChatCompletionParams) => { if (openAIMock.isEnabled()) { return openAIMock.mockChatCompletion(params); } return this.realClient.chat.completions.create(params); }, }, }; } get batches() { return { create: async (params: BatchCreateParams) => { if (openAIMock.isEnabled()) { return openAIMock.mockCreateBatch(params); } return this.realClient.batches.create(params); }, retrieve: async (batchId: string) => { if (openAIMock.isEnabled()) { return openAIMock.mockGetBatch(batchId); } return this.realClient.batches.retrieve(batchId); }, }; } get files() { return { create: async (params: FileCreateParams) => { if (openAIMock.isEnabled()) { return openAIMock.mockUploadFile(params); } return this.realClient.files.create(params); }, content: async (fileId: string) => { if (openAIMock.isEnabled()) { return openAIMock.mockGetFileContent(fileId); } return this.realClient.files.content(fileId); }, }; } } export default openAIMock;