2025-04-23 15:47:33 -04:00
/ * *
* models . js
* Core functionality for managing AI model configurations
* /
2025-06-07 20:30:51 -04:00
import https from 'https' ;
import http from 'http' ;
2025-04-23 15:47:33 -04:00
import {
2025-06-07 20:30:51 -04:00
getMainModelId ,
getResearchModelId ,
getFallbackModelId ,
getAvailableModels ,
getMainProvider ,
getResearchProvider ,
getFallbackProvider ,
isApiKeySet ,
getMcpApiKeyStatus ,
getConfig ,
writeConfig ,
isConfigFilePresent ,
getAllProviders ,
getBaseUrlForRole
} from '../config-manager.js' ;
import { findConfigPath } from '../../../src/utils/path-utils.js' ;
import { log } from '../utils.js' ;
2025-04-23 15:47:33 -04:00
2025-04-27 17:25:54 -04:00
/ * *
* Fetches the list of models from OpenRouter API .
* @ returns { Promise < Array | null > } A promise that resolves with the list of model IDs or null if fetch fails .
* /
function fetchOpenRouterModels ( ) {
2025-06-07 20:30:51 -04:00
return new Promise ( ( resolve ) => {
const options = {
hostname : 'openrouter.ai' ,
path : '/api/v1/models' ,
method : 'GET' ,
headers : {
Accept : 'application/json'
}
} ;
const req = https . request ( options , ( res ) => {
let data = '' ;
res . on ( 'data' , ( chunk ) => {
data += chunk ;
} ) ;
res . on ( 'end' , ( ) => {
if ( res . statusCode === 200 ) {
try {
const parsedData = JSON . parse ( data ) ;
resolve ( parsedData . data || [ ] ) ; // Return the array of models
} catch ( e ) {
console . error ( 'Error parsing OpenRouter response:' , e ) ;
resolve ( null ) ; // Indicate failure
}
} else {
console . error (
` OpenRouter API request failed with status code: ${ res . statusCode } `
) ;
resolve ( null ) ; // Indicate failure
}
} ) ;
} ) ;
req . on ( 'error' , ( e ) => {
console . error ( 'Error fetching OpenRouter models:' , e ) ;
resolve ( null ) ; // Indicate failure
} ) ;
req . end ( ) ;
} ) ;
2025-04-27 17:25:54 -04:00
}
2025-05-23 20:20:39 -04:00
/ * *
* Fetches the list of models from Ollama instance .
2025-05-28 00:42:31 +02:00
* @ param { string } baseURL - The base URL for the Ollama API ( e . g . , "http://localhost:11434/api" )
2025-05-23 20:20:39 -04:00
* @ returns { Promise < Array | null > } A promise that resolves with the list of model objects or null if fetch fails .
* /
2025-06-07 20:30:51 -04:00
function fetchOllamaModels ( baseURL = 'http://localhost:11434/api' ) {
return new Promise ( ( resolve ) => {
try {
// Parse the base URL to extract hostname, port, and base path
const url = new URL ( baseURL ) ;
const isHttps = url . protocol === 'https:' ;
const port = url . port || ( isHttps ? 443 : 80 ) ;
const basePath = url . pathname . endsWith ( '/' )
? url . pathname . slice ( 0 , - 1 )
: url . pathname ;
const options = {
hostname : url . hostname ,
port : parseInt ( port , 10 ) ,
path : ` ${ basePath } /tags ` ,
method : 'GET' ,
headers : {
Accept : 'application/json'
}
} ;
const requestLib = isHttps ? https : http ;
const req = requestLib . request ( options , ( res ) => {
let data = '' ;
res . on ( 'data' , ( chunk ) => {
data += chunk ;
} ) ;
res . on ( 'end' , ( ) => {
if ( res . statusCode === 200 ) {
try {
const parsedData = JSON . parse ( data ) ;
resolve ( parsedData . models || [ ] ) ; // Return the array of models
} catch ( e ) {
console . error ( 'Error parsing Ollama response:' , e ) ;
resolve ( null ) ; // Indicate failure
}
} else {
console . error (
` Ollama API request failed with status code: ${ res . statusCode } `
) ;
resolve ( null ) ; // Indicate failure
}
} ) ;
} ) ;
req . on ( 'error' , ( e ) => {
console . error ( 'Error fetching Ollama models:' , e ) ;
resolve ( null ) ; // Indicate failure
} ) ;
req . end ( ) ;
} catch ( e ) {
console . error ( 'Error parsing Ollama base URL:' , e ) ;
resolve ( null ) ; // Indicate failure
}
} ) ;
2025-05-23 20:20:39 -04:00
}
2025-04-23 15:47:33 -04:00
/ * *
* Get the current model configuration
* @ param { Object } [ options ] - Options for the operation
* @ param { Object } [ options . session ] - Session object containing environment variables ( for MCP )
* @ param { Function } [ options . mcpLog ] - MCP logger object ( for MCP )
* @ param { string } [ options . projectRoot ] - Project root directory
* @ returns { Object } RESTful response with current model configuration
* /
async function getModelConfiguration ( options = { } ) {
2025-06-07 20:30:51 -04:00
const { mcpLog , projectRoot , session } = options ;
const report = ( level , ... args ) => {
if ( mcpLog && typeof mcpLog [ level ] === 'function' ) {
mcpLog [ level ] ( ... args ) ;
}
} ;
if ( ! projectRoot ) {
throw new Error ( 'Project root is required but not found.' ) ;
}
// Use centralized config path finding instead of hardcoded path
const configPath = findConfigPath ( null , { projectRoot } ) ;
const configExists = isConfigFilePresent ( projectRoot ) ;
log (
'debug' ,
` Checking for config file using findConfigPath, found: ${ configPath } `
) ;
log (
'debug' ,
` Checking config file using isConfigFilePresent(), exists: ${ configExists } `
) ;
if ( ! configExists ) {
throw new Error (
'The configuration file is missing. Run "task-master models --setup" to create it.'
) ;
}
try {
// Get current settings - these should use the config from the found path automatically
const mainProvider = getMainProvider ( projectRoot ) ;
const mainModelId = getMainModelId ( projectRoot ) ;
const researchProvider = getResearchProvider ( projectRoot ) ;
const researchModelId = getResearchModelId ( projectRoot ) ;
const fallbackProvider = getFallbackProvider ( projectRoot ) ;
const fallbackModelId = getFallbackModelId ( projectRoot ) ;
// Check API keys
const mainCliKeyOk = isApiKeySet ( mainProvider , session , projectRoot ) ;
const mainMcpKeyOk = getMcpApiKeyStatus ( mainProvider , projectRoot ) ;
const researchCliKeyOk = isApiKeySet (
researchProvider ,
session ,
projectRoot
) ;
const researchMcpKeyOk = getMcpApiKeyStatus ( researchProvider , projectRoot ) ;
const fallbackCliKeyOk = fallbackProvider
? isApiKeySet ( fallbackProvider , session , projectRoot )
: true ;
const fallbackMcpKeyOk = fallbackProvider
? getMcpApiKeyStatus ( fallbackProvider , projectRoot )
: true ;
// Get available models to find detailed info
const availableModels = getAvailableModels ( projectRoot ) ;
// Find model details
const mainModelData = availableModels . find ( ( m ) => m . id === mainModelId ) ;
const researchModelData = availableModels . find (
( m ) => m . id === researchModelId
) ;
const fallbackModelData = fallbackModelId
? availableModels . find ( ( m ) => m . id === fallbackModelId )
: null ;
// Return structured configuration data
return {
success : true ,
data : {
activeModels : {
main : {
provider : mainProvider ,
modelId : mainModelId ,
sweScore : mainModelData ? . swe _score || null ,
cost : mainModelData ? . cost _per _1m _tokens || null ,
keyStatus : {
cli : mainCliKeyOk ,
mcp : mainMcpKeyOk
}
} ,
research : {
provider : researchProvider ,
modelId : researchModelId ,
sweScore : researchModelData ? . swe _score || null ,
cost : researchModelData ? . cost _per _1m _tokens || null ,
keyStatus : {
cli : researchCliKeyOk ,
mcp : researchMcpKeyOk
}
} ,
fallback : fallbackProvider
? {
provider : fallbackProvider ,
modelId : fallbackModelId ,
sweScore : fallbackModelData ? . swe _score || null ,
cost : fallbackModelData ? . cost _per _1m _tokens || null ,
keyStatus : {
cli : fallbackCliKeyOk ,
mcp : fallbackMcpKeyOk
}
}
: null
} ,
message : 'Successfully retrieved current model configuration'
}
} ;
} catch ( error ) {
report ( 'error' , ` Error getting model configuration: ${ error . message } ` ) ;
return {
success : false ,
error : {
code : 'CONFIG_ERROR' ,
message : error . message
}
} ;
}
2025-04-23 15:47:33 -04:00
}
/ * *
* Get all available models not currently in use
* @ param { Object } [ options ] - Options for the operation
* @ param { Object } [ options . session ] - Session object containing environment variables ( for MCP )
* @ param { Function } [ options . mcpLog ] - MCP logger object ( for MCP )
* @ param { string } [ options . projectRoot ] - Project root directory
* @ returns { Object } RESTful response with available models
* /
async function getAvailableModelsList ( options = { } ) {
2025-06-07 20:30:51 -04:00
const { mcpLog , projectRoot } = options ;
const report = ( level , ... args ) => {
if ( mcpLog && typeof mcpLog [ level ] === 'function' ) {
mcpLog [ level ] ( ... args ) ;
}
} ;
if ( ! projectRoot ) {
throw new Error ( 'Project root is required but not found.' ) ;
}
// Use centralized config path finding instead of hardcoded path
const configPath = findConfigPath ( null , { projectRoot } ) ;
const configExists = isConfigFilePresent ( projectRoot ) ;
log (
'debug' ,
` Checking for config file using findConfigPath, found: ${ configPath } `
) ;
log (
'debug' ,
` Checking config file using isConfigFilePresent(), exists: ${ configExists } `
) ;
if ( ! configExists ) {
throw new Error (
'The configuration file is missing. Run "task-master models --setup" to create it.'
) ;
}
try {
// Get all available models
const allAvailableModels = getAvailableModels ( projectRoot ) ;
if ( ! allAvailableModels || allAvailableModels . length === 0 ) {
return {
success : true ,
data : {
models : [ ] ,
message : 'No available models found'
}
} ;
}
// Get currently used model IDs
const mainModelId = getMainModelId ( projectRoot ) ;
const researchModelId = getResearchModelId ( projectRoot ) ;
const fallbackModelId = getFallbackModelId ( projectRoot ) ;
// Filter out placeholder models and active models
const activeIds = [ mainModelId , researchModelId , fallbackModelId ] . filter (
Boolean
) ;
const otherAvailableModels = allAvailableModels . map ( ( model ) => ( {
provider : model . provider || 'N/A' ,
modelId : model . id ,
sweScore : model . swe _score || null ,
cost : model . cost _per _1m _tokens || null ,
allowedRoles : model . allowed _roles || [ ]
} ) ) ;
return {
success : true ,
data : {
models : otherAvailableModels ,
message : ` Successfully retrieved ${ otherAvailableModels . length } available models `
}
} ;
} catch ( error ) {
report ( 'error' , ` Error getting available models: ${ error . message } ` ) ;
return {
success : false ,
error : {
code : 'MODELS_LIST_ERROR' ,
message : error . message
}
} ;
}
2025-04-23 15:47:33 -04:00
}
/ * *
* Update a specific model in the configuration
* @ param { string } role - The model role to update ( 'main' , 'research' , 'fallback' )
* @ param { string } modelId - The model ID to set for the role
* @ param { Object } [ options ] - Options for the operation
2025-04-27 17:25:54 -04:00
* @ param { string } [ options . providerHint ] - Provider hint if already determined ( 'openrouter' or 'ollama' )
2025-04-23 15:47:33 -04:00
* @ param { Object } [ options . session ] - Session object containing environment variables ( for MCP )
* @ param { Function } [ options . mcpLog ] - MCP logger object ( for MCP )
* @ param { string } [ options . projectRoot ] - Project root directory
* @ returns { Object } RESTful response with result of update operation
* /
async function setModel ( role , modelId , options = { } ) {
2025-06-07 20:30:51 -04:00
const { mcpLog , projectRoot , providerHint } = options ;
const report = ( level , ... args ) => {
if ( mcpLog && typeof mcpLog [ level ] === 'function' ) {
mcpLog [ level ] ( ... args ) ;
}
} ;
if ( ! projectRoot ) {
throw new Error ( 'Project root is required but not found.' ) ;
}
// Use centralized config path finding instead of hardcoded path
const configPath = findConfigPath ( null , { projectRoot } ) ;
const configExists = isConfigFilePresent ( projectRoot ) ;
log (
'debug' ,
` Checking for config file using findConfigPath, found: ${ configPath } `
) ;
log (
'debug' ,
` Checking config file using isConfigFilePresent(), exists: ${ configExists } `
) ;
if ( ! configExists ) {
throw new Error (
'The configuration file is missing. Run "task-master models --setup" to create it.'
) ;
}
// Validate role
if ( ! [ 'main' , 'research' , 'fallback' ] . includes ( role ) ) {
return {
success : false ,
error : {
code : 'INVALID_ROLE' ,
message : ` Invalid role: ${ role } . Must be one of: main, research, fallback. `
}
} ;
}
// Validate model ID
if ( typeof modelId !== 'string' || modelId . trim ( ) === '' ) {
return {
success : false ,
error : {
code : 'INVALID_MODEL_ID' ,
message : ` Invalid model ID: ${ modelId } . Must be a non-empty string. `
}
} ;
}
try {
const availableModels = getAvailableModels ( projectRoot ) ;
const currentConfig = getConfig ( projectRoot ) ;
let determinedProvider = null ; // Initialize provider
let warningMessage = null ;
// Find the model data in internal list initially to see if it exists at all
const modelData = availableModels . find ( ( m ) => m . id === modelId ) ;
// --- Revised Logic: Prioritize providerHint --- //
if ( providerHint ) {
// Hint provided (--ollama or --openrouter flag used)
if ( modelData && modelData . provider === providerHint ) {
// Found internally AND provider matches the hint
determinedProvider = providerHint ;
report (
'info' ,
` Model ${ modelId } found internally with matching provider hint ${ determinedProvider } . `
) ;
} 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' ) {
// Check OpenRouter ONLY because hint was openrouter
report ( 'info' , ` Checking OpenRouter for ${ modelId } (as hinted)... ` ) ;
const openRouterModels = await fetchOpenRouterModels ( ) ;
if (
openRouterModels &&
openRouterModels . some ( ( m ) => m . id === modelId )
) {
determinedProvider = 'openrouter' ;
// Check if this is a free model (ends with :free)
if ( modelId . endsWith ( ':free' ) ) {
warningMessage = ` Warning: OpenRouter free model ' ${ modelId } ' selected. Free models have significant limitations including lower context windows, reduced rate limits, and may not support advanced features like tool_use. Consider using the paid version ' ${ modelId . replace ( ':free' , '' ) } ' for full functionality. ` ;
} else {
warningMessage = ` Warning: Custom OpenRouter model ' ${ modelId } ' set. This model is not officially validated by Taskmaster and may not function as expected. ` ;
}
report ( 'warn' , warningMessage ) ;
} else {
// Hinted as OpenRouter but not found in live check
throw new Error (
` 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' ) {
// Check Ollama ONLY because hint was ollama
report ( 'info' , ` Checking Ollama for ${ modelId } (as hinted)... ` ) ;
// Get the Ollama base URL from config
const ollamaBaseURL = getBaseUrlForRole ( role , projectRoot ) ;
const ollamaModels = await fetchOllamaModels ( ollamaBaseURL ) ;
if ( ollamaModels === null ) {
// Connection failed - server probably not running
throw new Error (
` 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' ;
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 {
// Server is running but model not found
const tagsUrl = ` ${ ollamaBaseURL } /tags ` ;
throw new Error (
` 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' ) {
// Set provider without model validation since Bedrock models are managed by AWS
determinedProvider = '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 {
// Invalid provider hint - should not happen
throw new Error ( ` Invalid provider hint received: ${ providerHint } ` ) ;
}
}
} else {
// No hint provided (flags not used)
if ( modelData ) {
// Found internally, use the provider from the internal list
determinedProvider = modelData . provider ;
report (
'info' ,
` Model ${ modelId } found internally with provider ${ determinedProvider } . `
) ;
} else {
// Model not found and no provider hint was given
return {
success : false ,
error : {
code : 'MODEL_NOT_FOUND_NO_HINT' ,
message : ` Model ID " ${ modelId } " not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter or --ollama. `
}
} ;
}
}
// --- End of Revised Logic --- //
// At this point, we should have a determinedProvider if the model is valid (internally or custom)
if ( ! determinedProvider ) {
// This case acts as a safeguard
return {
success : false ,
error : {
code : 'PROVIDER_UNDETERMINED' ,
message : ` Could not determine the provider for model ID " ${ modelId } ". `
}
} ;
}
// Update configuration
currentConfig . models [ role ] = {
... currentConfig . models [ role ] , // Keep existing params like maxTokens
provider : determinedProvider ,
modelId : modelId
} ;
// Write updated configuration
const writeResult = writeConfig ( currentConfig , projectRoot ) ;
if ( ! writeResult ) {
return {
success : false ,
error : {
code : 'CONFIG_WRITE_ERROR' ,
message : 'Error writing updated configuration to configuration file'
}
} ;
}
const successMessage = ` Successfully set ${ role } model to ${ modelId } (Provider: ${ determinedProvider } ) ` ;
report ( 'info' , successMessage ) ;
return {
success : true ,
data : {
role ,
provider : determinedProvider ,
modelId ,
message : successMessage ,
warning : warningMessage // Include warning in the response data
}
} ;
} catch ( error ) {
report ( 'error' , ` Error setting ${ role } model: ${ error . message } ` ) ;
return {
success : false ,
error : {
code : 'SET_MODEL_ERROR' ,
message : error . message
}
} ;
}
2025-04-23 15:47:33 -04:00
}
2025-04-27 03:56:23 -04:00
/ * *
* Get API key status for all known providers .
* @ param { Object } [ options ] - Options for the operation
* @ param { Object } [ options . session ] - Session object containing environment variables ( for MCP )
* @ param { Function } [ options . mcpLog ] - MCP logger object ( for MCP )
* @ param { string } [ options . projectRoot ] - Project root directory
* @ returns { Object } RESTful response with API key status report
* /
async function getApiKeyStatusReport ( options = { } ) {
2025-06-07 20:30:51 -04:00
const { mcpLog , projectRoot , session } = options ;
const report = ( level , ... args ) => {
if ( mcpLog && typeof mcpLog [ level ] === 'function' ) {
mcpLog [ level ] ( ... args ) ;
}
} ;
try {
const providers = getAllProviders ( ) ;
const providersToCheck = providers . filter (
( p ) => p . toLowerCase ( ) !== 'ollama'
) ; // Ollama is not a provider, it's a service, doesn't need an api key usually
const statusReport = providersToCheck . map ( ( provider ) => {
// Use provided projectRoot for MCP status check
const cliOk = isApiKeySet ( provider , session , projectRoot ) ; // Pass session and projectRoot for CLI check
const mcpOk = getMcpApiKeyStatus ( provider , projectRoot ) ;
return {
provider ,
cli : cliOk ,
mcp : mcpOk
} ;
} ) ;
report ( 'info' , 'Successfully generated API key status report.' ) ;
return {
success : true ,
data : {
report : statusReport ,
message : 'API key status report generated.'
}
} ;
} catch ( error ) {
report ( 'error' , ` Error generating API key status report: ${ error . message } ` ) ;
return {
success : false ,
error : {
code : 'API_KEY_STATUS_ERROR' ,
message : error . message
}
} ;
}
2025-04-27 03:56:23 -04:00
}
export {
2025-06-07 20:30:51 -04:00
getModelConfiguration ,
getAvailableModelsList ,
setModel ,
getApiKeyStatusReport
2025-04-27 03:56:23 -04:00
} ;