Merge pull request #1372 from eyaltoledano/next

This commit is contained in:
Ralph Khreish
2025-11-01 18:23:09 +01:00
committed by GitHub
18 changed files with 267 additions and 48 deletions

View 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

View 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!

View File

@@ -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);

View File

@@ -36,6 +36,7 @@ export const CONTROL_VALUES = {
*/
export interface ModelInfo {
id: string;
name?: string;
provider: string;
cost_per_1m_tokens?: {
input: number;

View File

@@ -32,7 +32,8 @@ export {
checkForUpdate,
performAutoUpdate,
displayUpgradeNotification,
compareVersions
compareVersions,
restartWithNewVersion
} from './utils/auto-update.js';
export { runInteractiveSetup } from './commands/models/index.js';

View File

@@ -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);
});
}

View File

@@ -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 |

View File

@@ -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 }) => {

View File

@@ -9,6 +9,7 @@ export const VALIDATED_PROVIDERS = [
'openai',
'google',
'zai',
'zai-coding',
'perplexity',
'xai',
'groq',

View File

@@ -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(),

View File

@@ -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

View File

@@ -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';

View File

@@ -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
}
],

View File

@@ -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.

View File

@@ -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';

View 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/'
});
}
}

View 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();
});
});
});

View File

@@ -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(),