iflytek/astron-agent · error · CustomException
SPARK_REQUEST_ERROR
SPARK_REQUEST_ERROR
Error message
LLM response timeout ({timeout}s) What it means
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.
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.
Example fix
// before resp = llm.achat(flow_id, messages, span, timeout=10) // after resp = llm.achat(flow_id, messages, span, timeout=60) # tolerate slow Spark first token
Defensive patterns
Strategy: retry
Validate before calling
def safe_spark_timeout(seconds: float | None) -> float | None:
if seconds is not None and seconds < 30:
raise ValueError(f"Spark needs >=30s for slow first tokens; got {seconds}s")
return seconds
# call: llm.achat(..., timeout=safe_spark_timeout(user_timeout)) Type guard
def is_spark_timeout_error(e: BaseException) -> bool:
return isinstance(e, CustomException) and "LLM response timeout" in str(getattr(e, "err_msg", "")) Try / catch
for attempt in range(3):
try:
async for resp in llm.achat(flow_id, messages, span, timeout=60):
yield resp
break
except CustomException as e:
if "LLM response timeout" not in str(e.err_msg) or attempt == 2:
raise
await asyncio.sleep(2 ** attempt) Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12).
Data as JSON: /api/errors/0b300803e262c167.
Report an issue: GitHub.
Appendix: source
Thrown at core/workflow/infra/providers/llm/iflytek_spark/spark_chat_llm.py:180
:param ws_handle: WebSocket client protocol handle
:param timeout: Optional timeout in seconds for message reception
:return: Async iterator yielding received messages
"""
while True:
try:
if timeout is not None:
msg_json = await asyncio.wait_for(
recv_with_retry(ws_handle), timeout=timeout
)
else:
msg_json = await recv_with_retry(ws_handle)
yield msg_json
except asyncio.exceptions.CancelledError:
await ws_handle.close()
raise
except asyncio.TimeoutError as e:
raise CustomException(
err_code=CodeEnum.SPARK_REQUEST_ERROR,
err_msg=f"LLM response timeout ({timeout}s)",
cause_error=f"LLM response timeout ({timeout}s)",
) from e
except websockets.ConnectionClosed as err:
# After RETRY_CNT retries, this will catch the final ConnectionClosed exception
raise err
except CustomException as err:
raise err
except Exception as err:
raise CustomException(
err_code=CodeEnum.SPARK_REQUEST_ERROR,
err_msg=f"{str(err)}",
cause_error=f"{str(err)}",
) from err
async def achat(
self,View on GitHub (pinned to 5e758547a8)