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:
@@ -11,6 +11,7 @@ import fs from 'fs';
|
||||
import https from 'https';
|
||||
import inquirer from 'inquirer';
|
||||
import ora from 'ora';
|
||||
import Table from 'cli-table3';
|
||||
|
||||
import { CONFIG, log, readJSON, writeJSON } from './utils.js';
|
||||
import {
|
||||
@@ -40,6 +41,22 @@ import {
|
||||
fixDependenciesCommand
|
||||
} from './dependency-manager.js';
|
||||
|
||||
import {
|
||||
getMainModelId,
|
||||
getResearchModelId,
|
||||
getFallbackModelId,
|
||||
setMainModel,
|
||||
setResearchModel,
|
||||
setFallbackModel,
|
||||
getAvailableModels,
|
||||
VALID_PROVIDERS,
|
||||
getMainProvider,
|
||||
getResearchProvider,
|
||||
getFallbackProvider,
|
||||
hasApiKeyForProvider,
|
||||
getMcpApiKeyStatus
|
||||
} from './config-manager.js';
|
||||
|
||||
import {
|
||||
displayBanner,
|
||||
displayHelp,
|
||||
@@ -1548,7 +1565,527 @@ function registerCommands(programInstance) {
|
||||
}
|
||||
});
|
||||
|
||||
// Add more commands as needed...
|
||||
// models command
|
||||
programInstance
|
||||
.command('models')
|
||||
.description('Manage AI model configurations')
|
||||
.option(
|
||||
'--set-main <model_id>',
|
||||
'Set the primary model for task generation/updates'
|
||||
)
|
||||
.option(
|
||||
'--set-research <model_id>',
|
||||
'Set the model for research-backed operations'
|
||||
)
|
||||
.option(
|
||||
'--set-fallback <model_id>',
|
||||
'Set the model to use if the primary fails'
|
||||
)
|
||||
.option('--setup', 'Run interactive setup to configure models')
|
||||
.action(async (options) => {
|
||||
let modelSetAction = false; // Track if any set action was performed
|
||||
const availableModels = getAvailableModels(); // Get available models once
|
||||
|
||||
// Helper to find provider for a given model ID
|
||||
const findProvider = (modelId) => {
|
||||
const modelInfo = availableModels.find((m) => m.id === modelId);
|
||||
return modelInfo?.provider;
|
||||
};
|
||||
|
||||
try {
|
||||
if (options.setMain) {
|
||||
const modelId = options.setMain;
|
||||
if (typeof modelId !== 'string' || modelId.trim() === '') {
|
||||
console.error(
|
||||
chalk.red('Error: --set-main flag requires a valid model ID.')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const provider = findProvider(modelId);
|
||||
if (!provider) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: Model ID "${modelId}" not found in available models.`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (setMainModel(provider, modelId)) {
|
||||
// Call specific setter
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Main model set to: ${modelId} (Provider: ${provider})`
|
||||
)
|
||||
);
|
||||
modelSetAction = true;
|
||||
} else {
|
||||
console.error(chalk.red(`Failed to set main model.`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.setResearch) {
|
||||
const modelId = options.setResearch;
|
||||
if (typeof modelId !== 'string' || modelId.trim() === '') {
|
||||
console.error(
|
||||
chalk.red('Error: --set-research flag requires a valid model ID.')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const provider = findProvider(modelId);
|
||||
if (!provider) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: Model ID "${modelId}" not found in available models.`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (setResearchModel(provider, modelId)) {
|
||||
// Call specific setter
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Research model set to: ${modelId} (Provider: ${provider})`
|
||||
)
|
||||
);
|
||||
modelSetAction = true;
|
||||
} else {
|
||||
console.error(chalk.red(`Failed to set research model.`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.setFallback) {
|
||||
const modelId = options.setFallback;
|
||||
if (typeof modelId !== 'string' || modelId.trim() === '') {
|
||||
console.error(
|
||||
chalk.red('Error: --set-fallback flag requires a valid model ID.')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const provider = findProvider(modelId);
|
||||
if (!provider) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: Model ID "${modelId}" not found in available models.`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (setFallbackModel(provider, modelId)) {
|
||||
// Call specific setter
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Fallback model set to: ${modelId} (Provider: ${provider})`
|
||||
)
|
||||
);
|
||||
modelSetAction = true;
|
||||
} else {
|
||||
console.error(chalk.red(`Failed to set fallback model.`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle interactive setup first
|
||||
if (options.setup) {
|
||||
console.log(chalk.cyan.bold('\nInteractive Model Setup:'));
|
||||
|
||||
// Filter out placeholder models for selection
|
||||
const selectableModels = availableModels
|
||||
.filter(
|
||||
(model) => !(model.id.startsWith('[') && model.id.endsWith(']'))
|
||||
)
|
||||
.map((model) => ({
|
||||
name: `${model.provider} / ${model.id}`,
|
||||
value: { provider: model.provider, id: model.id }
|
||||
}));
|
||||
|
||||
if (selectableModels.length === 0) {
|
||||
console.error(
|
||||
chalk.red('Error: No selectable models found in configuration.')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'mainModel',
|
||||
message: 'Select the main model for generation/updates:',
|
||||
choices: selectableModels,
|
||||
default: selectableModels.findIndex(
|
||||
(m) => m.value.id === getMainModelId()
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'researchModel',
|
||||
message: 'Select the research model:',
|
||||
// Filter choices to only include models allowed for research
|
||||
choices: selectableModels.filter((modelChoice) => {
|
||||
// Need to find the original model data to check allowed_roles
|
||||
const originalModel = availableModels.find(
|
||||
(m) => m.id === modelChoice.value.id
|
||||
);
|
||||
return originalModel?.allowed_roles?.includes('research');
|
||||
}),
|
||||
default: selectableModels.findIndex(
|
||||
(m) => m.value.id === getResearchModelId()
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'fallbackModel',
|
||||
message: 'Select the fallback model (optional):',
|
||||
choices: [
|
||||
{ name: 'None (disable fallback)', value: null },
|
||||
new inquirer.Separator(),
|
||||
...selectableModels
|
||||
],
|
||||
default:
|
||||
selectableModels.findIndex(
|
||||
(m) => m.value.id === getFallbackModelId()
|
||||
) + 2 // Adjust for separator and None
|
||||
}
|
||||
]);
|
||||
|
||||
let setupSuccess = true;
|
||||
|
||||
// Set Main Model
|
||||
if (answers.mainModel) {
|
||||
if (
|
||||
!setMainModel(answers.mainModel.provider, answers.mainModel.id)
|
||||
) {
|
||||
console.error(chalk.red('Failed to set main model.'));
|
||||
setupSuccess = false;
|
||||
} else {
|
||||
// Success message printed by setMainModel
|
||||
}
|
||||
}
|
||||
|
||||
// Set Research Model
|
||||
if (answers.researchModel) {
|
||||
if (
|
||||
!setResearchModel(
|
||||
answers.researchModel.provider,
|
||||
answers.researchModel.id
|
||||
)
|
||||
) {
|
||||
console.error(chalk.red('Failed to set research model.'));
|
||||
setupSuccess = false;
|
||||
} else {
|
||||
// Success message printed by setResearchModel
|
||||
}
|
||||
}
|
||||
|
||||
// Set Fallback Model
|
||||
if (answers.fallbackModel) {
|
||||
if (
|
||||
!setFallbackModel(
|
||||
answers.fallbackModel.provider,
|
||||
answers.fallbackModel.id
|
||||
)
|
||||
) {
|
||||
console.error(chalk.red('Failed to set fallback model.'));
|
||||
setupSuccess = false;
|
||||
} else {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Fallback model set to: ${answers.fallbackModel.provider} / ${answers.fallbackModel.id}`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// User selected None - attempt to remove fallback from config
|
||||
const config = readConfig();
|
||||
if (config.models.fallback) {
|
||||
delete config.models.fallback;
|
||||
if (!writeConfig(config)) {
|
||||
console.error(
|
||||
chalk.red('Failed to remove fallback model configuration.')
|
||||
);
|
||||
setupSuccess = false;
|
||||
} else {
|
||||
console.log(chalk.green('Fallback model disabled.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (setupSuccess) {
|
||||
console.log(chalk.green.bold('\nModel setup complete!'));
|
||||
}
|
||||
return; // Exit after setup
|
||||
}
|
||||
|
||||
// If no set flags were used and not in setup mode, list the models
|
||||
if (!modelSetAction && !options.setup) {
|
||||
// Fetch current settings
|
||||
const mainProvider = getMainProvider();
|
||||
const mainModelId = getMainModelId();
|
||||
const researchProvider = getResearchProvider();
|
||||
const researchModelId = getResearchModelId();
|
||||
const fallbackProvider = getFallbackProvider(); // May be undefined
|
||||
const fallbackModelId = getFallbackModelId(); // May be undefined
|
||||
|
||||
// Check API keys for both CLI (.env) and MCP (mcp.json)
|
||||
const mainCliKeyOk = hasApiKeyForProvider(mainProvider);
|
||||
const mainMcpKeyOk = getMcpApiKeyStatus(mainProvider);
|
||||
const researchCliKeyOk = hasApiKeyForProvider(researchProvider);
|
||||
const researchMcpKeyOk = getMcpApiKeyStatus(researchProvider);
|
||||
const fallbackCliKeyOk = fallbackProvider
|
||||
? hasApiKeyForProvider(fallbackProvider)
|
||||
: true; // No key needed if no fallback is set
|
||||
const fallbackMcpKeyOk = fallbackProvider
|
||||
? getMcpApiKeyStatus(fallbackProvider)
|
||||
: true; // No key needed if no fallback is set
|
||||
|
||||
// --- Generate Warning Messages ---
|
||||
const warnings = [];
|
||||
if (!mainCliKeyOk || !mainMcpKeyOk) {
|
||||
warnings.push(
|
||||
`Main model (${mainProvider}): API key missing for ${!mainCliKeyOk ? 'CLI (.env)' : ''}${!mainCliKeyOk && !mainMcpKeyOk ? ' / ' : ''}${!mainMcpKeyOk ? 'MCP (.cursor/mcp.json)' : ''}`
|
||||
);
|
||||
}
|
||||
if (!researchCliKeyOk || !researchMcpKeyOk) {
|
||||
warnings.push(
|
||||
`Research model (${researchProvider}): API key missing for ${!researchCliKeyOk ? 'CLI (.env)' : ''}${!researchCliKeyOk && !researchMcpKeyOk ? ' / ' : ''}${!researchMcpKeyOk ? 'MCP (.cursor/mcp.json)' : ''}`
|
||||
);
|
||||
}
|
||||
if (fallbackProvider && (!fallbackCliKeyOk || !fallbackMcpKeyOk)) {
|
||||
warnings.push(
|
||||
`Fallback model (${fallbackProvider}): API key missing for ${!fallbackCliKeyOk ? 'CLI (.env)' : ''}${!fallbackCliKeyOk && !fallbackMcpKeyOk ? ' / ' : ''}${!fallbackMcpKeyOk ? 'MCP (.cursor/mcp.json)' : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
// --- Display Warning Banner (if any) ---
|
||||
if (warnings.length > 0) {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.red.bold('API Key Warnings:') +
|
||||
'\n\n' +
|
||||
warnings.join('\n'),
|
||||
{
|
||||
padding: 1,
|
||||
margin: { top: 1, bottom: 1 },
|
||||
borderColor: 'red',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// --- Active Configuration Section ---
|
||||
console.log(chalk.cyan.bold('\nActive Model Configuration:'));
|
||||
const activeTable = new Table({
|
||||
head: [
|
||||
'Role',
|
||||
'Provider',
|
||||
'Model ID',
|
||||
'SWE Score', // Update column name
|
||||
'Cost ($/1M tkns)', // Add Cost column
|
||||
'API Key Status'
|
||||
].map((h) => chalk.cyan.bold(h)),
|
||||
colWidths: [10, 14, 30, 18, 20, 28], // Adjust widths for stars
|
||||
style: { head: ['cyan', 'bold'] }
|
||||
});
|
||||
|
||||
const allAvailableModels = getAvailableModels(); // Get all models once for lookup
|
||||
|
||||
// --- Calculate Tertile Thresholds for SWE Scores ---
|
||||
const validScores = allAvailableModels
|
||||
.map((m) => m.swe_score)
|
||||
.filter((s) => s !== null && s !== undefined && s > 0);
|
||||
const sortedScores = [...validScores].sort((a, b) => b - a); // Sort descending
|
||||
const n = sortedScores.length;
|
||||
let minScore3Stars = -Infinity;
|
||||
let minScore2Stars = -Infinity;
|
||||
if (n > 0) {
|
||||
const topThirdIndex = Math.max(0, Math.floor(n / 3) - 1);
|
||||
const midThirdIndex = Math.max(0, Math.floor((2 * n) / 3) - 1);
|
||||
minScore3Stars = sortedScores[topThirdIndex];
|
||||
minScore2Stars = sortedScores[midThirdIndex];
|
||||
}
|
||||
|
||||
// Helper to find the full model object
|
||||
const findModelData = (modelId) => {
|
||||
return allAvailableModels.find((m) => m.id === modelId);
|
||||
};
|
||||
|
||||
// --- Helper to format SWE score and add tertile stars ---
|
||||
const formatSweScoreWithTertileStars = (score) => {
|
||||
if (score === null || score === undefined || score <= 0)
|
||||
return 'N/A'; // Handle non-positive scores
|
||||
|
||||
const formattedPercentage = `${(score * 100).toFixed(1)}%`;
|
||||
let stars = '';
|
||||
|
||||
if (n === 0) {
|
||||
// No valid scores to compare against
|
||||
stars = chalk.gray('☆☆☆');
|
||||
} else if (score >= minScore3Stars) {
|
||||
stars = chalk.yellow('★★★'); // Top Third
|
||||
} else if (score >= minScore2Stars) {
|
||||
stars = chalk.yellow('★★') + chalk.gray('☆'); // Middle Third
|
||||
} else {
|
||||
stars = chalk.yellow('★') + chalk.gray('☆☆'); // Bottom Third (but > 0)
|
||||
}
|
||||
|
||||
return `${formattedPercentage} ${stars}`;
|
||||
};
|
||||
|
||||
// Helper to format cost
|
||||
const formatCost = (costObj) => {
|
||||
if (!costObj) return 'N/A';
|
||||
|
||||
const formatSingleCost = (costValue) => {
|
||||
if (costValue === null || costValue === undefined) return 'N/A';
|
||||
// Check if the number is an integer
|
||||
const isInteger = Number.isInteger(costValue);
|
||||
return `$${costValue.toFixed(isInteger ? 0 : 2)}`;
|
||||
};
|
||||
|
||||
const inputCost = formatSingleCost(costObj.input);
|
||||
const outputCost = formatSingleCost(costObj.output);
|
||||
|
||||
return `${inputCost} in, ${outputCost} out`; // Use cleaner separator
|
||||
};
|
||||
|
||||
const getCombinedStatus = (cliOk, mcpOk) => {
|
||||
const cliSymbol = cliOk ? chalk.green('✓') : chalk.red('✗');
|
||||
const mcpSymbol = mcpOk ? chalk.green('✓') : chalk.red('✗');
|
||||
|
||||
if (cliOk && mcpOk) {
|
||||
// Both symbols green, default text color
|
||||
return `${cliSymbol} CLI & ${mcpSymbol} MCP OK`;
|
||||
} else if (cliOk && !mcpOk) {
|
||||
// Symbols colored individually, default text color
|
||||
return `${cliSymbol} CLI OK / ${mcpSymbol} MCP Missing`;
|
||||
} else if (!cliOk && mcpOk) {
|
||||
// Symbols colored individually, default text color
|
||||
return `${cliSymbol} CLI Missing / ${mcpSymbol} MCP OK`;
|
||||
} else {
|
||||
// Both symbols gray, apply overall gray to text as well
|
||||
return chalk.gray(`${cliSymbol} CLI & MCP Both Missing`);
|
||||
}
|
||||
};
|
||||
|
||||
const mainModelData = findModelData(mainModelId);
|
||||
const researchModelData = findModelData(researchModelId);
|
||||
const fallbackModelData = findModelData(fallbackModelId);
|
||||
|
||||
activeTable.push([
|
||||
chalk.white('Main'),
|
||||
mainProvider,
|
||||
mainModelId,
|
||||
formatSweScoreWithTertileStars(mainModelData?.swe_score), // Use tertile formatter
|
||||
formatCost(mainModelData?.cost_per_1m_tokens),
|
||||
getCombinedStatus(mainCliKeyOk, mainMcpKeyOk)
|
||||
]);
|
||||
activeTable.push([
|
||||
chalk.white('Research'),
|
||||
researchProvider,
|
||||
researchModelId,
|
||||
formatSweScoreWithTertileStars(researchModelData?.swe_score), // Use tertile formatter
|
||||
formatCost(researchModelData?.cost_per_1m_tokens),
|
||||
getCombinedStatus(researchCliKeyOk, researchMcpKeyOk)
|
||||
]);
|
||||
|
||||
if (fallbackProvider && fallbackModelId) {
|
||||
activeTable.push([
|
||||
chalk.white('Fallback'),
|
||||
fallbackProvider,
|
||||
fallbackModelId,
|
||||
formatSweScoreWithTertileStars(fallbackModelData?.swe_score), // Use tertile formatter
|
||||
formatCost(fallbackModelData?.cost_per_1m_tokens),
|
||||
getCombinedStatus(fallbackCliKeyOk, fallbackMcpKeyOk)
|
||||
]);
|
||||
}
|
||||
console.log(activeTable.toString());
|
||||
|
||||
// --- Available Models Section ---
|
||||
// const availableModels = getAvailableModels(); // Already fetched
|
||||
if (!allAvailableModels || allAvailableModels.length === 0) {
|
||||
console.log(chalk.yellow('\nNo available models defined.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out placeholders and active models for the available list
|
||||
const activeIds = [
|
||||
mainModelId,
|
||||
researchModelId,
|
||||
fallbackModelId
|
||||
].filter(Boolean);
|
||||
const filteredAvailable = allAvailableModels.filter(
|
||||
(model) =>
|
||||
!(model.id.startsWith('[') && model.id.endsWith(']')) &&
|
||||
!activeIds.includes(model.id)
|
||||
);
|
||||
|
||||
if (filteredAvailable.length > 0) {
|
||||
console.log(chalk.cyan.bold('\nOther Available Models:'));
|
||||
const availableTable = new Table({
|
||||
head: [
|
||||
'Provider',
|
||||
'Model ID',
|
||||
'SWE Score', // Update column name
|
||||
'Cost ($/1M tkns)' // Add Cost column
|
||||
].map((h) => chalk.cyan.bold(h)),
|
||||
colWidths: [15, 40, 18, 25], // Adjust widths for stars
|
||||
style: { head: ['cyan', 'bold'] }
|
||||
});
|
||||
|
||||
filteredAvailable.forEach((model) => {
|
||||
availableTable.push([
|
||||
model.provider || 'N/A',
|
||||
model.id,
|
||||
formatSweScoreWithTertileStars(model.swe_score), // Use tertile formatter
|
||||
formatCost(model.cost_per_1m_tokens)
|
||||
]);
|
||||
});
|
||||
console.log(availableTable.toString());
|
||||
} else {
|
||||
console.log(
|
||||
chalk.gray('\n(All available models are currently configured)')
|
||||
);
|
||||
}
|
||||
|
||||
// --- Suggested Actions Section ---
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold('Next Steps:') +
|
||||
'\n' +
|
||||
chalk.cyan(
|
||||
`1. Set main model: ${chalk.yellow('task-master models --set-main <model_id>')}`
|
||||
) +
|
||||
'\n' +
|
||||
chalk.cyan(
|
||||
`2. Set research model: ${chalk.yellow('task-master models --set-research <model_id>')}`
|
||||
) +
|
||||
'\n' +
|
||||
chalk.cyan(
|
||||
`3. Set fallback model: ${chalk.yellow('task-master models --set-fallback <model_id>')}`
|
||||
) +
|
||||
'\n' +
|
||||
chalk.cyan(
|
||||
`4. Run interactive setup: ${chalk.yellow('task-master models --setup')}`
|
||||
),
|
||||
{
|
||||
padding: 1,
|
||||
borderColor: 'yellow',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
log(`Error processing models command: ${error.message}`, 'error');
|
||||
if (error.stack && CONFIG.debug) {
|
||||
log(error.stack, 'debug');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
return programInstance;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Calculate __dirname in ESM
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Load supported models from JSON file using the calculated __dirname
|
||||
let MODEL_MAP;
|
||||
try {
|
||||
const supportedModelsRaw = fs.readFileSync(
|
||||
path.join(__dirname, 'supported-models.json'),
|
||||
'utf-8'
|
||||
);
|
||||
MODEL_MAP = JSON.parse(supportedModelsRaw);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
'FATAL ERROR: Could not load supported-models.json. Please ensure the file exists and is valid JSON.'
|
||||
),
|
||||
error
|
||||
);
|
||||
MODEL_MAP = {}; // Default to empty map on error to avoid crashing, though functionality will be limited
|
||||
process.exit(1); // Exit if models can't be loaded
|
||||
}
|
||||
|
||||
const CONFIG_FILE_NAME = '.taskmasterconfig';
|
||||
|
||||
@@ -21,17 +45,6 @@ const VALID_PROVIDERS = [
|
||||
'grok'
|
||||
];
|
||||
|
||||
// Optional: Define known models per provider primarily for informational display or non-blocking warnings
|
||||
const MODEL_MAP = {
|
||||
anthropic: ['claude-3.5-sonnet-20240620', 'claude-3-7-sonnet-20250219'],
|
||||
openai: ['gpt-4o', 'gpt-4-turbo'],
|
||||
google: ['gemini-2.5-pro-latest', 'gemini-1.5-flash-latest'],
|
||||
perplexity: ['sonar-pro', 'sonar-mini'],
|
||||
ollama: [], // Users configure specific Ollama models locally
|
||||
openrouter: [], // Users specify model string
|
||||
grok: [] // Specify Grok model if known
|
||||
};
|
||||
|
||||
let projectRoot = null;
|
||||
|
||||
function findProjectRoot() {
|
||||
@@ -106,11 +119,16 @@ function readConfig(explicitRoot = null) {
|
||||
modelId:
|
||||
parsedConfig?.models?.research?.modelId ??
|
||||
defaults.models.research.modelId
|
||||
},
|
||||
// Add merge logic for the fallback model
|
||||
fallback: {
|
||||
provider: parsedConfig?.models?.fallback?.provider,
|
||||
modelId: parsedConfig?.models?.fallback?.modelId
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Validate loaded provider (no longer split by main/research)
|
||||
// Validate loaded providers (main, research, and fallback if it exists)
|
||||
if (!validateProvider(config.models.main.provider)) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
@@ -139,6 +157,21 @@ function readConfig(explicitRoot = null) {
|
||||
// Optional: Add warning for model combination if desired, but don't block
|
||||
// else if (!validateProviderModelCombination(config.models.research.provider, config.models.research.modelId)) { ... }
|
||||
|
||||
// Add validation for fallback provider if it exists
|
||||
if (
|
||||
config.models.fallback &&
|
||||
config.models.fallback.provider &&
|
||||
!validateProvider(config.models.fallback.provider)
|
||||
) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`Warning: Invalid fallback provider "${config.models.fallback.provider}" in ${CONFIG_FILE_NAME}. Fallback model will be ignored.`
|
||||
)
|
||||
);
|
||||
// Unlike main/research, we don't set a default fallback, just ignore it
|
||||
delete config.models.fallback;
|
||||
}
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
@@ -177,7 +210,8 @@ function validateProviderModelCombination(providerName, modelId) {
|
||||
// If the provider is known, check if the model is in its list OR if the list is empty (meaning accept any)
|
||||
return (
|
||||
MODEL_MAP[providerName].length === 0 ||
|
||||
MODEL_MAP[providerName].includes(modelId)
|
||||
// Use .some() to check the 'id' property of objects in the array
|
||||
MODEL_MAP[providerName].some((modelObj) => modelObj.id === modelId)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,6 +255,26 @@ function getResearchModelId(explicitRoot = null) {
|
||||
return config.models.research.modelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently configured fallback AI provider.
|
||||
* @param {string|null} explicitRoot - Optional explicit path to the project root.
|
||||
* @returns {string|undefined} The name of the fallback provider, or undefined if not set.
|
||||
*/
|
||||
function getFallbackProvider(explicitRoot = null) {
|
||||
const config = readConfig(explicitRoot);
|
||||
return config.models?.fallback?.provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently configured fallback AI model ID.
|
||||
* @param {string|null} explicitRoot - Optional explicit path to the project root.
|
||||
* @returns {string|undefined} The ID of the fallback model, or undefined if not set.
|
||||
*/
|
||||
function getFallbackModelId(explicitRoot = null) {
|
||||
const config = readConfig(explicitRoot);
|
||||
return config.models?.fallback?.modelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the main AI model (provider and modelId) in the configuration file.
|
||||
* @param {string} providerName The name of the provider to set.
|
||||
@@ -229,6 +283,7 @@ function getResearchModelId(explicitRoot = null) {
|
||||
* @returns {boolean} True if successful, false otherwise.
|
||||
*/
|
||||
function setMainModel(providerName, modelId, explicitRoot = null) {
|
||||
// --- 1. Validate Provider First ---
|
||||
if (!validateProvider(providerName)) {
|
||||
console.error(
|
||||
chalk.red(`Error: "${providerName}" is not a valid provider.`)
|
||||
@@ -238,6 +293,35 @@ function setMainModel(providerName, modelId, explicitRoot = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- 2. Validate Role Second ---
|
||||
const allModels = getAvailableModels(); // Get all models to check roles
|
||||
const modelData = allModels.find(
|
||||
(m) => m.id === modelId && m.provider === providerName
|
||||
);
|
||||
|
||||
if (
|
||||
!modelData ||
|
||||
!modelData.allowed_roles ||
|
||||
!modelData.allowed_roles.includes('main')
|
||||
) {
|
||||
console.error(
|
||||
chalk.red(`Error: Model "${modelId}" is not allowed for the 'main' role.`)
|
||||
);
|
||||
// Try to suggest valid models for the role
|
||||
const allowedMainModels = allModels
|
||||
.filter((m) => m.allowed_roles?.includes('main'))
|
||||
.map((m) => ` - ${m.provider} / ${m.id}`)
|
||||
.join('\n');
|
||||
if (allowedMainModels) {
|
||||
console.log(
|
||||
chalk.yellow('\nAllowed models for main role:\n' + allowedMainModels)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- 3. Validate Model Combination (Optional Warning) ---
|
||||
if (!validateProviderModelCombination(providerName, modelId)) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
@@ -246,7 +330,7 @@ function setMainModel(providerName, modelId, explicitRoot = null) {
|
||||
);
|
||||
}
|
||||
|
||||
// Pass explicitRoot down
|
||||
// --- Proceed with setting ---
|
||||
const config = readConfig(explicitRoot);
|
||||
config.models.main = { provider: providerName, modelId: modelId };
|
||||
// Pass explicitRoot down
|
||||
@@ -268,6 +352,7 @@ function setMainModel(providerName, modelId, explicitRoot = null) {
|
||||
* @returns {boolean} True if successful, false otherwise.
|
||||
*/
|
||||
function setResearchModel(providerName, modelId, explicitRoot = null) {
|
||||
// --- 1. Validate Provider First ---
|
||||
if (!validateProvider(providerName)) {
|
||||
console.error(
|
||||
chalk.red(`Error: "${providerName}" is not a valid provider.`)
|
||||
@@ -277,6 +362,39 @@ function setResearchModel(providerName, modelId, explicitRoot = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- 2. Validate Role Second ---
|
||||
const allModels = getAvailableModels(); // Get all models to check roles
|
||||
const modelData = allModels.find(
|
||||
(m) => m.id === modelId && m.provider === providerName
|
||||
);
|
||||
|
||||
if (
|
||||
!modelData ||
|
||||
!modelData.allowed_roles ||
|
||||
!modelData.allowed_roles.includes('research')
|
||||
) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: Model "${modelId}" is not allowed for the 'research' role.`
|
||||
)
|
||||
);
|
||||
// Try to suggest valid models for the role
|
||||
const allowedResearchModels = allModels
|
||||
.filter((m) => m.allowed_roles?.includes('research'))
|
||||
.map((m) => ` - ${m.provider} / ${m.id}`)
|
||||
.join('\n');
|
||||
if (allowedResearchModels) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\nAllowed models for research role:\n' + allowedResearchModels
|
||||
)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- 3. Validate Model Combination (Optional Warning) ---
|
||||
if (!validateProviderModelCombination(providerName, modelId)) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
@@ -284,6 +402,8 @@ function setResearchModel(providerName, modelId, explicitRoot = null) {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// --- 4. Specific Research Warning (Optional) ---
|
||||
if (
|
||||
providerName === 'anthropic' ||
|
||||
(providerName === 'openai' && modelId.includes('3.5'))
|
||||
@@ -295,7 +415,7 @@ function setResearchModel(providerName, modelId, explicitRoot = null) {
|
||||
);
|
||||
}
|
||||
|
||||
// Pass explicitRoot down
|
||||
// --- Proceed with setting ---
|
||||
const config = readConfig(explicitRoot);
|
||||
config.models.research = { provider: providerName, modelId: modelId };
|
||||
// Pass explicitRoot down
|
||||
@@ -309,37 +429,257 @@ function setResearchModel(providerName, modelId, explicitRoot = null) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the fallback AI model (provider and modelId) in the configuration file.
|
||||
* @param {string} providerName The name of the provider to set.
|
||||
* @param {string} modelId The ID of the model to set.
|
||||
* @param {string|null} explicitRoot - Optional explicit path to the project root.
|
||||
* @returns {boolean} True if successful, false otherwise.
|
||||
*/
|
||||
function setFallbackModel(providerName, modelId, explicitRoot = null) {
|
||||
// --- 1. Validate Provider First ---
|
||||
if (!validateProvider(providerName)) {
|
||||
console.error(
|
||||
chalk.red(`Error: "${providerName}" is not a valid provider.`)
|
||||
);
|
||||
console.log(
|
||||
chalk.yellow(`Available providers: ${VALID_PROVIDERS.join(', ')}`)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- 2. Validate Role Second ---
|
||||
const allModels = getAvailableModels(); // Get all models to check roles
|
||||
const modelData = allModels.find(
|
||||
(m) => m.id === modelId && m.provider === providerName
|
||||
);
|
||||
|
||||
if (
|
||||
!modelData ||
|
||||
!modelData.allowed_roles ||
|
||||
!modelData.allowed_roles.includes('fallback')
|
||||
) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: Model "${modelId}" is not allowed for the 'fallback' role.`
|
||||
)
|
||||
);
|
||||
// Try to suggest valid models for the role
|
||||
const allowedFallbackModels = allModels
|
||||
.filter((m) => m.allowed_roles?.includes('fallback'))
|
||||
.map((m) => ` - ${m.provider} / ${m.id}`)
|
||||
.join('\n');
|
||||
if (allowedFallbackModels) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\nAllowed models for fallback role:\n' + allowedFallbackModels
|
||||
)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- 3. Validate Model Combination (Optional Warning) ---
|
||||
if (!validateProviderModelCombination(providerName, modelId)) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`Warning: Model "${modelId}" is not in the known list for provider "${providerName}". Ensure it is valid.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// --- Proceed with setting ---
|
||||
const config = readConfig(explicitRoot);
|
||||
if (!config.models) {
|
||||
config.models = {}; // Ensure models object exists
|
||||
}
|
||||
// Ensure fallback object exists
|
||||
if (!config.models.fallback) {
|
||||
config.models.fallback = {};
|
||||
}
|
||||
|
||||
config.models.fallback = { provider: providerName, modelId: modelId };
|
||||
|
||||
return writeConfig(config, explicitRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of available models based on the MODEL_MAP.
|
||||
* @returns {Array<{id: string, name: string, provider: string, swe_score: number|null, cost_per_1m_tokens: {input: number|null, output: number|null}|null, allowed_roles: string[]}>}
|
||||
*/
|
||||
function getAvailableModels() {
|
||||
const available = [];
|
||||
for (const [provider, models] of Object.entries(MODEL_MAP)) {
|
||||
if (models.length > 0) {
|
||||
models.forEach((modelObj) => {
|
||||
// Basic name generation - can be improved
|
||||
const modelId = modelObj.id;
|
||||
const sweScore = modelObj.swe_score;
|
||||
const cost = modelObj.cost_per_1m_tokens;
|
||||
const allowedRoles = modelObj.allowed_roles || ['main', 'fallback'];
|
||||
const nameParts = modelId
|
||||
.split('-')
|
||||
.map((p) => p.charAt(0).toUpperCase() + p.slice(1));
|
||||
// Handle specific known names better if needed
|
||||
let name = nameParts.join(' ');
|
||||
if (modelId === 'claude-3.5-sonnet-20240620')
|
||||
name = 'Claude 3.5 Sonnet';
|
||||
if (modelId === 'claude-3-7-sonnet-20250219')
|
||||
name = 'Claude 3.7 Sonnet';
|
||||
if (modelId === 'gpt-4o') name = 'GPT-4o';
|
||||
if (modelId === 'gpt-4-turbo') name = 'GPT-4 Turbo';
|
||||
if (modelId === 'sonar-pro') name = 'Perplexity Sonar Pro';
|
||||
if (modelId === 'sonar-mini') name = 'Perplexity Sonar Mini';
|
||||
|
||||
available.push({
|
||||
id: modelId,
|
||||
name: name,
|
||||
provider: provider,
|
||||
swe_score: sweScore,
|
||||
cost_per_1m_tokens: cost,
|
||||
allowed_roles: allowedRoles
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// For providers with empty lists (like ollama), maybe add a placeholder or skip
|
||||
available.push({
|
||||
id: `[${provider}-any]`,
|
||||
name: `Any (${provider})`,
|
||||
provider: provider
|
||||
});
|
||||
}
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the configuration object to the file.
|
||||
* @param {Object} config The configuration object to write.
|
||||
* @param {string|null} explicitRoot - Optional explicit path to the project root.
|
||||
* @returns {boolean} True if successful, false otherwise.
|
||||
*/
|
||||
function writeConfig(config, explicitRoot = null) {
|
||||
// Determine the root path to use
|
||||
const rootToUse = explicitRoot || findProjectRoot();
|
||||
|
||||
if (!rootToUse) {
|
||||
const rootPath = explicitRoot || findProjectRoot();
|
||||
if (!rootPath) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
'Error: Could not determine project root to write configuration.'
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const configPath = path.join(rootToUse, CONFIG_FILE_NAME);
|
||||
|
||||
// Check if file exists, as expected by tests
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: ${CONFIG_FILE_NAME} does not exist. Create it first or initialize project.`
|
||||
'Error: Could not determine project root. Configuration not saved.'
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Ensure we don't double-join if explicitRoot already contains the filename
|
||||
const configPath =
|
||||
path.basename(rootPath) === CONFIG_FILE_NAME
|
||||
? rootPath
|
||||
: path.join(rootPath, CONFIG_FILE_NAME);
|
||||
|
||||
try {
|
||||
// Added 'utf-8' encoding
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red(`Error writing to ${configPath}: ${error.message}.`)
|
||||
chalk.red(
|
||||
`Error writing configuration to ${configPath}: ${error.message}`
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the required API key environment variable is set for a given provider.
|
||||
* @param {string} providerName The name of the provider.
|
||||
* @returns {boolean} True if the API key environment variable exists and is non-empty, false otherwise.
|
||||
*/
|
||||
function hasApiKeyForProvider(providerName) {
|
||||
switch (providerName) {
|
||||
case 'anthropic':
|
||||
return !!process.env.ANTHROPIC_API_KEY;
|
||||
case 'openai':
|
||||
case 'openrouter': // OpenRouter uses OpenAI-compatible key
|
||||
return !!process.env.OPENAI_API_KEY;
|
||||
case 'google':
|
||||
return !!process.env.GOOGLE_API_KEY;
|
||||
case 'perplexity':
|
||||
return !!process.env.PERPLEXITY_API_KEY;
|
||||
case 'grok':
|
||||
case 'xai': // Added alias for Grok
|
||||
return !!process.env.GROK_API_KEY;
|
||||
case 'ollama':
|
||||
return true; // Ollama runs locally, no cloud API key needed
|
||||
default:
|
||||
return false; // Unknown provider cannot have a key checked
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the API key status within .cursor/mcp.json for a given provider.
|
||||
* Reads the mcp.json file, finds the taskmaster-ai server config, and checks the relevant env var.
|
||||
* @param {string} providerName The name of the provider.
|
||||
* @returns {boolean} True if the key exists and is not a placeholder, false otherwise.
|
||||
*/
|
||||
function getMcpApiKeyStatus(providerName) {
|
||||
const rootDir = findProjectRoot(); // Use existing root finding
|
||||
if (!rootDir) {
|
||||
console.warn(
|
||||
chalk.yellow('Warning: Could not find project root to check mcp.json.')
|
||||
);
|
||||
return false; // Cannot check without root
|
||||
}
|
||||
const mcpConfigPath = path.join(rootDir, '.cursor', 'mcp.json');
|
||||
|
||||
if (!fs.existsSync(mcpConfigPath)) {
|
||||
// console.warn(chalk.yellow('Warning: .cursor/mcp.json not found.'));
|
||||
return false; // File doesn't exist
|
||||
}
|
||||
|
||||
try {
|
||||
const mcpConfigRaw = fs.readFileSync(mcpConfigPath, 'utf-8');
|
||||
const mcpConfig = JSON.parse(mcpConfigRaw);
|
||||
|
||||
const mcpEnv = mcpConfig?.mcpServers?.['taskmaster-ai']?.env;
|
||||
if (!mcpEnv) {
|
||||
// console.warn(chalk.yellow('Warning: Could not find taskmaster-ai env in mcp.json.'));
|
||||
return false; // Structure missing
|
||||
}
|
||||
|
||||
let apiKeyToCheck = null;
|
||||
let placeholderValue = null;
|
||||
|
||||
switch (providerName) {
|
||||
case 'anthropic':
|
||||
apiKeyToCheck = mcpEnv.ANTHROPIC_API_KEY;
|
||||
placeholderValue = 'YOUR_ANTHROPIC_API_KEY_HERE';
|
||||
break;
|
||||
case 'openai':
|
||||
case 'openrouter':
|
||||
apiKeyToCheck = mcpEnv.OPENAI_API_KEY;
|
||||
placeholderValue = 'YOUR_OPENAI_API_KEY_HERE'; // Assuming placeholder matches OPENAI
|
||||
break;
|
||||
case 'google':
|
||||
apiKeyToCheck = mcpEnv.GOOGLE_API_KEY;
|
||||
placeholderValue = 'YOUR_GOOGLE_API_KEY_HERE';
|
||||
break;
|
||||
case 'perplexity':
|
||||
apiKeyToCheck = mcpEnv.PERPLEXITY_API_KEY;
|
||||
placeholderValue = 'YOUR_PERPLEXITY_API_KEY_HERE';
|
||||
break;
|
||||
case 'grok':
|
||||
case 'xai':
|
||||
apiKeyToCheck = mcpEnv.GROK_API_KEY;
|
||||
placeholderValue = 'YOUR_GROK_API_KEY_HERE';
|
||||
break;
|
||||
case 'ollama':
|
||||
return true; // No key needed
|
||||
default:
|
||||
return false; // Unknown provider
|
||||
}
|
||||
|
||||
return !!apiKeyToCheck && apiKeyToCheck !== placeholderValue;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red(`Error reading or parsing .cursor/mcp.json: ${error.message}`)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -355,8 +695,14 @@ export {
|
||||
getMainModelId,
|
||||
getResearchProvider,
|
||||
getResearchModelId,
|
||||
getFallbackProvider,
|
||||
getFallbackModelId,
|
||||
setMainModel,
|
||||
setResearchModel,
|
||||
setFallbackModel,
|
||||
VALID_PROVIDERS,
|
||||
MODEL_MAP
|
||||
MODEL_MAP,
|
||||
getAvailableModels,
|
||||
hasApiKeyForProvider,
|
||||
getMcpApiKeyStatus
|
||||
};
|
||||
|
||||
256
scripts/modules/supported-models.json
Normal file
256
scripts/modules/supported-models.json
Normal file
@@ -0,0 +1,256 @@
|
||||
{
|
||||
"anthropic": [
|
||||
{
|
||||
"id": "claude-3.5-sonnet-20240620",
|
||||
"swe_score": 0.49,
|
||||
"cost_per_1m_tokens": { "input": 3.0, "output": 15.0 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "claude-3-7-sonnet-20250219",
|
||||
"swe_score": 0.623,
|
||||
"cost_per_1m_tokens": { "input": 3.0, "output": 15.0 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "claude-3.5-haiku-20241022",
|
||||
"swe_score": 0.406,
|
||||
"cost_per_1m_tokens": { "input": 0.8, "output": 4.0 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "claude-3-haiku-20240307",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": { "input": 0.25, "output": 1.25 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "claude-3-opus-20240229",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
}
|
||||
],
|
||||
"openai": [
|
||||
{
|
||||
"id": "gpt-4o",
|
||||
"swe_score": 0.332,
|
||||
"cost_per_1m_tokens": { "input": 5.0, "output": 15.0 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "gpt-4-turbo",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": { "input": 10.0, "output": 30.0 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "o1",
|
||||
"swe_score": 0.489,
|
||||
"cost_per_1m_tokens": { "input": 15.0, "output": 60.0 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "o3-mini",
|
||||
"swe_score": 0.493,
|
||||
"cost_per_1m_tokens": { "input": 1.1, "output": 4.4 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "o1-pro",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": { "input": 150.0, "output": 600.0 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "gpt-4.1",
|
||||
"swe_score": 0.55,
|
||||
"cost_per_1m_tokens": { "input": 2.0, "output": 8.0 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "gpt-4.5-preview",
|
||||
"swe_score": 0.38,
|
||||
"cost_per_1m_tokens": { "input": 75.0, "output": 150.0 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "gpt-4.1-mini",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": { "input": 0.4, "output": 1.6 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "gpt-4.1-nano",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": { "input": 0.1, "output": 0.4 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "gpt-3.5-turbo",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": { "input": 0.5, "output": 1.5 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
}
|
||||
],
|
||||
"google": [
|
||||
{
|
||||
"id": "gemini-2.5-pro-latest",
|
||||
"swe_score": 0.638,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "gemini-1.5-flash-latest",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.0-flash-experimental",
|
||||
"swe_score": 0.754,
|
||||
"cost_per_1m_tokens": { "input": 0.15, "output": 0.6 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.0-flash-thinking-experimental",
|
||||
"swe_score": 0.754,
|
||||
"cost_per_1m_tokens": { "input": 0.15, "output": 0.6 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.0-pro",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "gemma-3-7b",
|
||||
"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"]
|
||||
},
|
||||
{
|
||||
"id": "sonar-mini",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback", "research"]
|
||||
},
|
||||
{
|
||||
"id": "deep-research",
|
||||
"swe_score": 0.211,
|
||||
"cost_per_1m_tokens": { "input": 2.0, "output": 8.0 },
|
||||
"allowed_roles": ["main", "fallback", "research"]
|
||||
}
|
||||
],
|
||||
"ollama": [
|
||||
{
|
||||
"id": "llava",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "deepseek-coder-v2",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "dolphin3",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "olmo2-7b",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "olmo2-13b",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
}
|
||||
],
|
||||
"openrouter": [
|
||||
{
|
||||
"id": "meta-llama/llama-4-scout",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "google/gemini-2.5-pro-exp-03-25",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "openrouter/optimus-alpha",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": { "input": 30.0, "output": 60.0 },
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "openrouter/quasar-alpha",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "kimi-vl-a3b-thinking",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "qwen2.5-max",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
}
|
||||
],
|
||||
"grok": [
|
||||
{
|
||||
"id": "grok3-beta",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback", "research"]
|
||||
},
|
||||
{
|
||||
"id": "grok-3-mini",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "grok-2",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "grok-2-mini",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "grok-1.5",
|
||||
"swe_score": 0,
|
||||
"cost_per_1m_tokens": null,
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user