feat(models): Add comprehensive Ollama model validation and interactive setup - Add 'Custom Ollama model' option to interactive setup (--setup) - Implement live validation against local Ollama instance via /api/tags - Support configurable Ollama endpoints from .taskmasterconfig - Add robust error handling for server connectivity and model existence - Enhance user experience with clear validation feedback - Support both MCP server and CLI interfaces
This commit is contained in:
15
.changeset/better-seas-sit.md
Normal file
15
.changeset/better-seas-sit.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
'task-master-ai': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added comprehensive Ollama model validation and interactive setup support
|
||||||
|
|
||||||
|
- **Interactive Setup Enhancement**: Added "Custom Ollama model" option to `task-master models --setup`, matching the existing OpenRouter functionality
|
||||||
|
- **Live Model Validation**: When setting Ollama models, Taskmaster now validates against the local Ollama instance by querying `/api/tags` endpoint
|
||||||
|
- **Configurable Endpoints**: Uses the `ollamaBaseUrl` from `.taskmasterconfig` (with role-specific `baseUrl` overrides supported)
|
||||||
|
- **Robust Error Handling**:
|
||||||
|
- Detects when Ollama server is not running and provides clear error messages
|
||||||
|
- Validates model existence and lists available alternatives when model not found
|
||||||
|
- Graceful fallback behavior for connection issues
|
||||||
|
- **Full Platform Support**: Both MCP server tools and CLI commands support the new validation
|
||||||
|
- **Improved User Experience**: Clear feedback during model validation with informative success/error messages
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"models": {
|
"models": {
|
||||||
"main": {
|
"main": {
|
||||||
"provider": "anthropic",
|
"provider": "anthropic",
|
||||||
"modelId": "claude-3-7-sonnet-20250219",
|
"modelId": "claude-sonnet-4-20250514",
|
||||||
"maxTokens": 120000,
|
"maxTokens": 120000,
|
||||||
"temperature": 0.2
|
"temperature": 0.2
|
||||||
},
|
},
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
},
|
},
|
||||||
"fallback": {
|
"fallback": {
|
||||||
"provider": "anthropic",
|
"provider": "anthropic",
|
||||||
"modelId": "claude-3-5-sonnet-20241022",
|
"modelId": "claude-3-7-sonnet-20250219",
|
||||||
"maxTokens": 8192,
|
"maxTokens": 8192,
|
||||||
"temperature": 0.2
|
"temperature": 0.2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import chalk from 'chalk';
|
|||||||
import boxen from 'boxen';
|
import boxen from 'boxen';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
|
import http from 'http';
|
||||||
import inquirer from 'inquirer';
|
import inquirer from 'inquirer';
|
||||||
import ora from 'ora'; // Import ora
|
import ora from 'ora'; // Import ora
|
||||||
|
|
||||||
@@ -48,7 +49,8 @@ import {
|
|||||||
writeConfig,
|
writeConfig,
|
||||||
ConfigurationError,
|
ConfigurationError,
|
||||||
isConfigFilePresent,
|
isConfigFilePresent,
|
||||||
getAvailableModels
|
getAvailableModels,
|
||||||
|
getBaseUrlForRole
|
||||||
} from './config-manager.js';
|
} from './config-manager.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -153,6 +155,64 @@ async function runInteractiveSetup(projectRoot) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to fetch Ollama models (duplicated for CLI context)
|
||||||
|
function fetchOllamaModelsCLI(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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to get choices and default index for a role
|
// Helper to get choices and default index for a role
|
||||||
const getPromptData = (role, allowNone = false) => {
|
const getPromptData = (role, allowNone = false) => {
|
||||||
const currentModel = currentModels[role]; // Use the fetched data
|
const currentModel = currentModels[role]; // Use the fetched data
|
||||||
@@ -180,6 +240,11 @@ async function runInteractiveSetup(projectRoot) {
|
|||||||
value: '__CUSTOM_OPENROUTER__'
|
value: '__CUSTOM_OPENROUTER__'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const customOllamaOption = {
|
||||||
|
name: '* Custom Ollama model', // Symbol updated
|
||||||
|
value: '__CUSTOM_OLLAMA__'
|
||||||
|
};
|
||||||
|
|
||||||
let choices = [];
|
let choices = [];
|
||||||
let defaultIndex = 0; // Default to 'Cancel'
|
let defaultIndex = 0; // Default to 'Cancel'
|
||||||
|
|
||||||
@@ -225,6 +290,7 @@ async function runInteractiveSetup(projectRoot) {
|
|||||||
}
|
}
|
||||||
commonPrefix.push(cancelOption);
|
commonPrefix.push(cancelOption);
|
||||||
commonPrefix.push(customOpenRouterOption);
|
commonPrefix.push(customOpenRouterOption);
|
||||||
|
commonPrefix.push(customOllamaOption);
|
||||||
|
|
||||||
let prefixLength = commonPrefix.length; // Initial prefix length
|
let prefixLength = commonPrefix.length; // Initial prefix length
|
||||||
|
|
||||||
@@ -355,6 +421,47 @@ async function runInteractiveSetup(projectRoot) {
|
|||||||
setupSuccess = false;
|
setupSuccess = false;
|
||||||
return true; // Continue setup, but mark as failed
|
return true; // Continue setup, but mark as failed
|
||||||
}
|
}
|
||||||
|
} else if (selectedValue === '__CUSTOM_OLLAMA__') {
|
||||||
|
isCustomSelection = true;
|
||||||
|
const { customId } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'customId',
|
||||||
|
message: `Enter the custom Ollama Model ID for the ${role} role:`
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
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 = 'ollama';
|
||||||
|
// Get the Ollama base URL from config for this role
|
||||||
|
const ollamaBaseUrl = getBaseUrlForRole(role, projectRoot);
|
||||||
|
// Validate against live Ollama list
|
||||||
|
const ollamaModels = await fetchOllamaModelsCLI(ollamaBaseUrl);
|
||||||
|
if (ollamaModels === null) {
|
||||||
|
console.error(
|
||||||
|
chalk.red(
|
||||||
|
`Error: Unable to connect to Ollama server at ${ollamaBaseUrl}. Please ensure Ollama is running and try again.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setupSuccess = false;
|
||||||
|
return true; // Continue setup, but mark as failed
|
||||||
|
} else if (!ollamaModels.some((m) => m.model === modelIdToSet)) {
|
||||||
|
console.error(
|
||||||
|
chalk.red(
|
||||||
|
`Error: Model ID "${modelIdToSet}" not found in the Ollama instance. Please verify the model is pulled and available.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
`You can check available models with: curl ${ollamaBaseUrl}/tags`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setupSuccess = false;
|
||||||
|
return true; // Continue setup, but mark as failed
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
selectedValue &&
|
selectedValue &&
|
||||||
typeof selectedValue === 'object' &&
|
typeof selectedValue === 'object' &&
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
|
import http from 'http';
|
||||||
import {
|
import {
|
||||||
getMainModelId,
|
getMainModelId,
|
||||||
getResearchModelId,
|
getResearchModelId,
|
||||||
@@ -19,7 +20,8 @@ import {
|
|||||||
getConfig,
|
getConfig,
|
||||||
writeConfig,
|
writeConfig,
|
||||||
isConfigFilePresent,
|
isConfigFilePresent,
|
||||||
getAllProviders
|
getAllProviders,
|
||||||
|
getBaseUrlForRole
|
||||||
} from '../config-manager.js';
|
} from '../config-manager.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,6 +70,68 @@ function fetchOpenRouterModels() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the list of models from Ollama instance.
|
||||||
|
* @param {string} baseUrl - The base URL for the Ollama API (e.g., "http://localhost:11434/api")
|
||||||
|
* @returns {Promise<Array|null>} A promise that resolves with the list of model objects or null if fetch fails.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current model configuration
|
* Get the current model configuration
|
||||||
* @param {Object} [options] - Options for the operation
|
* @param {Object} [options] - Options for the operation
|
||||||
@@ -416,10 +480,29 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (providerHint === 'ollama') {
|
} else if (providerHint === 'ollama') {
|
||||||
// Hinted as Ollama - set provider directly WITHOUT checking OpenRouter
|
// Check Ollama ONLY because hint was ollama
|
||||||
determinedProvider = 'ollama';
|
report('info', `Checking Ollama for ${modelId} (as hinted)...`);
|
||||||
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);
|
// 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 {
|
} else {
|
||||||
// Invalid provider hint - should not happen
|
// Invalid provider hint - should not happen
|
||||||
throw new Error(`Invalid provider hint received: ${providerHint}`);
|
throw new Error(`Invalid provider hint received: ${providerHint}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user