diff --git a/src/voice-provider.ts b/src/voice-provider.ts index 058d95e..7d39dbc 100644 --- a/src/voice-provider.ts +++ b/src/voice-provider.ts @@ -8,9 +8,10 @@ export async function voiceProviderFailure(response: Response): Promise<{ error: try { const body = await response.json() as { detail?: { status?: unknown } }; const status = body?.detail?.status; - if (typeof status === "string" && ["quota_exceeded", "invalid_api_key", "missing_permissions", "subscription_required", "subscription_expired", "payment_required", "too_many_concurrent_requests", "rate_limit_exceeded"].includes(status)) code = status; + if (typeof status === "string" && ["quota_exceeded", "invalid_api_key", "missing_permissions", "subscription_required", "subscription_expired", "payment_required", "payment_issue", "too_many_concurrent_requests", "rate_limit_exceeded"].includes(status)) code = status; } catch { /* Non-JSON provider failures still have an HTTP status. */ } if (code === "quota_exceeded") return { code, cooldownMs: 300_000, error: new SpeechError("ElevenLabs credits are exhausted. The site owner needs to check the provider balance.", 402) }; + if (code === "payment_issue") return { code, cooldownMs: 300_000, error: new SpeechError("ElevenLabs blocked transcription because its subscription payment failed. The site owner needs to complete the outstanding ElevenLabs invoice.", 402) }; if (["subscription_required", "subscription_expired", "payment_required"].includes(code) || response.status === 402) return { code, cooldownMs: 300_000, error: new SpeechError("ElevenLabs billing needs attention. The site owner needs to check the provider subscription.", 402) }; if (["invalid_api_key", "missing_permissions"].includes(code) || [401, 403].includes(response.status)) return { code, cooldownMs: 300_000, error: new SpeechError("ElevenLabs rejected transcription or voice access. The site owner needs to check the provider key, permissions, and billing.", 424) }; if (response.status === 429) { diff --git a/test/voice-provider.test.ts b/test/voice-provider.test.ts index 3206bbc..6f36f4d 100644 --- a/test/voice-provider.test.ts +++ b/test/voice-provider.test.ts @@ -4,12 +4,13 @@ import { voiceProviderFailure } from "../src/voice-provider.ts"; import { transientAudioError } from "../src/live-recovery.ts"; test("provider quota, billing and permissions are terminal even when ElevenLabs returns 401", async () => { - for (const [code, status] of [["quota_exceeded", 402], ["subscription_required", 402], ["invalid_api_key", 424], ["missing_permissions", 424]] as const) { + for (const [code, status] of [["quota_exceeded", 402], ["subscription_required", 402], ["payment_issue", 402], ["invalid_api_key", 424], ["missing_permissions", 424]] as const) { const failure = await voiceProviderFailure(Response.json({ detail: { status: code, message: "private provider account data" } }, { status: 401 })); assert.equal(failure.error.status, status); assert.equal(transientAudioError(failure.error), false); assert.equal(failure.cooldownMs, 300_000); assert.doesNotMatch(failure.error.message, /private provider/); + if (code === "payment_issue") assert.match(failure.error.message, /complete the outstanding ElevenLabs invoice/); } });