moeru-ai/airi · error · Error

streaming models upstream missing models[]

Error message

streaming models upstream missing models[]

What it means

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.

Solutions

  1. Inspect the raw response body via curl to see the actual shape returned.
  2. Deploy matching server/client revisions — the client expects `{ available, models: [{id,name,description}], default }`.
  3. If the server intentionally reports no models, have it return an empty `models: []` array rather than omitting the field.
  4. Treat this failure as 'streaming unavailable' in callers so the UI degrades gracefully.

Example fix

// before
if (!Array.isArray(data.models))
  throw new Error('streaming models upstream missing models[]')
// after — honor the availability flag and tolerate a missing catalog
if (!Array.isArray(data.models)) {
  streamingTtsAvailable.value = false
  return []
}
Defensive patterns

Strategy: type-guard

Validate before calling

const data: unknown = await res.json()
if (!isStreamingModelsPayload(data))
  streamingTtsAvailable.value = false

Type guard

function isStreamingModelsPayload(data: unknown): data is { available?: boolean, models: Array<{ id: string, name?: string, description?: string }>, default?: string | null } {
  if (typeof data !== 'object' || data === null)
    return false
  return Array.isArray((data as { models?: unknown }).models)
}

Try / catch

const models = isStreamingModelsPayload(data)
  ? data.models.map(toModelInfo)
  : []

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of moeru-ai/airi@438a067dde (2026-08-18). Data as JSON: /api/errors/81c25b3ad1672335. Report an issue: GitHub.

Appendix: source

Thrown at packages/stage-ui/src/libs/providers/providers/official/index.ts:261

  validationRequiredWhen: () => false,
  extraMethods: {
    listModelCatalog: listStreamingModelCatalog,
    listModels: async () => (await listStreamingModelCatalog()).models,
    voiceCatalogConfig: () => ({}),
    listVoices: async (_config, _provider, model, signal): Promise<VoiceInfo[]> => {
      // Streaming voices live behind a dedicated endpoint
      // (`/audio/voices/streaming`) because they come from the
      // `UNSPEECH_UPSTREAM.streaming` configKV subtree rather than the HTTP TTS
      // `?model=...` lookup. The server proxies to unspeech's
      // `/api/voices?provider=volcengine`, which ships an embed-time
      // catalogue without requiring credentials.
      //
      // `model` here is the unspeech-routed id (e.g. `volcengine/seed-tts-2.0`).
      // unspeech expects the bare `api_resource_id` for its filter, so we
      // strip the backend prefix before forwarding.
      const apiResourceId = model?.includes('/') ? model.split('/', 2)[1] : model
      const voicesURL = new URL(`${SERVER_URL}/api/v1/audio/voices/streaming`)
      if (apiResourceId)
        voicesURL.searchParams.set('model', apiResourceId)
      const res = await globalThis.fetch(
        voicesURL.toString(),
        { headers: authHeaders(), signal },
      )
      if (!res.ok)
        throw new Error(`streaming voices upstream ${res.status}: ${await res.text().catch(() => '')}`.slice(0, 256))

      const data = await res.json() as {
        voices?: {
          id: string
          name: string
          description?: string
          labels?: Record<string, unknown>
          languages?: { code: string, title: string }[]
          preview_audio_url?: string
        }[]
        recommended?: Record<string, string>

View on GitHub (pinned to 438a067dde)