When Claude Code sends requests with large thinking_budget values, the model may spend all tokens on "thinking" and return empty responses, causing Claude Code to stop mid-conversation. This commit adds a retry mechanism that: - Throws EmptyResponseError instead of emitting fake message on empty response - Retries up to 2 times before giving up - Emits fallback message only after all retries are exhausted Changes: - src/errors.js: Added EmptyResponseError class and isEmptyResponseError() - src/cloudcode/sse-streamer.js: Throw error instead of yielding fake message - src/cloudcode/streaming-handler.js: Added retry loop with fallback Tested for 6+ hours with 1,884 API requests and 88% recovery rate on empty responses. Fixes #61 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
204 lines
6.0 KiB
JavaScript
204 lines
6.0 KiB
JavaScript
/**
|
|
* Custom Error Classes
|
|
*
|
|
* Provides structured error types for better error handling and classification.
|
|
* Replaces string-based error detection with proper error class checking.
|
|
*/
|
|
|
|
/**
|
|
* Base error class for Antigravity proxy errors
|
|
*/
|
|
export class AntigravityError extends Error {
|
|
/**
|
|
* @param {string} message - Error message
|
|
* @param {string} code - Error code for programmatic handling
|
|
* @param {boolean} retryable - Whether the error is retryable
|
|
* @param {Object} metadata - Additional error metadata
|
|
*/
|
|
constructor(message, code, retryable = false, metadata = {}) {
|
|
super(message);
|
|
this.name = 'AntigravityError';
|
|
this.code = code;
|
|
this.retryable = retryable;
|
|
this.metadata = metadata;
|
|
}
|
|
|
|
/**
|
|
* Convert to JSON for API responses
|
|
*/
|
|
toJSON() {
|
|
return {
|
|
name: this.name,
|
|
code: this.code,
|
|
message: this.message,
|
|
retryable: this.retryable,
|
|
...this.metadata
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rate limit error (429 / RESOURCE_EXHAUSTED)
|
|
*/
|
|
export class RateLimitError extends AntigravityError {
|
|
/**
|
|
* @param {string} message - Error message
|
|
* @param {number|null} resetMs - Time in ms until rate limit resets
|
|
* @param {string} accountEmail - Email of the rate-limited account
|
|
*/
|
|
constructor(message, resetMs = null, accountEmail = null) {
|
|
super(message, 'RATE_LIMITED', true, { resetMs, accountEmail });
|
|
this.name = 'RateLimitError';
|
|
this.resetMs = resetMs;
|
|
this.accountEmail = accountEmail;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Authentication error (invalid credentials, token expired, etc.)
|
|
*/
|
|
export class AuthError extends AntigravityError {
|
|
/**
|
|
* @param {string} message - Error message
|
|
* @param {string} accountEmail - Email of the account with auth issues
|
|
* @param {string} reason - Specific reason for auth failure
|
|
*/
|
|
constructor(message, accountEmail = null, reason = null) {
|
|
super(message, 'AUTH_INVALID', false, { accountEmail, reason });
|
|
this.name = 'AuthError';
|
|
this.accountEmail = accountEmail;
|
|
this.reason = reason;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* No accounts available error
|
|
*/
|
|
export class NoAccountsError extends AntigravityError {
|
|
/**
|
|
* @param {string} message - Error message
|
|
* @param {boolean} allRateLimited - Whether all accounts are rate limited
|
|
*/
|
|
constructor(message = 'No accounts available', allRateLimited = false) {
|
|
super(message, 'NO_ACCOUNTS', allRateLimited, { allRateLimited });
|
|
this.name = 'NoAccountsError';
|
|
this.allRateLimited = allRateLimited;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Max retries exceeded error
|
|
*/
|
|
export class MaxRetriesError extends AntigravityError {
|
|
/**
|
|
* @param {string} message - Error message
|
|
* @param {number} attempts - Number of attempts made
|
|
*/
|
|
constructor(message = 'Max retries exceeded', attempts = 0) {
|
|
super(message, 'MAX_RETRIES', false, { attempts });
|
|
this.name = 'MaxRetriesError';
|
|
this.attempts = attempts;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* API error from upstream service
|
|
*/
|
|
export class ApiError extends AntigravityError {
|
|
/**
|
|
* @param {string} message - Error message
|
|
* @param {number} statusCode - HTTP status code
|
|
* @param {string} errorType - Type of API error
|
|
*/
|
|
constructor(message, statusCode = 500, errorType = 'api_error') {
|
|
super(message, errorType.toUpperCase(), statusCode >= 500, { statusCode, errorType });
|
|
this.name = 'ApiError';
|
|
this.statusCode = statusCode;
|
|
this.errorType = errorType;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Native module error (version mismatch, rebuild required)
|
|
*/
|
|
export class NativeModuleError extends AntigravityError {
|
|
/**
|
|
* @param {string} message - Error message
|
|
* @param {boolean} rebuildSucceeded - Whether auto-rebuild succeeded
|
|
* @param {boolean} restartRequired - Whether server restart is needed
|
|
*/
|
|
constructor(message, rebuildSucceeded = false, restartRequired = false) {
|
|
super(message, 'NATIVE_MODULE_ERROR', false, { rebuildSucceeded, restartRequired });
|
|
this.name = 'NativeModuleError';
|
|
this.rebuildSucceeded = rebuildSucceeded;
|
|
this.restartRequired = restartRequired;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Empty response error - thrown when API returns no content
|
|
* Used to trigger retry logic in streaming handler
|
|
*/
|
|
export class EmptyResponseError extends AntigravityError {
|
|
/**
|
|
* @param {string} message - Error message
|
|
*/
|
|
constructor(message = 'No content received from API') {
|
|
super(message, 'EMPTY_RESPONSE', true, {});
|
|
this.name = 'EmptyResponseError';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if an error is a rate limit error
|
|
* Works with both custom error classes and legacy string-based errors
|
|
* @param {Error} error - Error to check
|
|
* @returns {boolean}
|
|
*/
|
|
export function isRateLimitError(error) {
|
|
if (error instanceof RateLimitError) return true;
|
|
const msg = (error.message || '').toLowerCase();
|
|
return msg.includes('429') ||
|
|
msg.includes('resource_exhausted') ||
|
|
msg.includes('quota_exhausted') ||
|
|
msg.includes('rate limit');
|
|
}
|
|
|
|
/**
|
|
* Check if an error is an authentication error
|
|
* Works with both custom error classes and legacy string-based errors
|
|
* @param {Error} error - Error to check
|
|
* @returns {boolean}
|
|
*/
|
|
export function isAuthError(error) {
|
|
if (error instanceof AuthError) return true;
|
|
const msg = (error.message || '').toUpperCase();
|
|
return msg.includes('AUTH_INVALID') ||
|
|
msg.includes('INVALID_GRANT') ||
|
|
msg.includes('TOKEN REFRESH FAILED');
|
|
}
|
|
|
|
/**
|
|
* Check if an error is an empty response error
|
|
* @param {Error} error - Error to check
|
|
* @returns {boolean}
|
|
*/
|
|
export function isEmptyResponseError(error) {
|
|
return error instanceof EmptyResponseError ||
|
|
error?.name === 'EmptyResponseError';
|
|
}
|
|
|
|
export default {
|
|
AntigravityError,
|
|
RateLimitError,
|
|
AuthError,
|
|
NoAccountsError,
|
|
MaxRetriesError,
|
|
ApiError,
|
|
NativeModuleError,
|
|
EmptyResponseError,
|
|
isRateLimitError,
|
|
isAuthError,
|
|
isEmptyResponseError
|
|
};
|