{"record":{"id":"2186e077f39428ad","repo":"moeru-ai/airi","slug":"audio-voices-upstream-res-status-await-res-t","errorCode":null,"errorMessage":"audio voices upstream ${res.status}: ${await res.text().catch(() => '')}","messagePattern":"audio voices upstream (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/libs/providers/providers/official/index.ts","lineNumber":157,"sourceCode":"\n      return data.models.map(m => ({\n        id: m.id,\n        name: m.name,\n        description: m.description,\n        provider: OFFICIAL_SPEECH_PROVIDER_ID,\n      }))\n    },\n    listVoices: async (_config, _provider, model): Promise<VoiceInfo[]> => {\n      // Voice catalogs are model-scoped on the server side. Pass the active\n      // model through so Azure / cosyvoice / future provider voices route to\n      // the right adapter. If model discovery has not completed yet, keep the\n      // legacy `auto` request as a startup fallback.\n      const target = model && model.length > 0 ? model : 'auto'\n      const url = new URL(`${SERVER_URL}/api/v1/audio/voices`)\n      url.searchParams.set('model', target)\n      const res = await globalThis.fetch(url.toString(), { headers: authHeaders() })\n      if (!res.ok)\n        throw new Error(`audio voices upstream ${res.status}: ${await res.text().catch(() => '')}`.slice(0, 256))\n\n      // Shape aligned with unspeech's types.ListVoicesResponse, plus the\n      // `recommended` field our server injects from configKV DEFAULT_TTS_VOICES.\n      // https://github.com/moeru-ai/unspeech/blob/main/pkg/backend/types/voices.go\n      const data = await res.json() as {\n        voices?: {\n          id: string\n          name: string\n          description?: string\n          labels?: Record<string, unknown>\n          tags?: string[]\n          languages?: { code: string, title: string }[]\n          compatible_models?: string[]\n          preview_audio_url?: string\n        }[]\n        recommended?: Record<string, string>\n      }\n","sourceCodeStart":139,"sourceCodeEnd":175,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/libs/providers/providers/official/index.ts#L139-L175","documentation":"Thrown by the official speech provider's `listVoices()` when `GET ${SERVER_URL}/api/v1/audio/voices?model=<id|auto>` replies non-2xx. The `model` query param is passed through so the server routes to the right TTS adapter (Azure, cosyvoice, …), defaulting to `auto` when model discovery has not run yet. Auth header comes from `getAuthToken()`.","triggerScenarios":"Expired/missing JWT (401); requesting voices for a `model` id the server does not know (400/404); the server's per-backend voice adapter failing (502) because `UNSPEECH_UPSTREAM` names a backend whose credentials are missing; older server without the voices route (404).","commonSituations":"Voice list loads on the speech settings page right after token expiry; selecting a model id that came from a stale provider cache after the server catalog changed; partial server config where the HTTP TTS upstream is set but its voice catalog fetch fails.","solutions":["Retry after re-authenticating if the status is 401/403.","Check that the active model id is one returned by `listModels()` for this provider, not a streaming id or stale id.","Verify with curl `'/api/v1/audio/voices?model=auto'` using a valid bearer token and read the server-side upstream error.","Ensure `UNSPEECH_UPSTREAM` is fully configured on the server (backend ids, credentials, voice catalogs)."],"exampleFix":"// before\nconst voices = await provider.extraMethods.listVoices(config, provider, model)\n// after — fall back to the catalog-wide query when the model-scoped one fails\nlet voices\ntry {\n  voices = await provider.extraMethods.listVoices(config, provider, model)\n}\ncatch {\n  voices = await provider.extraMethods.listVoices(config, provider, 'auto')\n}","handlingStrategy":"try-catch","validationCode":"const token = getAuthToken()\nif (!token)\n  throw new Error('Authentication required to list official voices')\nif (model && !model.includes('/') && model !== 'auto')\n  // ensure the model id came from listModels()","typeGuard":null,"tryCatchPattern":"try {\n  voices = await provider.extraMethods.listVoices(config, provider, model)\n}\ncatch (error) {\n  voices = await provider.extraMethods.listVoices(config, provider, 'auto')\n    .catch(() => { throw error })\n}","preventionTips":["Always pass a model id obtained from this provider's `listModels()`.","Keep `UNSPEECH_UPSTREAM` backend ids and credentials in sync with the voice catalogs.","Fall back to the `auto` (catalog-wide) query when the model-scoped query fails.","Re-probe voices after re-authentication, not with the stale token."],"tags":["network","api","auth","server","tts","voices"],"backgroundTag":"upstream-api-error","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-08-23T16:17:53.355Z"}