diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8e82cac --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# Environment Variables for Production API Tests +# Copy this file to .env and fill in your actual API keys + +# Enable production test mode +PRODUCTION_TEST_MODE=true + +# GPT-5 Codex Test Configuration +TEST_GPT5_API_KEY=your_gpt5_api_key_here +TEST_GPT5_BASE_URL=http://127.0.0.1:3000/openai + +# MiniMax Codex Test Configuration +TEST_MINIMAX_API_KEY=your_minimax_api_key_here +TEST_MINIMAX_BASE_URL=https://api.minimaxi.com/v1 + +# WARNING: +# - Never commit .env files to version control! +# - The .env file is already in .gitignore +# - API keys should be kept secret and secure diff --git a/src/constants/modelCapabilities.ts b/src/constants/modelCapabilities.ts index ecbf433..a9522b9 100644 --- a/src/constants/modelCapabilities.ts +++ b/src/constants/modelCapabilities.ts @@ -64,6 +64,7 @@ export const MODEL_CAPABILITIES_REGISTRY: Record = { 'gpt-5-mini': GPT5_CAPABILITIES, 'gpt-5-nano': GPT5_CAPABILITIES, 'gpt-5-chat-latest': GPT5_CAPABILITIES, + 'gpt-5-codex': GPT5_CAPABILITIES, // GPT-4 series 'gpt-4o': CHAT_COMPLETIONS_CAPABILITIES, diff --git a/src/services/adapters/base.ts b/src/services/adapters/base.ts index 4dfeb17..f34c4c0 100644 --- a/src/services/adapters/base.ts +++ b/src/services/adapters/base.ts @@ -10,7 +10,7 @@ export abstract class ModelAPIAdapter { // Subclasses must implement these methods abstract createRequest(params: UnifiedRequestParams): any - abstract parseResponse(response: any): UnifiedResponse + abstract parseResponse(response: any): Promise abstract buildTools(tools: Tool[]): any // Shared utility methods diff --git a/src/services/adapters/chatCompletions.ts b/src/services/adapters/chatCompletions.ts index 9420956..1547ba6 100644 --- a/src/services/adapters/chatCompletions.ts +++ b/src/services/adapters/chatCompletions.ts @@ -64,9 +64,9 @@ export class ChatCompletionsAdapter extends ModelAPIAdapter { })) } - parseResponse(response: any): UnifiedResponse { + async parseResponse(response: any): Promise { const choice = response.choices?.[0] - + return { id: response.id || `chatcmpl_${Date.now()}`, content: choice?.message?.content || '', diff --git a/src/services/adapters/responsesAPI.ts b/src/services/adapters/responsesAPI.ts index 93ceceb..6ff3b87 100644 --- a/src/services/adapters/responsesAPI.ts +++ b/src/services/adapters/responsesAPI.ts @@ -5,22 +5,21 @@ import { zodToJsonSchema } from 'zod-to-json-schema' export class ResponsesAPIAdapter extends ModelAPIAdapter { createRequest(params: UnifiedRequestParams): any { - const { messages, systemPrompt, tools, maxTokens } = params - - // Separate system messages and user messages - const systemMessages = messages.filter(m => m.role === 'system') - const nonSystemMessages = messages.filter(m => m.role !== 'system') - + const { messages, systemPrompt, tools, maxTokens, stream } = params + // Build base request const request: any = { model: this.modelProfile.modelName, - input: this.convertMessagesToInput(nonSystemMessages), - instructions: this.buildInstructions(systemPrompt, systemMessages) + input: this.convertMessagesToInput(messages), + instructions: this.buildInstructions(systemPrompt) } - // Add token limit - request[this.getMaxTokensParam()] = maxTokens - + // Add token limit - Responses API uses max_output_tokens + request.max_output_tokens = maxTokens + + // Add streaming support - Responses API always returns streaming + request.stream = true + // Add temperature (GPT-5 only supports 1) if (this.getTemperature() === 1) { request.temperature = 1 @@ -101,10 +100,20 @@ export class ResponsesAPIAdapter extends ModelAPIAdapter { }) } - parseResponse(response: any): UnifiedResponse { + async parseResponse(response: any): Promise { + // Check if this is a streaming response (Response object with body) + if (response && typeof response === 'object' && 'body' in response && response.body) { + return await this.parseStreamingResponse(response) + } + + // Process non-streaming response + return this.parseNonStreamingResponse(response) + } + + private parseNonStreamingResponse(response: any): UnifiedResponse { // Process basic text output let content = response.output_text || '' - + // Process structured output if (response.output && Array.isArray(response.output)) { const messageItems = response.output.filter(item => item.type === 'message') @@ -123,10 +132,10 @@ export class ResponsesAPIAdapter extends ModelAPIAdapter { .join('\n\n') } } - + // Parse tool calls const toolCalls = this.parseToolCalls(response) - + // Build unified response return { id: response.id || `resp_${Date.now()}`, @@ -140,17 +149,192 @@ export class ResponsesAPIAdapter extends ModelAPIAdapter { responseId: response.id // Save for state management } } - - private convertMessagesToInput(messages: any[]): any { - // Convert messages to Responses API input format - // May need adjustment based on actual API specification - return messages + + private async parseStreamingResponse(response: any): Promise { + // Handle streaming response from Responses API + // Collect all chunks and build a unified response + + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + + let fullContent = '' + let toolCalls = [] + let responseId = response.id || `resp_${Date.now()}` + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() || '' + + for (const line of lines) { + if (line.trim()) { + const parsed = this.parseSSEChunk(line) + if (parsed) { + // Extract response ID + if (parsed.response?.id) { + responseId = parsed.response.id + } + + // Handle text content + if (parsed.type === 'response.output_text.delta') { + fullContent += parsed.delta || '' + } + + // Handle tool calls + if (parsed.type === 'response.output_item.done') { + const item = parsed.item || {} + if (item.type === 'function_call') { + toolCalls.push({ + id: item.call_id || item.id || `tool_${Date.now()}`, + type: 'tool_call', + name: item.name, + arguments: item.arguments + }) + } + } + } + } + } + } + } catch (error) { + console.error('Error reading streaming response:', error) + } + + // Build unified response + return { + id: responseId, + content: fullContent, + toolCalls, + usage: { + promptTokens: 0, // Will be filled in by the caller + completionTokens: 0, + reasoningTokens: 0 + }, + responseId: responseId + } + } + + private parseSSEChunk(line: string): any | null { + if (line.startsWith('data: ')) { + const data = line.slice(6).trim() + if (data === '[DONE]') { + return null + } + if (data) { + try { + return JSON.parse(data) + } catch (error) { + console.error('Error parsing SSE chunk:', error) + return null + } + } + } + return null } - private buildInstructions(systemPrompt: string[], systemMessages: any[]): string { - const systemContent = systemMessages.map(m => m.content).join('\n\n') - const promptContent = systemPrompt.join('\n\n') - return [systemContent, promptContent].filter(Boolean).join('\n\n') + private convertMessagesToInput(messages: any[]): any[] { + // Convert Chat Completions messages to Response API input format + // Following reference implementation pattern + const inputItems = [] + + for (const message of messages) { + const role = message.role + + if (role === 'tool') { + // Handle tool call results + const callId = message.tool_call_id || message.id + if (typeof callId === 'string' && callId) { + let content = message.content || '' + if (Array.isArray(content)) { + const texts = content + .filter(part => typeof part === 'object' && part !== null) + .map(part => part.text || part.content) + .filter(text => typeof text === 'string' && text) + content = texts.join('\n') + } + if (typeof content === 'string') { + inputItems.push({ + type: 'function_call_output', + call_id: callId, + output: content + }) + } + } + continue + } + + if (role === 'assistant' && Array.isArray(message.tool_calls)) { + // Handle assistant tool calls + for (const tc of message.tool_calls) { + if (typeof tc !== 'object' || tc === null) continue + const tcType = tc.type || 'function' + if (tcType !== 'function') continue + + const callId = tc.id || tc.call_id + const fn = tc.function + const name = typeof fn === 'object' && fn !== null ? fn.name : null + const args = typeof fn === 'object' && fn !== null ? fn.arguments : null + + if (typeof callId === 'string' && typeof name === 'string' && typeof args === 'string') { + inputItems.push({ + type: 'function_call', + name: name, + arguments: args, + call_id: callId + }) + } + } + continue + } + + // Handle regular text content + const content = message.content || '' + const contentItems = [] + + if (Array.isArray(content)) { + for (const part of content) { + if (typeof part !== 'object' || part === null) continue + const ptype = part.type + if (ptype === 'text') { + const text = part.text || part.content || '' + if (typeof text === 'string' && text) { + const kind = role === 'assistant' ? 'output_text' : 'input_text' + contentItems.push({ type: kind, text: text }) + } + } else if (ptype === 'image_url') { + const image = part.image_url + const url = typeof image === 'object' && image !== null ? image.url : image + if (typeof url === 'string' && url) { + contentItems.push({ type: 'input_image', image_url: url }) + } + } + } + } else if (typeof content === 'string' && content) { + const kind = role === 'assistant' ? 'output_text' : 'input_text' + contentItems.push({ type: kind, text: content }) + } + + if (contentItems.length) { + const roleOut = role === 'assistant' ? 'assistant' : 'user' + inputItems.push({ type: 'message', role: roleOut, content: contentItems }) + } + } + + return inputItems + } + + private buildInstructions(systemPrompt: string[]): string { + // Join system prompts into instructions (following reference implementation) + const systemContent = systemPrompt + .filter(content => content.trim()) + .join('\n\n') + + return systemContent } private parseToolCalls(response: any): any[] { diff --git a/src/services/claude.ts b/src/services/claude.ts index 62c05e8..2efa6c6 100644 --- a/src/services/claude.ts +++ b/src/services/claude.ts @@ -1956,7 +1956,7 @@ async function queryOpenAI( // Use Responses API for GPT-5 and similar models const { callGPT5ResponsesAPI } = await import('./openai') const response = await callGPT5ResponsesAPI(modelProfile, request, signal) - const unifiedResponse = adapter.parseResponse(response) + const unifiedResponse = await adapter.parseResponse(response) // Convert unified response back to Anthropic format const apiMessage = { diff --git a/src/services/modelAdapterFactory.ts b/src/services/modelAdapterFactory.ts index a16b4f7..091f1da 100644 --- a/src/services/modelAdapterFactory.ts +++ b/src/services/modelAdapterFactory.ts @@ -41,11 +41,11 @@ export class ModelAdapterFactory { const isOfficialOpenAI = !modelProfile.baseURL || modelProfile.baseURL.includes('api.openai.com') - // Non-official endpoints use Chat Completions (even if model supports Responses API) + // Non-official endpoints can use Responses API if model supports it if (!isOfficialOpenAI) { // If there's a fallback option, use fallback if (capabilities.apiArchitecture.fallback === 'chat_completions') { - return 'chat_completions' + return capabilities.apiArchitecture.primary // ← FIXED: Use primary instead of fallback } // Otherwise use primary (might fail, but let it try) return capabilities.apiArchitecture.primary diff --git a/src/test/production-api-tests.test.ts b/src/test/production-api-tests.test.ts new file mode 100644 index 0000000..0b24485 --- /dev/null +++ b/src/test/production-api-tests.test.ts @@ -0,0 +1,460 @@ +import { test, expect, describe } from 'bun:test' +import { ModelAdapterFactory } from '../services/modelAdapterFactory' +import { getModelCapabilities } from '../constants/modelCapabilities' +import { ModelProfile } from '../utils/config' + +// ⚠️ PRODUCTION TEST MODE ⚠️ +// This test file makes REAL API calls to external services +// Set PRODUCTION_TEST_MODE=true to enable +// Costs may be incurred - use with caution! + +const PRODUCTION_TEST_MODE = process.env.PRODUCTION_TEST_MODE === 'true' + +// Test model profiles from environment variables +// Create a .env file with these values to run production tests +// WARNING: Never commit .env files or API keys to version control! + +const GPT5_CODEX_PROFILE: ModelProfile = { + name: 'gpt-5-codex', + provider: 'openai', + modelName: 'gpt-5-codex', + baseURL: process.env.TEST_GPT5_BASE_URL || 'https://api.openai.com/v1', + apiKey: process.env.TEST_GPT5_API_KEY || '', + maxTokens: 8192, + contextLength: 128000, + reasoningEffort: 'high', + isActive: true, + createdAt: 1731099900000, + isGPT5: true, + validationStatus: 'auto_repaired', + lastValidation: 1762636302289, +} + +const MINIMAX_CODEX_PROFILE: ModelProfile = { + name: 'minimax codex-MiniMax-M2', + provider: 'minimax', + modelName: 'codex-MiniMax-M2', + baseURL: process.env.TEST_MINIMAX_BASE_URL || 'https://api.minimaxi.com/v1', + apiKey: process.env.TEST_MINIMAX_API_KEY || '', + maxTokens: 8192, + contextLength: 128000, + reasoningEffort: null, + createdAt: 1762660466723, + isActive: true, +} + +describe('🌐 Production API Integration Tests', () => { + if (!PRODUCTION_TEST_MODE) { + test('⚠️ PRODUCTION TEST MODE DISABLED', () => { + console.log('\n🚨 PRODUCTION TEST MODE IS DISABLED 🚨') + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + console.log('To enable production tests, run:') + console.log(' PRODUCTION_TEST_MODE=true bun test src/test/production-api-tests.ts') + console.log('') + console.log('⚠️ WARNING: This will make REAL API calls and may incur costs!') + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + expect(true).toBe(true) // This test always passes + }) + return + } + + // Validate that required environment variables are set + if (!process.env.TEST_GPT5_API_KEY || !process.env.TEST_MINIMAX_API_KEY) { + test('⚠️ ENVIRONMENT VARIABLES NOT CONFIGURED', () => { + console.log('\n🚨 ENVIRONMENT VARIABLES NOT CONFIGURED 🚨') + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + console.log('Create a .env file with the following variables:') + console.log(' TEST_GPT5_API_KEY=your_api_key_here') + console.log(' TEST_GPT5_BASE_URL=http://127.0.0.1:3000/openai') + console.log(' TEST_MINIMAX_API_KEY=your_api_key_here') + console.log(' TEST_MINIMAX_BASE_URL=https://api.minimaxi.com/v1') + console.log('') + console.log('⚠️ Never commit .env files to version control!') + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + expect(true).toBe(true) // This test always passes + }) + return + } + + describe('📡 GPT-5 Codex Production Test', () => { + test('🚀 Making real API call to GPT-5 Codex endpoint', async () => { + const adapter = ModelAdapterFactory.createAdapter(GPT5_CODEX_PROFILE) + const shouldUseResponses = ModelAdapterFactory.shouldUseResponsesAPI(GPT5_CODEX_PROFILE) + + console.log('\n🚀 GPT-5 CODEX PRODUCTION TEST:') + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + console.log('🔗 Adapter:', adapter.constructor.name) + console.log('📍 Endpoint:', shouldUseResponses + ? `${GPT5_CODEX_PROFILE.baseURL}/responses` + : `${GPT5_CODEX_PROFILE.baseURL}/chat/completions`) + console.log('🤖 Model:', GPT5_CODEX_PROFILE.modelName) + console.log('🔑 API Key:', GPT5_CODEX_PROFILE.apiKey.substring(0, 8) + '...') + + // Create test request + const testPrompt = "Write a simple Python function that adds two numbers" + const mockParams = { + messages: [ + { role: 'user', content: testPrompt } + ], + systemPrompt: ['You are a helpful coding assistant. Provide clear, concise code examples.'], + maxTokens: 100, // Small limit to minimize costs + // Note: stream=true would return SSE format, which requires special handling + } + + try { + const request = adapter.createRequest(mockParams) + + // Make the actual API call + const endpoint = shouldUseResponses + ? `${GPT5_CODEX_PROFILE.baseURL}/responses` + : `${GPT5_CODEX_PROFILE.baseURL}/chat/completions` + + console.log('📡 Making request to:', endpoint) + console.log('📝 Request body:', JSON.stringify(request, null, 2)) + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${GPT5_CODEX_PROFILE.apiKey}`, + }, + body: JSON.stringify(request), + }) + + console.log('📊 Response status:', response.status) + console.log('📊 Response headers:', Object.fromEntries(response.headers.entries())) + + if (response.ok) { + // Use the adapter's parseResponse method to handle both streaming and non-streaming + const unifiedResponse = await adapter.parseResponse(response) + console.log('✅ SUCCESS! Response received:') + console.log('📄 Unified Response:', JSON.stringify(unifiedResponse, null, 2)) + + expect(response.status).toBe(200) + expect(unifiedResponse).toBeDefined() + expect(unifiedResponse.content).toBeDefined() + } else { + const errorText = await response.text() + console.log('❌ API ERROR:', response.status, errorText) + throw new Error(`API call failed: ${response.status} ${errorText}`) + } + + } catch (error) { + console.log('💥 Request failed:', error.message) + throw error + } + }, 30000) // 30 second timeout + }) + + describe('📡 MiniMax Codex Production Test', () => { + test('🚀 Making real API call to MiniMax Codex endpoint', async () => { + const adapter = ModelAdapterFactory.createAdapter(MINIMAX_CODEX_PROFILE) + const shouldUseResponses = ModelAdapterFactory.shouldUseResponsesAPI(MINIMAX_CODEX_PROFILE) + + console.log('\n🚀 MINIMAX CODEX PRODUCTION TEST:') + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + console.log('🔗 Adapter:', adapter.constructor.name) + console.log('📍 Endpoint:', shouldUseResponses + ? `${MINIMAX_CODEX_PROFILE.baseURL}/responses` + : `${MINIMAX_CODEX_PROFILE.baseURL}/chat/completions`) + console.log('🤖 Model:', MINIMAX_CODEX_PROFILE.modelName) + console.log('🔑 API Key:', MINIMAX_CODEX_PROFILE.apiKey.substring(0, 16) + '...') + + // Create test request + const testPrompt = "Write a simple JavaScript function that adds two numbers" + const mockParams = { + messages: [ + { role: 'user', content: testPrompt } + ], + systemPrompt: ['You are a helpful coding assistant. Provide clear, concise code examples.'], + maxTokens: 100, // Small limit to minimize costs + temperature: 0.7, + } + + try { + const request = adapter.createRequest(mockParams) + + // Make the actual API call + const endpoint = shouldUseResponses + ? `${MINIMAX_CODEX_PROFILE.baseURL}/responses` + : `${MINIMAX_CODEX_PROFILE.baseURL}/chat/completions` + + console.log('📡 Making request to:', endpoint) + console.log('📝 Request body:', JSON.stringify(request, null, 2)) + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${MINIMAX_CODEX_PROFILE.apiKey}`, + }, + body: JSON.stringify(request), + }) + + console.log('📊 Response status:', response.status) + console.log('📊 Response headers:', Object.fromEntries(response.headers.entries())) + + if (response.ok) { + // Use the adapter's parseResponse method to handle the response + const unifiedResponse = adapter.parseResponse(response) + console.log('✅ SUCCESS! Response received:') + console.log('📄 Unified Response:', JSON.stringify(unifiedResponse, null, 2)) + + expect(response.status).toBe(200) + expect(unifiedResponse).toBeDefined() + } else { + const errorText = await response.text() + console.log('❌ API ERROR:', response.status, errorText) + throw new Error(`API call failed: ${response.status} ${errorText}`) + } + + } catch (error) { + console.log('💥 Request failed:', error.message) + throw error + } + }, 30000) // 30 second timeout + }) + + describe('⚡ Quick Health Check Tests', () => { + test('🏥 GPT-5 Codex endpoint health check', async () => { + const adapter = ModelAdapterFactory.createAdapter(GPT5_CODEX_PROFILE) + const shouldUseResponses = ModelAdapterFactory.shouldUseResponsesAPI(GPT5_CODEX_PROFILE) + + const endpoint = shouldUseResponses + ? `${GPT5_CODEX_PROFILE.baseURL}/responses` + : `${GPT5_CODEX_PROFILE.baseURL}/chat/completions` + + try { + console.log(`\n🏥 Health check: ${endpoint}`) + + // Use the adapter to build the request properly + const minimalRequest = adapter.createRequest({ + messages: [{ role: 'user', content: 'Hi' }], + systemPrompt: [], + maxTokens: 1 + }) + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${GPT5_CODEX_PROFILE.apiKey}`, + }, + body: JSON.stringify(minimalRequest), + }) + + console.log('📊 Health status:', response.status, response.statusText) + expect(response.status).toBeLessThan(500) // Any response < 500 is OK for health check + + } catch (error) { + console.log('💥 Health check failed:', error.message) + // Don't fail the test for network issues + expect(error.message).toBeDefined() + } + }) + + test('🏥 MiniMax endpoint health check', async () => { + const adapter = ModelAdapterFactory.createAdapter(MINIMAX_CODEX_PROFILE) + const shouldUseResponses = ModelAdapterFactory.shouldUseResponsesAPI(MINIMAX_CODEX_PROFILE) + + const endpoint = shouldUseResponses + ? `${MINIMAX_CODEX_PROFILE.baseURL}/responses` + : `${MINIMAX_CODEX_PROFILE.baseURL}/chat/completions` + + try { + console.log(`\n🏥 Health check: ${endpoint}`) + + // Use the adapter to build the request properly + const minimalRequest = adapter.createRequest({ + messages: [{ role: 'user', content: 'Hi' }], + systemPrompt: [], + maxTokens: 1 + }) + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${MINIMAX_CODEX_PROFILE.apiKey}`, + }, + body: JSON.stringify(minimalRequest), + }) + + console.log('📊 Health status:', response.status, response.statusText) + expect(response.status).toBeLessThan(500) // Any response < 500 is OK for health check + + } catch (error) { + console.log('💥 Health check failed:', error.message) + // Don't fail the test for network issues + expect(error.message).toBeDefined() + } + }) + }) + + describe('📊 Performance & Cost Metrics', () => { + test('⏱️ API response time measurement', async () => { + const startTime = performance.now() + + try { + // Quick test call + const adapter = ModelAdapterFactory.createAdapter(GPT5_CODEX_PROFILE) + const shouldUseResponses = ModelAdapterFactory.shouldUseResponsesAPI(GPT5_CODEX_PROFILE) + + const endpoint = shouldUseResponses + ? `${GPT5_CODEX_PROFILE.baseURL}/responses` + : `${GPT5_CODEX_PROFILE.baseURL}/chat/completions` + + const request = adapter.createRequest({ + messages: [{ role: 'user', content: 'Hello' }], + systemPrompt: [], + maxTokens: 5 + }) + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${GPT5_CODEX_PROFILE.apiKey}`, + }, + body: JSON.stringify(request), + }) + + const endTime = performance.now() + const duration = endTime - startTime + + console.log(`\n⏱️ Performance Metrics:`) + console.log(` Response time: ${duration.toFixed(2)}ms`) + console.log(` Status: ${response.status}`) + + expect(duration).toBeGreaterThan(0) + expect(response.status).toBeDefined() + + } catch (error) { + console.log('⚠️ Performance test failed:', error.message) + // Don't fail for network issues + expect(error.message).toBeDefined() + } + }) + }) + + describe('🎯 Integration Validation Report', () => { + test('📋 Complete production test summary', async () => { + const results = { + timestamp: new Date().toISOString(), + tests: [], + endpoints: [], + performance: {}, + recommendations: [] as string[], + } + + // Test both endpoints + const profiles = [ + { name: 'GPT-5 Codex', profile: GPT5_CODEX_PROFILE }, + { name: 'MiniMax Codex', profile: MINIMAX_CODEX_PROFILE }, + ] + + for (const { name, profile } of profiles) { + try { + const adapter = ModelAdapterFactory.createAdapter(profile) + const shouldUseResponses = ModelAdapterFactory.shouldUseResponsesAPI(profile) + const endpoint = shouldUseResponses + ? `${profile.baseURL}/responses` + : `${profile.baseURL}/chat/completions` + + // Quick connectivity test + const testRequest = { + model: profile.modelName, + messages: [{ role: 'user', content: 'test' }], + max_tokens: 1 + } + + const startTime = performance.now() + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${profile.apiKey}`, + }, + body: JSON.stringify(testRequest), + }) + const endTime = performance.now() + + results.tests.push({ + name, + status: response.ok ? 'success' : 'failed', + statusCode: response.status, + endpoint, + responseTime: `${(endTime - startTime).toFixed(2)}ms`, + }) + + results.endpoints.push({ + name, + url: endpoint, + accessible: response.ok, + }) + + } catch (error) { + results.tests.push({ + name, + status: 'error', + error: error.message, + endpoint: `${profile.baseURL}/...`, + }) + } + } + + // Generate recommendations + const successCount = results.tests.filter(t => t.status === 'success').length + if (successCount === results.tests.length) { + results.recommendations.push('🎉 All endpoints are accessible and working!') + results.recommendations.push('✅ Integration tests passed - ready for production use') + } else { + results.recommendations.push('⚠️ Some endpoints failed - check configuration') + results.recommendations.push('🔧 Verify API keys and endpoint URLs') + } + + // 📨 COMPREHENSIVE PRODUCTION TEST REPORT + console.log('\n🎯 PRODUCTION INTEGRATION REPORT:') + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + console.log(`📅 Test Date: ${results.timestamp}`) + console.log(`🎯 Tests Run: ${results.tests.length}`) + console.log(`✅ Successful: ${successCount}`) + console.log(`❌ Failed: ${results.tests.length - successCount}`) + console.log('') + + console.log('📊 ENDPOINT TEST RESULTS:') + results.tests.forEach(test => { + const icon = test.status === 'success' ? '✅' : '❌' + console.log(` ${icon} ${test.name}: ${test.status} (${test.statusCode || 'N/A'})`) + if (test.responseTime) { + console.log(` ⏱️ Response time: ${test.responseTime}`) + } + if (test.error) { + console.log(` 💥 Error: ${test.error}`) + } + }) + + console.log('') + console.log('🌐 ACCESSIBLE ENDPOINTS:') + results.endpoints.forEach(endpoint => { + const icon = endpoint.accessible ? '🟢' : '🔴' + console.log(` ${icon} ${endpoint.name}: ${endpoint.url}`) + }) + + console.log('') + console.log('💡 RECOMMENDATIONS:') + results.recommendations.forEach(rec => console.log(` ${rec}`)) + + console.log('') + console.log('🚀 NEXT STEPS:') + console.log(' 1. ✅ Integration tests complete') + console.log(' 2. 🔍 Review any failed tests above') + console.log(' 3. 🎯 Configure your applications to use working endpoints') + console.log(' 4. 📊 Monitor API usage and costs') + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + + expect(results.tests.length).toBeGreaterThan(0) + return results + }) + }) +})