/** * Plain-English explanations for the useapi Google Flow refusals that a retry * can never fix. Pure: every Flow HTTP caller in `src/` passes a failed * response's status and body here BEFORE deciding whether to retry, so * the retry decision and the message cannot drift apart between callers. * * Spec: https://useapi.net/assets/aibot/api-google-flow-v1.txt (generated * 2026-09-25). Changelog entries this module covers: * - 2026-09-16: every account added before that date runs on Google's retiring * `labs` backend. `GET /characters`, `GET /voices`, `GET /assets/media/{email}` * and `POST /videos/concatenate` already return 404 "Flow RPCs have been * deprecated and disabled"; generation stops once Google finishes the * switch-off. The fix is to re-add the account. Code does not change. * - 2026-09-23: `403 PUBLIC_ERROR_MODEL_ACCESS_DENIED` means the account's plan * does not offer the model. That includes `veo-3.1-lite-low-priority` on an * invited member of an Ultra family plan. It passed the captcha, so raising * `captchaRetry` cannot help, and the 503 useapi briefly documented for it * was withdrawn. * - `429 no_eligible_account`: every account is quarantined for this model. * The body carries `message`, `retryAfter` and a per-account `skipReasons[]`. */ export type FlowRefusalKind = 'model-access-denied' | 'labs-backend-retired' | 'no-eligible-account'; export interface FlowRefusal { kind: FlowRefusalKind; /** True when retrying the same request cannot succeed. */ terminal: boolean; message: string; } const MODEL_ACCESS_DENIED_RE = /PUBLIC_ERROR_MODEL_ACCESS_DENIED/; const LABS_RETIRED_RE = /Flow RPCs have been deprecated and disabled/i; export const FLOW_READD_ACCOUNT_STEP = 'Re-add the account at https://useapi.net/docs/start-here/setup-google-flow (about a minute, needs the Google login). ' + 'It keeps its project, characters, voices and credits, and no code changes.'; function safeJson(text: string): Record { try { const parsed = JSON.parse(text) as unknown; return parsed && typeof parsed === 'object' ? (parsed as Record) : {}; } catch { return {}; } } function describeSkipReasons(value: unknown): string { if (!Array.isArray(value) || value.length === 0) return ''; const rows = value .map((row) => { const r = (row ?? {}) as { email?: unknown; reason?: unknown; model?: unknown }; const model = r.model === '*' ? 'every model' : r.model === 'upload' ? 'uploads' : String(r.model ?? '?'); return `${String(r.email ?? '?')}: ${String(r.reason ?? '?')} (${model})`; }) .join('; '); return ` Skipped accounts: ${rows}.`; } /** * Classify a failed useapi Google Flow response. Returns null for everything * else, so callers keep their existing handling (captcha retry, throttle * cooldown, moderation) unchanged. */ export function explainFlowRefusal(status: number, bodyText: string): FlowRefusal | null { if (status === 403 && MODEL_ACCESS_DENIED_RE.test(bodyText)) { const lowPriority = /veo-3\.1-lite-low-priority|lite_low_priority/i.test(bodyText); return { kind: 'model-access-denied', terminal: true, message: 'Google refused this model on this account (403 PUBLIC_ERROR_MODEL_ACCESS_DENIED): the account\'s plan does not offer it. ' + (lowPriority ? 'Since 2026-09-23 Google keeps veo-3.1-lite-low-priority for the MANAGER of an Ultra family plan only; an invited member account loses it. ' : 'Since 2026-09-23 this includes veo-3.1-lite-low-priority on an invited member of an Ultra family plan. ') + 'This is not a captcha failure, and retrying cannot fix it. Use the family manager\'s account, or choose a model the plan includes.', }; } if (status === 404 && LABS_RETIRED_RE.test(bodyText)) { return { kind: 'labs-backend-retired', terminal: true, message: 'This Google Flow account is still on Google\'s retired "labs" backend, which no longer serves characters, voices, ' + 'the media library or concatenate ("Flow RPCs have been deprecated and disabled"). Generation still works for now, ' + `but stops once Google finishes the switch-off. ${FLOW_READD_ACCOUNT_STEP}`, }; } if (status === 429) { const body = safeJson(bodyText); if (body.error === 'no_eligible_account') { const why = typeof body.message === 'string' && body.message ? ` ${body.message}` : ''; const until = typeof body.retryAfter === 'string' ? ` Earliest retry: ${body.retryAfter}.` : ''; return { kind: 'no-eligible-account', terminal: false, message: `Every Google Flow account is quarantined for this request (429 no_eligible_account).${why}${until}${describeSkipReasons(body.skipReasons)}`, }; } } return null; } /** Account `backend` field (2026-09-16). Returns operator advice for `labs`, else null. */ export function flowAccountBackendAdvice(backend: unknown): string | null { if (backend !== 'labs') return null; return ( 'This Google Flow account runs on Google\'s retiring "labs" backend: characters, voices, the media library and ' + `concatenate already fail, and generation stops when Google finishes the switch-off. ${FLOW_READD_ACCOUNT_STEP}` ); }