{"record":{"id":"0b300803e262c167","repo":"iflytek/astron-agent","slug":"spark-request-error-llm-response-timeout-timeout-s","errorCode":"SPARK_REQUEST_ERROR","errorMessage":"LLM response timeout ({timeout}s)","messagePattern":"LLM response timeout \\((.+?)s\\)","errorType":"error_code","errorClass":"CustomException","httpStatus":null,"severity":"error","filePath":"core/workflow/infra/providers/llm/iflytek_spark/spark_chat_llm.py","lineNumber":180,"sourceCode":"\n        :param ws_handle: WebSocket client protocol handle\n        :param timeout: Optional timeout in seconds for message reception\n        :return: Async iterator yielding received messages\n        \"\"\"\n        while True:\n            try:\n                if timeout is not None:\n                    msg_json = await asyncio.wait_for(\n                        recv_with_retry(ws_handle), timeout=timeout\n                    )\n                else:\n                    msg_json = await recv_with_retry(ws_handle)\n                yield msg_json\n            except asyncio.exceptions.CancelledError:\n                await ws_handle.close()\n                raise\n            except asyncio.TimeoutError as e:\n                raise CustomException(\n                    err_code=CodeEnum.SPARK_REQUEST_ERROR,\n                    err_msg=f\"LLM response timeout ({timeout}s)\",\n                    cause_error=f\"LLM response timeout ({timeout}s)\",\n                ) from e\n            except websockets.ConnectionClosed as err:\n                # After RETRY_CNT retries, this will catch the final ConnectionClosed exception\n                raise err\n            except CustomException as err:\n                raise err\n            except Exception as err:\n                raise CustomException(\n                    err_code=CodeEnum.SPARK_REQUEST_ERROR,\n                    err_msg=f\"{str(err)}\",\n                    cause_error=f\"{str(err)}\",\n                ) from err\n\n    async def achat(\n        self,","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/core/workflow/infra/providers/llm/iflytek_spark/spark_chat_llm.py#L162-L198","documentation":"This error is raised by the Spark LLM provider's _recv_messages loop when awaiting the next WebSocket frame from iFlytek Spark exceeds the per-message timeout configured via asyncio.wait_for. Spark streams tokens over a WebSocket, and if the server stalls (no frame within `timeout` seconds), the receive is cancelled and converted into a CustomException with code SPARK_REQUEST_ERROR. It indicates the LLM backend did not respond in time, not a malformed request.","triggerScenarios":"Calling SparkChatLLM.achat() with a `timeout` value while the Spark WebSocket delivers no message within that window; asyncio.TimeoutError from asyncio.wait_for(recv_with_retry(ws_handle), timeout=timeout) is caught at spark_chat_llm.py:179 and wrapped. Also triggered when Spark's first token is slow (cold start, long prompt) exceeding the caller-supplied timeout, or when network instability stalls frames mid-stream.","commonSituations":"Very low timeout configured for large/slow prompts (e.g. long system prompts with reasoning enabled); Spark service degradation or region outage; first-token latency spikes noted in the code TODO (60s default for slow first frame); proxy/firewall dropping idle WebSocket connections so frames stop arriving.","solutions":["Increase the `timeout` argument passed to achat() (or leave it None) so slow first-token responses are tolerated.","Reduce prompt size or disable slow features (e.g. set enable_thinking=false for supported flows via QUICKLY_THINK_* env vars) to cut first-token latency.","Check iFlytek Spark service status/quotas and network path (proxy, firewall idle-connection drops) between the service and Spark.","Add caller-side retry with backoff around achat() for transient stalls, since the streaming response is abandoned on timeout."],"exampleFix":"// before\nresp = llm.achat(flow_id, messages, span, timeout=10)\n// after\nresp = llm.achat(flow_id, messages, span, timeout=60)  # tolerate slow Spark first token","handlingStrategy":"retry","validationCode":"def safe_spark_timeout(seconds: float | None) -> float | None:\n    if seconds is not None and seconds < 30:\n        raise ValueError(f\"Spark needs >=30s for slow first tokens; got {seconds}s\")\n    return seconds\n# call: llm.achat(..., timeout=safe_spark_timeout(user_timeout))","typeGuard":"def is_spark_timeout_error(e: BaseException) -> bool:\n    return isinstance(e, CustomException) and \"LLM response timeout\" in str(getattr(e, \"err_msg\", \"\"))","tryCatchPattern":"for attempt in range(3):\n    try:\n        async for resp in llm.achat(flow_id, messages, span, timeout=60):\n            yield resp\n        break\n    except CustomException as e:\n        if \"LLM response timeout\" not in str(e.err_msg) or attempt == 2:\n            raise\n        await asyncio.sleep(2 ** attempt)","preventionTips":["Set Spark timeouts generously (>=60s) to account for slow first-token responses.","Disable thinking/search features for latency-sensitive flows.","Monitor Spark first-token latency metrics and alert on degradation.","Use exponential-backoff retry only around request start, not mid-stream after partial output was emitted."],"tags":["websocket","timeout","llm","network","spark"],"backgroundTag":"request-timeout","analyzedSha":"5e758547a83371a5a4b29dadf4ac03e8dd527635","analyzedAt":"2026-09-12T08:03:51.356Z","contentChangedAt":"2026-09-12T08:03:51.356Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}