Surface Billing/Credit Exhaustion Errors to UI (Issue #1580) (#1617)

* auto-claude: subtask-1-1 - Add BILLING_FAILURE_PATTERNS regex array with patterns for credit/billing errors

Added comprehensive regex patterns to detect billing and credit failures:
- Credit balance patterns (insufficient, low, empty, exhausted)
- Billing error patterns (payment failed, subscription expired)
- Usage quota patterns (monthly limits, plan limits)
- API error patterns (billing_error, insufficient_credits, 402)
- Balance/funds patterns and add credits messages

* auto-claude: subtask-1-2 - Add BillingFailureDetectionResult interface parallel to AuthFailureDetectionResult

Add new TypeScript interface for billing failure detection that mirrors
the AuthFailureDetectionResult pattern with:
- isBillingFailure: boolean flag
- profileId: optional profile identifier
- failureType: specific billing failure types (insufficient_credits,
  payment_required, subscription_inactive, unknown)
- message: user-friendly error message
- originalError: raw error from process output

* auto-claude: subtask-1-3 - Add classifyBillingFailureType() and getBillingFailureMessage() helpers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-1-4 - Add detectBillingFailure() function to detect billing errors in output

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-2-1 - Add BillingFailureInfo interface to terminal.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-3-1 - Add onBillingFailure callback to SubprocessOptions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-3-2 - Add checkBillingFailure helper function in runPythonSubprocess

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-3-3 - Integrate checkBillingFailure into stdout/stderr handlers and close handler

- Added checkBillingFailure(line) call after checkAuthFailure(line) in stdout handler
- Added checkBillingFailure(line) call after checkAuthFailure(line) in stderr handler
- Added killedDueToBillingFailure check in close handler with proper error message
- Follows the exact same pattern as auth failure detection integration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: billing detection patterns and add unit tests (qa-requested)

Fixes:
- Fix regex for "credit balance is too low" by adding optional (too\s+)? group
- Add extra_usage pattern to BILLING_FAILURE_PATTERNS for Claude API errors
- Fix 402 false positive by requiring HTTP/status/code/error context prefix
- Add 31 unit tests for detectBillingFailure, isBillingFailureError, and
  classifyBillingFailureType covering spec appendix messages, negative cases,
  false positive checks, and cross-detection tests

Verified:
- All 84 rate-limit-detector tests pass (53 existing + 31 new)
- TypeScript compilation succeeds with zero errors
- Full test suite passes (2599/2599 + 6 skipped)

QA Fix Session: 1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Andy
2026-01-30 10:46:23 +01:00
committed by GitHub
parent 54d0cd2f4e
commit 7bf12e8566
4 changed files with 630 additions and 2 deletions
@@ -609,3 +609,392 @@ Please authenticate and try again.`;
});
});
});
describe('Billing Failure Detection', () => {
beforeEach(() => {
vi.resetModules();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('detectBillingFailure - spec appendix messages', () => {
it('should detect "Your credit balance is too low to access the Anthropic API"', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('Your credit balance is too low to access the Anthropic API');
expect(result.isBillingFailure).toBe(true);
expect(result.failureType).toBe('insufficient_credits');
expect(result.message).toBeDefined();
});
it('should detect "insufficient credits"', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('insufficient credits');
expect(result.isBillingFailure).toBe(true);
expect(result.failureType).toBe('insufficient_credits');
});
it('should detect "billing error"', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('billing error');
expect(result.isBillingFailure).toBe(true);
expect(result.failureType).toBe('payment_required');
});
it('should detect "extra_usage exceeded"', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('extra_usage exceeded');
expect(result.isBillingFailure).toBe(true);
expect(result.failureType).toBe('insufficient_credits');
});
it('should detect standalone "extra_usage"', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('extra_usage');
expect(result.isBillingFailure).toBe(true);
});
it('should detect "payment required"', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('payment required');
expect(result.isBillingFailure).toBe(true);
expect(result.failureType).toBe('payment_required');
});
it('should detect "subscription expired"', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('subscription expired');
expect(result.isBillingFailure).toBe(true);
expect(result.failureType).toBe('subscription_inactive');
});
it('should detect credit balance variations', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const messages = [
'credit balance is insufficient',
'credit balance is empty',
'credit balance is zero',
'credit balance is exhausted',
'credit balance is too low',
];
for (const msg of messages) {
const result = detectBillingFailure(msg);
expect(result.isBillingFailure).toBe(true);
}
});
});
describe('detectBillingFailure - negative cases', () => {
it('should return false for normal output', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const normalMessages = [
'Task completed successfully',
'Processing 100 files',
'Build succeeded',
'Deploying to production',
'',
];
for (const msg of normalMessages) {
const result = detectBillingFailure(msg);
expect(result.isBillingFailure).toBe(false);
}
});
it('should NOT detect rate limit errors as billing failures', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const rateLimitMessages = [
'Limit reached · resets Dec 17 at 6am',
'rate limit exceeded',
'too many requests',
];
for (const msg of rateLimitMessages) {
const result = detectBillingFailure(msg);
expect(result.isBillingFailure).toBe(false);
}
});
it('should NOT detect auth errors as billing failures', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const authMessages = [
'authentication required',
'not authenticated',
'unauthorized',
'invalid token',
'session expired',
];
for (const msg of authMessages) {
const result = detectBillingFailure(msg);
expect(result.isBillingFailure).toBe(false);
}
});
it('should NOT match "line 402" as billing failure (false positive check)', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const falsePositives = [
'line 402',
'Processing line 402 of 1000',
'Found 4020 records',
'Error on line 402',
'The user has 402 files',
];
for (const msg of falsePositives) {
const result = detectBillingFailure(msg);
expect(result.isBillingFailure).toBe(false);
}
});
});
describe('detectBillingFailure - 402 with proper context', () => {
it('should detect "HTTP 402 Payment Required"', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('HTTP 402 Payment Required');
expect(result.isBillingFailure).toBe(true);
});
it('should detect "API Error: 402"', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('API Error: 402');
expect(result.isBillingFailure).toBe(true);
});
it('should detect "status: 402"', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('status: 402');
expect(result.isBillingFailure).toBe(true);
});
it('should detect "error 402"', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('error 402');
expect(result.isBillingFailure).toBe(true);
});
it('should detect "402 payment required" (lowercase)', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('402 payment required');
expect(result.isBillingFailure).toBe(true);
});
});
describe('isBillingFailureError', () => {
it('should return true for billing failure errors', async () => {
const { isBillingFailureError } = await import('../rate-limit-detector');
expect(isBillingFailureError('credit balance is too low')).toBe(true);
expect(isBillingFailureError('insufficient credits')).toBe(true);
expect(isBillingFailureError('billing error')).toBe(true);
expect(isBillingFailureError('extra_usage exceeded')).toBe(true);
});
it('should return false for non-billing errors', async () => {
const { isBillingFailureError } = await import('../rate-limit-detector');
expect(isBillingFailureError('Task completed')).toBe(false);
expect(isBillingFailureError('')).toBe(false);
expect(isBillingFailureError('rate limit exceeded')).toBe(false);
});
});
describe('classifyBillingFailureType via detectBillingFailure', () => {
it('should classify credit-related failures as insufficient_credits', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const creditMessages = [
'Your credit balance is too low',
'insufficient credits',
'out of credits',
'extra_usage exceeded',
];
for (const msg of creditMessages) {
const result = detectBillingFailure(msg);
expect(result.isBillingFailure).toBe(true);
expect(result.failureType).toBe('insufficient_credits');
}
});
it('should classify payment failures as payment_required', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const paymentMessages = [
'payment required',
'billing error',
'billing failure',
];
for (const msg of paymentMessages) {
const result = detectBillingFailure(msg);
expect(result.isBillingFailure).toBe(true);
expect(result.failureType).toBe('payment_required');
}
});
it('should classify subscription failures as subscription_inactive', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const subscriptionMessages = [
'subscription expired',
'subscription inactive',
'subscription cancelled',
'account suspended',
];
for (const msg of subscriptionMessages) {
const result = detectBillingFailure(msg);
expect(result.isBillingFailure).toBe(true);
expect(result.failureType).toBe('subscription_inactive');
}
});
});
describe('detectBillingFailure - result structure', () => {
it('should include profile ID in result', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('credit balance is too low', 'custom-profile');
expect(result.isBillingFailure).toBe(true);
expect(result.profileId).toBe('custom-profile');
});
it('should use active profile ID when not specified', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('credit balance is too low');
expect(result.isBillingFailure).toBe(true);
expect(result.profileId).toBe('test-profile-id');
});
it('should include original error in result', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const output = 'Error: credit balance is too low to access the API';
const result = detectBillingFailure(output);
expect(result.isBillingFailure).toBe(true);
expect(result.originalError).toBe(output);
});
it('should include user-friendly message', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const result = detectBillingFailure('credit balance is too low');
expect(result.isBillingFailure).toBe(true);
expect(result.message).toContain('credit');
expect(result.message).toContain('Settings');
});
});
describe('cross-detection tests', () => {
it('should not detect billing errors as auth failures', async () => {
const { detectAuthFailure } = await import('../rate-limit-detector');
const billingMessages = [
'credit balance is too low',
'insufficient credits',
'billing error',
'extra_usage exceeded',
'payment required',
];
for (const msg of billingMessages) {
const result = detectAuthFailure(msg);
expect(result.isAuthFailure).toBe(false);
}
});
it('should not detect billing errors as rate limits', async () => {
const { detectRateLimit } = await import('../rate-limit-detector');
const billingMessages = [
'credit balance is too low',
'insufficient credits',
'billing error',
'extra_usage exceeded',
];
for (const msg of billingMessages) {
const result = detectRateLimit(msg);
expect(result.isRateLimited).toBe(false);
}
});
});
describe('edge cases', () => {
it('should handle case-insensitive billing messages', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const testCases = [
'CREDIT BALANCE IS TOO LOW',
'Credit Balance Is Too Low',
'BILLING ERROR',
'Insufficient Credits',
'EXTRA_USAGE EXCEEDED',
];
for (const output of testCases) {
const result = detectBillingFailure(output);
expect(result.isBillingFailure).toBe(true);
}
});
it('should handle multiline output with billing failure', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const output = `Starting API call...
Processing request...
Error: Your credit balance is too low to access the Anthropic API
Please add credits to continue.`;
const result = detectBillingFailure(output);
expect(result.isBillingFailure).toBe(true);
});
it('should handle JSON error responses with billing failure', async () => {
const { detectBillingFailure } = await import('../rate-limit-detector');
const output = '{"type":"billing_error","message":"Insufficient credits"}';
const result = detectBillingFailure(output);
expect(result.isBillingFailure).toBe(true);
});
});
});
@@ -16,9 +16,9 @@ import fs from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import type { Project } from '../../../../shared/types';
import type { AuthFailureInfo } from '../../../../shared/types/terminal';
import type { AuthFailureInfo, BillingFailureInfo } from '../../../../shared/types/terminal';
import { parsePythonCommand } from '../../../python-detector';
import { detectAuthFailure } from '../../../rate-limit-detector';
import { detectAuthFailure, detectBillingFailure } from '../../../rate-limit-detector';
import { getClaudeProfileManager } from '../../../claude-profile-manager';
import { isWindows, isMacOS } from '../../../platform';
@@ -68,6 +68,8 @@ export interface SubprocessOptions {
onError?: (error: string) => void;
/** Callback when auth failure (401) is detected in output */
onAuthFailure?: (authFailureInfo: AuthFailureInfo) => void;
/** Callback when billing/credit exhaustion failure is detected in output */
onBillingFailure?: (billingFailureInfo: BillingFailureInfo) => void;
progressPattern?: RegExp;
/** Additional environment variables to pass to the subprocess */
env?: Record<string, string>;
@@ -130,6 +132,8 @@ export function runPythonSubprocess<T = unknown>(
let stderr = '';
let authFailureEmitted = false; // Track if we've already emitted an auth failure
let killedDueToAuthFailure = false; // Track if subprocess was killed due to auth failure
let billingFailureEmitted = false; // Track if we've already emitted a billing failure
let killedDueToBillingFailure = false; // Track if subprocess was killed due to billing failure
// Default progress pattern: [ 30%] message OR [30%] message
const progressPattern = options.progressPattern ?? /\[\s*(\d+)%\]\s*(.+)/;
@@ -193,6 +197,65 @@ export function runPythonSubprocess<T = unknown>(
}
};
// Helper to check for billing/credit failures in output and emit once
const checkBillingFailure = (line: string) => {
if (billingFailureEmitted || !options.onBillingFailure) return;
const billingResult = detectBillingFailure(line);
if (billingResult.isBillingFailure) {
billingFailureEmitted = true;
console.log('[SubprocessRunner] Billing failure detected in real-time:', billingResult);
// Get profile info for display
const profileManager = getClaudeProfileManager();
const profile = billingResult.profileId
? profileManager.getProfile(billingResult.profileId)
: profileManager.getActiveProfile();
const billingFailureInfo: BillingFailureInfo = {
profileId: billingResult.profileId || profile?.id || 'unknown',
profileName: profile?.name,
failureType: billingResult.failureType || 'unknown',
message: billingResult.message || 'Billing or credit error. Please check your account.',
originalError: billingResult.originalError,
detectedAt: new Date(),
};
try {
options.onBillingFailure(billingFailureInfo);
} catch (e) {
console.error('[SubprocessRunner] onBillingFailure callback threw:', e);
}
// Kill the subprocess to stop the billing failure spam
killedDueToBillingFailure = true;
// The process is stuck in billing errors - no point continuing
console.log('[SubprocessRunner] Killing subprocess due to billing failure, pid:', child.pid);
// Use process.kill with negative PID to kill the entire process group on Unix
// This ensures child processes (like the Claude SDK subprocess) are also killed
if (child.pid) {
try {
// On Unix, negative PID kills the process group
if (!isWindows()) {
process.kill(-child.pid, 'SIGKILL');
} else {
// On Windows, use taskkill to kill the process tree
execFile('taskkill', ['/pid', String(child.pid), '/T', '/F'], (err: Error | null) => {
if (err) console.warn('[SubprocessRunner] taskkill error (process may have already exited):', err.message);
});
}
} catch (err) {
// Fallback to regular kill if process group kill fails
console.log('[SubprocessRunner] Process group kill failed, using regular kill:', err);
child.kill('SIGKILL');
}
} else {
child.kill('SIGKILL');
}
}
};
child.stdout.on('data', (data: Buffer) => {
const text = data.toString('utf-8');
stdout += text;
@@ -206,6 +269,9 @@ export function runPythonSubprocess<T = unknown>(
// Check for auth failures in real-time (only emit once)
checkAuthFailure(line);
// Check for billing/credit failures in real-time (only emit once)
checkBillingFailure(line);
// Parse progress updates
const match = line.match(progressPattern);
if (match && options.onProgress) {
@@ -228,6 +294,9 @@ export function runPythonSubprocess<T = unknown>(
// Also check stderr for auth failures
checkAuthFailure(line);
// Also check stderr for billing/credit failures
checkBillingFailure(line);
}
}
});
@@ -260,6 +329,18 @@ export function runPythonSubprocess<T = unknown>(
return;
}
// Check if subprocess was killed due to billing/credit failure
if (killedDueToBillingFailure) {
resolve({
success: false,
exitCode: exitCode,
stdout,
stderr,
error: 'Billing or credit error. Please check your account.',
});
return;
}
if (exitCode === 0) {
try {
const data = options.onComplete?.(stdout, stderr);
@@ -53,6 +53,45 @@ const AUTH_FAILURE_PATTERNS = [
/·\s*Please\s+run\s+\/login/i,
];
/**
* Patterns that indicate billing/credit failures
* These patterns detect when Claude API fails due to insufficient credits or billing issues
*/
const BILLING_FAILURE_PATTERNS = [
// Credit balance patterns
/credit\s*balance\s*(is\s+)?(too\s+)?(insufficient|low|empty|zero|exhausted)/i,
/insufficient\s*credit(s)?/i,
/no\s*(remaining\s*)?credit(s)?/i,
/credit(s)?\s*(are\s*)?(exhausted|depleted|used\s*up)/i,
/out\s*of\s*credit(s)?/i,
/credit\s*limit\s*(reached|exceeded)/i,
// Billing error patterns
/billing\s*(error|issue|problem|failure)/i,
/payment\s*(required|failed|issue|problem)/i,
/subscription\s*(expired|inactive|cancelled|canceled)/i,
/account\s*(suspended|inactive)\s*(due\s*to\s*billing)?/i,
// Usage limit patterns (billing-related, not rate limits)
/usage\s*quota\s*(exceeded|reached)/i,
/monthly\s*(usage\s*)?(limit|quota)\s*(exceeded|reached)/i,
/plan\s*(limit|quota)\s*(exceeded|reached)/i,
// API error patterns for billing
/["']?type["']?\s*:\s*["']?billing_error["']?/i,
/["']?type["']?\s*:\s*["']?insufficient_credits["']?/i,
/["']?error["']?\s*:\s*["']?insufficient_credits["']?/i,
// extra_usage patterns from Claude API
/extra_usage\s*(exceeded|limit|error)?/i,
// Match HTTP 402 Payment Required (require context to avoid false positives on "line 402" etc.)
/(?:HTTP|status|code|error)\s*:?\s*402\b/i,
/\b402\s+payment\s+required/i,
/API\s*Error:\s*402/i,
// Balance/funds patterns
/insufficient\s*(funds|balance)/i,
/balance\s*(is\s*)?(zero|empty|insufficient)/i,
// Add funds/credits messages
/please\s*(add|purchase)\s*(more\s*)?(credits?|funds)/i,
/top\s*up\s*(your\s*)?(account|credits|balance)/i
];
/**
* Result of rate limit detection
*/
@@ -90,6 +129,22 @@ export interface AuthFailureDetectionResult {
originalError?: string;
}
/**
* Result of billing failure detection
*/
export interface BillingFailureDetectionResult {
/** Whether a billing failure was detected */
isBillingFailure: boolean;
/** The profile ID that has billing issues (if known) */
profileId?: string;
/** The type of billing failure detected */
failureType?: 'insufficient_credits' | 'payment_required' | 'subscription_inactive' | 'unknown';
/** User-friendly message describing the failure */
message?: string;
/** Original error message from the process output */
originalError?: string;
}
/**
* Classify rate limit type based on reset time string
*/
@@ -216,6 +271,44 @@ function getAuthFailureMessage(failureType: 'missing' | 'invalid' | 'expired' |
}
}
/**
* Classify the type of billing failure based on the error message
*/
function classifyBillingFailureType(output: string): 'insufficient_credits' | 'payment_required' | 'subscription_inactive' | 'unknown' {
const lowerOutput = output.toLowerCase();
// Check for credit-related failures (including extra_usage which indicates usage exhaustion)
if (/credit\s*(balance|s)?|insufficient\s*(credit|funds|balance)|out\s*of\s*credit|no\s*(remaining\s*)?credit|extra_usage/.test(lowerOutput)) {
return 'insufficient_credits';
}
// Check for subscription-related failures
if (/subscription\s*(expired|inactive|cancelled|canceled)|account\s*(suspended|inactive)/.test(lowerOutput)) {
return 'subscription_inactive';
}
// Check for payment-related failures
if (/payment\s*(required|failed)|402|billing\s*(error|issue|problem|failure)/.test(lowerOutput)) {
return 'payment_required';
}
return 'unknown';
}
/**
* Get a user-friendly message for the billing failure
*/
function getBillingFailureMessage(failureType: 'insufficient_credits' | 'payment_required' | 'subscription_inactive' | 'unknown'): string {
switch (failureType) {
case 'insufficient_credits':
return 'Your Claude API credit balance is too low. Please add credits to your account or switch to another profile in Settings > Claude Profiles.';
case 'payment_required':
return 'A billing error occurred with your Claude API account. Please check your payment method or switch to another profile in Settings > Claude Profiles.';
case 'subscription_inactive':
return 'Your Claude API subscription is inactive or expired. Please renew your subscription or switch to another profile in Settings > Claude Profiles.';
case 'unknown':
default:
return 'A billing issue was detected with your Claude API account. Please check your account status or switch to another profile in Settings > Claude Profiles.';
}
}
/**
* Detect authentication failure from output (stdout + stderr combined)
*/
@@ -255,6 +348,48 @@ export function isAuthFailureError(output: string): boolean {
return detectAuthFailure(output).isAuthFailure;
}
/**
* Detect billing failure from output (stdout + stderr combined)
*/
export function detectBillingFailure(
output: string,
profileId?: string
): BillingFailureDetectionResult {
// First, make sure this isn't a rate limit or auth error (those should be handled separately)
if (detectRateLimit(output).isRateLimited) {
return { isBillingFailure: false };
}
if (detectAuthFailure(output).isAuthFailure) {
return { isBillingFailure: false };
}
// Check for billing failure patterns
for (const pattern of BILLING_FAILURE_PATTERNS) {
if (pattern.test(output)) {
const profileManager = getClaudeProfileManager();
const effectiveProfileId = profileId || profileManager.getActiveProfile().id;
const failureType = classifyBillingFailureType(output);
return {
isBillingFailure: true,
profileId: effectiveProfileId,
failureType,
message: getBillingFailureMessage(failureType),
originalError: output
};
}
}
return { isBillingFailure: false };
}
/**
* Check if output contains billing failure error
*/
export function isBillingFailureError(output: string): boolean {
return detectBillingFailure(output).isBillingFailure;
}
/**
* Get environment variables for a specific Claude profile.
*
@@ -157,6 +157,29 @@ export interface AuthFailureInfo {
detectedAt: Date;
}
/**
* Billing/credit exhaustion failure information for SDK/CLI operations.
* Emitted when Claude API returns billing-related errors (HTTP 400 with
* invalid_request_error), indicating the account has insufficient credits,
* exceeded usage limits, or has a billing configuration issue.
*/
export interface BillingFailureInfo {
/** The profile ID that has billing issues */
profileId: string;
/** The profile name for display */
profileName?: string;
/** Type of billing failure */
failureType: 'insufficient_credits' | 'payment_required' | 'subscription_inactive' | 'unknown';
/** User-friendly message describing the failure */
message: string;
/** Original error message from the process output */
originalError?: string;
/** Task ID if applicable (for task-related billing failures) */
taskId?: string;
/** When detected (Note: serialized as ISO string over IPC) */
detectedAt: Date;
}
/**
* Request to retry a rate-limited operation with a different profile
*/