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.
Source
Thrown at packages/stage-ui/src/libs/providers/providers/official/index.ts:273
listModels: async (): Promise<ModelInfo[]> => {
// Streaming TTS catalog is operator-controlled via configKV
// (`UNSPEECH_UPSTREAM.streaming`). Wire shape uses `<backend>/<api_resource_id>`
// (see `unspeech/docs/wire-protocols/audio-speech-stream-v1.md`); the
// server returns whatever the operator put there, no client-side
// defaults. `default` (when set) seeds initial model selection via
// {@link getDefaultStreamingModel}.
// Reset the operator-driven signals up front so a failed/aborted probe
// leaves the provider hidden rather than stuck on a stale "available".
streamingTtsAvailable.value = false
defaultStreamingModelId = null
const res = await globalThis.fetch(`${SERVER_URL}/api/v1/audio/models/streaming`, { headers: authHeaders() })
if (!res.ok)
throw new Error(`streaming models upstream ${res.status}: ${await res.text().catch(() => '')}`.slice(0, 256))
const data = await res.json() as { available?: boolean, models: { id: string, name?: string, description?: string }[], default?: string | null }
if (!Array.isArray(data.models))
throw new Error('streaming models upstream missing models[]')
streamingTtsAvailable.value = data.available === true
defaultStreamingModelId = typeof data.default === 'string' && data.default.length > 0 ? data.default : null
return data.models.map(m => ({
id: m.id,
name: m.name ?? m.id,
provider: OFFICIAL_SPEECH_STREAMING_PROVIDER_ID,
description: m.description,
}))
},
listVoices: async (_config, _provider, model): 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.View on GitHub (pinned to 677329427f)
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.
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
- 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.
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
- streaming voices upstream returned malformed body
- audio models upstream returned malformed body
- audio voices upstream returned malformed body
- streaming models upstream ${res.status}: ${await res.text().
- streaming voices upstream ${res.status}: ${await res.text().
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/81c25b3ad1672335.
Report an issue: GitHub.