mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
fix: add z.ai coding plan support (#1370)
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
|
||||||
@@ -67,17 +67,21 @@ export function buildPromptChoices(
|
|||||||
.flatMap(([provider, models]) => {
|
.flatMap(([provider, models]) => {
|
||||||
return models
|
return models
|
||||||
.filter((m) => m.allowed_roles && m.allowed_roles.includes(role))
|
.filter((m) => m.allowed_roles && m.allowed_roles.includes(role))
|
||||||
.map((m) => ({
|
.map((m) => {
|
||||||
name: `${provider} / ${m.id} ${
|
// Use model name if available, otherwise fall back to model ID
|
||||||
m.cost_per_1m_tokens
|
const displayName = m.name || m.id;
|
||||||
? chalk.gray(
|
return {
|
||||||
`($${m.cost_per_1m_tokens.input.toFixed(2)} input | $${m.cost_per_1m_tokens.output.toFixed(2)} output)`
|
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}/${m.id}`
|
: ''
|
||||||
}));
|
}`,
|
||||||
|
value: { id: m.id, provider },
|
||||||
|
short: `${provider}/${displayName}`
|
||||||
|
};
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.filter((choice) => choice !== null);
|
.filter((choice) => choice !== null);
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export const CONTROL_VALUES = {
|
|||||||
*/
|
*/
|
||||||
export interface ModelInfo {
|
export interface ModelInfo {
|
||||||
id: string;
|
id: string;
|
||||||
|
name?: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
cost_per_1m_tokens?: {
|
cost_per_1m_tokens?: {
|
||||||
input: number;
|
input: number;
|
||||||
|
|||||||
@@ -275,7 +275,7 @@
|
|||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"@tm/core": "*",
|
"@tm/core": "*",
|
||||||
"task-master-ai": "0.31.0-rc.0"
|
"task-master-ai": "*"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"glob@<8": "^10.4.5",
|
"glob@<8": "^10.4.5",
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function registerModelsTool(server) {
|
|||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.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 }) => {
|
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.30.2",
|
"version": "0.31.0-rc.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.30.2",
|
"version": "0.31.0-rc.0",
|
||||||
"license": "MIT WITH Commons-Clause",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const VALIDATED_PROVIDERS = [
|
|||||||
'openai',
|
'openai',
|
||||||
'google',
|
'google',
|
||||||
'zai',
|
'zai',
|
||||||
|
'zai-coding',
|
||||||
'perplexity',
|
'perplexity',
|
||||||
'xai',
|
'xai',
|
||||||
'groq',
|
'groq',
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ import {
|
|||||||
PerplexityAIProvider,
|
PerplexityAIProvider,
|
||||||
VertexAIProvider,
|
VertexAIProvider,
|
||||||
XAIProvider,
|
XAIProvider,
|
||||||
ZAIProvider
|
ZAIProvider,
|
||||||
|
ZAICodingProvider
|
||||||
} from '../../src/ai-providers/index.js';
|
} from '../../src/ai-providers/index.js';
|
||||||
|
|
||||||
// Import the provider registry
|
// Import the provider registry
|
||||||
@@ -64,6 +65,7 @@ const PROVIDERS = {
|
|||||||
perplexity: new PerplexityAIProvider(),
|
perplexity: new PerplexityAIProvider(),
|
||||||
google: new GoogleAIProvider(),
|
google: new GoogleAIProvider(),
|
||||||
zai: new ZAIProvider(),
|
zai: new ZAIProvider(),
|
||||||
|
'zai-coding': new ZAICodingProvider(),
|
||||||
lmstudio: new LMStudioProvider(),
|
lmstudio: new LMStudioProvider(),
|
||||||
openai: new OpenAIProvider(),
|
openai: new OpenAIProvider(),
|
||||||
xai: new XAIProvider(),
|
xai: new XAIProvider(),
|
||||||
|
|||||||
@@ -836,6 +836,8 @@ function isApiKeySet(providerName, session = null, projectRoot = null) {
|
|||||||
azure: 'AZURE_OPENAI_API_KEY',
|
azure: 'AZURE_OPENAI_API_KEY',
|
||||||
openrouter: 'OPENROUTER_API_KEY',
|
openrouter: 'OPENROUTER_API_KEY',
|
||||||
xai: 'XAI_API_KEY',
|
xai: 'XAI_API_KEY',
|
||||||
|
zai: 'ZAI_API_KEY',
|
||||||
|
'zai-coding': 'ZAI_API_KEY',
|
||||||
groq: 'GROQ_API_KEY',
|
groq: 'GROQ_API_KEY',
|
||||||
vertex: 'GOOGLE_API_KEY', // Vertex uses the same key as Google
|
vertex: 'GOOGLE_API_KEY', // Vertex uses the same key as Google
|
||||||
'claude-code': 'CLAUDE_CODE_API_KEY', // Not actually used, but included for consistency
|
'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;
|
apiKeyToCheck = mcpEnv.XAI_API_KEY;
|
||||||
placeholderValue = 'YOUR_XAI_API_KEY_HERE';
|
placeholderValue = 'YOUR_XAI_API_KEY_HERE';
|
||||||
break;
|
break;
|
||||||
|
case 'zai':
|
||||||
|
case 'zai-coding':
|
||||||
|
apiKeyToCheck = mcpEnv.ZAI_API_KEY;
|
||||||
|
placeholderValue = 'YOUR_ZAI_API_KEY_HERE';
|
||||||
|
break;
|
||||||
case 'groq':
|
case 'groq':
|
||||||
apiKeyToCheck = mcpEnv.GROQ_API_KEY;
|
apiKeyToCheck = mcpEnv.GROQ_API_KEY;
|
||||||
placeholderValue = 'YOUR_GROQ_API_KEY_HERE';
|
placeholderValue = 'YOUR_GROQ_API_KEY_HERE';
|
||||||
|
|||||||
@@ -922,19 +922,43 @@
|
|||||||
"input": 0.2,
|
"input": 0.2,
|
||||||
"output": 1.1
|
"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,
|
"max_tokens": 131072,
|
||||||
"supported": true
|
"supported": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "glm-4.5v",
|
"id": "glm-4.5-air",
|
||||||
"swe_score": 0.63,
|
"swe_score": 0.62,
|
||||||
"cost_per_1m_tokens": {
|
"cost_per_1m_tokens": {
|
||||||
"input": 0.6,
|
"input": 0,
|
||||||
"output": 1.8
|
"output": 0
|
||||||
},
|
},
|
||||||
"allowed_roles": ["main", "fallback"],
|
"allowed_roles": ["main", "fallback", "research"],
|
||||||
"max_tokens": 64000,
|
"max_tokens": 131072,
|
||||||
"supported": true
|
"supported": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -429,19 +429,30 @@ async function setModel(role, modelId, options = {}) {
|
|||||||
let determinedProvider = null; // Initialize provider
|
let determinedProvider = null; // Initialize provider
|
||||||
let warningMessage = null;
|
let warningMessage = null;
|
||||||
|
|
||||||
// Find the model data in internal list initially to see if it exists at all
|
// Find the model data in internal list
|
||||||
let modelData = availableModels.find((m) => m.id === modelId);
|
// 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 --- //
|
// --- Revised Logic: Prioritize providerHint --- //
|
||||||
|
|
||||||
if (providerHint) {
|
if (providerHint) {
|
||||||
// Hint provided (--ollama or --openrouter flag used)
|
// Hint provided (from interactive setup or flag)
|
||||||
if (modelData && modelData.provider === providerHint) {
|
if (modelData && modelData.provider === providerHint) {
|
||||||
// Found internally AND provider matches the hint
|
// Found internally with matching provider
|
||||||
determinedProvider = providerHint;
|
determinedProvider = providerHint;
|
||||||
report(
|
report(
|
||||||
'info',
|
'info',
|
||||||
`Model ${modelId} found internally with matching provider hint ${determinedProvider}.`
|
`Model ${modelId} found internally with provider ${determinedProvider}.`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Either not found internally, OR found but under a DIFFERENT provider than hinted.
|
// 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 { CodexCliProvider } from './codex-cli.js';
|
||||||
export { OpenAICompatibleProvider } from './openai-compatible.js';
|
export { OpenAICompatibleProvider } from './openai-compatible.js';
|
||||||
export { ZAIProvider } from './zai.js';
|
export { ZAIProvider } from './zai.js';
|
||||||
|
export { ZAICodingProvider } from './zai-coding.js';
|
||||||
export { LMStudioProvider } from './lmstudio.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'),
|
getRequiredApiKeyName: jest.fn(() => 'ZAI_API_KEY'),
|
||||||
isRequiredApiKey: jest.fn(() => true)
|
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(() => ({
|
LMStudioProvider: jest.fn(() => ({
|
||||||
generateText: jest.fn(),
|
generateText: jest.fn(),
|
||||||
streamText: jest.fn(),
|
streamText: jest.fn(),
|
||||||
|
|||||||
Reference in New Issue
Block a user