diff --git a/.changeset/spicy-teams-travel.md b/.changeset/spicy-teams-travel.md new file mode 100644 index 00000000..b9551e5a --- /dev/null +++ b/.changeset/spicy-teams-travel.md @@ -0,0 +1,11 @@ +--- +"task-master-ai": patch +--- + +Improve provider validation system with clean constants structure + +- **Fixed "Invalid provider hint" errors**: Resolved validation failures for Azure, Vertex, and Bedrock providers +- **Improved search UX**: Integrated search for better model discovery with real-time filtering +- **Better organization**: Moved custom provider options to bottom of model selection with clear section separators + +This change ensures all custom providers (Azure, Vertex, Bedrock, OpenRouter, Ollama) work correctly in `task-master models --setup` diff --git a/.taskmaster/config.json b/.taskmaster/config.json index 0dede04f..3bd2b3f8 100644 --- a/.taskmaster/config.json +++ b/.taskmaster/config.json @@ -1,14 +1,14 @@ { "models": { "main": { - "provider": "anthropic", - "modelId": "claude-sonnet-4-20250514", + "provider": "vertex", + "modelId": "gemini-1.5-pro-002", "maxTokens": 50000, "temperature": 0.2 }, "research": { "provider": "perplexity", - "modelId": "sonar-pro", + "modelId": "sonar", "maxTokens": 8700, "temperature": 0.1 }, @@ -20,7 +20,6 @@ } }, "global": { - "userId": "1234567890", "logLevel": "info", "debug": false, "defaultSubtasks": 5, @@ -28,6 +27,7 @@ "projectName": "Taskmaster", "ollamaBaseURL": "http://localhost:11434/api", "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", + "userId": "1234567890", "azureBaseURL": "https://your-endpoint.azure.com/", "defaultTag": "master" } diff --git a/package-lock.json b/package-lock.json index d69a7aa9..8e4678bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@ai-sdk/xai": "^1.2.15", "@anthropic-ai/sdk": "^0.39.0", "@aws-sdk/credential-providers": "^3.817.0", + "@inquirer/search": "^3.0.15", "@openrouter/ai-sdk-provider": "^0.4.5", "ai": "^4.3.10", "boxen": "^8.0.1", @@ -2696,13 +2697,13 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.9.tgz", - "integrity": "sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==", + "version": "10.1.13", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz", + "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==", "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.5", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", @@ -2822,9 +2823,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", - "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", + "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", "license": "MIT", "engines": { "node": ">=18" @@ -2946,14 +2947,14 @@ } }, "node_modules/@inquirer/search": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.11.tgz", - "integrity": "sha512-9CWQT0ikYcg6Ls3TOa7jljsD7PgjcsYEM0bYE+Gkz+uoW9u8eaJCRHJKkucpRE5+xKtaaDbrND+nPDoxzjYyew==", + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.15.tgz", + "integrity": "sha512-YBMwPxYBrADqyvP4nNItpwkBnGGglAvCLVW8u4pRmmvOsHUtCAUIMbUrLX5B3tFL1/WsLGdQ2HNzkqswMs5Uaw==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.9", - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.5", + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -2993,9 +2994,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz", - "integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index d227c474..ffdc0021 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@ai-sdk/xai": "^1.2.15", "@anthropic-ai/sdk": "^0.39.0", "@aws-sdk/credential-providers": "^3.817.0", + "@inquirer/search": "^3.0.15", "@openrouter/ai-sdk-provider": "^0.4.5", "ai": "^4.3.10", "boxen": "^8.0.1", diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index 16b0d925..5000ac78 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -11,6 +11,7 @@ import fs from 'fs'; import https from 'https'; import http from 'http'; import inquirer from 'inquirer'; +import search from '@inquirer/search'; import ora from 'ora'; // Import ora import { @@ -71,6 +72,8 @@ import { getBaseUrlForRole } from './config-manager.js'; +import { CUSTOM_PROVIDERS } from '../../src/constants/providers.js'; + import { COMPLEXITY_REPORT_FILE, PRD_FILE, @@ -306,6 +309,16 @@ async function runInteractiveSetup(projectRoot) { value: '__CUSTOM_BEDROCK__' }; + const customAzureOption = { + name: '* Custom Azure OpenAI model', // Add Azure custom option + value: '__CUSTOM_AZURE__' + }; + + const customVertexOption = { + name: '* Custom Vertex AI model', // Add Vertex custom option + value: '__CUSTOM_VERTEX__' + }; + let choices = []; let defaultIndex = 0; // Default to 'Cancel' @@ -344,43 +357,50 @@ async function runInteractiveSetup(projectRoot) { ); } - // Construct final choices list based on whether 'None' is allowed - const commonPrefix = []; + // Construct final choices list with custom options moved to bottom + const systemOptions = []; if (noChangeOption) { - commonPrefix.push(noChangeOption); + systemOptions.push(noChangeOption); } - commonPrefix.push(cancelOption); - commonPrefix.push(customOpenRouterOption); - commonPrefix.push(customOllamaOption); - commonPrefix.push(customBedrockOption); + systemOptions.push(cancelOption); - const prefixLength = commonPrefix.length; // Initial prefix length + const customOptions = [ + customOpenRouterOption, + customOllamaOption, + customBedrockOption, + customAzureOption, + customVertexOption + ]; + + const systemLength = systemOptions.length; if (allowNone) { choices = [ - ...commonPrefix, - new inquirer.Separator(), - { name: '⚪ None (disable)', value: null }, // Symbol updated - new inquirer.Separator(), - ...roleChoices + ...systemOptions, + new inquirer.Separator('── Standard Models ──'), + { name: '⚪ None (disable)', value: null }, + ...roleChoices, + new inquirer.Separator('── Custom Providers ──'), + ...customOptions ]; - // Adjust default index: Prefix + Sep1 + None + Sep2 (+3) - const noneOptionIndex = prefixLength + 1; + // Adjust default index: System + Sep1 + None (+2) + const noneOptionIndex = systemLength + 1; defaultIndex = currentChoiceIndex !== -1 - ? currentChoiceIndex + prefixLength + 3 // Offset by prefix and separators + ? currentChoiceIndex + systemLength + 2 // Offset by system options and separators : noneOptionIndex; // Default to 'None' if no current model matched } else { choices = [ - ...commonPrefix, - new inquirer.Separator(), + ...systemOptions, + new inquirer.Separator('── Standard Models ──'), ...roleChoices, - new inquirer.Separator() + new inquirer.Separator('── Custom Providers ──'), + ...customOptions ]; - // Adjust default index: Prefix + Sep (+1) + // Adjust default index: System + Sep (+1) defaultIndex = currentChoiceIndex !== -1 - ? currentChoiceIndex + prefixLength + 1 // Offset by prefix and separator + ? currentChoiceIndex + systemLength + 1 // Offset by system options and separator : noChangeOption ? 1 : 0; // Default to 'No Change' if present, else 'Cancel' @@ -403,32 +423,63 @@ async function runInteractiveSetup(projectRoot) { const researchPromptData = getPromptData('research'); const fallbackPromptData = getPromptData('fallback', true); // Allow 'None' for fallback - const answers = await inquirer.prompt([ - { - type: 'list', - name: 'mainModel', - message: 'Select the main model for generation/updates:', - choices: mainPromptData.choices, - default: mainPromptData.default - }, - { - type: 'list', - name: 'researchModel', + // Display helpful intro message + console.log(chalk.cyan('\n🎯 Interactive Model Setup')); + console.log(chalk.gray('━'.repeat(50))); + console.log(chalk.yellow('💡 Navigation tips:')); + console.log(chalk.gray(' • Type to search and filter options')); + console.log(chalk.gray(' • Use ↑↓ arrow keys to navigate results')); + console.log( + chalk.gray( + ' • Standard models are listed first, custom providers at bottom' + ) + ); + console.log(chalk.gray(' • Press Enter to select\n')); + + // Helper function to create search source for models + const createSearchSource = (choices, defaultValue) => { + return (searchTerm = '') => { + const filteredChoices = choices.filter((choice) => { + if (choice.type === 'separator') return true; // Always show separators + const searchText = choice.name || ''; + return searchText.toLowerCase().includes(searchTerm.toLowerCase()); + }); + return Promise.resolve(filteredChoices); + }; + }; + + const answers = {}; + + // Main model selection + answers.mainModel = await search({ + message: 'Select the main model for generation/updates:', + source: createSearchSource(mainPromptData.choices, mainPromptData.default), + pageSize: 15 + }); + + if (answers.mainModel !== '__CANCEL__') { + // Research model selection + answers.researchModel = await search({ message: 'Select the research model:', - choices: researchPromptData.choices, - default: researchPromptData.default, - when: (ans) => ans.mainModel !== '__CANCEL__' - }, - { - type: 'list', - name: 'fallbackModel', - message: 'Select the fallback model (optional):', - choices: fallbackPromptData.choices, - default: fallbackPromptData.default, - when: (ans) => - ans.mainModel !== '__CANCEL__' && ans.researchModel !== '__CANCEL__' + source: createSearchSource( + researchPromptData.choices, + researchPromptData.default + ), + pageSize: 15 + }); + + if (answers.researchModel !== '__CANCEL__') { + // Fallback model selection + answers.fallbackModel = await search({ + message: 'Select the fallback model (optional):', + source: createSearchSource( + fallbackPromptData.choices, + fallbackPromptData.default + ), + pageSize: 15 + }); } - ]); + } let setupSuccess = true; let setupConfigModified = false; @@ -468,7 +519,7 @@ async function runInteractiveSetup(projectRoot) { return true; // Continue setup, but don't set this role } modelIdToSet = customId; - providerHint = 'openrouter'; + providerHint = CUSTOM_PROVIDERS.OPENROUTER; // Validate against live OpenRouter list const openRouterModels = await fetchOpenRouterModelsCLI(); if ( @@ -497,7 +548,7 @@ async function runInteractiveSetup(projectRoot) { return true; // Continue setup, but don't set this role } modelIdToSet = customId; - providerHint = 'ollama'; + providerHint = CUSTOM_PROVIDERS.OLLAMA; // Get the Ollama base URL from config for this role const ollamaBaseURL = getBaseUrlForRole(role, projectRoot); // Validate against live Ollama list @@ -538,7 +589,7 @@ async function runInteractiveSetup(projectRoot) { return true; // Continue setup, but don't set this role } modelIdToSet = customId; - providerHint = 'bedrock'; + providerHint = CUSTOM_PROVIDERS.BEDROCK; // Check if AWS environment variables exist if ( @@ -559,6 +610,76 @@ async function runInteractiveSetup(projectRoot) { `Custom Bedrock model "${modelIdToSet}" will be used. No validation performed.` ) ); + } else if (selectedValue === '__CUSTOM_AZURE__') { + isCustomSelection = true; + const { customId } = await inquirer.prompt([ + { + type: 'input', + name: 'customId', + message: `Enter the custom Azure OpenAI Model ID for the ${role} role (e.g., gpt-4o):` + } + ]); + if (!customId) { + console.log(chalk.yellow('No custom ID entered. Skipping role.')); + return true; // Continue setup, but don't set this role + } + modelIdToSet = customId; + providerHint = CUSTOM_PROVIDERS.AZURE; + + // Check if Azure environment variables exist + if ( + !process.env.AZURE_OPENAI_API_KEY || + !process.env.AZURE_OPENAI_ENDPOINT + ) { + console.error( + chalk.red( + 'Error: AZURE_OPENAI_API_KEY and/or AZURE_OPENAI_ENDPOINT environment variables are missing. Please set them before using custom Azure models.' + ) + ); + setupSuccess = false; + return true; // Continue setup, but mark as failed + } + + console.log( + chalk.blue( + `Custom Azure OpenAI model "${modelIdToSet}" will be used. No validation performed.` + ) + ); + } else if (selectedValue === '__CUSTOM_VERTEX__') { + isCustomSelection = true; + const { customId } = await inquirer.prompt([ + { + type: 'input', + name: 'customId', + message: `Enter the custom Vertex AI Model ID for the ${role} role (e.g., gemini-1.5-pro-002):` + } + ]); + if (!customId) { + console.log(chalk.yellow('No custom ID entered. Skipping role.')); + return true; // Continue setup, but don't set this role + } + modelIdToSet = customId; + providerHint = CUSTOM_PROVIDERS.VERTEX; + + // Check if Google/Vertex environment variables exist + if ( + !process.env.GOOGLE_API_KEY && + !process.env.GOOGLE_APPLICATION_CREDENTIALS + ) { + console.error( + chalk.red( + 'Error: Either GOOGLE_API_KEY or GOOGLE_APPLICATION_CREDENTIALS environment variable is required. Please set one before using custom Vertex models.' + ) + ); + setupSuccess = false; + return true; // Continue setup, but mark as failed + } + + console.log( + chalk.blue( + `Custom Vertex AI model "${modelIdToSet}" will be used. No validation performed.` + ) + ); } else if ( selectedValue && typeof selectedValue === 'object' && @@ -3307,6 +3428,14 @@ ${result.result} '--bedrock', 'Allow setting a custom Bedrock model ID (use with --set-*) ' ) + .option( + '--azure', + 'Allow setting a custom Azure OpenAI model ID (use with --set-*) ' + ) + .option( + '--vertex', + 'Allow setting a custom Vertex AI model ID (use with --set-*) ' + ) .addHelpText( 'after', ` @@ -3318,6 +3447,8 @@ Examples: $ task-master models --set-main my-custom-model --ollama # Set custom Ollama model for main role $ task-master models --set-main anthropic.claude-3-sonnet-20240229-v1:0 --bedrock # Set custom Bedrock model for main role $ task-master models --set-main some/other-model --openrouter # Set custom OpenRouter model for main role + $ task-master models --set-main gpt-4o --azure # Set custom Azure OpenAI model for main role + $ task-master models --set-main claude-3-5-sonnet@20241022 --vertex # Set custom Vertex AI model for main role $ task-master models --setup # Run interactive setup` ) .action(async (options) => { diff --git a/scripts/modules/config-manager.js b/scripts/modules/config-manager.js index c4a52f70..d289e2e2 100644 --- a/scripts/modules/config-manager.js +++ b/scripts/modules/config-manager.js @@ -5,6 +5,12 @@ import { fileURLToPath } from 'url'; import { log, findProjectRoot, resolveEnvVariable } from './utils.js'; import { LEGACY_CONFIG_FILE } from '../../src/constants/paths.js'; import { findConfigPath } from '../../src/utils/path-utils.js'; +import { + VALIDATED_PROVIDERS, + CUSTOM_PROVIDERS, + CUSTOM_PROVIDERS_ARRAY, + ALL_PROVIDERS +} from '../../src/constants/providers.js'; // Calculate __dirname in ESM const __filename = fileURLToPath(import.meta.url); @@ -29,9 +35,6 @@ try { process.exit(1); // Exit if models can't be loaded } -// Define valid providers dynamically from the loaded MODEL_MAP -const VALID_PROVIDERS = Object.keys(MODEL_MAP || {}); - // Default configuration values (used if config file is missing or incomplete) const DEFAULTS = { models: { @@ -233,12 +236,25 @@ function getConfig(explicitRoot = null, forceReload = false) { } /** - * Validates if a provider name is in the list of supported providers. + * Validates if a provider name is supported. + * Custom providers (azure, vertex, bedrock, openrouter, ollama) are always allowed. + * Validated providers must exist in the MODEL_MAP from supported-models.json. * @param {string} providerName The name of the provider. * @returns {boolean} True if the provider is valid, false otherwise. */ function validateProvider(providerName) { - return VALID_PROVIDERS.includes(providerName); + // Custom providers are always allowed + if (CUSTOM_PROVIDERS_ARRAY.includes(providerName)) { + return true; + } + + // Validated providers must exist in MODEL_MAP + if (VALIDATED_PROVIDERS.includes(providerName)) { + return !!(MODEL_MAP && MODEL_MAP[providerName]); + } + + // Unknown providers are not allowed + return false; } /** @@ -736,11 +752,11 @@ function getUserId(explicitRoot = null) { } /** - * Gets a list of all provider names defined in the MODEL_MAP. - * @returns {string[]} An array of provider names. + * Gets a list of all known provider names (both validated and custom). + * @returns {string[]} An array of all provider names. */ function getAllProviders() { - return Object.keys(MODEL_MAP || {}); + return ALL_PROVIDERS; } function getBaseUrlForRole(role, explicitRoot = null) { @@ -759,7 +775,9 @@ export { // Validation validateProvider, validateProviderModelCombination, - VALID_PROVIDERS, + VALIDATED_PROVIDERS, + CUSTOM_PROVIDERS, + ALL_PROVIDERS, MODEL_MAP, getAvailableModels, // Role-specific getters (No env var overrides) diff --git a/scripts/modules/supported-models.json b/scripts/modules/supported-models.json index 3e774d21..8a0cb2b2 100644 --- a/scripts/modules/supported-models.json +++ b/scripts/modules/supported-models.json @@ -3,28 +3,40 @@ { "id": "claude-sonnet-4-20250514", "swe_score": 0.727, - "cost_per_1m_tokens": { "input": 3.0, "output": 15.0 }, + "cost_per_1m_tokens": { + "input": 3.0, + "output": 15.0 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 64000 }, { "id": "claude-opus-4-20250514", "swe_score": 0.725, - "cost_per_1m_tokens": { "input": 15.0, "output": 75.0 }, + "cost_per_1m_tokens": { + "input": 15.0, + "output": 75.0 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 32000 }, { "id": "claude-3-7-sonnet-20250219", "swe_score": 0.623, - "cost_per_1m_tokens": { "input": 3.0, "output": 15.0 }, + "cost_per_1m_tokens": { + "input": 3.0, + "output": 15.0 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 120000 }, { "id": "claude-3-5-sonnet-20241022", "swe_score": 0.49, - "cost_per_1m_tokens": { "input": 3.0, "output": 15.0 }, + "cost_per_1m_tokens": { + "input": 3.0, + "output": 15.0 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 64000 } @@ -33,81 +45,120 @@ { "id": "gpt-4o", "swe_score": 0.332, - "cost_per_1m_tokens": { "input": 2.5, "output": 10.0 }, + "cost_per_1m_tokens": { + "input": 2.5, + "output": 10.0 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 16384 }, { "id": "o1", "swe_score": 0.489, - "cost_per_1m_tokens": { "input": 15.0, "output": 60.0 }, + "cost_per_1m_tokens": { + "input": 15.0, + "output": 60.0 + }, "allowed_roles": ["main"] }, { "id": "o3", "swe_score": 0.5, - "cost_per_1m_tokens": { "input": 2.0, "output": 8.0 }, + "cost_per_1m_tokens": { + "input": 2.0, + "output": 8.0 + }, "allowed_roles": ["main", "fallback"] }, { "id": "o3-mini", "swe_score": 0.493, - "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, + "cost_per_1m_tokens": { + "input": 1.1, + "output": 4.4 + }, "allowed_roles": ["main"], "max_tokens": 100000 }, { "id": "o4-mini", "swe_score": 0.45, - "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, + "cost_per_1m_tokens": { + "input": 1.1, + "output": 4.4 + }, "allowed_roles": ["main", "fallback"] }, { "id": "o1-mini", "swe_score": 0.4, - "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, + "cost_per_1m_tokens": { + "input": 1.1, + "output": 4.4 + }, "allowed_roles": ["main"] }, { "id": "o1-pro", "swe_score": 0, - "cost_per_1m_tokens": { "input": 150.0, "output": 600.0 }, + "cost_per_1m_tokens": { + "input": 150.0, + "output": 600.0 + }, "allowed_roles": ["main"] }, { "id": "gpt-4-5-preview", "swe_score": 0.38, - "cost_per_1m_tokens": { "input": 75.0, "output": 150.0 }, + "cost_per_1m_tokens": { + "input": 75.0, + "output": 150.0 + }, "allowed_roles": ["main"] }, { "id": "gpt-4-1-mini", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.4, "output": 1.6 }, + "cost_per_1m_tokens": { + "input": 0.4, + "output": 1.6 + }, "allowed_roles": ["main"] }, { "id": "gpt-4-1-nano", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.1, "output": 0.4 }, + "cost_per_1m_tokens": { + "input": 0.1, + "output": 0.4 + }, "allowed_roles": ["main"] }, { "id": "gpt-4o-mini", "swe_score": 0.3, - "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, + "cost_per_1m_tokens": { + "input": 0.15, + "output": 0.6 + }, "allowed_roles": ["main"] }, { "id": "gpt-4o-search-preview", "swe_score": 0.33, - "cost_per_1m_tokens": { "input": 2.5, "output": 10.0 }, + "cost_per_1m_tokens": { + "input": 2.5, + "output": 10.0 + }, "allowed_roles": ["research"] }, { "id": "gpt-4o-mini-search-preview", "swe_score": 0.3, - "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, + "cost_per_1m_tokens": { + "input": 0.15, + "output": 0.6 + }, "allowed_roles": ["research"] } ], @@ -136,7 +187,10 @@ { "id": "gemini-2.0-flash", "swe_score": 0.518, - "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, + "cost_per_1m_tokens": { + "input": 0.15, + "output": 0.6 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 1048000 }, @@ -152,35 +206,50 @@ { "id": "sonar-pro", "swe_score": 0, - "cost_per_1m_tokens": { "input": 3, "output": 15 }, + "cost_per_1m_tokens": { + "input": 3, + "output": 15 + }, "allowed_roles": ["main", "research"], "max_tokens": 8700 }, { "id": "sonar", "swe_score": 0, - "cost_per_1m_tokens": { "input": 1, "output": 1 }, + "cost_per_1m_tokens": { + "input": 1, + "output": 1 + }, "allowed_roles": ["research"], "max_tokens": 8700 }, { "id": "deep-research", "swe_score": 0.211, - "cost_per_1m_tokens": { "input": 2, "output": 8 }, + "cost_per_1m_tokens": { + "input": 2, + "output": 8 + }, "allowed_roles": ["research"], "max_tokens": 8700 }, { "id": "sonar-reasoning-pro", "swe_score": 0.211, - "cost_per_1m_tokens": { "input": 2, "output": 8 }, + "cost_per_1m_tokens": { + "input": 2, + "output": 8 + }, "allowed_roles": ["main", "research", "fallback"], "max_tokens": 8700 }, { "id": "sonar-reasoning", "swe_score": 0.211, - "cost_per_1m_tokens": { "input": 1, "output": 5 }, + "cost_per_1m_tokens": { + "input": 1, + "output": 5 + }, "allowed_roles": ["main", "research", "fallback"], "max_tokens": 8700 } @@ -190,7 +259,10 @@ "id": "grok-3", "name": "Grok 3", "swe_score": null, - "cost_per_1m_tokens": { "input": 3, "output": 15 }, + "cost_per_1m_tokens": { + "input": 3, + "output": 15 + }, "allowed_roles": ["main", "fallback", "research"], "max_tokens": 131072 }, @@ -198,7 +270,10 @@ "id": "grok-3-fast", "name": "Grok 3 Fast", "swe_score": 0, - "cost_per_1m_tokens": { "input": 5, "output": 25 }, + "cost_per_1m_tokens": { + "input": 5, + "output": 25 + }, "allowed_roles": ["main", "fallback", "research"], "max_tokens": 131072 } @@ -207,43 +282,64 @@ { "id": "devstral:latest", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "cost_per_1m_tokens": { + "input": 0, + "output": 0 + }, "allowed_roles": ["main", "fallback"] }, { "id": "qwen3:latest", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "cost_per_1m_tokens": { + "input": 0, + "output": 0 + }, "allowed_roles": ["main", "fallback"] }, { "id": "qwen3:14b", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "cost_per_1m_tokens": { + "input": 0, + "output": 0 + }, "allowed_roles": ["main", "fallback"] }, { "id": "qwen3:32b", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "cost_per_1m_tokens": { + "input": 0, + "output": 0 + }, "allowed_roles": ["main", "fallback"] }, { "id": "mistral-small3.1:latest", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "cost_per_1m_tokens": { + "input": 0, + "output": 0 + }, "allowed_roles": ["main", "fallback"] }, { "id": "llama3.3:latest", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "cost_per_1m_tokens": { + "input": 0, + "output": 0 + }, "allowed_roles": ["main", "fallback"] }, { "id": "phi4:latest", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "cost_per_1m_tokens": { + "input": 0, + "output": 0 + }, "allowed_roles": ["main", "fallback"] } ], @@ -251,175 +347,250 @@ { "id": "google/gemini-2.5-flash-preview-05-20", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, + "cost_per_1m_tokens": { + "input": 0.15, + "output": 0.6 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 1048576 }, { "id": "google/gemini-2.5-flash-preview-05-20:thinking", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.15, "output": 3.5 }, + "cost_per_1m_tokens": { + "input": 0.15, + "output": 3.5 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 1048576 }, { "id": "google/gemini-2.5-pro-exp-03-25", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "cost_per_1m_tokens": { + "input": 0, + "output": 0 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 1000000 }, { "id": "deepseek/deepseek-chat-v3-0324:free", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "cost_per_1m_tokens": { + "input": 0, + "output": 0 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 163840 }, { "id": "deepseek/deepseek-chat-v3-0324", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.27, "output": 1.1 }, + "cost_per_1m_tokens": { + "input": 0.27, + "output": 1.1 + }, "allowed_roles": ["main"], "max_tokens": 64000 }, { "id": "openai/gpt-4.1", "swe_score": 0, - "cost_per_1m_tokens": { "input": 2, "output": 8 }, + "cost_per_1m_tokens": { + "input": 2, + "output": 8 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 1000000 }, { "id": "openai/gpt-4.1-mini", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.4, "output": 1.6 }, + "cost_per_1m_tokens": { + "input": 0.4, + "output": 1.6 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 1000000 }, { "id": "openai/gpt-4.1-nano", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.1, "output": 0.4 }, + "cost_per_1m_tokens": { + "input": 0.1, + "output": 0.4 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 1000000 }, { "id": "openai/o3", "swe_score": 0, - "cost_per_1m_tokens": { "input": 10, "output": 40 }, + "cost_per_1m_tokens": { + "input": 10, + "output": 40 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 200000 }, { "id": "openai/codex-mini", "swe_score": 0, - "cost_per_1m_tokens": { "input": 1.5, "output": 6 }, + "cost_per_1m_tokens": { + "input": 1.5, + "output": 6 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 100000 }, { "id": "openai/gpt-4o-mini", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.15, "output": 0.6 }, + "cost_per_1m_tokens": { + "input": 0.15, + "output": 0.6 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 100000 }, { "id": "openai/o4-mini", "swe_score": 0.45, - "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, + "cost_per_1m_tokens": { + "input": 1.1, + "output": 4.4 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 100000 }, { "id": "openai/o4-mini-high", "swe_score": 0, - "cost_per_1m_tokens": { "input": 1.1, "output": 4.4 }, + "cost_per_1m_tokens": { + "input": 1.1, + "output": 4.4 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 100000 }, { "id": "openai/o1-pro", "swe_score": 0, - "cost_per_1m_tokens": { "input": 150, "output": 600 }, + "cost_per_1m_tokens": { + "input": 150, + "output": 600 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 100000 }, { "id": "meta-llama/llama-3.3-70b-instruct", "swe_score": 0, - "cost_per_1m_tokens": { "input": 120, "output": 600 }, + "cost_per_1m_tokens": { + "input": 120, + "output": 600 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 1048576 }, { "id": "meta-llama/llama-4-maverick", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.18, "output": 0.6 }, + "cost_per_1m_tokens": { + "input": 0.18, + "output": 0.6 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 1000000 }, { "id": "meta-llama/llama-4-scout", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.08, "output": 0.3 }, + "cost_per_1m_tokens": { + "input": 0.08, + "output": 0.3 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 1000000 }, { "id": "qwen/qwen-max", "swe_score": 0, - "cost_per_1m_tokens": { "input": 1.6, "output": 6.4 }, + "cost_per_1m_tokens": { + "input": 1.6, + "output": 6.4 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 32768 }, { "id": "qwen/qwen-turbo", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.05, "output": 0.2 }, + "cost_per_1m_tokens": { + "input": 0.05, + "output": 0.2 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 1000000 }, { "id": "qwen/qwen3-235b-a22b", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.14, "output": 2 }, + "cost_per_1m_tokens": { + "input": 0.14, + "output": 2 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 24000 }, { "id": "mistralai/mistral-small-3.1-24b-instruct:free", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "cost_per_1m_tokens": { + "input": 0, + "output": 0 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 96000 }, { "id": "mistralai/mistral-small-3.1-24b-instruct", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.1, "output": 0.3 }, + "cost_per_1m_tokens": { + "input": 0.1, + "output": 0.3 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 128000 }, { "id": "mistralai/devstral-small", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.1, "output": 0.3 }, + "cost_per_1m_tokens": { + "input": 0.1, + "output": 0.3 + }, "allowed_roles": ["main"], "max_tokens": 110000 }, { "id": "mistralai/mistral-nemo", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0.03, "output": 0.07 }, + "cost_per_1m_tokens": { + "input": 0.03, + "output": 0.07 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 100000 }, { "id": "thudm/glm-4-32b:free", "swe_score": 0, - "cost_per_1m_tokens": { "input": 0, "output": 0 }, + "cost_per_1m_tokens": { + "input": 0, + "output": 0 + }, "allowed_roles": ["main", "fallback"], "max_tokens": 32768 } diff --git a/scripts/modules/task-manager/models.js b/scripts/modules/task-manager/models.js index b5b18538..6bfdfae8 100644 --- a/scripts/modules/task-manager/models.js +++ b/scripts/modules/task-manager/models.js @@ -23,6 +23,7 @@ import { } from '../config-manager.js'; import { findConfigPath } from '../../../src/utils/path-utils.js'; import { log } from '../utils.js'; +import { CUSTOM_PROVIDERS } from '../../../src/constants/providers.js'; /** * Fetches the list of models from OpenRouter API. @@ -440,7 +441,7 @@ async function setModel(role, modelId, options = {}) { } else { // Either not found internally, OR found but under a DIFFERENT provider than hinted. // Proceed with custom logic based ONLY on the hint. - if (providerHint === 'openrouter') { + if (providerHint === CUSTOM_PROVIDERS.OPENROUTER) { // Check OpenRouter ONLY because hint was openrouter report('info', `Checking OpenRouter for ${modelId} (as hinted)...`); const openRouterModels = await fetchOpenRouterModels(); @@ -449,7 +450,7 @@ async function setModel(role, modelId, options = {}) { openRouterModels && openRouterModels.some((m) => m.id === modelId) ) { - determinedProvider = 'openrouter'; + determinedProvider = CUSTOM_PROVIDERS.OPENROUTER; // Check if this is a free model (ends with :free) if (modelId.endsWith(':free')) { @@ -465,7 +466,7 @@ async function setModel(role, modelId, options = {}) { `Model ID "${modelId}" not found in the live OpenRouter model list. Please verify the ID and ensure it's available on OpenRouter.` ); } - } else if (providerHint === 'ollama') { + } else if (providerHint === CUSTOM_PROVIDERS.OLLAMA) { // Check Ollama ONLY because hint was ollama report('info', `Checking Ollama for ${modelId} (as hinted)...`); @@ -479,7 +480,7 @@ async function setModel(role, modelId, options = {}) { `Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.` ); } else if (ollamaModels.some((m) => m.model === modelId)) { - determinedProvider = 'ollama'; + determinedProvider = CUSTOM_PROVIDERS.OLLAMA; warningMessage = `Warning: Custom Ollama model '${modelId}' set. Ensure your Ollama server is running and has pulled this model. Taskmaster cannot guarantee compatibility.`; report('warn', warningMessage); } else { @@ -489,13 +490,23 @@ async function setModel(role, modelId, options = {}) { `Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}` ); } - } else if (providerHint === 'bedrock') { + } else if (providerHint === CUSTOM_PROVIDERS.BEDROCK) { // Set provider without model validation since Bedrock models are managed by AWS - determinedProvider = 'bedrock'; + determinedProvider = CUSTOM_PROVIDERS.BEDROCK; warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`; report('warn', warningMessage); + } else if (providerHint === CUSTOM_PROVIDERS.AZURE) { + // Set provider without model validation since Azure models are managed by Azure + determinedProvider = CUSTOM_PROVIDERS.AZURE; + warningMessage = `Warning: Custom Azure model '${modelId}' set. Please ensure the model deployment is valid and accessible in your Azure account.`; + report('warn', warningMessage); + } else if (providerHint === CUSTOM_PROVIDERS.VERTEX) { + // Set provider without model validation since Vertex models are managed by Google Cloud + determinedProvider = CUSTOM_PROVIDERS.VERTEX; + warningMessage = `Warning: Custom Vertex AI model '${modelId}' set. Please ensure the model is valid and accessible in your Google Cloud project.`; + report('warn', warningMessage); } else { - // Invalid provider hint - should not happen + // Invalid provider hint - should not happen with our constants throw new Error(`Invalid provider hint received: ${providerHint}`); } } diff --git a/src/constants/providers.js b/src/constants/providers.js new file mode 100644 index 00000000..90346701 --- /dev/null +++ b/src/constants/providers.js @@ -0,0 +1,32 @@ +/** + * Provider validation constants + * Defines which providers should be validated against the supported-models.json file + */ + +// Providers that have predefined model lists and should be validated +export const VALIDATED_PROVIDERS = [ + 'anthropic', + 'openai', + 'google', + 'perplexity', + 'xai', + 'mistral' +]; + +// Custom providers object for easy named access +export const CUSTOM_PROVIDERS = { + AZURE: 'azure', + VERTEX: 'vertex', + BEDROCK: 'bedrock', + OPENROUTER: 'openrouter', + OLLAMA: 'ollama' +}; + +// Custom providers array (for backward compatibility and iteration) +export const CUSTOM_PROVIDERS_ARRAY = Object.values(CUSTOM_PROVIDERS); + +// All known providers (for reference) +export const ALL_PROVIDERS = [ + ...VALIDATED_PROVIDERS, + ...CUSTOM_PROVIDERS_ARRAY +]; diff --git a/tests/unit/config-manager.test.js b/tests/unit/config-manager.test.js index 95686903..851818bb 100644 --- a/tests/unit/config-manager.test.js +++ b/tests/unit/config-manager.test.js @@ -713,17 +713,25 @@ describe('isConfigFilePresent', () => { // --- getAllProviders Tests --- describe('getAllProviders', () => { - test('should return list of providers from supported-models.json', () => { + test('should return all providers from ALL_PROVIDERS constant', () => { // Arrange: Ensure config is loaded with real data configManager.getConfig(null, true); // Force load using the mock that returns real data // Act const providers = configManager.getAllProviders(); + // Assert - // Assert against the actual keys in the REAL loaded data - const expectedProviders = Object.keys(REAL_SUPPORTED_MODELS_DATA); - expect(providers).toEqual(expect.arrayContaining(expectedProviders)); - expect(providers.length).toBe(expectedProviders.length); + // getAllProviders() should return the same as the ALL_PROVIDERS constant + expect(providers).toEqual(configManager.ALL_PROVIDERS); + expect(providers.length).toBe(configManager.ALL_PROVIDERS.length); + + // Verify it includes both validated and custom providers + expect(providers).toEqual( + expect.arrayContaining(configManager.VALIDATED_PROVIDERS) + ); + expect(providers).toEqual( + expect.arrayContaining(Object.values(configManager.CUSTOM_PROVIDERS)) + ); }); });