{"record":{"id":"81c25b3ad1672335","repo":"moeru-ai/airi","slug":"streaming-models-upstream-missing-models","errorCode":null,"errorMessage":"streaming models upstream missing models[]","messagePattern":"streaming models upstream missing models\\[\\]","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/libs/providers/providers/official/index.ts","lineNumber":273,"sourceCode":"    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\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.","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/libs/providers/providers/official/index.ts#L255-L291","documentation":"Thrown by the official streaming speech provider's `listModels()` when `/api/v1/audio/models/streaming` returned 200 but `data.models` is not an array. The declared type marks `models` as required (unlike the HTTP provider where it is optional), so any 200 body without `models[]` violates the contract and aborts model discovery.","triggerScenarios":"Server answers 200 with `{ available: false }` and no `models` key; an older server shape (e.g. plain array or different key) after version skew; a proxy rewriting error pages to 200.","commonSituations":"Client newer than server during staged rollout; `STREAMING_TTS_UPSTREAM` half-configured so the gateway reports availability metadata without a model catalog; API edge returning 200 JSON errors.","solutions":["Inspect the raw response body via curl to see the actual shape returned.","Deploy matching server/client revisions — the client expects `{ available, models: [{id,name,description}], default }`.","If the server intentionally reports no models, have it return an empty `models: []` array rather than omitting the field.","Treat this failure as 'streaming unavailable' in callers so the UI degrades gracefully."],"exampleFix":"// before\nif (!Array.isArray(data.models))\n  throw new Error('streaming models upstream missing models[]')\n// after — honor the availability flag and tolerate a missing catalog\nif (!Array.isArray(data.models)) {\n  streamingTtsAvailable.value = false\n  return []\n}","handlingStrategy":"type-guard","validationCode":"const data: unknown = await res.json()\nif (!isStreamingModelsPayload(data))\n  streamingTtsAvailable.value = false","typeGuard":"function isStreamingModelsPayload(data: unknown): data is { available?: boolean, models: Array<{ id: string, name?: string, description?: string }>, default?: string | null } {\n  if (typeof data !== 'object' || data === null)\n    return false\n  return Array.isArray((data as { models?: unknown }).models)\n}","tryCatchPattern":"const models = isStreamingModelsPayload(data)\n  ? data.models.map(toModelInfo)\n  : []","preventionTips":["Server should always include `models` (empty array when none) — make that an API invariant.","Contract-test the streaming models payload shape on the server.","Tolerate a missing catalog while honoring the `available` flag.","Deploy server and client revisions together."],"tags":["schema","validation","api","server","streaming"],"backgroundTag":"schema-validation-failed","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}