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

  1. Increase the `timeout` argument passed to achat() (or leave it None) so slow first-token responses are tolerated.
  2. 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.
  3. Check iFlytek Spark service status/quotas and network path (proxy, firewall idle-connection drops) between the service and Spark.
  4. 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

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.

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)