moeru-ai/airi · warning · Error
streaming models upstream ${res.status}: ${await res.text().
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).
Source
Thrown at packages/stage-ui/src/libs/providers/providers/official/index.ts:269
return provider
},
validationRequiredWhen: () => false,
extraMethods: {
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 theView on GitHub (pinned to 677329427f)
Solutions
- Ensure the user is authenticated before the probe runs (the UI should hide streaming until signed in).
- curl `/api/v1/audio/models/streaming` with a bearer token to see the real status and body.
- 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).
- 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
- Only probe the streaming endpoint after sign-in.
- Remember a failed probe is the designed 'unavailable' path (`streamingTtsAvailable` stays false) — do not surface it as a user error.
- Configure `STREAMING_TTS_UPSTREAM` on servers that should offer streaming TTS.
- Cache the probe result per session to avoid repeated failing calls.
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
- audio voices upstream ${res.status}: ${await res.text().catc
- streaming voices upstream ${res.status}: ${await res.text().
- audio models upstream ${res.status}: ${await res.text().catc
- MiniMax TTS request failed: ${response.status} ${response.st
- audio voices upstream returned malformed body
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/4e39a4b7f564b777.
Report an issue: GitHub.