Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/cli/commands/run-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,31 @@ describe('run-config validation logic', () => {
);
});

it('should pass through a custom model object with multiple keys (id, url, modelName)', async () => {
const customModel = { id: 'my-custom-agent', url: 'http://localhost:3001/v1', modelName: 'gpt-4o' };
const result = await resolveModelCollections([customModel], undefined, mockLogger as any);

expect(result).toEqual([customModel]);
// It must NOT be treated as a malformed single-key provider object.
expect(mockLogger.warn).not.toHaveBeenCalledWith(
expect.stringContaining('Invalid object entry in models array'),
);
});

it('should pass custom model objects through alongside strings and collections', async () => {
const customModel = { id: 'my-custom-agent', url: 'http://localhost:3001/v1', modelName: 'gpt-4o' };
const collectionsRepoPath = '/fake/repo';
mockedFs.readFile.mockResolvedValue(JSON.stringify(['google:gemini-1.5-flash-latest']));

const result = await resolveModelCollections(
['openai:gpt-4o-mini', customModel, 'TEST_COLLECTION'],
collectionsRepoPath,
mockLogger as any,
);

expect(result).toEqual(['openai:gpt-4o-mini', customModel, 'google:gemini-1.5-flash-latest']);
});

it('should deduplicate models from multiple collections and literals', async () => {
const models = ['openai:gpt-4o-mini', 'COLLECTION_A', 'COLLECTION_B'];
const collectionsRepoPath = '/fake/repo';
Expand Down
35 changes: 19 additions & 16 deletions src/cli/commands/run-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,28 @@ export async function resolveModelCollections(configModels: any[], collectionsRe
}
normalizedConfigModels.push(correctedEntry);
} else if (typeof modelEntry === 'object' && modelEntry !== null && !Array.isArray(modelEntry)) {
const keys = Object.keys(modelEntry);
if (keys.length === 1) {
const provider = keys[0].trim();
const modelNameValue = modelEntry[keys[0]];

if (typeof modelNameValue === 'string') {
const modelName = modelNameValue.trim();
const corrected = `${provider}:${modelName}`;
logger.warn(`Found model entry as a key-value pair: ${JSON.stringify(modelEntry)}. Interpreting as '${corrected}'. For clarity, please use the string format "provider:model" in your blueprint.`);
normalizedConfigModels.push(corrected);
if (modelEntry.id) {
// Custom model definition (e.g. { id, url, modelName }). Pass through directly.
normalizedConfigModels.push(modelEntry);
} else {
// Provider shorthand — expect exactly one key: { provider: modelName }.
const keys = Object.keys(modelEntry);
if (keys.length === 1) {
const provider = keys[0].trim();
const modelNameValue = modelEntry[keys[0]];

if (typeof modelNameValue === 'string') {
const modelName = modelNameValue.trim();
const corrected = `${provider}:${modelName}`;
logger.warn(`Found model entry as a key-value pair: ${JSON.stringify(modelEntry)}. Interpreting as '${corrected}'. For clarity, please use the string format "provider:model" in your blueprint.`);
normalizedConfigModels.push(corrected);
} else {
logger.warn(`Invalid object entry in models array: Key '${provider}' has a non-string value. Skipping: ${JSON.stringify(modelEntry)}.`);
}
} else {
logger.warn(`Invalid object entry in models array: Key '${provider}' has a non-string value. Skipping: ${JSON.stringify(modelEntry)}.`);
logger.warn(`Invalid object entry in models array: expected either an 'id' field (for a custom model definition) or exactly one key (the provider, as { "provider": "modelName" }). Found ${keys.length} keys and no 'id'. Skipping: ${JSON.stringify(modelEntry)}.`);
}
} else {
logger.warn(`Invalid object entry in models array: Must have exactly one key (the provider). Found ${keys.length} keys. Skipping: ${JSON.stringify(modelEntry)}.`);
}
} else if (typeof modelEntry === 'object' && modelEntry !== null && !Array.isArray(modelEntry) && modelEntry.id) {
// This is a custom model definition. Add it directly.
normalizedConfigModels.push(modelEntry);
} else {
logger.warn(`Invalid entry in models array found: ${JSON.stringify(modelEntry)}. It is not a string or a single key-value object. Skipping this entry.`);
}
Expand Down
Loading