moeru-ai/airi · error · Error

No streaming TTS model selected and server returned no defau

Error message

No streaming TTS model selected and server returned no default

What it means

Guard in the official streaming speech playground: the model to synthesize with comes from the stored provider config or, failing that, a server-provided default. The catalog and default are fetched from the server's `/api/v1/audio/models/streaming` endpoint (operator-controlled via `UNSPEECH_UPSTREAM.streaming`); `serverDefaultModel` is `getDefaultStreamingModel() ?? providerModels[0]?.id ?? null`. If the stored model is unset AND the server returned an empty catalog with no curated default, `model.value` resolves to `''` and this error is thrown before opening the WebSocket session.

Source

Thrown at packages/stage-pages/src/pages/settings/providers/speech/official-provider-speech-streaming.vue:95

})

// Volcengine TTS 1.0 and 2.0 ship different voice catalogues (mars/moon/ICL
// vs uranus/saturn; see unspeech voices.go). Re-fetch on model change so the
// list switches accordingly.
watch(model, async () => {
  await loadVoices()
})

// Synthesize via the streaming session helper. The page uses the SAME
// transport the runtime pipeline uses (ws → API proxy → unspeech
// bridge → Volcengine v3 bidirectional) so the preview faithfully
// represents what the user hears in actual chat. The session is opened
// per-preview because there's no LLM token stream here — we just send
// one `text` frame containing the static preview prompt.
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean): Promise<ArrayBuffer> {
  const requestedModel = model.value
  if (!requestedModel)
    throw new Error('No streaming TTS model selected and server returned no default')
  // `model` looks like `volcengine/seed-tts-2.0`. The trailing path is
  // forwarded as Volcengine's `api_resource_id` so the upstream knows which
  // model variant to use; matches the wiring in `Stage.vue`. We require the
  // `<backend>/<resource>` shape and refuse anything else — silently picking
  // a fallback resource id hides config drift.
  const slashIndex = requestedModel.indexOf('/')
  if (slashIndex < 0)
    throw new Error(`Streaming model id missing backend prefix: ${requestedModel}`)
  const apiResourceId = requestedModel.slice(slashIndex + 1)
  const result = await streamingSynthesize({
    model: requestedModel,
    voice: voiceId,
    input,
    extraBody: {
      api_resource_id: apiResourceId,
      audio: { sample_rate: 24000, bit_rate: 64000 },
    },
  })

View on GitHub (pinned to 677329427f)

Solutions

  1. Wait for the model picker to populate (modelsLoading) and explicitly select a streaming model, which persists it to provider config.
  2. Verify the server exposes streaming TTS: check the `/api/v1/audio/models/streaming` response in the network tab and have the operator set `UNSPEECH_UPSTREAM.streaming` with a default model.
  3. Re-login if the session expired — an unauthenticated models fetch can come back empty.
  4. Restart the app after the operator fixes server config so onMounted re-fetches the catalog.
  5. If the catalog is intentionally empty, remove/hide the provider page rather than letting users hit this dead end.

Example fix

// before
const requestedModel = model.value
if (!requestedModel)
  throw new Error('No streaming TTS model selected and server returned no default')

// after
const requestedModel = model.value
if (!requestedModel)
  throw new Error(modelsLoading.value
    ? 'Streaming model catalog is still loading — try again in a moment'
    : 'No streaming TTS model available: select one, or ask the operator to configure UNSPEECH_UPSTREAM.streaming')
Defensive patterns

Strategy: validation

Validate before calling

const canGenerate = computed(() => !!model.value && !modelsLoading.value)
// template: <button :disabled="!canGenerate">Generate</button>
if (modelsLoading.value)
  throw new Error('Streaming model catalog still loading')
if (!model.value)
  throw new Error('Select a streaming model first')

Type guard

function hasStreamingModel(m: string | null | undefined): m is string {
  return typeof m === 'string' && m.length > 0
}

Try / catch

try { return await streamingSynthesize({ ... }) }
catch (error) { showError(errorMessageFrom(error)) }

Prevention

When it happens

Trigger: Operator has not configured `UNSPEECH_UPSTREAM.streaming` (or no default model) server-side, so the models request returns an empty list; the models request failed (auth expired, network) leaving both stored config and server default empty; user cleared the stored model value; generating before the onMounted fetch resolves.

Common situations: Self-hosted deployments without the streaming upstream configured; expired login so the API returns an empty/unauthorized payload treated as no models; new backend versions that renamed models leaving stale-empty state; pointing the client at a server that does not expose the streaming endpoint.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/31db7ddc194834e5. Report an issue: GitHub.