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]) => {
|
||||
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;
|
||||
|
||||
@@ -275,7 +275,7 @@
|
||||
"tailwindcss": "4.1.11",
|
||||
"typescript": "^5.9.2",
|
||||
"@tm/core": "*",
|
||||
"task-master-ai": "0.31.0-rc.0"
|
||||
"task-master-ai": "*"
|
||||
},
|
||||
"overrides": {
|
||||
"glob@<8": "^10.4.5",
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.30.2",
|
||||
"version": "0.31.0-rc.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "task-master-ai",
|
||||
"version": "0.30.2",
|
||||
"version": "0.31.0-rc.0",
|
||||
"license": "MIT WITH Commons-Clause",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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