diff --git a/.changeset/vertex-anthropic-provider.md b/.changeset/vertex-anthropic-provider.md new file mode 100644 index 0000000000..9ea10048b7 --- /dev/null +++ b/.changeset/vertex-anthropic-provider.md @@ -0,0 +1,5 @@ +--- +'task-master-ai': minor +--- + +Add support for Vertex AI Anthropic provider to use Claude models through Google Vertex AI diff --git a/apps/cli/src/commands/models/custom-providers.ts b/apps/cli/src/commands/models/custom-providers.ts index 76d44197fa..f84ecb4500 100644 --- a/apps/cli/src/commands/models/custom-providers.ts +++ b/apps/cli/src/commands/models/custom-providers.ts @@ -125,6 +125,27 @@ export const customProviderConfigs: Record< return true; } }, + VERTEX_ANTHROPIC: { + id: '__CUSTOM_VERTEX_ANTHROPIC__', + name: '* Custom Vertex Anthropic model', + provider: CUSTOM_PROVIDERS.VERTEX_ANTHROPIC, + promptMessage: (role) => + `Enter the custom Vertex AI Anthropic Model ID for the ${role} role (e.g., claude-sonnet-4-6):`, + checkEnvVars: () => { + if ( + !process.env.GOOGLE_API_KEY && + !process.env.GOOGLE_APPLICATION_CREDENTIALS + ) { + console.error( + chalk.red( + 'Error: Either GOOGLE_API_KEY or GOOGLE_APPLICATION_CREDENTIALS environment variable is required. Please set one before using Vertex Anthropic models.' + ) + ); + return false; + } + return true; + } + }, LMSTUDIO: { id: '__CUSTOM_LMSTUDIO__', name: '* Custom LMStudio model', diff --git a/apps/cli/src/commands/models/types.ts b/apps/cli/src/commands/models/types.ts index 1c5cade881..d105a22c37 100644 --- a/apps/cli/src/commands/models/types.ts +++ b/apps/cli/src/commands/models/types.ts @@ -16,6 +16,7 @@ export const CUSTOM_PROVIDER_IDS = { BEDROCK: '__CUSTOM_BEDROCK__', AZURE: '__CUSTOM_AZURE__', VERTEX: '__CUSTOM_VERTEX__', + VERTEX_ANTHROPIC: '__CUSTOM_VERTEX_ANTHROPIC__', LMSTUDIO: '__CUSTOM_LMSTUDIO__', OPENAI_COMPATIBLE: '__CUSTOM_OPENAI_COMPATIBLE__' } as const; diff --git a/packages/tm-core/src/common/constants/providers.ts b/packages/tm-core/src/common/constants/providers.ts index 5a47a042af..31b6487762 100644 --- a/packages/tm-core/src/common/constants/providers.ts +++ b/packages/tm-core/src/common/constants/providers.ts @@ -26,6 +26,7 @@ export type ValidatedProvider = (typeof VALIDATED_PROVIDERS)[number]; export const CUSTOM_PROVIDERS = { AZURE: 'azure', VERTEX: 'vertex', + VERTEX_ANTHROPIC: 'vertex-anthropic', BEDROCK: 'bedrock', OPENROUTER: 'openrouter', OLLAMA: 'ollama', diff --git a/scripts/modules/ai-services-unified.js b/scripts/modules/ai-services-unified.js index 42f9b02847..1923448465 100644 --- a/scripts/modules/ai-services-unified.js +++ b/scripts/modules/ai-services-unified.js @@ -51,6 +51,7 @@ import { OpenRouterAIProvider, PerplexityAIProvider, VertexAIProvider, + VertexAnthropicProvider, XAIProvider, ZAICodingProvider, ZAIProvider @@ -81,6 +82,7 @@ const PROVIDERS = { bedrock: new BedrockAIProvider(), azure: new AzureProvider(), vertex: new VertexAIProvider(), + 'vertex-anthropic': new VertexAnthropicProvider(), 'claude-code': new ClaudeCodeProvider(), 'codex-cli': new CodexCliProvider(), 'gemini-cli': new GeminiCliProvider(), @@ -627,8 +629,11 @@ async function _unifiedServiceRunner(serviceType, params) { // Prepare provider-specific configuration let providerSpecificParams = {}; - // Handle Vertex AI specific configuration - if (providerName?.toLowerCase() === 'vertex') { + // Handle Vertex AI specific configuration (both vertex and vertex-anthropic) + if ( + providerName?.toLowerCase() === 'vertex' || + providerName?.toLowerCase() === 'vertex-anthropic' + ) { providerSpecificParams = _getVertexConfiguration( effectiveProjectRoot, session diff --git a/scripts/modules/task-manager/models.js b/scripts/modules/task-manager/models.js index 24887c1d87..3eab6e9b03 100644 --- a/scripts/modules/task-manager/models.js +++ b/scripts/modules/task-manager/models.js @@ -583,6 +583,11 @@ async function setModel(role, modelId, options = {}) { 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; @@ -693,7 +698,7 @@ async function setModel(role, modelId, options = {}) { 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, --lmstudio, --openai-compatible, --gemini-cli, or --codex-cli.` + 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.` } }; } diff --git a/src/ai-providers/google-vertex-anthropic.js b/src/ai-providers/google-vertex-anthropic.js new file mode 100644 index 0000000000..cec9f552bf --- /dev/null +++ b/src/ai-providers/google-vertex-anthropic.js @@ -0,0 +1,59 @@ +/** + * google-vertex-anthropic.js + * AI provider implementation for Anthropic models on Google Vertex AI using Vercel AI SDK. + * This provider uses the createVertexAnthropic client to route requests to the + * publishers/anthropic endpoint instead of publishers/google. + * + * Extends VertexAIProvider — only the client factory and display name differ. + */ + +import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; +import { VertexAIProvider } from './google-vertex.js'; + +export class VertexAnthropicProvider extends VertexAIProvider { + constructor() { + super(); + this.name = 'Google Vertex AI (Anthropic)'; + } + + /** + * Creates and returns a Google Vertex AI Anthropic client instance. + * Uses createVertexAnthropic to route to the publishers/anthropic endpoint. + * @param {object} params - Parameters for client initialization + * @param {string} [params.apiKey] - Google API key + * @param {string} params.projectId - Google Cloud project ID + * @param {string} params.location - Google Cloud location (e.g., "us-central1") + * @param {object} [params.credentials] - Service account credentials object + * @param {string} [params.baseURL] - Optional custom API endpoint + * @returns {Function} Google Vertex AI Anthropic client function + * @throws {Error} If required parameters are missing or initialization fails + */ + getClient(params) { + try { + const { apiKey, projectId, location, credentials, baseURL } = params; + const fetchImpl = this.createProxyFetch(); + + // Configure auth options - either API key or service account + const authOptions = {}; + if (apiKey) { + authOptions.googleAuthOptions = { + ...credentials, + apiKey + }; + } else if (credentials) { + authOptions.googleAuthOptions = credentials; + } + + // Return Vertex AI Anthropic client (publishers/anthropic endpoint) + return createVertexAnthropic({ + ...authOptions, + project: projectId, + location, + ...(baseURL && { baseURL }), + ...(fetchImpl && { fetch: fetchImpl }) + }); + } catch (error) { + this.handleError('client initialization', error); + } + } +} diff --git a/src/ai-providers/google-vertex.js b/src/ai-providers/google-vertex.js index fcdfbb09be..9cb9aeb4df 100644 --- a/src/ai-providers/google-vertex.js +++ b/src/ai-providers/google-vertex.js @@ -4,7 +4,6 @@ */ import { createVertex } from '@ai-sdk/google-vertex'; -import { resolveEnvVariable } from '../../scripts/modules/utils.js'; import { log } from '../../scripts/modules/utils.js'; import { BaseAIProvider } from './base-provider.js'; diff --git a/src/ai-providers/index.js b/src/ai-providers/index.js index b25bb0ce68..a4db87dd88 100644 --- a/src/ai-providers/index.js +++ b/src/ai-providers/index.js @@ -14,6 +14,7 @@ export { OllamaAIProvider } from './ollama.js'; export { BedrockAIProvider } from './bedrock.js'; export { AzureProvider } from './azure.js'; export { VertexAIProvider } from './google-vertex.js'; +export { VertexAnthropicProvider } from './google-vertex-anthropic.js'; export { ClaudeCodeProvider } from './claude-code.js'; export { GeminiCliProvider } from './gemini-cli.js'; export { GrokCliProvider } from './grok-cli.js'; diff --git a/tests/unit/ai-services-unified.test.js b/tests/unit/ai-services-unified.test.js index 0c4cc2fca4..04938cc067 100644 --- a/tests/unit/ai-services-unified.test.js +++ b/tests/unit/ai-services-unified.test.js @@ -231,6 +231,13 @@ jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({ getRequiredApiKeyName: jest.fn(() => null), isRequiredApiKey: jest.fn(() => false) })), + VertexAnthropicProvider: jest.fn(() => ({ + generateText: jest.fn(), + streamText: jest.fn(), + generateObject: jest.fn(), + getRequiredApiKeyName: jest.fn(() => null), + isRequiredApiKey: jest.fn(() => false) + })), ClaudeCodeProvider: jest.fn(() => mockClaudeProvider), GeminiCliProvider: jest.fn(() => ({ generateText: jest.fn(), diff --git a/tests/unit/scripts/modules/task-manager/models-baseurl.test.js b/tests/unit/scripts/modules/task-manager/models-baseurl.test.js index 6474899cd6..eea45954e8 100644 --- a/tests/unit/scripts/modules/task-manager/models-baseurl.test.js +++ b/tests/unit/scripts/modules/task-manager/models-baseurl.test.js @@ -54,6 +54,7 @@ jest.unstable_mockModule('@tm/core', () => ({ CLAUDE_CODE: 'claude-code', AZURE: 'azure', VERTEX: 'vertex', + VERTEX_ANTHROPIC: 'vertex-anthropic', GEMINI_CLI: 'gemini-cli', CODEX_CLI: 'codex-cli', OPENAI_COMPATIBLE: 'openai-compatible'