moeru-ai/airi · warning

streaming models upstream

Error message

streaming models upstream ${res.status}: ${await res.text().catch(() => '')}

What it means

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).

Solutions

  1. Ensure the user is authenticated before the probe runs (the UI should hide streaming until signed in).
  2. curl `/api/v1/audio/models/streaming` with a bearer token to see the real status and body.
  3. 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).
  4. Update both server and client so the route and the `available`/`models[]` contract match.

Example fix

// before
const models = await streamingProvider.extraMethods.listModels()
// after — treat a failed probe as 'streaming unavailable' instead of an error
let models: ModelInfo[] = []
try {
  models = await streamingProvider.extraMethods.listModels()
}
catch (error) {
  console.warn('streaming TTS unavailable', error)
}
Defensive patterns

Strategy: try-catch

Validate before calling

const token = getAuthToken()
if (!token)
  return { models: [], available: false } // skip the probe entirely

Try / catch

try {
  models = await streamingProvider.extraMethods.listModels()
}
catch (error) {
  console.warn('streaming TTS probe failed — keeping provider hidden', error)
  models = []
}

Prevention

When it happens

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

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

Related errors


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

Appendix: source

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

const officialConfigSchema = z.object({})

function authHeaders(): Record<string, string> {
  const headers: Record<string, string> = { Accept: 'application/json' }
  const token = getAuthToken()
  if (token)
    headers.Authorization = `Bearer ${token}`
  return headers
}

async function listStreamingModelCatalog(): Promise<ProviderModelCatalog> {
  // 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.
  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
  }

  return {
    available: data.available,
    defaultModel: data.default ?? null,
    models: data.models.map(m => ({
      id: m.id,
      name: m.name ?? m.id,
      provider: OFFICIAL_SPEECH_STREAMING_PROVIDER_ID,
      description: m.description,
    })),
  }
}

View on GitHub (pinned to 438a067dde)