diff --git a/scripts/modules/ai-services.js b/scripts/modules/ai-services.js index cc3e8169..9be55b19 100644 --- a/scripts/modules/ai-services.js +++ b/scripts/modules/ai-services.js @@ -3,11 +3,14 @@ * AI service interactions for the Task Master CLI */ +// NOTE/TODO: Include the beta header output-128k-2025-02-19 in your API request to increase the maximum output token length to 128k tokens for Claude 3.7 Sonnet. + import { Anthropic } from '@anthropic-ai/sdk'; import OpenAI from 'openai'; import dotenv from 'dotenv'; import { CONFIG, log, sanitizePrompt } from './utils.js'; import { startLoadingIndicator, stopLoadingIndicator } from './ui.js'; +import chalk from 'chalk'; // Load environment variables dotenv.config(); @@ -37,6 +40,38 @@ function getPerplexityClient() { return perplexity; } +/** + * Handle Claude API errors with user-friendly messages + * @param {Error} error - The error from Claude API + * @returns {string} User-friendly error message + */ +function handleClaudeError(error) { + // Check if it's a structured error response + if (error.type === 'error' && error.error) { + switch (error.error.type) { + case 'overloaded_error': + return 'Claude is currently experiencing high demand and is overloaded. Please wait a few minutes and try again.'; + case 'rate_limit_error': + return 'You have exceeded the rate limit. Please wait a few minutes before making more requests.'; + case 'invalid_request_error': + return 'There was an issue with the request format. If this persists, please report it as a bug.'; + default: + return `Claude API error: ${error.error.message}`; + } + } + + // Check for network/timeout errors + if (error.message?.toLowerCase().includes('timeout')) { + return 'The request to Claude timed out. Please try again.'; + } + if (error.message?.toLowerCase().includes('network')) { + return 'There was a network error connecting to Claude. Please check your internet connection and try again.'; + } + + // Default error message + return `Error communicating with Claude: ${error.message}`; +} + /** * Call Claude to generate tasks from a PRD * @param {string} prdContent - PRD content @@ -99,14 +134,27 @@ Important: Your response must be valid JSON only, with no additional explanation // Use streaming request to handle large responses and show progress return await handleStreamingRequest(prdContent, prdPath, numTasks, CONFIG.maxTokens, systemPrompt); } catch (error) { - log('error', 'Error calling Claude:', error.message); + // Get user-friendly error message + const userMessage = handleClaudeError(error); + log('error', userMessage); - // Retry logic - if (retryCount < 2) { - log('info', `Retrying (${retryCount + 1}/2)...`); + // Retry logic for certain errors + if (retryCount < 2 && ( + error.error?.type === 'overloaded_error' || + error.error?.type === 'rate_limit_error' || + error.message?.toLowerCase().includes('timeout') || + error.message?.toLowerCase().includes('network') + )) { + const waitTime = (retryCount + 1) * 5000; // 5s, then 10s + log('info', `Waiting ${waitTime/1000} seconds before retry ${retryCount + 1}/2...`); + await new Promise(resolve => setTimeout(resolve, waitTime)); return await callClaude(prdContent, prdPath, numTasks, retryCount + 1); } else { - throw error; + console.error(chalk.red(userMessage)); + if (CONFIG.debug) { + log('debug', 'Full error:', error); + } + throw new Error(userMessage); } } } @@ -166,7 +214,17 @@ async function handleStreamingRequest(prdContent, prdPath, numTasks, maxTokens, } catch (error) { if (streamingInterval) clearInterval(streamingInterval); stopLoadingIndicator(loadingIndicator); - throw error; + + // Get user-friendly error message + const userMessage = handleClaudeError(error); + log('error', userMessage); + console.error(chalk.red(userMessage)); + + if (CONFIG.debug) { + log('debug', 'Full error:', error); + } + + throw new Error(userMessage); } } @@ -613,5 +671,6 @@ export { generateSubtasks, generateSubtasksWithPerplexity, parseSubtasksFromText, - generateComplexityAnalysisPrompt + generateComplexityAnalysisPrompt, + handleClaudeError }; \ No newline at end of file diff --git a/tests/unit/ai-services.test.js b/tests/unit/ai-services.test.js index cadc4850..52821897 100644 --- a/tests/unit/ai-services.test.js +++ b/tests/unit/ai-services.test.js @@ -285,4 +285,89 @@ These subtasks will help you implement the parent task efficiently.`; }); }); }); + + describe('handleClaudeError function', () => { + // Import the function directly for testing + let handleClaudeError; + + beforeAll(async () => { + // Dynamic import to get the actual function + const module = await import('../../scripts/modules/ai-services.js'); + handleClaudeError = module.handleClaudeError; + }); + + test('should handle overloaded_error type', () => { + const error = { + type: 'error', + error: { + type: 'overloaded_error', + message: 'Claude is experiencing high volume' + } + }; + + const result = handleClaudeError(error); + + expect(result).toContain('Claude is currently experiencing high demand'); + expect(result).toContain('overloaded'); + }); + + test('should handle rate_limit_error type', () => { + const error = { + type: 'error', + error: { + type: 'rate_limit_error', + message: 'Rate limit exceeded' + } + }; + + const result = handleClaudeError(error); + + expect(result).toContain('exceeded the rate limit'); + }); + + test('should handle invalid_request_error type', () => { + const error = { + type: 'error', + error: { + type: 'invalid_request_error', + message: 'Invalid request parameters' + } + }; + + const result = handleClaudeError(error); + + expect(result).toContain('issue with the request format'); + }); + + test('should handle timeout errors', () => { + const error = { + message: 'Request timed out after 60000ms' + }; + + const result = handleClaudeError(error); + + expect(result).toContain('timed out'); + }); + + test('should handle network errors', () => { + const error = { + message: 'Network error occurred' + }; + + const result = handleClaudeError(error); + + expect(result).toContain('network error'); + }); + + test('should handle generic errors', () => { + const error = { + message: 'Something unexpected happened' + }; + + const result = handleClaudeError(error); + + expect(result).toContain('Error communicating with Claude'); + expect(result).toContain('Something unexpected happened'); + }); + }); }); \ No newline at end of file