📥 fix: ChatGPT Conversation Import Failures (#13637)

This commit is contained in:
Danny Avila 2026-06-09 19:04:21 -04:00 committed by GitHub
parent 7eafe317cc
commit ba5778d0df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 107 additions and 5 deletions

View file

@ -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);

View file

@ -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);
}

View file

@ -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', () => {