mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
Merge pull request #1372 from eyaltoledano/next
This commit is contained in:
13
.changeset/light-clowns-hope.md
Normal file
13
.changeset/light-clowns-hope.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Add support for ZAI (GLM) Coding Plan subscription endpoint as a separate provider. Users can now select between two ZAI providers:
|
||||
|
||||
- **zai**: Standard ZAI endpoint (`https://api.z.ai/api/paas/v4/`)
|
||||
- **zai-coding**: Coding Plan endpoint (`https://api.z.ai/api/coding/paas/v4/`)
|
||||
|
||||
Both providers use the same model IDs (glm-4.6, glm-4.5) but route to different API endpoints based on your subscription. When running `tm models --setup`, you'll see both providers listed separately:
|
||||
|
||||
- `zai / glm-4.6` - Standard endpoint
|
||||
- `zai-coding / glm-4.6` - Coding Plan endpoint
|
||||
8
.changeset/polite-jokes-flash.md
Normal file
8
.changeset/polite-jokes-flash.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Improved auto-update experience:
|
||||
|
||||
- updates now happen before your CLI command runs and automatically restart to execute your command with the new version.
|
||||
- No more manual restarts needed!
|
||||
@@ -67,17 +67,21 @@ export function buildPromptChoices(
|
||||
.flatMap(([provider, models]) => {
|
||||
return models
|
||||
.filter((m) => m.allowed_roles && m.allowed_roles.includes(role))
|
||||
.map((m) => ({
|
||||
name: `${provider} / ${m.id} ${
|
||||
m.cost_per_1m_tokens
|
||||
? chalk.gray(
|
||||
`($${m.cost_per_1m_tokens.input.toFixed(2)} input | $${m.cost_per_1m_tokens.output.toFixed(2)} output)`
|
||||
)
|
||||
: ''
|
||||
}`,
|
||||
value: { id: m.id, provider },
|
||||
short: `${provider}/${m.id}`
|
||||
}));
|
||||
.map((m) => {
|
||||
// Use model name if available, otherwise fall back to model ID
|
||||
const displayName = m.name || m.id;
|
||||
return {
|
||||
name: `${provider} / ${displayName} ${
|
||||
m.cost_per_1m_tokens
|
||||
? chalk.gray(
|
||||
`($${m.cost_per_1m_tokens.input.toFixed(2)} input | $${m.cost_per_1m_tokens.output.toFixed(2)} output)`
|
||||
)
|
||||
: ''
|
||||
}`,
|
||||
value: { id: m.id, provider },
|
||||
short: `${provider}/${displayName}`
|
||||
};
|
||||
});
|
||||
})
|
||||
.filter((choice) => choice !== null);
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ export const CONTROL_VALUES = {
|
||||
*/
|
||||
export interface ModelInfo {
|
||||
id: string;
|
||||
name?: string;
|
||||
provider: string;
|
||||
cost_per_1m_tokens?: {
|
||||
input: number;
|
||||
|
||||
@@ -32,7 +32,8 @@ export {
|
||||
checkForUpdate,
|
||||
performAutoUpdate,
|
||||
displayUpgradeNotification,
|
||||
compareVersions
|
||||
compareVersions,
|
||||
restartWithNewVersion
|
||||
} from './utils/auto-update.js';
|
||||
|
||||
export { runInteractiveSetup } from './commands/models/index.js';
|
||||
|
||||
@@ -7,6 +7,7 @@ import https from 'https';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import boxen from 'boxen';
|
||||
import process from 'process';
|
||||
|
||||
export interface UpdateInfo {
|
||||
currentVersion: string;
|
||||
@@ -345,9 +346,6 @@ export async function performAutoUpdate(
|
||||
`Successfully updated to version ${chalk.bold(latestVersion)}`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
chalk.dim('Please restart your command to use the new version.')
|
||||
);
|
||||
resolve(true);
|
||||
} else {
|
||||
spinner.fail(chalk.red('Auto-update failed'));
|
||||
@@ -375,3 +373,37 @@ export async function performAutoUpdate(
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the CLI with the newly installed version
|
||||
* @param argv - Original command-line arguments (process.argv)
|
||||
*/
|
||||
export function restartWithNewVersion(argv: string[]): void {
|
||||
const args = argv.slice(2); // Remove 'node' and script path
|
||||
|
||||
console.log(chalk.dim('Restarting with updated version...\n'));
|
||||
|
||||
// Spawn the updated task-master command
|
||||
const child = spawn('task-master', args, {
|
||||
stdio: 'inherit', // Inherit stdin/stdout/stderr so it looks seamless
|
||||
detached: false,
|
||||
shell: process.platform === 'win32' // Windows compatibility
|
||||
});
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal);
|
||||
return;
|
||||
}
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
console.error(
|
||||
chalk.red('Failed to restart with new version:'),
|
||||
error.message
|
||||
);
|
||||
console.log(chalk.yellow('Please run your command again manually.'));
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Available Models as of October 31, 2025
|
||||
# Available Models as of November 1, 2025
|
||||
|
||||
## Main Models
|
||||
|
||||
@@ -80,7 +80,9 @@
|
||||
| zai | glm-4.6 | 0.68 | 0.6 | 2.2 |
|
||||
| zai | glm-4.5 | 0.65 | 0.6 | 2.2 |
|
||||
| zai | glm-4.5-air | 0.62 | 0.2 | 1.1 |
|
||||
| zai | glm-4.5v | 0.63 | 0.6 | 1.8 |
|
||||
| zai-coding | glm-4.6 | 0.68 | 0 | 0 |
|
||||
| zai-coding | glm-4.5 | 0.65 | 0 | 0 |
|
||||
| zai-coding | glm-4.5-air | 0.62 | 0 | 0 |
|
||||
| ollama | gpt-oss:latest | 0.607 | 0 | 0 |
|
||||
| ollama | gpt-oss:20b | 0.607 | 0 | 0 |
|
||||
| ollama | gpt-oss:120b | 0.624 | 0 | 0 |
|
||||
@@ -136,6 +138,10 @@
|
||||
| perplexity | sonar-reasoning | 0.211 | 1 | 5 |
|
||||
| zai | glm-4.6 | 0.68 | 0.6 | 2.2 |
|
||||
| zai | glm-4.5 | 0.65 | 0.6 | 2.2 |
|
||||
| zai | glm-4.5-air | 0.62 | 0.2 | 1.1 |
|
||||
| zai-coding | glm-4.6 | 0.68 | 0 | 0 |
|
||||
| zai-coding | glm-4.5 | 0.65 | 0 | 0 |
|
||||
| zai-coding | glm-4.5-air | 0.62 | 0 | 0 |
|
||||
| bedrock | us.anthropic.claude-3-opus-20240229-v1:0 | 0.725 | 15 | 75 |
|
||||
| bedrock | us.anthropic.claude-3-5-sonnet-20240620-v1:0 | 0.49 | 3 | 15 |
|
||||
| bedrock | us.anthropic.claude-3-5-sonnet-20241022-v2:0 | 0.49 | 3 | 15 |
|
||||
@@ -211,7 +217,9 @@
|
||||
| zai | glm-4.6 | 0.68 | 0.6 | 2.2 |
|
||||
| zai | glm-4.5 | 0.65 | 0.6 | 2.2 |
|
||||
| zai | glm-4.5-air | 0.62 | 0.2 | 1.1 |
|
||||
| zai | glm-4.5v | 0.63 | 0.6 | 1.8 |
|
||||
| zai-coding | glm-4.6 | 0.68 | 0 | 0 |
|
||||
| zai-coding | glm-4.5 | 0.65 | 0 | 0 |
|
||||
| zai-coding | glm-4.5-air | 0.62 | 0 | 0 |
|
||||
| ollama | gpt-oss:latest | 0.607 | 0 | 0 |
|
||||
| ollama | gpt-oss:20b | 0.607 | 0 | 0 |
|
||||
| ollama | gpt-oss:120b | 0.624 | 0 | 0 |
|
||||
|
||||
@@ -82,7 +82,7 @@ export function registerModelsTool(server) {
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Custom base URL for openai-compatible provider (e.g., https://api.example.com/v1)'
|
||||
'Custom base URL for providers that support it (e.g., https://api.example.com/v1).'
|
||||
)
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ export const VALIDATED_PROVIDERS = [
|
||||
'openai',
|
||||
'google',
|
||||
'zai',
|
||||
'zai-coding',
|
||||
'perplexity',
|
||||
'xai',
|
||||
'groq',
|
||||
|
||||
@@ -52,7 +52,8 @@ import {
|
||||
PerplexityAIProvider,
|
||||
VertexAIProvider,
|
||||
XAIProvider,
|
||||
ZAIProvider
|
||||
ZAIProvider,
|
||||
ZAICodingProvider
|
||||
} from '../../src/ai-providers/index.js';
|
||||
|
||||
// Import the provider registry
|
||||
@@ -64,6 +65,7 @@ const PROVIDERS = {
|
||||
perplexity: new PerplexityAIProvider(),
|
||||
google: new GoogleAIProvider(),
|
||||
zai: new ZAIProvider(),
|
||||
'zai-coding': new ZAICodingProvider(),
|
||||
lmstudio: new LMStudioProvider(),
|
||||
openai: new OpenAIProvider(),
|
||||
xai: new XAIProvider(),
|
||||
|
||||
@@ -8,10 +8,7 @@ import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import fs from 'fs';
|
||||
import https from 'https';
|
||||
import http from 'http';
|
||||
import inquirer from 'inquirer';
|
||||
import search from '@inquirer/search';
|
||||
|
||||
import { log, readJSON } from './utils.js';
|
||||
// Import command registry and utilities from @tm/cli
|
||||
@@ -20,6 +17,7 @@ import {
|
||||
checkForUpdate,
|
||||
performAutoUpdate,
|
||||
displayUpgradeNotification,
|
||||
restartWithNewVersion,
|
||||
displayError,
|
||||
runInteractiveSetup
|
||||
} from '@tm/cli';
|
||||
@@ -4447,18 +4445,10 @@ async function runCLI(argv = process.argv) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Start the update check in the background - don't await yet
|
||||
// Check for updates BEFORE executing the command
|
||||
const currentVersion = getTaskMasterVersion();
|
||||
const updateCheckPromise = checkForUpdate(currentVersion);
|
||||
const updateInfo = await checkForUpdate(currentVersion);
|
||||
|
||||
// Setup and parse
|
||||
// NOTE: getConfig() might be called during setupCLI->registerCommands if commands need config
|
||||
// This means the ConfigurationError might be thrown here if configuration file is missing.
|
||||
const programInstance = setupCLI();
|
||||
await programInstance.parseAsync(argv);
|
||||
|
||||
// After command execution, check if an update is available
|
||||
const updateInfo = await updateCheckPromise;
|
||||
if (updateInfo.needsUpdate) {
|
||||
// Display the upgrade notification first
|
||||
displayUpgradeNotification(
|
||||
@@ -4467,14 +4457,22 @@ async function runCLI(argv = process.argv) {
|
||||
updateInfo.highlights
|
||||
);
|
||||
|
||||
// Then automatically perform the update
|
||||
// Automatically perform the update
|
||||
const updateSuccess = await performAutoUpdate(updateInfo.latestVersion);
|
||||
if (updateSuccess) {
|
||||
// Exit gracefully after successful update
|
||||
process.exit(0);
|
||||
// Restart with the new version - this will execute the user's command
|
||||
restartWithNewVersion(argv);
|
||||
return; // Never reached, but for clarity
|
||||
}
|
||||
// If update fails, continue with current version
|
||||
}
|
||||
|
||||
// Setup and parse
|
||||
// NOTE: getConfig() might be called during setupCLI->registerCommands if commands need config
|
||||
// This means the ConfigurationError might be thrown here if configuration file is missing.
|
||||
const programInstance = setupCLI();
|
||||
await programInstance.parseAsync(argv);
|
||||
|
||||
// Check if migration has occurred and show FYI notice once
|
||||
try {
|
||||
// Use initTaskMaster with no required fields - will only fail if no project root
|
||||
|
||||
@@ -836,6 +836,8 @@ function isApiKeySet(providerName, session = null, projectRoot = null) {
|
||||
azure: 'AZURE_OPENAI_API_KEY',
|
||||
openrouter: 'OPENROUTER_API_KEY',
|
||||
xai: 'XAI_API_KEY',
|
||||
zai: 'ZAI_API_KEY',
|
||||
'zai-coding': 'ZAI_API_KEY',
|
||||
groq: 'GROQ_API_KEY',
|
||||
vertex: 'GOOGLE_API_KEY', // Vertex uses the same key as Google
|
||||
'claude-code': 'CLAUDE_CODE_API_KEY', // Not actually used, but included for consistency
|
||||
@@ -922,6 +924,11 @@ function getMcpApiKeyStatus(providerName, projectRoot = null) {
|
||||
apiKeyToCheck = mcpEnv.XAI_API_KEY;
|
||||
placeholderValue = 'YOUR_XAI_API_KEY_HERE';
|
||||
break;
|
||||
case 'zai':
|
||||
case 'zai-coding':
|
||||
apiKeyToCheck = mcpEnv.ZAI_API_KEY;
|
||||
placeholderValue = 'YOUR_ZAI_API_KEY_HERE';
|
||||
break;
|
||||
case 'groq':
|
||||
apiKeyToCheck = mcpEnv.GROQ_API_KEY;
|
||||
placeholderValue = 'YOUR_GROQ_API_KEY_HERE';
|
||||
|
||||
@@ -922,19 +922,43 @@
|
||||
"input": 0.2,
|
||||
"output": 1.1
|
||||
},
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"allowed_roles": ["main", "fallback", "research"],
|
||||
"max_tokens": 131072,
|
||||
"supported": true
|
||||
}
|
||||
],
|
||||
"zai-coding": [
|
||||
{
|
||||
"id": "glm-4.6",
|
||||
"swe_score": 0.68,
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 0,
|
||||
"output": 0
|
||||
},
|
||||
"allowed_roles": ["main", "fallback", "research"],
|
||||
"max_tokens": 204800,
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"id": "glm-4.5",
|
||||
"swe_score": 0.65,
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 0,
|
||||
"output": 0
|
||||
},
|
||||
"allowed_roles": ["main", "fallback", "research"],
|
||||
"max_tokens": 131072,
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"id": "glm-4.5v",
|
||||
"swe_score": 0.63,
|
||||
"id": "glm-4.5-air",
|
||||
"swe_score": 0.62,
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 0.6,
|
||||
"output": 1.8
|
||||
"input": 0,
|
||||
"output": 0
|
||||
},
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"max_tokens": 64000,
|
||||
"allowed_roles": ["main", "fallback", "research"],
|
||||
"max_tokens": 131072,
|
||||
"supported": true
|
||||
}
|
||||
],
|
||||
|
||||
@@ -429,19 +429,30 @@ async function setModel(role, modelId, options = {}) {
|
||||
let determinedProvider = null; // Initialize provider
|
||||
let warningMessage = null;
|
||||
|
||||
// Find the model data in internal list initially to see if it exists at all
|
||||
let modelData = availableModels.find((m) => m.id === modelId);
|
||||
// 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 (--ollama or --openrouter flag used)
|
||||
// Hint provided (from interactive setup or flag)
|
||||
if (modelData && modelData.provider === providerHint) {
|
||||
// Found internally AND provider matches the hint
|
||||
// Found internally with matching provider
|
||||
determinedProvider = providerHint;
|
||||
report(
|
||||
'info',
|
||||
`Model ${modelId} found internally with matching provider hint ${determinedProvider}.`
|
||||
`Model ${modelId} found internally with provider ${determinedProvider}.`
|
||||
);
|
||||
} else {
|
||||
// Either not found internally, OR found but under a DIFFERENT provider than hinted.
|
||||
|
||||
@@ -20,4 +20,5 @@ export { GrokCliProvider } from './grok-cli.js';
|
||||
export { CodexCliProvider } from './codex-cli.js';
|
||||
export { OpenAICompatibleProvider } from './openai-compatible.js';
|
||||
export { ZAIProvider } from './zai.js';
|
||||
export { ZAICodingProvider } from './zai-coding.js';
|
||||
export { LMStudioProvider } from './lmstudio.js';
|
||||
|
||||
21
src/ai-providers/zai-coding.js
Normal file
21
src/ai-providers/zai-coding.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* zai-coding.js
|
||||
* AI provider implementation for Z.ai (GLM) Coding Plan models.
|
||||
* Uses the exclusive coding API endpoint with OpenAI-compatible API.
|
||||
*/
|
||||
|
||||
import { OpenAICompatibleProvider } from './openai-compatible.js';
|
||||
|
||||
/**
|
||||
* Z.ai Coding Plan provider supporting GLM models through the dedicated coding endpoint.
|
||||
*/
|
||||
export class ZAICodingProvider extends OpenAICompatibleProvider {
|
||||
constructor() {
|
||||
super({
|
||||
name: 'Z.ai (Coding Plan)',
|
||||
apiKeyEnvVar: 'ZAI_API_KEY',
|
||||
requiresApiKey: true,
|
||||
defaultBaseURL: 'https://api.z.ai/api/coding/paas/v4/'
|
||||
});
|
||||
}
|
||||
}
|
||||
80
tests/unit/ai-providers/zai-coding.test.js
Normal file
80
tests/unit/ai-providers/zai-coding.test.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Tests for ZAICodingProvider
|
||||
*/
|
||||
|
||||
import { ZAICodingProvider } from '../../../src/ai-providers/zai-coding.js';
|
||||
|
||||
describe('ZAICodingProvider', () => {
|
||||
let provider;
|
||||
|
||||
beforeEach(() => {
|
||||
provider = new ZAICodingProvider();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with correct name', () => {
|
||||
expect(provider.name).toBe('Z.ai (Coding Plan)');
|
||||
});
|
||||
|
||||
it('should initialize with correct coding endpoint baseURL', () => {
|
||||
expect(provider.defaultBaseURL).toBe(
|
||||
'https://api.z.ai/api/coding/paas/v4/'
|
||||
);
|
||||
});
|
||||
|
||||
it('should inherit from OpenAICompatibleProvider', () => {
|
||||
expect(provider).toHaveProperty('generateText');
|
||||
expect(provider).toHaveProperty('streamText');
|
||||
expect(provider).toHaveProperty('generateObject');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRequiredApiKeyName', () => {
|
||||
it('should return ZAI_API_KEY environment variable name', () => {
|
||||
expect(provider.getRequiredApiKeyName()).toBe('ZAI_API_KEY');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRequiredApiKey', () => {
|
||||
it('should return true as API key is required', () => {
|
||||
expect(provider.isRequiredApiKey()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getClient', () => {
|
||||
it('should create client with API key', () => {
|
||||
const params = { apiKey: 'test-key' };
|
||||
const client = provider.getClient(params);
|
||||
expect(client).toBeDefined();
|
||||
});
|
||||
|
||||
it('should use coding endpoint by default', () => {
|
||||
const params = {
|
||||
apiKey: 'test-key'
|
||||
};
|
||||
const client = provider.getClient(params);
|
||||
expect(client).toBeDefined();
|
||||
// The provider should use the coding endpoint
|
||||
});
|
||||
|
||||
it('should throw error when API key is missing', () => {
|
||||
expect(() => {
|
||||
provider.getClient({});
|
||||
}).toThrow('Z.ai (Coding Plan) API key is required.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAuth', () => {
|
||||
it('should validate API key is present', () => {
|
||||
expect(() => {
|
||||
provider.validateAuth({});
|
||||
}).toThrow('Z.ai (Coding Plan) API key is required');
|
||||
});
|
||||
|
||||
it('should pass with valid API key', () => {
|
||||
expect(() => {
|
||||
provider.validateAuth({ apiKey: 'test-key' });
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -261,6 +261,13 @@ jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
|
||||
getRequiredApiKeyName: jest.fn(() => 'ZAI_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => true)
|
||||
})),
|
||||
ZAICodingProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'ZAI_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => true)
|
||||
})),
|
||||
LMStudioProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
|
||||
Reference in New Issue
Block a user