{"record":{"id":"7f93ce8ac971b228","repo":"HKUDS/DeepTutor","slug":"openrouter-chat-audio-error-message","errorCode":null,"errorMessage":"OpenRouter chat audio error: {message}","messagePattern":"OpenRouter chat audio error: (.+?)","errorType":"exception","errorClass":"VoiceProviderError","httpStatus":null,"severity":"error","filePath":"deeptutor/services/voice/adapters/openai_compat.py","lineNumber":273,"sourceCode":"    @staticmethod\n    def _collect_audio_line(line: str, audio_chunks: list[str]) -> None:\n        if not line:\n            return\n        raw = line.strip()\n        if not raw.startswith(\"data:\"):\n            return\n        data = raw[len(\"data:\") :].strip()\n        if not data or data == \"[DONE]\":\n            return\n        try:\n            chunk = json.loads(data)\n        except json.JSONDecodeError:\n            logger.debug(\"Ignoring malformed OpenRouter SSE line: %s\", data[:160])\n            return\n        error = chunk.get(\"error\")\n        if isinstance(error, dict):\n            message = error.get(\"message\") or error.get(\"code\") or \"unknown error\"\n            raise VoiceProviderError(f\"OpenRouter chat audio error: {message}\")\n        choices = chunk.get(\"choices\")\n        if not isinstance(choices, list):\n            return\n        for choice in choices:\n            if not isinstance(choice, dict):\n                continue\n            delta = choice.get(\"delta\") or {}\n            if not isinstance(delta, dict):\n                continue\n            audio = delta.get(\"audio\") or {}\n            if isinstance(audio, dict) and isinstance(audio.get(\"data\"), str):\n                audio_chunks.append(audio[\"data\"])\n\n\nclass OpenAICompatSTTAdapter(BaseSTTAdapter):\n    \"\"\"POST ``{base}/audio/transcriptions``.\n\n    Multipart ``file`` upload by default; OpenRouter uses a base64-JSON body","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/HKUDS/DeepTutor/blob/3e82f130422a813cdd73c10b21a44e9325f5821a/deeptutor/services/voice/adapters/openai_compat.py#L255-L291","documentation":"_collect_audio_line parses each SSE line from the OpenRouter chat stream; if the decoded JSON chunk contains an \"error\" object, it extracts message/code and raises VoiceProviderError(\"OpenRouter chat audio error: {message}\"). This surfaces mid-stream errors that arrive with HTTP 200.","triggerScenarios":"The chat/completions stream starts with HTTP 200 but emits an error event — e.g. invalid model, insufficient credits, or content-policy rejection delivered as an SSE error payload.","commonSituations":"OpenRouter credit exhaustion mid-request, deprecated/renamed model slug, or moderation blocks — all delivered as in-stream error objects instead of HTTP status codes.","solutions":["Act on the embedded message: top up credits (402-style), fix the model slug, or adjust content that triggered moderation","Retry once for transient credit/auth propagation delays","Verify the model exists and supports audio on OpenRouter's model listing","If errors persist, disable the chat fallback and use a native TTS provider"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    audio, ct = await adapter.synthesize(text, config)\nexcept VoiceProviderError as exc:\n    if \"chat audio error\" in str(exc):\n        log.error(\"OpenRouter stream error: %s\", exc)  # credits/model/moderation\n    raise","preventionTips":["Monitor OpenRouter credit balance with alerts","Pin known-good model slugs; verify them after upgrades","Handle in-stream errors distinctly from HTTP-status errors"],"tags":["voice","tts","openrouter","sse","stream-error"],"backgroundTag":"provider-stream-error","analyzedSha":"3e82f130422a813cdd73c10b21a44e9325f5821a","analyzedAt":"2026-08-27T06:57:25.364Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}