mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
📥 fix: Resolve Imported-Conversation Default Model From Runtime modelsConfig (#12885)
* 📥 fix: Use Endpoint-Aware Default Model on Imported Conversations Claude conversations imported from claude.ai's data export display "gpt-4o-mini" in the chat UI until the page is refreshed, and any attempt to send a message before refreshing fails with "The model 'gpt-4o-mini' is not available for Anthropic." Root cause: ImportBatchBuilder.finishConversation() unconditionally defaulted the saved conversation's `model` field to openAISettings.model.default, regardless of `this.endpoint`. Claude exports don't carry a model name, so every imported Claude conversation landed with endpoint=anthropic but model=gpt-4o-mini. Fix: pick the default based on `this.endpoint` via a small lookup (openAI -> gpt-4o-mini, anthropic -> claude-3-5-sonnet-latest), keeping the existing OpenAI default as the fallback for unknown endpoints. Fixes #12844 * 🪄 refactor: Resolve Import Default Model From `modelsConfig` Replace the hardcoded per-endpoint default lookup added in the previous commit with a runtime resolver that consults the same models config the chat UI uses (`getModelsConfig` in ModelController -> `loadDefaultModels` + `loadConfigModels`). This way an imported conversation defaults to a model the LibreChat instance has actually configured / discovered for the endpoint, instead of a hardcoded constant that may not exist on this deployment. Resolution order: 1. First non-empty model in `modelsConfig[endpoint]`. 2. Per-endpoint hardcoded fallback (anthropic/openAI settings) if the runtime config is empty for the endpoint or `getModelsConfig` throws. 3. `openAISettings.model.default` if even the per-endpoint fallback is missing (unknown endpoint). `importBatchBuilder.finishConversation` now accepts an optional `defaultModel` argument; each importer resolves it once at the top via `resolveImportDefaultModel({ endpoint, requestUserId, userRole })` and threads it through. ChatGPT message-level model selection also falls back to the resolved default before the hardcoded gpt-4o-mini.
This commit is contained in:
parent
3758380c61
commit
65990a33e9
5 changed files with 418 additions and 10 deletions
67
api/server/utils/import/defaults.js
Normal file
67
api/server/utils/import/defaults.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
const { logger, getTenantId } = require('@librechat/data-schemas');
|
||||
const { EModelEndpoint, openAISettings, anthropicSettings } = require('librechat-data-provider');
|
||||
const { getModelsConfig } = require('~/server/controllers/ModelController');
|
||||
|
||||
/**
|
||||
* Last-resort hardcoded defaults used only when the runtime models config is
|
||||
* unavailable or returns no models for the endpoint.
|
||||
*/
|
||||
const FALLBACK_MODEL_BY_ENDPOINT = {
|
||||
[EModelEndpoint.openAI]: openAISettings.model.default,
|
||||
[EModelEndpoint.anthropic]: anthropicSettings.model.default,
|
||||
};
|
||||
|
||||
/**
|
||||
* Picks the first available model for an endpoint from a runtime models config.
|
||||
*
|
||||
* @param {string} endpoint - The endpoint key (e.g. EModelEndpoint.anthropic).
|
||||
* @param {TModelsConfig} [modelsConfig] - Map of endpoint -> available model list.
|
||||
* @returns {string | undefined} The first model for the endpoint, or undefined.
|
||||
*/
|
||||
function pickFirstConfiguredModel(endpoint, modelsConfig) {
|
||||
const models = modelsConfig?.[endpoint];
|
||||
if (!Array.isArray(models)) {
|
||||
return undefined;
|
||||
}
|
||||
for (const model of models) {
|
||||
if (typeof model === 'string' && model.length > 0) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the default model that imported conversations should be saved with
|
||||
* for a given endpoint. Prefers the first model exposed by the runtime models
|
||||
* config (admin-configured / provider-discovered), and only falls back to the
|
||||
* hardcoded per-endpoint default if the runtime config is empty or fails.
|
||||
*
|
||||
* @param {object} args
|
||||
* @param {string} args.endpoint - The endpoint key the import is targeting.
|
||||
* @param {string} args.requestUserId - The id of the importing user.
|
||||
* @param {string} [args.userRole] - The role of the importing user.
|
||||
* @returns {Promise<string>} The default model name to persist on the conversation.
|
||||
*/
|
||||
async function resolveImportDefaultModel({ endpoint, requestUserId, userRole }) {
|
||||
try {
|
||||
const modelsConfig = await getModelsConfig({
|
||||
user: { id: requestUserId, role: userRole, tenantId: getTenantId() },
|
||||
});
|
||||
const configured = pickFirstConfiguredModel(endpoint, modelsConfig);
|
||||
if (configured) {
|
||||
return configured;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`[import] Failed to resolve default model from modelsConfig for ${endpoint}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
return FALLBACK_MODEL_BY_ENDPOINT[endpoint] ?? openAISettings.model.default;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FALLBACK_MODEL_BY_ENDPOINT,
|
||||
pickFirstConfiguredModel,
|
||||
resolveImportDefaultModel,
|
||||
};
|
||||
122
api/server/utils/import/defaults.spec.js
Normal file
122
api/server/utils/import/defaults.spec.js
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
const { EModelEndpoint, openAISettings, anthropicSettings } = require('librechat-data-provider');
|
||||
|
||||
const mockGetModelsConfig = jest.fn();
|
||||
|
||||
jest.mock('~/server/controllers/ModelController', () => ({
|
||||
getModelsConfig: (...args) => mockGetModelsConfig(...args),
|
||||
}));
|
||||
|
||||
jest.mock('@librechat/data-schemas', () => {
|
||||
const actual = jest.requireActual('@librechat/data-schemas');
|
||||
return {
|
||||
...actual,
|
||||
getTenantId: () => 'test-tenant',
|
||||
logger: { warn: jest.fn(), error: jest.fn(), info: jest.fn(), debug: jest.fn() },
|
||||
};
|
||||
});
|
||||
|
||||
const {
|
||||
pickFirstConfiguredModel,
|
||||
resolveImportDefaultModel,
|
||||
FALLBACK_MODEL_BY_ENDPOINT,
|
||||
} = require('./defaults');
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('pickFirstConfiguredModel', () => {
|
||||
it('returns the first non-empty string for the endpoint', () => {
|
||||
const modelsConfig = {
|
||||
[EModelEndpoint.anthropic]: ['claude-opus-4-7', 'claude-3-5-sonnet-latest'],
|
||||
};
|
||||
expect(pickFirstConfiguredModel(EModelEndpoint.anthropic, modelsConfig)).toBe(
|
||||
'claude-opus-4-7',
|
||||
);
|
||||
});
|
||||
|
||||
it('skips empty strings', () => {
|
||||
const modelsConfig = {
|
||||
[EModelEndpoint.openAI]: ['', 'gpt-4o'],
|
||||
};
|
||||
expect(pickFirstConfiguredModel(EModelEndpoint.openAI, modelsConfig)).toBe('gpt-4o');
|
||||
});
|
||||
|
||||
it('returns undefined when modelsConfig is missing', () => {
|
||||
expect(pickFirstConfiguredModel(EModelEndpoint.anthropic, undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined when the endpoint has no models', () => {
|
||||
expect(pickFirstConfiguredModel(EModelEndpoint.anthropic, {})).toBeUndefined();
|
||||
expect(
|
||||
pickFirstConfiguredModel(EModelEndpoint.anthropic, { [EModelEndpoint.anthropic]: [] }),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined when the endpoint value is not an array', () => {
|
||||
expect(
|
||||
pickFirstConfiguredModel(EModelEndpoint.anthropic, {
|
||||
[EModelEndpoint.anthropic]: 'claude-opus-4-7',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveImportDefaultModel', () => {
|
||||
it('returns the first model from modelsConfig when present', async () => {
|
||||
mockGetModelsConfig.mockResolvedValueOnce({
|
||||
[EModelEndpoint.anthropic]: ['claude-opus-4-7'],
|
||||
});
|
||||
|
||||
const result = await resolveImportDefaultModel({
|
||||
endpoint: EModelEndpoint.anthropic,
|
||||
requestUserId: 'user-1',
|
||||
userRole: 'USER',
|
||||
});
|
||||
|
||||
expect(result).toBe('claude-opus-4-7');
|
||||
expect(mockGetModelsConfig).toHaveBeenCalledWith({
|
||||
user: { id: 'user-1', role: 'USER', tenantId: 'test-tenant' },
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to the per-endpoint default when modelsConfig has no models for the endpoint', async () => {
|
||||
mockGetModelsConfig.mockResolvedValueOnce({});
|
||||
|
||||
const result = await resolveImportDefaultModel({
|
||||
endpoint: EModelEndpoint.anthropic,
|
||||
requestUserId: 'user-1',
|
||||
});
|
||||
|
||||
expect(result).toBe(anthropicSettings.model.default);
|
||||
});
|
||||
|
||||
it('falls back to the openAI default for unknown endpoints with no modelsConfig entry', async () => {
|
||||
mockGetModelsConfig.mockResolvedValueOnce({});
|
||||
|
||||
const result = await resolveImportDefaultModel({
|
||||
endpoint: 'some-custom-endpoint',
|
||||
requestUserId: 'user-1',
|
||||
});
|
||||
|
||||
expect(result).toBe(openAISettings.model.default);
|
||||
});
|
||||
|
||||
it('falls back to the per-endpoint default when getModelsConfig rejects', async () => {
|
||||
mockGetModelsConfig.mockRejectedValueOnce(new Error('boom'));
|
||||
|
||||
const result = await resolveImportDefaultModel({
|
||||
endpoint: EModelEndpoint.anthropic,
|
||||
requestUserId: 'user-1',
|
||||
});
|
||||
|
||||
expect(result).toBe(anthropicSettings.model.default);
|
||||
});
|
||||
|
||||
it('exposes hardcoded fallbacks for openAI and anthropic', () => {
|
||||
expect(FALLBACK_MODEL_BY_ENDPOINT[EModelEndpoint.openAI]).toBe(openAISettings.model.default);
|
||||
expect(FALLBACK_MODEL_BY_ENDPOINT[EModelEndpoint.anthropic]).toBe(
|
||||
anthropicSettings.model.default,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@ const { v4: uuidv4 } = require('uuid');
|
|||
const { logger } = require('@librechat/data-schemas');
|
||||
const { EModelEndpoint, Constants, openAISettings } = require('librechat-data-provider');
|
||||
const { bulkIncrementTagCounts, bulkSaveConvos, bulkSaveMessages } = require('~/models');
|
||||
const { FALLBACK_MODEL_BY_ENDPOINT } = require('./defaults');
|
||||
|
||||
/**
|
||||
* Factory function for creating an instance of ImportBatchBuilder.
|
||||
|
|
@ -70,9 +71,14 @@ class ImportBatchBuilder {
|
|||
* @param {string} [title='Imported Chat'] - The title of the conversation. Defaults to 'Imported Chat'.
|
||||
* @param {Date} [createdAt] - The creation date of the conversation.
|
||||
* @param {TConversation} [originalConvo] - The original conversation.
|
||||
* @param {string} [defaultModel] - Resolved default model for this endpoint
|
||||
* (typically derived from the runtime models config). Used only when
|
||||
* originalConvo.model is unset.
|
||||
* @returns {{ conversation: TConversation, messages: TMessage[] }} The resulting conversation and messages.
|
||||
*/
|
||||
finishConversation(title, createdAt, originalConvo = {}) {
|
||||
finishConversation(title, createdAt, originalConvo = {}, defaultModel) {
|
||||
const fallbackModel =
|
||||
defaultModel ?? FALLBACK_MODEL_BY_ENDPOINT[this.endpoint] ?? openAISettings.model.default;
|
||||
const convo = {
|
||||
...originalConvo,
|
||||
user: this.requestUserId,
|
||||
|
|
@ -82,7 +88,7 @@ class ImportBatchBuilder {
|
|||
updatedAt: createdAt,
|
||||
overrideTimestamp: true,
|
||||
endpoint: this.endpoint,
|
||||
model: originalConvo.model ?? openAISettings.model.default,
|
||||
model: originalConvo.model ?? fallbackModel,
|
||||
};
|
||||
convo._id && delete convo._id;
|
||||
this.conversations.push(convo);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ const { logger, getTenantId } = require('@librechat/data-schemas');
|
|||
const { EModelEndpoint, Constants, openAISettings } = require('librechat-data-provider');
|
||||
const { getEndpointsConfig } = require('~/server/services/Config');
|
||||
const { createImportBatchBuilder } = require('./importBatchBuilder');
|
||||
const { resolveImportDefaultModel } = require('./defaults');
|
||||
const { cloneMessagesWithTimestamps } = require('./fork');
|
||||
|
||||
/**
|
||||
|
|
@ -53,11 +54,17 @@ async function importChatBotUiConvo(
|
|||
jsonData,
|
||||
requestUserId,
|
||||
builderFactory = createImportBatchBuilder,
|
||||
userRole,
|
||||
) {
|
||||
// this have been tested with chatbot-ui V1 export https://github.com/mckaywrigley/chatbot-ui/tree/b865b0555f53957e96727bc0bbb369c9eaecd83b#legacy-code
|
||||
try {
|
||||
/** @type {ImportBatchBuilder} */
|
||||
const importBatchBuilder = builderFactory(requestUserId);
|
||||
const defaultModel = await resolveImportDefaultModel({
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
requestUserId,
|
||||
userRole,
|
||||
});
|
||||
|
||||
for (const historyItem of jsonData.history) {
|
||||
importBatchBuilder.startConversation(EModelEndpoint.openAI);
|
||||
|
|
@ -68,7 +75,7 @@ async function importChatBotUiConvo(
|
|||
importBatchBuilder.addUserMessage(message.content);
|
||||
}
|
||||
}
|
||||
importBatchBuilder.finishConversation(historyItem.name, new Date());
|
||||
importBatchBuilder.finishConversation(historyItem.name, new Date(), {}, defaultModel);
|
||||
}
|
||||
await importBatchBuilder.saveBatch();
|
||||
logger.info(`user: ${requestUserId} | ChatbotUI conversation imported`);
|
||||
|
|
@ -115,9 +122,15 @@ async function importClaudeConvo(
|
|||
jsonData,
|
||||
requestUserId,
|
||||
builderFactory = createImportBatchBuilder,
|
||||
userRole,
|
||||
) {
|
||||
try {
|
||||
const importBatchBuilder = builderFactory(requestUserId);
|
||||
const defaultModel = await resolveImportDefaultModel({
|
||||
endpoint: EModelEndpoint.anthropic,
|
||||
requestUserId,
|
||||
userRole,
|
||||
});
|
||||
|
||||
for (const conv of jsonData) {
|
||||
importBatchBuilder.startConversation(EModelEndpoint.anthropic);
|
||||
|
|
@ -172,7 +185,12 @@ async function importClaudeConvo(
|
|||
}
|
||||
|
||||
const createdAt = conv.created_at ? new Date(conv.created_at) : new Date();
|
||||
importBatchBuilder.finishConversation(conv.name || 'Imported Claude Chat', createdAt);
|
||||
importBatchBuilder.finishConversation(
|
||||
conv.name || 'Imported Claude Chat',
|
||||
createdAt,
|
||||
{},
|
||||
defaultModel,
|
||||
);
|
||||
}
|
||||
|
||||
await importBatchBuilder.saveBatch();
|
||||
|
|
@ -215,6 +233,12 @@ async function importLibreChatConvo(
|
|||
|
||||
importBatchBuilder.startConversation(endpoint);
|
||||
|
||||
const defaultModel = await resolveImportDefaultModel({
|
||||
endpoint,
|
||||
requestUserId,
|
||||
userRole,
|
||||
});
|
||||
|
||||
let firstMessageDate = null;
|
||||
|
||||
const messagesToImport = jsonData.messagesTree || jsonData.messages;
|
||||
|
|
@ -271,7 +295,12 @@ async function importLibreChatConvo(
|
|||
firstMessageDate = null;
|
||||
}
|
||||
|
||||
importBatchBuilder.finishConversation(jsonData.title, firstMessageDate ?? new Date(), options);
|
||||
importBatchBuilder.finishConversation(
|
||||
jsonData.title,
|
||||
firstMessageDate ?? new Date(),
|
||||
options,
|
||||
defaultModel,
|
||||
);
|
||||
await importBatchBuilder.saveBatch();
|
||||
logger.debug(`user: ${requestUserId} | Conversation "${jsonData.title}" imported`);
|
||||
} catch (error) {
|
||||
|
|
@ -292,11 +321,17 @@ async function importChatGptConvo(
|
|||
jsonData,
|
||||
requestUserId,
|
||||
builderFactory = createImportBatchBuilder,
|
||||
userRole,
|
||||
) {
|
||||
try {
|
||||
const importBatchBuilder = builderFactory(requestUserId);
|
||||
const defaultModel = await resolveImportDefaultModel({
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
requestUserId,
|
||||
userRole,
|
||||
});
|
||||
for (const conv of jsonData) {
|
||||
processConversation(conv, importBatchBuilder, requestUserId);
|
||||
processConversation(conv, importBatchBuilder, requestUserId, defaultModel);
|
||||
}
|
||||
await importBatchBuilder.saveBatch();
|
||||
} catch (error) {
|
||||
|
|
@ -311,9 +346,10 @@ async function importChatGptConvo(
|
|||
* @param {ChatGPTConvo} conv - A single conversation object that contains multiple messages and other details.
|
||||
* @param {ImportBatchBuilder} importBatchBuilder - The batch builder instance used to manage and batch conversation data.
|
||||
* @param {string} requestUserId - The ID of the user who initiated the import process.
|
||||
* @param {string} [defaultModel] - Resolved default model for the openAI endpoint.
|
||||
* @returns {void}
|
||||
*/
|
||||
function processConversation(conv, importBatchBuilder, requestUserId) {
|
||||
function processConversation(conv, importBatchBuilder, requestUserId, defaultModel) {
|
||||
importBatchBuilder.startConversation(EModelEndpoint.openAI);
|
||||
|
||||
// Map all message IDs to new UUIDs
|
||||
|
|
@ -437,7 +473,8 @@ function processConversation(conv, importBatchBuilder, requestUserId) {
|
|||
|
||||
const isCreatedByUser = role === 'user';
|
||||
let sender = isCreatedByUser ? 'user' : 'assistant';
|
||||
const model = mapping.message.metadata.model_slug || openAISettings.model.default;
|
||||
const model =
|
||||
mapping.message.metadata.model_slug || defaultModel || openAISettings.model.default;
|
||||
|
||||
if (!isCreatedByUser) {
|
||||
/** Extracted model name from model slug */
|
||||
|
|
@ -487,7 +524,12 @@ function processConversation(conv, importBatchBuilder, requestUserId) {
|
|||
importBatchBuilder.saveMessage(message);
|
||||
}
|
||||
|
||||
importBatchBuilder.finishConversation(conv.title, new Date(conv.create_time * 1000));
|
||||
importBatchBuilder.finishConversation(
|
||||
conv.title,
|
||||
new Date(conv.create_time * 1000),
|
||||
{},
|
||||
defaultModel,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { EModelEndpoint, Constants, openAISettings } = require('librechat-data-provider');
|
||||
const {
|
||||
EModelEndpoint,
|
||||
Constants,
|
||||
openAISettings,
|
||||
anthropicSettings,
|
||||
} = require('librechat-data-provider');
|
||||
const { getImporter, processAssistantMessage } = require('./importers');
|
||||
const { ImportBatchBuilder } = require('./importBatchBuilder');
|
||||
const { bulkSaveMessages, bulkSaveConvos: _bulkSaveConvos } = require('~/models');
|
||||
|
|
@ -9,10 +14,16 @@ const mockGetEndpointsConfig = jest.fn().mockResolvedValue({
|
|||
[EModelEndpoint.openAI]: { userProvide: false },
|
||||
});
|
||||
|
||||
const mockGetModelsConfig = jest.fn().mockResolvedValue({});
|
||||
|
||||
jest.mock('~/server/services/Config', () => ({
|
||||
getEndpointsConfig: (...args) => mockGetEndpointsConfig(...args),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/controllers/ModelController', () => ({
|
||||
getModelsConfig: (...args) => mockGetModelsConfig(...args),
|
||||
}));
|
||||
|
||||
// Mock the database methods
|
||||
jest.mock('~/models', () => ({
|
||||
bulkSaveConvos: jest.fn(),
|
||||
|
|
@ -1013,6 +1024,28 @@ describe('importLibreChatConvo', () => {
|
|||
expect(result.conversation.title).toBe('Imported Chat');
|
||||
expect(result.conversation.model).toBe(openAISettings.model.default);
|
||||
});
|
||||
|
||||
it('should default to the anthropic model for anthropic-endpoint conversations', () => {
|
||||
const requestUserId = 'user-123';
|
||||
const builder = new ImportBatchBuilder(requestUserId);
|
||||
builder.conversationId = 'conv-id-123';
|
||||
builder.messages = [{ text: 'Hello, world!' }];
|
||||
builder.endpoint = EModelEndpoint.anthropic;
|
||||
const result = builder.finishConversation();
|
||||
expect(result.conversation.endpoint).toBe(EModelEndpoint.anthropic);
|
||||
expect(result.conversation.model).toBe(anthropicSettings.model.default);
|
||||
});
|
||||
|
||||
it('should default to the openAI model for openAI-endpoint conversations', () => {
|
||||
const requestUserId = 'user-123';
|
||||
const builder = new ImportBatchBuilder(requestUserId);
|
||||
builder.conversationId = 'conv-id-123';
|
||||
builder.messages = [{ text: 'Hello, world!' }];
|
||||
builder.endpoint = EModelEndpoint.openAI;
|
||||
const result = builder.finishConversation();
|
||||
expect(result.conversation.endpoint).toBe(EModelEndpoint.openAI);
|
||||
expect(result.conversation.model).toBe(openAISettings.model.default);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1063,11 +1096,15 @@ describe('importChatBotUiConvo', () => {
|
|||
1,
|
||||
'Hello what are you able to do?',
|
||||
expect.any(Date),
|
||||
{},
|
||||
expect.any(String),
|
||||
);
|
||||
expect(importBatchBuilder.finishConversation).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'Give me the code that inverts ...',
|
||||
expect.any(Date),
|
||||
{},
|
||||
expect.any(String),
|
||||
);
|
||||
|
||||
expect(importBatchBuilder.saveBatch).toHaveBeenCalled();
|
||||
|
|
@ -1347,6 +1384,8 @@ describe('importClaudeConvo', () => {
|
|||
expect(importBatchBuilder.finishConversation).toHaveBeenCalledWith(
|
||||
'Test Conversation',
|
||||
expect.any(Date),
|
||||
{},
|
||||
expect.any(String),
|
||||
);
|
||||
|
||||
const savedMessages = importBatchBuilder.saveMessage.mock.calls.map((call) => call[0]);
|
||||
|
|
@ -1437,6 +1476,136 @@ describe('importClaudeConvo', () => {
|
|||
expect(savedMessages[0]).not.toHaveProperty('model');
|
||||
});
|
||||
|
||||
it('should set the conversation endpoint and a Claude model so the chat UI loads correctly without a refresh', async () => {
|
||||
const jsonData = [
|
||||
{
|
||||
uuid: 'conv-123',
|
||||
name: 'Claude Conversation',
|
||||
created_at: '2025-01-15T10:00:00.000Z',
|
||||
chat_messages: [
|
||||
{
|
||||
uuid: 'msg-1',
|
||||
sender: 'human',
|
||||
created_at: '2025-01-15T10:00:01.000Z',
|
||||
content: [{ type: 'text', text: 'Hello' }],
|
||||
},
|
||||
{
|
||||
uuid: 'msg-2',
|
||||
sender: 'assistant',
|
||||
created_at: '2025-01-15T10:00:02.000Z',
|
||||
content: [{ type: 'text', text: 'Hi there!' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const requestUserId = 'user-123';
|
||||
const importBatchBuilder = new ImportBatchBuilder(requestUserId);
|
||||
|
||||
const importer = getImporter(jsonData);
|
||||
await importer(jsonData, requestUserId, () => importBatchBuilder);
|
||||
|
||||
expect(importBatchBuilder.conversations).toHaveLength(1);
|
||||
const convo = importBatchBuilder.conversations[0];
|
||||
expect(convo.endpoint).toBe(EModelEndpoint.anthropic);
|
||||
expect(convo.model).toBe(anthropicSettings.model.default);
|
||||
expect(convo.model).not.toBe(openAISettings.model.default);
|
||||
});
|
||||
|
||||
it('should prefer the first runtime-configured anthropic model over the hardcoded default', async () => {
|
||||
mockGetModelsConfig.mockResolvedValueOnce({
|
||||
[EModelEndpoint.anthropic]: ['claude-opus-4-7', 'claude-3-5-sonnet-latest'],
|
||||
});
|
||||
|
||||
const jsonData = [
|
||||
{
|
||||
uuid: 'conv-456',
|
||||
name: 'Configured Claude Conversation',
|
||||
created_at: '2025-01-15T10:00:00.000Z',
|
||||
chat_messages: [
|
||||
{
|
||||
uuid: 'msg-1',
|
||||
sender: 'human',
|
||||
created_at: '2025-01-15T10:00:01.000Z',
|
||||
content: [{ type: 'text', text: 'Hello' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const requestUserId = 'user-123';
|
||||
const importBatchBuilder = new ImportBatchBuilder(requestUserId);
|
||||
|
||||
const importer = getImporter(jsonData);
|
||||
await importer(jsonData, requestUserId, () => importBatchBuilder);
|
||||
|
||||
const convo = importBatchBuilder.conversations[0];
|
||||
expect(convo.endpoint).toBe(EModelEndpoint.anthropic);
|
||||
expect(convo.model).toBe('claude-opus-4-7');
|
||||
});
|
||||
|
||||
it('should fall back to the anthropic hardcoded default when modelsConfig has no anthropic models', async () => {
|
||||
mockGetModelsConfig.mockResolvedValueOnce({
|
||||
[EModelEndpoint.anthropic]: [],
|
||||
});
|
||||
|
||||
const jsonData = [
|
||||
{
|
||||
uuid: 'conv-789',
|
||||
name: 'Empty modelsConfig Conversation',
|
||||
created_at: '2025-01-15T10:00:00.000Z',
|
||||
chat_messages: [
|
||||
{
|
||||
uuid: 'msg-1',
|
||||
sender: 'human',
|
||||
created_at: '2025-01-15T10:00:01.000Z',
|
||||
content: [{ type: 'text', text: 'Hello' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const requestUserId = 'user-123';
|
||||
const importBatchBuilder = new ImportBatchBuilder(requestUserId);
|
||||
|
||||
const importer = getImporter(jsonData);
|
||||
await importer(jsonData, requestUserId, () => importBatchBuilder);
|
||||
|
||||
const convo = importBatchBuilder.conversations[0];
|
||||
expect(convo.endpoint).toBe(EModelEndpoint.anthropic);
|
||||
expect(convo.model).toBe(anthropicSettings.model.default);
|
||||
});
|
||||
|
||||
it('should fall back to the anthropic hardcoded default when getModelsConfig throws', async () => {
|
||||
mockGetModelsConfig.mockRejectedValueOnce(new Error('boom'));
|
||||
|
||||
const jsonData = [
|
||||
{
|
||||
uuid: 'conv-fail',
|
||||
name: 'modelsConfig failure',
|
||||
created_at: '2025-01-15T10:00:00.000Z',
|
||||
chat_messages: [
|
||||
{
|
||||
uuid: 'msg-1',
|
||||
sender: 'human',
|
||||
created_at: '2025-01-15T10:00:01.000Z',
|
||||
content: [{ type: 'text', text: 'Hello' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const requestUserId = 'user-123';
|
||||
const importBatchBuilder = new ImportBatchBuilder(requestUserId);
|
||||
|
||||
const importer = getImporter(jsonData);
|
||||
await importer(jsonData, requestUserId, () => importBatchBuilder);
|
||||
|
||||
const convo = importBatchBuilder.conversations[0];
|
||||
expect(convo.endpoint).toBe(EModelEndpoint.anthropic);
|
||||
expect(convo.model).toBe(anthropicSettings.model.default);
|
||||
});
|
||||
|
||||
it('should correct timestamp inversions (child before parent)', async () => {
|
||||
const jsonData = [
|
||||
{
|
||||
|
|
@ -1603,6 +1772,8 @@ describe('importClaudeConvo', () => {
|
|||
expect(importBatchBuilder.finishConversation).toHaveBeenCalledWith(
|
||||
'Imported Claude Chat',
|
||||
expect.any(Date),
|
||||
{},
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue