{"record":{"id":"b32b49e27a0cf3a4","repo":"moeru-ai/airi","slug":"streaming-voices-upstream-res-status-await-r","errorCode":null,"errorMessage":"streaming voices upstream ${res.status}: ${await res.text().catch(() => '')}","messagePattern":"streaming voices upstream (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/libs/providers/providers/official/index.ts","lineNumber":305,"sourceCode":"      // (`/audio/voices/streaming`) because they come from the\n      // `UNSPEECH_UPSTREAM.streaming` configKV subtree rather than the HTTP TTS\n      // `?model=...` lookup. The server proxies to unspeech's\n      // `/api/voices?provider=volcengine`, which ships an embed-time\n      // catalogue without requiring credentials.\n      //\n      // `model` here is the unspeech-routed id (e.g. `volcengine/seed-tts-2.0`).\n      // unspeech expects the bare `api_resource_id` for its filter, so we\n      // strip the backend prefix before forwarding.\n      const apiResourceId = model?.includes('/') ? model.split('/', 2)[1] : model\n      const voicesURL = new URL(`${SERVER_URL}/api/v1/audio/voices/streaming`)\n      if (apiResourceId)\n        voicesURL.searchParams.set('model', apiResourceId)\n      const res = await globalThis.fetch(\n        voicesURL.toString(),\n        { headers: authHeaders() },\n      )\n      if (!res.ok)\n        throw new Error(`streaming voices upstream ${res.status}: ${await res.text().catch(() => '')}`.slice(0, 256))\n\n      const data = await res.json() as {\n        voices?: {\n          id: string\n          name: string\n          description?: string\n          labels?: Record<string, unknown>\n          languages?: { code: string, title: string }[]\n          preview_audio_url?: string\n        }[]\n        recommended?: Record<string, string>\n      }\n\n      // Mirror the HTTP provider: stash the server's per-locale recommendations\n      // so setupOfficialSpeechAutoPick can seed a curated default voice when\n      // the streaming provider becomes active.\n      recommendedVoicesByProvider[OFFICIAL_SPEECH_STREAMING_PROVIDER_ID] = (data.recommended && typeof data.recommended === 'object') ? data.recommended : {}\n","sourceCodeStart":287,"sourceCodeEnd":323,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/libs/providers/providers/official/index.ts#L287-L323","documentation":"Thrown by the official streaming speech provider's `listVoices()` when `GET ${SERVER_URL}/api/v1/audio/voices/streaming?model=<apiResourceId>` replies non-2xx. Before the fetch the provider strips the backend prefix from the unspeech-routed id (`volcengine/seed-tts-2.0` → `seed-tts-2.0`) because unspeech's voice filter expects the bare `api_resource_id`.","triggerScenarios":"401/403 from a missing or expired bearer token; 400/404 when the stripped `api_resource_id` does not match any configured streaming backend; 502 when the streaming upstream (e.g. Volcengine) rejects the server's voice-catalog call due to bad credentials.","commonSituations":"Voice list requested for a streaming model id that came from a stale cache or a different server config; server-side `STREAMING_TTS_UPSTREAM` cluster/section ids (like Volcengine `api_resource_id`) misconfigured; token expired between model probe and voice fetch.","solutions":["Verify the bearer token is still valid (re-sign-in) and retry.","Confirm the model id is a streaming id from `listModels()` and that its `api_resource_id` exists in the server's streaming upstream config.","curl `/api/v1/audio/voices/streaming?model=<apiResourceId>` with auth to see the gateway's upstream error.","Fix or complete `STREAMING_TTS_UPSTREAM` credentials on the server for the failing backend."],"exampleFix":"// before\nconst voices = await streamingProvider.extraMethods.listVoices(config, provider, model)\n// after — probe availability first, then degrade to an empty catalog\nconst voices = getStreamingTtsAvailable()\n  ? await streamingProvider.extraMethods.listVoices(config, provider, model).catch(() => [])\n  : []","handlingStrategy":"try-catch","validationCode":"const apiResourceId = model?.includes('/') ? model.split('/', 2)[1] : model\nif (!apiResourceId)\n  throw new Error('A streaming model id is required to list voices')","typeGuard":null,"tryCatchPattern":"try {\n  voices = await streamingProvider.extraMethods.listVoices(config, provider, model)\n}\ncatch (error) {\n  if (errorMessageFrom(error).includes('401'))\n    // re-authenticate and retry once\n  else\n    voices = [] // streaming voice catalog unavailable\n}","preventionTips":["Only list streaming voices for ids returned by the streaming `listModels()`.","Verify the server's `api_resource_id` config matches the routed model prefix.","Gate the call behind `getStreamingTtsAvailable()`.","Degrade to an empty catalog rather than blocking TTS setup."],"tags":["network","api","auth","server","streaming","voices"],"backgroundTag":"upstream-api-error","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}