{"record":{"id":"7c0c8be4d0ba2b6d","repo":"moeru-ai/airi","slug":"streaming-model-id-missing-backend-prefix-reque","errorCode":null,"errorMessage":"Streaming model id missing backend prefix: ${requestedModel}","messagePattern":"Streaming model id missing backend prefix: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/stage-pages/src/pages/settings/providers/speech/official-provider-speech-streaming.vue","lineNumber":103,"sourceCode":"\n// Synthesize via the streaming session helper. The page uses the SAME\n// transport the runtime pipeline uses (ws → API proxy → unspeech\n// bridge → Volcengine v3 bidirectional) so the preview faithfully\n// represents what the user hears in actual chat. The session is opened\n// per-preview because there's no LLM token stream here — we just send\n// one `text` frame containing the static preview prompt.\nasync function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean): Promise<ArrayBuffer> {\n  const requestedModel = model.value\n  if (!requestedModel)\n    throw new Error('No streaming TTS model selected and server returned no default')\n  // `model` looks like `volcengine/seed-tts-2.0`. The trailing path is\n  // forwarded as Volcengine's `api_resource_id` so the upstream knows which\n  // model variant to use; matches the wiring in `Stage.vue`. We require the\n  // `<backend>/<resource>` shape and refuse anything else — silently picking\n  // a fallback resource id hides config drift.\n  const slashIndex = requestedModel.indexOf('/')\n  if (slashIndex < 0)\n    throw new Error(`Streaming model id missing backend prefix: ${requestedModel}`)\n  const apiResourceId = requestedModel.slice(slashIndex + 1)\n  const result = await streamingSynthesize({\n    model: requestedModel,\n    voice: voiceId,\n    input,\n    extraBody: {\n      api_resource_id: apiResourceId,\n      audio: { sample_rate: 24000, bit_rate: 64000 },\n    },\n  })\n  return result.audio\n}\n\nfunction handleLogin() {\n  needsLogin.value = true\n}\n</script>\n","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-pages/src/pages/settings/providers/speech/official-provider-speech-streaming.vue#L85-L121","documentation":"Format guard in the official streaming speech playground: streaming model ids must be shaped `<backend>/<resource>` (e.g. `volcengine/seed-tts-2.0`) because the trailing path segment is forwarded to the upstream as Volcengine's `api_resource_id`. The code computes `slashIndex = requestedModel.indexOf('/')` and refuses ids without a slash, deliberately, so a bare resource id never silently degrades to a wrong/default upstream resource — the comment in source calls this out as hiding config drift.","triggerScenarios":"A stored provider config `model` value like `seed-tts-2.0` (no backend prefix) — from manual config edits, older formats, or a server catalog entry missing its prefix; user typing a custom model id directly into the picker; a backend rename that dropped the prefix from returned catalog entries.","commonSituations":"Migrating stored settings from an older version that stored bare resource ids; hand-editing provider config files; a server catalog regression listing resource-only ids; copy-pasting model names from Volcengine docs without the backend namespace.","solutions":["Pick the model from the server-populated dropdown instead of typing one — catalog entries carry the `<backend>/<resource>` shape.","If the stored value is stale, edit the provider config so `model` includes the backend prefix (e.g. `volcengine/seed-tts-2.0`).","If catalog entries themselves arrive without a prefix, report the server bug — the client intentionally refuses to guess.","Add a migration/default when reading stored config so legacy bare ids are upgraded rather than crashing at generate time."],"exampleFix":"// before\nconst slashIndex = requestedModel.indexOf('/')\nif (slashIndex < 0)\n  throw new Error(`Streaming model id missing backend prefix: ${requestedModel}`)\n\n// after\nif (!/^[a-z0-9-]+\\/.+$/i.test(requestedModel))\n  throw new Error(`Streaming model id must look like '<backend>/<resource>', got: ${requestedModel}`)","handlingStrategy":"type-guard","validationCode":"const STREAMING_MODEL_ID_PATTERN = /^[a-z0-9-]+\\/.+$/i\nconst modelIdValid = computed(() => STREAMING_MODEL_ID_PATTERN.test(model.value ?? ''))\n// template: <button :disabled=\"!modelIdValid\">Generate</button>","typeGuard":"function isStreamingModelId(id: string): boolean {\n  // '<backend>/<resource>' — backend segment non-empty, resource segment non-empty\n  return /^[^/\\s]+\\/.+[^/\\s]$/.test(id) && !id.startsWith('/') && !id.endsWith('/')\n}","tryCatchPattern":"try { const apiResourceId = requestedModel.slice(requestedModel.indexOf('/') + 1) }\ncatch { /* validation happens before slicing; keep the explicit error message with the offending id */ }","preventionTips":["Only allow selecting catalog-provided ids; disable free-text entry of model ids.","Migrate stored configs: upgrade legacy bare ids to backend-prefixed form on read.","Validate the shape when saving provider config, not only when generating."],"tags":["speech","streaming","model-id","format-validation"],"backgroundTag":"malformed-model-identifier","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}