fix: elegantly exit if running into a claude error like overloaded api + integration test.

This commit is contained in:
Eyal Toledano
2025-03-25 16:35:25 -04:00
parent 1d70a7c32c
commit fce55dd0c4
2 changed files with 151 additions and 7 deletions

View File

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

View File

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