-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Expand file tree
/
Copy pathmodels.js
More file actions
841 lines (770 loc) · 28.3 KB
/
models.js
File metadata and controls
841 lines (770 loc) · 28.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
/**
* models.js
* Core functionality for managing AI model configurations
*/
import http from 'http';
import https from 'https';
import { CUSTOM_PROVIDERS } from '@tm/core';
import { findConfigPath } from '../../../src/utils/path-utils.js';
import {
getAllProviders,
getAvailableModels,
getBaseUrlForRole,
getConfig,
getFallbackModelId,
getFallbackProvider,
getMainModelId,
getMainProvider,
getMcpApiKeyStatus,
getResearchModelId,
getResearchProvider,
isApiKeySet,
isConfigFilePresent,
writeConfig
} from '../config-manager.js';
import { log } from '../utils.js';
// Constants
const CONFIG_MISSING_ERROR =
'The configuration file is missing. Run "task-master init" to create it.';
/**
* 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() {
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();
});
}
/**
* 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
* @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 = {}) {
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(CONFIG_MISSING_ERROR);
}
try {
// Get current settings - these should use the config from the found path automatically
const mainProvider = getMainProvider(projectRoot);
const mainModelId = getMainModelId(projectRoot);
const mainBaseURL = getBaseUrlForRole('main', projectRoot);
const researchProvider = getResearchProvider(projectRoot);
const researchModelId = getResearchModelId(projectRoot);
const researchBaseURL = getBaseUrlForRole('research', projectRoot);
const fallbackProvider = getFallbackProvider(projectRoot);
const fallbackModelId = getFallbackModelId(projectRoot);
const fallbackBaseURL = getBaseUrlForRole('fallback', 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,
baseURL: mainBaseURL,
sweScore: mainModelData?.swe_score || null,
cost: mainModelData?.cost_per_1m_tokens || null,
keyStatus: {
cli: mainCliKeyOk,
mcp: mainMcpKeyOk
}
},
research: {
provider: researchProvider,
modelId: researchModelId,
baseURL: researchBaseURL,
sweScore: researchModelData?.swe_score || null,
cost: researchModelData?.cost_per_1m_tokens || null,
keyStatus: {
cli: researchCliKeyOk,
mcp: researchMcpKeyOk
}
},
fallback: fallbackProvider
? {
provider: fallbackProvider,
modelId: fallbackModelId,
baseURL: fallbackBaseURL,
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
}
};
}
}
/**
* 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 = {}) {
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(CONFIG_MISSING_ERROR);
}
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
}
};
}
}
/**
* 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
* @param {string} [options.providerHint] - Provider hint if already determined ('openrouter' or 'ollama')
* @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 = {}) {
const { mcpLog, projectRoot, providerHint, baseURL } = options;
let computedBaseURL = baseURL; // Track the computed baseURL separately
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(CONFIG_MISSING_ERROR);
}
// 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
// If we have a provider hint, search for exact provider+model match
// Otherwise, just search by model ID (will get first match)
let modelData;
if (providerHint) {
// Search for model with specific provider
modelData = availableModels.find(
(m) => m.id === modelId && m.provider === providerHint
);
} else {
// Search by ID only
modelData = availableModels.find((m) => m.id === modelId);
}
// --- Revised Logic: Prioritize providerHint --- //
if (providerHint) {
// Hint provided (from interactive setup or flag)
if (modelData && modelData.provider === providerHint) {
// Found internally with matching provider
determinedProvider = providerHint;
report(
'info',
`Model ${modelId} found internally with provider ${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 === CUSTOM_PROVIDERS.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 = CUSTOM_PROVIDERS.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 === CUSTOM_PROVIDERS.OLLAMA) {
// Check Ollama ONLY because hint was ollama
report('info', `Checking Ollama for ${modelId} (as hinted)...`);
// Get current provider for this role to check if we should preserve baseURL
let currentProvider;
if (role === 'main') {
currentProvider = getMainProvider(projectRoot);
} else if (role === 'research') {
currentProvider = getResearchProvider(projectRoot);
} else if (role === 'fallback') {
currentProvider = getFallbackProvider(projectRoot);
}
// Only preserve baseURL if we're already using OLLAMA
const existingBaseURL =
currentProvider === CUSTOM_PROVIDERS.OLLAMA
? getBaseUrlForRole(role, projectRoot)
: null;
// Get the Ollama base URL - use provided, existing, or default
const ollamaBaseURL =
baseURL || existingBaseURL || 'http://localhost:11434/api';
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 = 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);
// Store the computed baseURL so it gets saved in config
computedBaseURL = ollamaBaseURL;
} 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 === CUSTOM_PROVIDERS.BEDROCK) {
// Set provider without model validation since Bedrock models are managed by AWS
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.CLAUDE_CODE) {
// Claude Code provider - check if model exists in our list
determinedProvider = CUSTOM_PROVIDERS.CLAUDE_CODE;
// Re-find modelData specifically for claude-code provider
const claudeCodeModels = availableModels.filter(
(m) => m.provider === 'claude-code'
);
const claudeCodeModelData = claudeCodeModels.find(
(m) => m.id === modelId
);
if (claudeCodeModelData) {
// Update modelData to the found claude-code model
modelData = claudeCodeModelData;
report('info', `Setting Claude Code model '${modelId}'.`);
} else {
warningMessage = `Warning: Claude Code model '${modelId}' not found in supported models. Setting without validation.`;
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;
// Get current provider for this role to check if we should preserve baseURL
let currentProvider;
if (role === 'main') {
currentProvider = getMainProvider(projectRoot);
} else if (role === 'research') {
currentProvider = getResearchProvider(projectRoot);
} else if (role === 'fallback') {
currentProvider = getFallbackProvider(projectRoot);
}
// Only preserve baseURL if we're already using AZURE
const existingBaseURL =
currentProvider === CUSTOM_PROVIDERS.AZURE
? getBaseUrlForRole(role, projectRoot)
: null;
const resolvedBaseURL = baseURL || existingBaseURL;
if (!resolvedBaseURL) {
throw new Error(
`Base URL is required for Azure providers. Please provide a baseURL or set global.azureBaseURL in config.`
);
}
warningMessage = `Warning: Custom Azure model '${modelId}' set with base URL '${resolvedBaseURL}'. Please ensure the model deployment is valid and accessible in your Azure account.`;
report('warn', warningMessage);
// Store the computed baseURL so it gets saved in config
computedBaseURL = resolvedBaseURL;
} 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 if (providerHint === CUSTOM_PROVIDERS.VERTEX_ANTHROPIC) {
// Set provider without model validation since Vertex Anthropic models are managed by Google Cloud
determinedProvider = CUSTOM_PROVIDERS.VERTEX_ANTHROPIC;
warningMessage = `Warning: Custom Vertex AI Anthropic model '${modelId}' set. Please ensure the model is valid and accessible in your Google Cloud project.`;
report('warn', warningMessage);
} else if (providerHint === CUSTOM_PROVIDERS.GEMINI_CLI) {
// Gemini CLI provider - check if model exists in our list
determinedProvider = CUSTOM_PROVIDERS.GEMINI_CLI;
// Re-find modelData specifically for gemini-cli provider
const geminiCliModels = availableModels.filter(
(m) => m.provider === 'gemini-cli'
);
const geminiCliModelData = geminiCliModels.find(
(m) => m.id === modelId
);
if (geminiCliModelData) {
// Update modelData to the found gemini-cli model
modelData = geminiCliModelData;
report('info', `Setting Gemini CLI model '${modelId}'.`);
} else {
warningMessage = `Warning: Gemini CLI model '${modelId}' not found in supported models. Setting without validation.`;
report('warn', warningMessage);
}
} else if (providerHint === CUSTOM_PROVIDERS.CODEX_CLI) {
// Codex CLI provider - enforce supported model list
determinedProvider = CUSTOM_PROVIDERS.CODEX_CLI;
const codexCliModels = availableModels.filter(
(m) => m.provider === 'codex-cli'
);
const codexCliModelData = codexCliModels.find(
(m) => m.id === modelId
);
if (codexCliModelData) {
modelData = codexCliModelData;
report('info', `Setting Codex CLI model '${modelId}'.`);
} else {
warningMessage = `Warning: Codex CLI model '${modelId}' not found in supported models. Setting without validation.`;
report('warn', warningMessage);
}
} else if (providerHint === CUSTOM_PROVIDERS.LMSTUDIO) {
// LM Studio provider - set without validation since it's a local server
determinedProvider = CUSTOM_PROVIDERS.LMSTUDIO;
// Get current provider for this role to check if we should preserve baseURL
let currentProvider;
if (role === 'main') {
currentProvider = getMainProvider(projectRoot);
} else if (role === 'research') {
currentProvider = getResearchProvider(projectRoot);
} else if (role === 'fallback') {
currentProvider = getFallbackProvider(projectRoot);
}
// Only preserve baseURL if we're already using LMSTUDIO
const existingBaseURL =
currentProvider === CUSTOM_PROVIDERS.LMSTUDIO
? getBaseUrlForRole(role, projectRoot)
: null;
const lmStudioBaseURL =
baseURL || existingBaseURL || 'http://localhost:1234/v1';
warningMessage = `Warning: Custom LM Studio model '${modelId}' set with base URL '${lmStudioBaseURL}'. Please ensure LM Studio server is running and has loaded this model. Taskmaster cannot guarantee compatibility.`;
report('warn', warningMessage);
// Store the computed baseURL so it gets saved in config
computedBaseURL = lmStudioBaseURL;
} else if (providerHint === CUSTOM_PROVIDERS.OPENAI_COMPATIBLE) {
// OpenAI-compatible provider - set without validation, requires baseURL
determinedProvider = CUSTOM_PROVIDERS.OPENAI_COMPATIBLE;
// Get current provider for this role to check if we should preserve baseURL
let currentProvider;
if (role === 'main') {
currentProvider = getMainProvider(projectRoot);
} else if (role === 'research') {
currentProvider = getResearchProvider(projectRoot);
} else if (role === 'fallback') {
currentProvider = getFallbackProvider(projectRoot);
}
// Only preserve baseURL if we're already using OPENAI_COMPATIBLE
const existingBaseURL =
currentProvider === CUSTOM_PROVIDERS.OPENAI_COMPATIBLE
? getBaseUrlForRole(role, projectRoot)
: null;
const resolvedBaseURL = baseURL || existingBaseURL;
if (!resolvedBaseURL) {
throw new Error(
`Base URL is required for OpenAI-compatible providers. Please provide a baseURL.`
);
}
warningMessage = `Warning: Custom OpenAI-compatible model '${modelId}' set with base URL '${resolvedBaseURL}'. Taskmaster cannot guarantee compatibility. Ensure your API endpoint follows the OpenAI API specification.`;
report('warn', warningMessage);
// Store the computed baseURL so it gets saved in config
computedBaseURL = resolvedBaseURL;
} else {
// Invalid provider hint - should not happen with our constants
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, --ollama, --bedrock, --azure, --vertex, --vertex-anthropic, --lmstudio, --openai-compatible, --gemini-cli, or --codex-cli.`
}
};
}
}
// --- 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 temperature
provider: determinedProvider,
modelId: modelId
};
// Handle baseURL for providers that support it
if (
computedBaseURL &&
(determinedProvider === CUSTOM_PROVIDERS.OPENAI_COMPATIBLE ||
determinedProvider === CUSTOM_PROVIDERS.LMSTUDIO ||
determinedProvider === CUSTOM_PROVIDERS.OLLAMA ||
determinedProvider === CUSTOM_PROVIDERS.AZURE)
) {
currentConfig.models[role].baseURL = computedBaseURL;
} else {
// Remove baseURL when switching to a provider that doesn't need it
delete currentConfig.models[role].baseURL;
}
// If model data is available, update maxTokens from supported-models.json
if (modelData && modelData.max_tokens) {
currentConfig.models[role].maxTokens = modelData.max_tokens;
}
// 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
}
};
}
}
/**
* 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 = {}) {
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
}
};
}
}
export {
getModelConfiguration,
getAvailableModelsList,
setModel,
getApiKeyStatusReport
};