# Providers Every adapter implements the same interface (`src/types.ts`): ```ts interface ProviderAdapter { id: string capabilities: { speech: boolean singing: boolean formats: Array<'wav' | 'mp3'> voices: Array<{ id: string; label?: string; description?: string }> requiresKey: boolean voiceDesign?: boolean voiceClone?: boolean platforms?: NodeJS.Platform[] } synthesize(request, config, runtime): Promise<{ data: Uint8Array; format; voice; meta? }> } ``` `synthesize` receives an abortable `request.signal`, the merged provider config, and a runtime object with an **injectable `fetch`**, the credential resolver, the logger, the environment, the platform and the bundled script paths. The adapters never call the global `fetch` directly and never fall back on their own — retries and fallback live in `src/core/chain.ts`. ## Error classification | Condition | `TtsError.kind` | Retried | Falls back | |---|---|---|---| | 400 / 404 / 422 (bad request from provider) | `provider` | no | yes | | 401 / 403 | `auth` | no | yes | | 402 | `quota` | no | yes | | 429 | `rate-limit` | yes (1s, then 2s) | yes | | 5xx | `server` | yes | yes | | connection failure | `network` | yes | yes | | timeout (AbortSignal.timeout) | `timeout` | yes | yes | | provider content filter (`finish_reason: content_filter`) | `content-filter` | no | yes | | caller aborted | `aborted` | no | no | | input validation inside an adapter | `validation` | no | no | | platform mismatch (SAPI on non-Windows) | `platform` | no | yes | | stub provider | `not-implemented` | no | yes | Messages never contain the API key or the synthesised text; provider echo is redacted before it leaves the host. ## openai-compatible ``` POST {baseURL}/audio/speech Authorization: Bearer Content-Type: application/json { "model": "", "voice": "", "input": "", "response_format": "mp3" | "wav", "instructions": "