{"record":{"id":"b1d721f81497339f","repo":"moeru-ai/airi","slug":"audio-models-upstream-res-status-await-res-t","errorCode":null,"errorMessage":"audio models upstream ${res.status}: ${await res.text().catch(() => '')}","messagePattern":"audio models upstream (.+?): (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/libs/providers/providers/official/index.ts","lineNumber":118,"sourceCode":"  createProvider(_config) {\n    const provider = createOfficialAudioProvider()\n    const originalSpeech = provider.speech.bind(provider)\n    provider.speech = (model: string, extraOptions?: Record<string, unknown>) => {\n      const result = {\n        ...originalSpeech(model),\n        ...extraOptions,\n      }\n      result.fetch = withCredentials()\n      return result\n    }\n    return provider\n  },\n  validationRequiredWhen: () => false,\n  extraMethods: {\n    listModelCatalog: async (): Promise<ProviderModelCatalog> => {\n      const res = await globalThis.fetch(`${SERVER_URL}/api/v1/audio/models`, { headers: authHeaders() })\n      if (!res.ok)\n        throw new Error(`audio models upstream ${res.status}: ${await res.text().catch(() => '')}`.slice(0, 256))\n\n      const data = await res.json() as { models?: { id: string, name: string, description?: string }[], default?: string | null }\n      if (!Array.isArray(data.models))\n        throw new Error('audio models upstream returned malformed body')\n\n      // Replicate the server default with its models so a new leader can select it.\n      return {\n        defaultModel: typeof data.default === 'string' && data.default.length > 0 ? data.default : null,\n        models: data.models.map(m => ({\n          id: m.id,\n          name: m.name,\n          description: m.description,\n          provider: OFFICIAL_SPEECH_PROVIDER_ID,\n        })),\n      }\n    },\n    voiceCatalogConfig: () => ({}),\n    listVoices: async (_config, _provider, model, signal): Promise<VoiceInfo[]> => {","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/packages/stage-ui/src/libs/providers/providers/official/index.ts#L100-L136","documentation":"Thrown by the official speech provider's `listModels()` when the AIRI server endpoint `${SERVER_URL}/api/v1/audio/models` (proxied to the unspeech gateway) replies with a non-2xx status. The request carries `Authorization: Bearer <token>` from `getAuthToken()`, so auth failures and server-side upstream configuration failures are the two dominant causes. The body is truncated to 256 chars in the message.","triggerScenarios":"Calling `listModels()` while signed out or with an expired JWT (401), pointing `SERVER_URL` at a server that lacks the audio route (404), or the server's `UNSPEECH_UPSTREAM` being unreachable/misconfigured so the gateway returns 502/503.","commonSituations":"Session expired overnight and the speech settings page probes models on mount; local docker-compose backend not running while the client still points at it; server deployed from an older revision without the unspeech routes; auth service (server/apps/auth) token audience mismatch after redeploying only one service.","solutions":["Re-authenticate (sign out/in) so `getAuthToken()` returns a fresh JWT, then retry the model listing.","Confirm the client's `SERVER_URL` points at a running server that serves `/api/v1/audio/models` (curl it with the bearer token).","Inspect the server logs for the unspeech upstream error behind a 502/503 and fix `UNSPEECH_UPSTREAM` configuration.","If the server is old, update it to a revision shipping the audio models route."],"exampleFix":"// before\nconst models = await provider.extraMethods.listModels()\n// after — only probe when authenticated, and report the upstream cause\nimport { getAuthToken } from '../libs/auth'\nconst token = getAuthToken()\nif (!token)\n  throw new Error('Sign in before listing official speech models')\nconst models = await provider.extraMethods.listModels()","handlingStrategy":"try-catch","validationCode":"import { getAuthToken } from '../libs/auth'\nconst token = getAuthToken()\nif (!token)\n  throw new Error('Authentication required to list official speech models')","typeGuard":null,"tryCatchPattern":"try {\n  const models = await provider.extraMethods.listModels()\n}\ncatch (error) {\n  const message = errorMessageFrom(error)\n  if (message.includes('401'))\n    // re-authenticate then retry\n  else\n    // treat as server/upstream misconfiguration; surface upstream body (truncated to 256 chars)\n}","preventionTips":["Probe model lists only after sign-in completes.","Keep client and server revisions in lockstep for `/api/v1/audio/*` contracts.","Health-check the unspeech gateway config (`UNSPEECH_UPSTREAM`) when deploying.","Cache the last successful model list to keep the UI usable during outages."],"tags":["network","api","auth","server","speech","models"],"backgroundTag":"upstream-api-error","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}