Skip to content
TriedDocs
Custom LLM

Custom LLM troubleshooting

Diagnose custom LLM calls that fail to connect, end early, stay silent or ignore your actions.

Start with the call in Call History: its disconnection_reason, its transcript, and when it ended. Then match the symptom below.

Disconnection reasons#

Reason Meaning Check
error_llm_websocket_open The worker could not open {llm_websocket_url}/{call_id} at the start of the call, or the agent has no URL. URL scheme (ws:// or wss://), DNS, TLS certificate, firewall, that your server accepts any call id as the last path segment. Run Test connection.
error_llm_websocket_lost_connection The socket closed and could not be restored: no auto_reconnect, a reconnect failed, or 2 reconnects were already used. Server restarts and deploys, load balancer idle timeouts, ping handling, auto_reconnect in config.
error_llm_websocket_corrupt_payload A frame was not valid JSON, had an unknown response_type, or lacked a required field. Frame shapes (see WebSocket server). A common cause is a missing response_id.
error_llm_websocket_runtime The worker's reader failed, or no frame of a requested response arrived for 30 seconds. Slow model or tool calls without intermediate chunks, an exception that stopped your send loop.

The call connects but the agent never speaks#

  • Nobody speaks first. A custom LLM agent only opens the call if your server sends response with response_id: 0 before the first request. Otherwise the agent waits for the caller.
  • The opening line arrived late. A response_id: 0 frame that arrives after the first response_required is ignored.
  • No content_complete: true. Opening lines and agent_interrupt text are buffered until the last chunk. Without it nothing is spoken.
  • Wrong response_id. Frames must carry the id of the latest request. Older ids are dropped silently.

The agent cuts off or replies to the wrong turn#

  • Your server answered an old response_id after the caller interrupted. Cancel work when a newer request arrives.
  • Your server built its reply from update_only frames, which can arrive just after the request. Use the transcript in response_required.

call_details never arrives#

The worker sends call_details only if your config frame with call_details: true arrives within 0.5 seconds of the socket opening. Send config as the first frame on open.

Test connection sends a call_details frame without waiting for config. A server can receive call_details during the test and still miss it on real calls if config is slow.

Pings stop or calls reconnect mid-turn#

  • Pings only start if auto_reconnect: true was in a config received within 0.5 seconds of connecting.
  • If your server handles ping_pong behind a model call, replies arrive late and the worker reconnects after 5 seconds. Answer pings on their own path.
  • Reverse proxies with short idle timeouts can close quiet sockets. Allow at least the call's maximum duration.

Actions do not happen#

Symptom Cause
end_call ignored Set on a frame for an old response_id, which was dropped. Or transfer_number was also set; transfer wins.
Transfer fails Web calls cannot be transferred. On phone calls, check the number is E.164 and the trunk allows outbound calls.
Digits not sent digit_to_press needs a phone call. Digits are sent after the speech finishes.
update_agent has no effect Only responsiveness changes live behaviour in the current worker. interruption_sensitivity, reminder_trigger_ms and reminder_max_count are stored but read at call start.
metadata not on the call metadata frames are accepted but not stored.
No transcript_with_tool_calls in requests The worker sends transcript only, even when config.transcript_with_tool_calls is true.

Reminders never arrive#

reminder_required needs reminder_max_count above 0 on the agent (default 1) and reminder_trigger_ms of silence (default 10000). Silence is measured while neither side is speaking. If end_call_after_silence_ms is shorter, the call ends with inactivity first.

Test connection fails but calls work (or the reverse)#

Test connection runs from the API and refuses private, loopback and reserved addresses unless EGRESS_ALLOW_PRIVATE=1 is set on the API. The voice worker connects from wherever it runs. A URL on your local network can pass on calls from a local worker and fail the test, and a firewall that allows only the worker's address can fail the test and pass on calls.

Responses are slow#

  • Measure your time to first chunk per response_id.
  • Compare latency.llm_websocket_network_rtt on the call with your own logs. A high round trip means distance; a low round trip with slow turns means your server.
  • See Best practices.

Still stuck#

Reproduce the call with the text simulator and --events -. It uses the same engine as a live call and prints every event, including the final call_ended reason.