From ba5778d0df1af8e1ddde8a58150d16ae30e72f2c Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 9 Jun 2026 19:04:21 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=A5=20fix:=20ChatGPT=20Conversation=20?= =?UTF-8?q?Import=20Failures=20(#13637)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../utils/import/importers-timestamp.spec.js | 1 + api/server/utils/import/importers.js | 20 +++- api/server/utils/import/importers.spec.js | 91 +++++++++++++++++++ 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/api/server/utils/import/importers-timestamp.spec.js b/api/server/utils/import/importers-timestamp.spec.js index e12c099abb..268cc74c0d 100644 --- a/api/server/utils/import/importers-timestamp.spec.js +++ b/api/server/utils/import/importers-timestamp.spec.js @@ -7,6 +7,7 @@ const { getImporter } = require('./importers'); jest.mock('~/models', () => ({ bulkSaveConvos: jest.fn(), bulkSaveMessages: jest.fn(), + bulkIncrementTagCounts: jest.fn(), })); const mockGetEndpointsConfig = jest.fn().mockResolvedValue(null); diff --git a/api/server/utils/import/importers.js b/api/server/utils/import/importers.js index b86be3798e..435572d65a 100644 --- a/api/server/utils/import/importers.js +++ b/api/server/utils/import/importers.js @@ -22,8 +22,11 @@ function getImporter(jsonData) { return importClaudeConvo; } // ChatGPT format has mapping object in each conversation - logger.info('Importing ChatGPT conversation'); - return importChatGptConvo; + if (jsonData.length === 0 || jsonData[0]?.mapping) { + logger.info('Importing ChatGPT conversation'); + return importChatGptConvo; + } + throw new Error('Unsupported import type'); } // For ChatbotUI @@ -81,6 +84,7 @@ async function importChatBotUiConvo( logger.info(`user: ${requestUserId} | ChatbotUI conversation imported`); } catch (error) { logger.error(`user: ${requestUserId} | Error creating conversation from ChatbotUI file`, error); + throw error; } } @@ -197,6 +201,7 @@ async function importClaudeConvo( logger.info(`user: ${requestUserId} | Claude conversation imported`); } catch (error) { logger.error(`user: ${requestUserId} | Error creating conversation from Claude file`, error); + throw error; } } @@ -305,6 +310,7 @@ async function importLibreChatConvo( logger.debug(`user: ${requestUserId} | Conversation "${jsonData.title}" imported`); } catch (error) { logger.error(`user: ${requestUserId} | Error creating conversation from LibreChat file`, error); + throw error; } } @@ -336,6 +342,7 @@ async function importChatGptConvo( await importBatchBuilder.saveBatch(); } catch (error) { logger.error(`user: ${requestUserId} | Error creating conversation from imported file`, error); + throw error; } } @@ -355,7 +362,7 @@ function processConversation(conv, importBatchBuilder, requestUserId, defaultMod // Map all message IDs to new UUIDs const messageMap = new Map(); for (const [id, mapping] of Object.entries(conv.mapping)) { - if (mapping.message && mapping.message.content.content_type) { + if (mapping.message?.content?.content_type) { const newMessageId = uuidv4(); messageMap.set(id, newMessageId); } @@ -467,6 +474,9 @@ function processConversation(conv, importBatchBuilder, requestUserId, defaultMod } const newMessageId = messageMap.get(id); + if (!newMessageId) { + continue; + } const parentMessageId = findValidParent(mapping.parent); const messageText = formatMessageText(mapping.message); @@ -474,7 +484,7 @@ function processConversation(conv, importBatchBuilder, requestUserId, defaultMod const isCreatedByUser = role === 'user'; let sender = isCreatedByUser ? 'user' : 'assistant'; const model = - mapping.message.metadata.model_slug || defaultModel || openAISettings.model.default; + mapping.message.metadata?.model_slug || defaultModel || openAISettings.model.default; if (!isCreatedByUser) { /** Extracted model name from model slug */ @@ -598,7 +608,7 @@ function formatMessageText(messageData) { messageText = `\`\`\`json\n${JSON.stringify(messageData.content, null, 2)}\n\`\`\``; } - if (isText && messageData.author.role !== 'user') { + if (isText && messageData.author?.role !== 'user') { messageText = processAssistantMessage(messageData, messageText); } diff --git a/api/server/utils/import/importers.spec.js b/api/server/utils/import/importers.spec.js index 6ccd2f3728..a9bd679f55 100644 --- a/api/server/utils/import/importers.spec.js +++ b/api/server/utils/import/importers.spec.js @@ -764,6 +764,86 @@ describe('importChatGptConvo', () => { expect(userMsg.createdAt).toEqual(new Date(1000 * 1000)); expect(assistantMsg.createdAt).toEqual(new Date(2000 * 1000)); }); + + it('should import messages missing metadata without failing (newer ChatGPT exports)', async () => { + const testData = [ + { + title: 'Missing Metadata Test', + create_time: 1714585031.148505, + update_time: 1714585060.879308, + mapping: { + 'root-node': { + id: 'root-node', + message: null, + parent: null, + children: ['user-msg-1'], + }, + 'user-msg-1': { + id: 'user-msg-1', + message: { + id: 'user-msg-1', + author: { role: 'user' }, + create_time: 1714585031.150442, + content: { content_type: 'text', parts: ['User message without metadata'] }, + }, + parent: 'root-node', + children: ['assistant-msg-1'], + }, + 'assistant-msg-1': { + id: 'assistant-msg-1', + message: { + id: 'assistant-msg-1', + author: { role: 'assistant' }, + create_time: 1714585032.150442, + content: { content_type: 'text', parts: ['Assistant response without metadata'] }, + }, + parent: 'user-msg-1', + children: ['no-content-msg'], + }, + 'no-content-msg': { + id: 'no-content-msg', + message: { + id: 'no-content-msg', + author: { role: 'tool' }, + create_time: 1714585033.150442, + }, + parent: 'assistant-msg-1', + children: [], + }, + }, + }, + ]; + + const requestUserId = 'user-123'; + const importBatchBuilder = new ImportBatchBuilder(requestUserId); + jest.spyOn(importBatchBuilder, 'saveMessage'); + + const importer = getImporter(testData); + await importer(testData, requestUserId, () => importBatchBuilder); + + const savedMessages = importBatchBuilder.saveMessage.mock.calls.map((call) => call[0]); + expect(savedMessages).toHaveLength(2); + + const userMessage = savedMessages.find((msg) => msg.isCreatedByUser); + const assistantMessage = savedMessages.find((msg) => !msg.isCreatedByUser); + expect(userMessage.model).toBe(openAISettings.model.default); + expect(assistantMessage.model).toBe(openAISettings.model.default); + expect(assistantMessage.parentMessageId).toBe(userMessage.messageId); + }); + + it('should rethrow errors so failed imports are not reported as successful', async () => { + const jsonData = JSON.parse( + fs.readFileSync(path.join(__dirname, '__data__', 'chatgpt-export.json'), 'utf8'), + ); + const requestUserId = 'user-123'; + const importBatchBuilder = new ImportBatchBuilder(requestUserId); + jest.spyOn(importBatchBuilder, 'saveBatch').mockRejectedValue(new Error('db unavailable')); + + const importer = getImporter(jsonData); + await expect(importer(jsonData, requestUserId, () => importBatchBuilder)).rejects.toThrow( + 'db unavailable', + ); + }); }); describe('importLibreChatConvo', () => { @@ -1135,6 +1215,17 @@ describe('getImporter', () => { const jsonData = { unsupported: 'data' }; expect(() => getImporter(jsonData)).toThrow('Unsupported import type'); }); + + it('should throw for array-based files that are not ChatGPT or Claude exports', () => { + const openWebUiExport = [ + { id: 'abc', title: 'Open WebUI Chat', chat: { history: { messages: {} } } }, + ]; + expect(() => getImporter(openWebUiExport)).toThrow('Unsupported import type'); + }); + + it('should route empty arrays to the ChatGPT importer without throwing', () => { + expect(() => getImporter([])).not.toThrow(); + }); }); describe('processAssistantMessage', () => {