feat(config): Add Fallback Model and Expanded Provider Support

Introduces a configurable fallback model and adds support for additional AI provider API keys in the environment setup.

- **Add Fallback Model Configuration (.taskmasterconfig):**
  - Implemented a new  section in .
  - Configured  as the default fallback model, enhancing resilience if the primary model fails.

- **Update Default Model Configuration (.taskmasterconfig):**
  - Changed the default  model to .
  - Changed the default  model to .

- **Add API Key Examples (assets/env.example):**
  - Added example environment variables for:
    -  (for OpenAI/OpenRouter)
    -  (for Google Gemini)
    -  (for XAI Grok)
  - Included format comments for clarity.
This commit is contained in:
Eyal Toledano
2025-04-16 00:35:30 -04:00
parent d84c2486e4
commit 1ab836f191
12 changed files with 1638 additions and 68 deletions

View File

@@ -59,7 +59,8 @@ const DEFAULT_CONFIG = {
const VALID_CUSTOM_CONFIG = {
models: {
main: { provider: 'openai', modelId: 'gpt-4o' },
research: { provider: 'google', modelId: 'gemini-1.5-pro-latest' }
research: { provider: 'google', modelId: 'gemini-1.5-pro-latest' },
fallback: { provider: undefined, modelId: undefined }
}
};
@@ -67,6 +68,7 @@ const PARTIAL_CONFIG = {
models: {
main: { provider: 'openai', modelId: 'gpt-4-turbo' }
// research missing
// fallback will be added by readConfig
}
};
@@ -90,9 +92,66 @@ const resetMocks = () => {
mockWriteFileSync.mockReset();
mockMkdirSync.mockReset();
// Default behaviors
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(JSON.stringify(DEFAULT_CONFIG));
// Default behaviors - CRITICAL: Mock supported-models.json read
mockReadFileSync.mockImplementation((filePath) => {
if (filePath.endsWith('supported-models.json')) {
// Return a mock structure including allowed_roles
return JSON.stringify({
openai: [
{
id: 'gpt-4o',
swe_score: 0,
cost_per_1m_tokens: null,
allowed_roles: ['main', 'fallback']
},
{
id: 'gpt-4',
swe_score: 0,
cost_per_1m_tokens: null,
allowed_roles: ['main', 'fallback']
}
],
google: [
{
id: 'gemini-1.5-pro-latest',
swe_score: 0,
cost_per_1m_tokens: null,
allowed_roles: ['main', 'fallback']
}
],
perplexity: [
{
id: 'sonar-pro',
swe_score: 0,
cost_per_1m_tokens: null,
allowed_roles: ['main', 'fallback', 'research']
}
],
anthropic: [
{
id: 'claude-3-opus-20240229',
swe_score: 0,
cost_per_1m_tokens: null,
allowed_roles: ['main', 'fallback']
},
{
id: 'claude-3.5-sonnet-20240620',
swe_score: 0,
cost_per_1m_tokens: null,
allowed_roles: ['main', 'fallback']
}
]
// Add other providers/models as needed for specific tests
});
} else if (filePath === MOCK_CONFIG_PATH) {
// Default for .taskmasterconfig reads
return JSON.stringify(DEFAULT_CONFIG);
}
// Handle other potential reads or throw an error for unexpected paths
throw new Error(`Unexpected readFileSync call in test: ${filePath}`);
});
mockExistsSync.mockReturnValue(true); // Default to file existing
};
// Set up module before tests
@@ -253,10 +312,9 @@ describe('readConfig', () => {
// --- writeConfig Tests ---
describe('writeConfig', () => {
test('should write valid config to file', () => {
mockExistsSync.mockReturnValue(true);
const success = configManager.writeConfig(
VALID_CUSTOM_CONFIG,
MOCK_PROJECT_ROOT
MOCK_CONFIG_PATH
);
expect(success).toBe(true);
expect(mockExistsSync).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
@@ -265,34 +323,29 @@ describe('writeConfig', () => {
JSON.stringify(VALID_CUSTOM_CONFIG, null, 2),
'utf-8'
);
expect(console.error).not.toHaveBeenCalled();
});
test('should return false and log error if write fails', () => {
mockExistsSync.mockReturnValue(true);
const writeError = new Error('Disk full');
mockWriteFileSync.mockImplementation(() => {
throw writeError;
throw new Error('Disk full');
});
const success = configManager.writeConfig(
VALID_CUSTOM_CONFIG,
MOCK_PROJECT_ROOT
MOCK_CONFIG_PATH
);
expect(success).toBe(false);
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining(
'Error writing to /mock/project/.taskmasterconfig: Disk full.'
`Error writing configuration to ${MOCK_CONFIG_PATH}: Disk full`
)
);
});
test('should return false if config file does not exist', () => {
mockExistsSync.mockReturnValue(false);
const success = configManager.writeConfig(
VALID_CUSTOM_CONFIG,
MOCK_PROJECT_ROOT
);
const success = configManager.writeConfig(VALID_CUSTOM_CONFIG);
expect(success).toBe(false);
expect(mockWriteFileSync).not.toHaveBeenCalled();