{"record":{"id":"4e39a4b7f564b777","repo":"moeru-ai/airi","slug":"streaming-models-upstream-res-status-await-r","errorCode":null,"errorMessage":"streaming models upstream ${res.status}: ${await res.text().catch(() => '')}","messagePattern":"streaming models upstream (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/stage-ui/src/libs/providers/providers/official/index.ts","lineNumber":269,"sourceCode":"    return provider\n  },\n  validationRequiredWhen: () => false,\n  extraMethods: {\n    listModels: async (): Promise<ModelInfo[]> => {\n      // Streaming TTS catalog is operator-controlled via configKV\n      // (`UNSPEECH_UPSTREAM.streaming`). Wire shape uses `<backend>/<api_resource_id>`\n      // (see `unspeech/docs/wire-protocols/audio-speech-stream-v1.md`); the\n      // server returns whatever the operator put there, no client-side\n      // defaults. `default` (when set) seeds initial model selection via\n      // {@link getDefaultStreamingModel}.\n      // Reset the operator-driven signals up front so a failed/aborted probe\n      // leaves the provider hidden rather than stuck on a stale \"available\".\n      streamingTtsAvailable.value = false\n      defaultStreamingModelId = null\n\n      const res = await globalThis.fetch(`${SERVER_URL}/api/v1/audio/models/streaming`, { headers: authHeaders() })\n      if (!res.ok)\n        throw new Error(`streaming models upstream ${res.status}: ${await res.text().catch(() => '')}`.slice(0, 256))\n\n      const data = await res.json() as { available?: boolean, models: { id: string, name?: string, description?: string }[], default?: string | null }\n      if (!Array.isArray(data.models))\n        throw new Error('streaming models upstream missing models[]')\n\n      streamingTtsAvailable.value = data.available === true\n      defaultStreamingModelId = typeof data.default === 'string' && data.default.length > 0 ? data.default : null\n\n      return data.models.map(m => ({\n        id: m.id,\n        name: m.name ?? m.id,\n        provider: OFFICIAL_SPEECH_STREAMING_PROVIDER_ID,\n        description: m.description,\n      }))\n    },\n    listVoices: async (_config, _provider, model): Promise<VoiceInfo[]> => {\n      // Streaming voices live behind a dedicated endpoint\n      // (`/audio/voices/streaming`) because they come from the","sourceCodeStart":251,"sourceCodeEnd":287,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/libs/providers/providers/official/index.ts#L251-L287","documentation":"Thrown by the official streaming speech provider's `listModels()` when `GET ${SERVER_URL}/api/v1/audio/models/streaming` replies non-2xx. This probe also drives `streamingTtsAvailable` — the flag is reset to `false` before the fetch so a failed probe hides the provider instead of leaving a stale 'available'. Typical statuses: 401 (no/expired bearer token) or 5xx (server-side `STREAMING_TTS_UPSTREAM` not configured/unreachable).","triggerScenarios":"Probing streaming models while signed out or with an expired JWT; server revision without the `/models/streaming` route (404); `STREAMING_TTS_UPSTREAM` unset or its upstream down, surfacing as 502/503 from the gateway.","commonSituations":"Fresh sign-in flow where the probe races token refresh; self-hosted deployment that never configured streaming TTS but runs a new client that probes it; partial outage of the volcengine/other streaming upstream behind the gateway.","solutions":["Ensure the user is authenticated before the probe runs (the UI should hide streaming until signed in).","curl `/api/v1/audio/models/streaming` with a bearer token to see the real status and body.","On the server, set `STREAMING_TTS_UPSTREAM` with a valid backend, or expect this probe to fail and keep the streaming provider hidden (that is by design).","Update both server and client so the route and the `available`/`models[]` contract match."],"exampleFix":"// before\nconst models = await streamingProvider.extraMethods.listModels()\n// after — treat a failed probe as 'streaming unavailable' instead of an error\nlet models: ModelInfo[] = []\ntry {\n  models = await streamingProvider.extraMethods.listModels()\n}\ncatch (error) {\n  console.warn('streaming TTS unavailable', error)\n}","handlingStrategy":"try-catch","validationCode":"const token = getAuthToken()\nif (!token)\n  return { models: [], available: false } // skip the probe entirely","typeGuard":null,"tryCatchPattern":"try {\n  models = await streamingProvider.extraMethods.listModels()\n}\ncatch (error) {\n  console.warn('streaming TTS probe failed — keeping provider hidden', error)\n  models = []\n}","preventionTips":["Only probe the streaming endpoint after sign-in.","Remember a failed probe is the designed 'unavailable' path (`streamingTtsAvailable` stays false) — do not surface it as a user error.","Configure `STREAMING_TTS_UPSTREAM` on servers that should offer streaming TTS.","Cache the probe result per session to avoid repeated failing calls."],"tags":["network","api","auth","server","streaming","tts"],"backgroundTag":"upstream-api-error","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}